Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
e24153b0cb
commit
3fc9a8e695
|
@ -82,3 +82,4 @@ jsdoc/
|
||||||
**/tmp/rubocop_cache/**
|
**/tmp/rubocop_cache/**
|
||||||
.overcommit.yml
|
.overcommit.yml
|
||||||
.projections.json
|
.projections.json
|
||||||
|
/qa/.rakeTasks
|
||||||
|
|
1
Gemfile
1
Gemfile
|
@ -159,6 +159,7 @@ gem 'icalendar'
|
||||||
|
|
||||||
# Diffs
|
# Diffs
|
||||||
gem 'diffy', '~> 3.1.0'
|
gem 'diffy', '~> 3.1.0'
|
||||||
|
gem 'diff_match_patch', '~> 0.1.0'
|
||||||
|
|
||||||
# Application server
|
# Application server
|
||||||
gem 'rack', '~> 2.0.7'
|
gem 'rack', '~> 2.0.7'
|
||||||
|
|
|
@ -224,6 +224,7 @@ GEM
|
||||||
railties
|
railties
|
||||||
rotp (~> 2.0)
|
rotp (~> 2.0)
|
||||||
diff-lcs (1.3)
|
diff-lcs (1.3)
|
||||||
|
diff_match_patch (0.1.0)
|
||||||
diffy (3.1.0)
|
diffy (3.1.0)
|
||||||
discordrb-webhooks-blackst0ne (3.3.0)
|
discordrb-webhooks-blackst0ne (3.3.0)
|
||||||
rest-client (~> 2.0)
|
rest-client (~> 2.0)
|
||||||
|
@ -1133,6 +1134,7 @@ DEPENDENCIES
|
||||||
device_detector
|
device_detector
|
||||||
devise (~> 4.6)
|
devise (~> 4.6)
|
||||||
devise-two-factor (~> 3.0.0)
|
devise-two-factor (~> 3.0.0)
|
||||||
|
diff_match_patch (~> 0.1.0)
|
||||||
diffy (~> 3.1.0)
|
diffy (~> 3.1.0)
|
||||||
discordrb-webhooks-blackst0ne (~> 3.3)
|
discordrb-webhooks-blackst0ne (~> 3.3)
|
||||||
doorkeeper (~> 4.3)
|
doorkeeper (~> 4.3)
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
/* eslint-disable import/prefer-default-export */
|
|
||||||
import _ from 'underscore';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Array} queryResults - Array of Result objects
|
* @param {Array} queryResults - Array of Result objects
|
||||||
* @param {Object} defaultConfig - Default chart config values (e.g. lineStyle, name)
|
* @param {Object} defaultConfig - Default chart config values (e.g. lineStyle, name)
|
||||||
* @returns {Array} The formatted values
|
* @returns {Array} The formatted values
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
export const makeDataSeries = (queryResults, defaultConfig) =>
|
export const makeDataSeries = (queryResults, defaultConfig) =>
|
||||||
queryResults
|
queryResults
|
||||||
.map(result => {
|
.map(result => {
|
||||||
|
@ -19,10 +17,13 @@ export const makeDataSeries = (queryResults, defaultConfig) =>
|
||||||
if (name) {
|
if (name) {
|
||||||
series.name = `${defaultConfig.name}: ${name}`;
|
series.name = `${defaultConfig.name}: ${name}`;
|
||||||
} else {
|
} else {
|
||||||
const template = _.template(defaultConfig.name, {
|
series.name = defaultConfig.name;
|
||||||
interpolate: /\{\{(.+?)\}\}/g,
|
Object.keys(result.metric).forEach(templateVar => {
|
||||||
|
const value = result.metric[templateVar];
|
||||||
|
const regex = new RegExp(`{{\\s*${templateVar}\\s*}}`, 'g');
|
||||||
|
|
||||||
|
series.name = series.name.replace(regex, value);
|
||||||
});
|
});
|
||||||
series.name = template(result.metric);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ...defaultConfig, ...series };
|
return { ...defaultConfig, ...series };
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* eslint-disable no-var, one-var, consistent-return */
|
/* eslint-disable consistent-return */
|
||||||
|
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import axios from './lib/utils/axios_utils';
|
import axios from './lib/utils/axios_utils';
|
||||||
|
@ -91,18 +91,17 @@ export default class Issue {
|
||||||
'click',
|
'click',
|
||||||
'.js-issuable-actions a.btn-close, .js-issuable-actions a.btn-reopen',
|
'.js-issuable-actions a.btn-close, .js-issuable-actions a.btn-reopen',
|
||||||
e => {
|
e => {
|
||||||
var $button, shouldSubmit, url;
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
$button = $(e.currentTarget);
|
const $button = $(e.currentTarget);
|
||||||
shouldSubmit = $button.hasClass('btn-comment');
|
const shouldSubmit = $button.hasClass('btn-comment');
|
||||||
if (shouldSubmit) {
|
if (shouldSubmit) {
|
||||||
Issue.submitNoteForm($button.closest('form'));
|
Issue.submitNoteForm($button.closest('form'));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.disableCloseReopenButton($button);
|
this.disableCloseReopenButton($button);
|
||||||
|
|
||||||
url = $button.attr('href');
|
const url = $button.attr('href');
|
||||||
return axios
|
return axios
|
||||||
.put(url)
|
.put(url)
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
|
@ -139,16 +138,14 @@ export default class Issue {
|
||||||
}
|
}
|
||||||
|
|
||||||
static submitNoteForm(form) {
|
static submitNoteForm(form) {
|
||||||
var noteText;
|
const noteText = form.find('textarea.js-note-text').val();
|
||||||
noteText = form.find('textarea.js-note-text').val();
|
|
||||||
if (noteText && noteText.trim().length > 0) {
|
if (noteText && noteText.trim().length > 0) {
|
||||||
return form.submit();
|
return form.submit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static initRelatedBranches() {
|
static initRelatedBranches() {
|
||||||
var $container;
|
const $container = $('#related-branches');
|
||||||
$container = $('#related-branches');
|
|
||||||
return axios
|
return axios
|
||||||
.get($container.data('url'))
|
.get($container.data('url'))
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* eslint-disable no-useless-return, func-names, no-var, no-underscore-dangle, one-var, no-new, consistent-return, no-shadow, no-param-reassign, vars-on-top, no-lonely-if, no-else-return, dot-notation, no-empty */
|
/* eslint-disable no-useless-return, func-names, no-underscore-dangle, no-new, consistent-return, no-shadow, no-param-reassign, no-lonely-if, no-else-return, dot-notation, no-empty */
|
||||||
/* global Issuable */
|
/* global Issuable */
|
||||||
/* global ListLabel */
|
/* global ListLabel */
|
||||||
|
|
||||||
|
@ -15,63 +15,39 @@ import { isScopedLabel } from '~/lib/utils/common_utils';
|
||||||
|
|
||||||
export default class LabelsSelect {
|
export default class LabelsSelect {
|
||||||
constructor(els, options = {}) {
|
constructor(els, options = {}) {
|
||||||
var _this, $els;
|
const _this = this;
|
||||||
_this = this;
|
|
||||||
|
|
||||||
$els = $(els);
|
let $els = $(els);
|
||||||
|
|
||||||
if (!els) {
|
if (!els) {
|
||||||
$els = $('.js-label-select');
|
$els = $('.js-label-select');
|
||||||
}
|
}
|
||||||
|
|
||||||
$els.each((i, dropdown) => {
|
$els.each((i, dropdown) => {
|
||||||
var $block,
|
const $dropdown = $(dropdown);
|
||||||
$dropdown,
|
const $dropdownContainer = $dropdown.closest('.labels-filter');
|
||||||
$form,
|
const namespacePath = $dropdown.data('namespacePath');
|
||||||
$loading,
|
const projectPath = $dropdown.data('projectPath');
|
||||||
$selectbox,
|
const issueUpdateURL = $dropdown.data('issueUpdate');
|
||||||
$sidebarCollapsedValue,
|
let selectedLabel = $dropdown.data('selected');
|
||||||
$value,
|
|
||||||
$dropdownMenu,
|
|
||||||
abilityName,
|
|
||||||
defaultLabel,
|
|
||||||
issueUpdateURL,
|
|
||||||
labelUrl,
|
|
||||||
namespacePath,
|
|
||||||
projectPath,
|
|
||||||
saveLabelData,
|
|
||||||
selectedLabel,
|
|
||||||
showAny,
|
|
||||||
showNo,
|
|
||||||
$sidebarLabelTooltip,
|
|
||||||
initialSelected,
|
|
||||||
fieldName,
|
|
||||||
showMenuAbove,
|
|
||||||
$dropdownContainer;
|
|
||||||
$dropdown = $(dropdown);
|
|
||||||
$dropdownContainer = $dropdown.closest('.labels-filter');
|
|
||||||
namespacePath = $dropdown.data('namespacePath');
|
|
||||||
projectPath = $dropdown.data('projectPath');
|
|
||||||
issueUpdateURL = $dropdown.data('issueUpdate');
|
|
||||||
selectedLabel = $dropdown.data('selected');
|
|
||||||
if (selectedLabel != null && !$dropdown.hasClass('js-multiselect')) {
|
if (selectedLabel != null && !$dropdown.hasClass('js-multiselect')) {
|
||||||
selectedLabel = selectedLabel.split(',');
|
selectedLabel = selectedLabel.split(',');
|
||||||
}
|
}
|
||||||
showNo = $dropdown.data('showNo');
|
const showNo = $dropdown.data('showNo');
|
||||||
showAny = $dropdown.data('showAny');
|
const showAny = $dropdown.data('showAny');
|
||||||
showMenuAbove = $dropdown.data('showMenuAbove');
|
const showMenuAbove = $dropdown.data('showMenuAbove');
|
||||||
defaultLabel = $dropdown.data('defaultLabel') || __('Label');
|
const defaultLabel = $dropdown.data('defaultLabel') || __('Label');
|
||||||
abilityName = $dropdown.data('abilityName');
|
const abilityName = $dropdown.data('abilityName');
|
||||||
$selectbox = $dropdown.closest('.selectbox');
|
const $selectbox = $dropdown.closest('.selectbox');
|
||||||
$block = $selectbox.closest('.block');
|
const $block = $selectbox.closest('.block');
|
||||||
$form = $dropdown.closest('form, .js-issuable-update');
|
const $form = $dropdown.closest('form, .js-issuable-update');
|
||||||
$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span');
|
const $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span');
|
||||||
$sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
|
const $sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
|
||||||
$value = $block.find('.value');
|
const $value = $block.find('.value');
|
||||||
$dropdownMenu = $dropdown.parent().find('.dropdown-menu');
|
const $dropdownMenu = $dropdown.parent().find('.dropdown-menu');
|
||||||
$loading = $block.find('.block-loading').fadeOut();
|
const $loading = $block.find('.block-loading').fadeOut();
|
||||||
fieldName = $dropdown.data('fieldName');
|
const fieldName = $dropdown.data('fieldName');
|
||||||
initialSelected = $selectbox
|
let initialSelected = $selectbox
|
||||||
.find(`input[name="${$dropdown.data('fieldName')}"]`)
|
.find(`input[name="${$dropdown.data('fieldName')}"]`)
|
||||||
.map(function() {
|
.map(function() {
|
||||||
return this.value;
|
return this.value;
|
||||||
|
@ -90,9 +66,8 @@ export default class LabelsSelect {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
saveLabelData = function() {
|
const saveLabelData = function() {
|
||||||
var data, selected;
|
const selected = $dropdown
|
||||||
selected = $dropdown
|
|
||||||
.closest('.selectbox')
|
.closest('.selectbox')
|
||||||
.find(`input[name='${fieldName}']`)
|
.find(`input[name='${fieldName}']`)
|
||||||
.map(function() {
|
.map(function() {
|
||||||
|
@ -103,7 +78,7 @@ export default class LabelsSelect {
|
||||||
if (_.isEqual(initialSelected, selected)) return;
|
if (_.isEqual(initialSelected, selected)) return;
|
||||||
initialSelected = selected;
|
initialSelected = selected;
|
||||||
|
|
||||||
data = {};
|
const data = {};
|
||||||
data[abilityName] = {};
|
data[abilityName] = {};
|
||||||
data[abilityName].label_ids = selected;
|
data[abilityName].label_ids = selected;
|
||||||
if (!selected.length) {
|
if (!selected.length) {
|
||||||
|
@ -114,12 +89,13 @@ export default class LabelsSelect {
|
||||||
axios
|
axios
|
||||||
.put(issueUpdateURL, data)
|
.put(issueUpdateURL, data)
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
var labelCount, template, labelTooltipTitle, labelTitles;
|
let labelTooltipTitle;
|
||||||
|
let template;
|
||||||
$loading.fadeOut();
|
$loading.fadeOut();
|
||||||
$dropdown.trigger('loaded.gl.dropdown');
|
$dropdown.trigger('loaded.gl.dropdown');
|
||||||
$selectbox.hide();
|
$selectbox.hide();
|
||||||
data.issueUpdateURL = issueUpdateURL;
|
data.issueUpdateURL = issueUpdateURL;
|
||||||
labelCount = 0;
|
let labelCount = 0;
|
||||||
if (data.labels.length && issueUpdateURL) {
|
if (data.labels.length && issueUpdateURL) {
|
||||||
template = LabelsSelect.getLabelTemplate({
|
template = LabelsSelect.getLabelTemplate({
|
||||||
labels: _.sortBy(data.labels, 'title'),
|
labels: _.sortBy(data.labels, 'title'),
|
||||||
|
@ -174,7 +150,7 @@ export default class LabelsSelect {
|
||||||
$sidebarCollapsedValue.text(labelCount);
|
$sidebarCollapsedValue.text(labelCount);
|
||||||
|
|
||||||
if (data.labels.length) {
|
if (data.labels.length) {
|
||||||
labelTitles = data.labels.map(label => label.title);
|
let labelTitles = data.labels.map(label => label.title);
|
||||||
|
|
||||||
if (labelTitles.length > 5) {
|
if (labelTitles.length > 5) {
|
||||||
labelTitles = labelTitles.slice(0, 5);
|
labelTitles = labelTitles.slice(0, 5);
|
||||||
|
@ -199,13 +175,13 @@ export default class LabelsSelect {
|
||||||
$dropdown.glDropdown({
|
$dropdown.glDropdown({
|
||||||
showMenuAbove,
|
showMenuAbove,
|
||||||
data(term, callback) {
|
data(term, callback) {
|
||||||
labelUrl = $dropdown.attr('data-labels');
|
const labelUrl = $dropdown.attr('data-labels');
|
||||||
axios
|
axios
|
||||||
.get(labelUrl)
|
.get(labelUrl)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
let { data } = res;
|
let { data } = res;
|
||||||
if ($dropdown.hasClass('js-extra-options')) {
|
if ($dropdown.hasClass('js-extra-options')) {
|
||||||
var extraData = [];
|
const extraData = [];
|
||||||
if (showNo) {
|
if (showNo) {
|
||||||
extraData.unshift({
|
extraData.unshift({
|
||||||
id: 0,
|
id: 0,
|
||||||
|
@ -232,22 +208,14 @@ export default class LabelsSelect {
|
||||||
.catch(() => flash(__('Error fetching labels.')));
|
.catch(() => flash(__('Error fetching labels.')));
|
||||||
},
|
},
|
||||||
renderRow(label) {
|
renderRow(label) {
|
||||||
var linkEl,
|
let colorEl;
|
||||||
listItemEl,
|
|
||||||
colorEl,
|
|
||||||
indeterminate,
|
|
||||||
removesAll,
|
|
||||||
selectedClass,
|
|
||||||
i,
|
|
||||||
marked,
|
|
||||||
dropdownValue;
|
|
||||||
|
|
||||||
selectedClass = [];
|
const selectedClass = [];
|
||||||
removesAll = label.id <= 0 || label.id == null;
|
const removesAll = label.id <= 0 || label.id == null;
|
||||||
|
|
||||||
if ($dropdown.hasClass('js-filter-bulk-update')) {
|
if ($dropdown.hasClass('js-filter-bulk-update')) {
|
||||||
indeterminate = $dropdown.data('indeterminate') || [];
|
const indeterminate = $dropdown.data('indeterminate') || [];
|
||||||
marked = $dropdown.data('marked') || [];
|
const marked = $dropdown.data('marked') || [];
|
||||||
|
|
||||||
if (indeterminate.indexOf(label.id) !== -1) {
|
if (indeterminate.indexOf(label.id) !== -1) {
|
||||||
selectedClass.push('is-indeterminate');
|
selectedClass.push('is-indeterminate');
|
||||||
|
@ -255,7 +223,7 @@ export default class LabelsSelect {
|
||||||
|
|
||||||
if (marked.indexOf(label.id) !== -1) {
|
if (marked.indexOf(label.id) !== -1) {
|
||||||
// Remove is-indeterminate class if the item will be marked as active
|
// Remove is-indeterminate class if the item will be marked as active
|
||||||
i = selectedClass.indexOf('is-indeterminate');
|
const i = selectedClass.indexOf('is-indeterminate');
|
||||||
if (i !== -1) {
|
if (i !== -1) {
|
||||||
selectedClass.splice(i, 1);
|
selectedClass.splice(i, 1);
|
||||||
}
|
}
|
||||||
|
@ -263,7 +231,7 @@ export default class LabelsSelect {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (this.id(label)) {
|
if (this.id(label)) {
|
||||||
dropdownValue = this.id(label)
|
const dropdownValue = this.id(label)
|
||||||
.toString()
|
.toString()
|
||||||
.replace(/'/g, "\\'");
|
.replace(/'/g, "\\'");
|
||||||
|
|
||||||
|
@ -287,7 +255,7 @@ export default class LabelsSelect {
|
||||||
colorEl = '';
|
colorEl = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
linkEl = document.createElement('a');
|
const linkEl = document.createElement('a');
|
||||||
linkEl.href = '#';
|
linkEl.href = '#';
|
||||||
|
|
||||||
// We need to identify which items are actually labels
|
// We need to identify which items are actually labels
|
||||||
|
@ -300,7 +268,7 @@ export default class LabelsSelect {
|
||||||
linkEl.className = selectedClass.join(' ');
|
linkEl.className = selectedClass.join(' ');
|
||||||
linkEl.innerHTML = `${colorEl} ${_.escape(label.title)}`;
|
linkEl.innerHTML = `${colorEl} ${_.escape(label.title)}`;
|
||||||
|
|
||||||
listItemEl = document.createElement('li');
|
const listItemEl = document.createElement('li');
|
||||||
listItemEl.appendChild(linkEl);
|
listItemEl.appendChild(linkEl);
|
||||||
|
|
||||||
return listItemEl;
|
return listItemEl;
|
||||||
|
@ -312,12 +280,12 @@ export default class LabelsSelect {
|
||||||
filterable: true,
|
filterable: true,
|
||||||
selected: $dropdown.data('selected') || [],
|
selected: $dropdown.data('selected') || [],
|
||||||
toggleLabel(selected, el) {
|
toggleLabel(selected, el) {
|
||||||
var $dropdownParent = $dropdown.parent();
|
const $dropdownParent = $dropdown.parent();
|
||||||
var $dropdownInputField = $dropdownParent.find('.dropdown-input-field');
|
const $dropdownInputField = $dropdownParent.find('.dropdown-input-field');
|
||||||
var isSelected = el !== null ? el.hasClass('is-active') : false;
|
const isSelected = el !== null ? el.hasClass('is-active') : false;
|
||||||
|
|
||||||
var title = selected ? selected.title : null;
|
const title = selected ? selected.title : null;
|
||||||
var selectedLabels = this.selected;
|
const selectedLabels = this.selected;
|
||||||
|
|
||||||
if ($dropdownInputField.length && $dropdownInputField.val().length) {
|
if ($dropdownInputField.length && $dropdownInputField.val().length) {
|
||||||
$dropdownParent.find('.dropdown-input-clear').trigger('click');
|
$dropdownParent.find('.dropdown-input-clear').trigger('click');
|
||||||
|
@ -329,7 +297,7 @@ export default class LabelsSelect {
|
||||||
} else if (isSelected) {
|
} else if (isSelected) {
|
||||||
this.selected.push(title);
|
this.selected.push(title);
|
||||||
} else if (!isSelected && title) {
|
} else if (!isSelected && title) {
|
||||||
var index = this.selected.indexOf(title);
|
const index = this.selected.indexOf(title);
|
||||||
this.selected.splice(index, 1);
|
this.selected.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -359,10 +327,9 @@ export default class LabelsSelect {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hidden() {
|
hidden() {
|
||||||
var isIssueIndex, isMRIndex, page;
|
const page = $('body').attr('data-page');
|
||||||
page = $('body').attr('data-page');
|
const isIssueIndex = page === 'projects:issues:index';
|
||||||
isIssueIndex = page === 'projects:issues:index';
|
const isMRIndex = page === 'projects:merge_requests:index';
|
||||||
isMRIndex = page === 'projects:merge_requests:index';
|
|
||||||
$selectbox.hide();
|
$selectbox.hide();
|
||||||
// display:block overrides the hide-collapse rule
|
// display:block overrides the hide-collapse rule
|
||||||
$value.removeAttr('style');
|
$value.removeAttr('style');
|
||||||
|
@ -393,14 +360,13 @@ export default class LabelsSelect {
|
||||||
const { $el, e, isMarking } = clickEvent;
|
const { $el, e, isMarking } = clickEvent;
|
||||||
const label = clickEvent.selectedObj;
|
const label = clickEvent.selectedObj;
|
||||||
|
|
||||||
var isIssueIndex, isMRIndex, page, boardsModel;
|
const fadeOutLoader = () => {
|
||||||
var fadeOutLoader = () => {
|
|
||||||
$loading.fadeOut();
|
$loading.fadeOut();
|
||||||
};
|
};
|
||||||
|
|
||||||
page = $('body').attr('data-page');
|
const page = $('body').attr('data-page');
|
||||||
isIssueIndex = page === 'projects:issues:index';
|
const isIssueIndex = page === 'projects:issues:index';
|
||||||
isMRIndex = page === 'projects:merge_requests:index';
|
const isMRIndex = page === 'projects:merge_requests:index';
|
||||||
|
|
||||||
if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) {
|
if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) {
|
||||||
$dropdown
|
$dropdown
|
||||||
|
@ -419,6 +385,7 @@ export default class LabelsSelect {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let boardsModel;
|
||||||
if ($dropdown.closest('.add-issues-modal').length) {
|
if ($dropdown.closest('.add-issues-modal').length) {
|
||||||
boardsModel = ModalStore.store.filter;
|
boardsModel = ModalStore.store.filter;
|
||||||
}
|
}
|
||||||
|
@ -450,7 +417,7 @@ export default class LabelsSelect {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
var { labels } = boardsStore.detail.issue;
|
let { labels } = boardsStore.detail.issue;
|
||||||
labels = labels.filter(selectedLabel => selectedLabel.id !== label.id);
|
labels = labels.filter(selectedLabel => selectedLabel.id !== label.id);
|
||||||
boardsStore.detail.issue.labels = labels;
|
boardsStore.detail.issue.labels = labels;
|
||||||
}
|
}
|
||||||
|
@ -578,16 +545,14 @@ export default class LabelsSelect {
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
setDropdownData($dropdown, isMarking, value) {
|
setDropdownData($dropdown, isMarking, value) {
|
||||||
var i, markedIds, unmarkedIds, indeterminateIds;
|
const markedIds = $dropdown.data('marked') || [];
|
||||||
|
const unmarkedIds = $dropdown.data('unmarked') || [];
|
||||||
markedIds = $dropdown.data('marked') || [];
|
const indeterminateIds = $dropdown.data('indeterminate') || [];
|
||||||
unmarkedIds = $dropdown.data('unmarked') || [];
|
|
||||||
indeterminateIds = $dropdown.data('indeterminate') || [];
|
|
||||||
|
|
||||||
if (isMarking) {
|
if (isMarking) {
|
||||||
markedIds.push(value);
|
markedIds.push(value);
|
||||||
|
|
||||||
i = indeterminateIds.indexOf(value);
|
let i = indeterminateIds.indexOf(value);
|
||||||
if (i > -1) {
|
if (i > -1) {
|
||||||
indeterminateIds.splice(i, 1);
|
indeterminateIds.splice(i, 1);
|
||||||
}
|
}
|
||||||
|
@ -598,7 +563,7 @@ export default class LabelsSelect {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If marked item (not common) is unmarked
|
// If marked item (not common) is unmarked
|
||||||
i = markedIds.indexOf(value);
|
const i = markedIds.indexOf(value);
|
||||||
if (i > -1) {
|
if (i > -1) {
|
||||||
markedIds.splice(i, 1);
|
markedIds.splice(i, 1);
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,7 +84,10 @@ export const fetchDashboard = ({ state, dispatch }, params) => {
|
||||||
return backOffRequest(() => axios.get(state.dashboardEndpoint, { params }))
|
return backOffRequest(() => axios.get(state.dashboardEndpoint, { params }))
|
||||||
.then(resp => resp.data)
|
.then(resp => resp.data)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
dispatch('receiveMetricsDashboardSuccess', { response, params });
|
dispatch('receiveMetricsDashboardSuccess', {
|
||||||
|
response,
|
||||||
|
params,
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
dispatch('receiveMetricsDashboardFailure', error);
|
dispatch('receiveMetricsDashboardFailure', error);
|
||||||
|
|
|
@ -94,7 +94,7 @@ export default {
|
||||||
state.emptyState = 'noData';
|
state.emptyState = 'noData';
|
||||||
},
|
},
|
||||||
[types.SET_ALL_DASHBOARDS](state, dashboards) {
|
[types.SET_ALL_DASHBOARDS](state, dashboards) {
|
||||||
state.allDashboards = dashboards;
|
state.allDashboards = dashboards || [];
|
||||||
},
|
},
|
||||||
[types.SET_SHOW_ERROR_BANNER](state, enabled) {
|
[types.SET_SHOW_ERROR_BANNER](state, enabled) {
|
||||||
state.showErrorBanner = enabled;
|
state.showErrorBanner = enabled;
|
||||||
|
|
|
@ -101,6 +101,7 @@ export default {
|
||||||
<time-ago-tooltip :time="createdAt" tooltip-placement="bottom" />
|
<time-ago-tooltip :time="createdAt" tooltip-placement="bottom" />
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
|
<slot name="extra-controls"></slot>
|
||||||
<i
|
<i
|
||||||
class="fa fa-spinner fa-spin editing-spinner"
|
class="fa fa-spinner fa-spin editing-spinner"
|
||||||
:aria-label="__('Comment is being updated')"
|
:aria-label="__('Comment is being updated')"
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Placeholder for GitLab FOSS
|
||||||
|
// Actual implementation: ee/app/assets/javascripts/notes/mixins/description_version_history.js
|
||||||
|
export default {
|
||||||
|
computed: {
|
||||||
|
canSeeDescriptionVersion() {},
|
||||||
|
shouldShowDescriptionVersion() {},
|
||||||
|
descriptionVersionToggleIcon() {},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleDescriptionVersion() {},
|
||||||
|
},
|
||||||
|
};
|
|
@ -12,6 +12,7 @@ import service from '../services/notes_service';
|
||||||
import loadAwardsHandler from '../../awards_handler';
|
import loadAwardsHandler from '../../awards_handler';
|
||||||
import sidebarTimeTrackingEventHub from '../../sidebar/event_hub';
|
import sidebarTimeTrackingEventHub from '../../sidebar/event_hub';
|
||||||
import { isInViewport, scrollToElement, isInMRPage } from '../../lib/utils/common_utils';
|
import { isInViewport, scrollToElement, isInMRPage } from '../../lib/utils/common_utils';
|
||||||
|
import { mergeUrlParams } from '../../lib/utils/url_utility';
|
||||||
import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub';
|
import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub';
|
||||||
import { __ } from '~/locale';
|
import { __ } from '~/locale';
|
||||||
import Api from '~/api';
|
import Api from '~/api';
|
||||||
|
@ -475,5 +476,20 @@ export const convertToDiscussion = ({ commit }, noteId) =>
|
||||||
export const removeConvertedDiscussion = ({ commit }, noteId) =>
|
export const removeConvertedDiscussion = ({ commit }, noteId) =>
|
||||||
commit(types.REMOVE_CONVERTED_DISCUSSION, noteId);
|
commit(types.REMOVE_CONVERTED_DISCUSSION, noteId);
|
||||||
|
|
||||||
|
export const fetchDescriptionVersion = (_, { endpoint, startingVersion }) => {
|
||||||
|
let requestUrl = endpoint;
|
||||||
|
|
||||||
|
if (startingVersion) {
|
||||||
|
requestUrl = mergeUrlParams({ start_version_id: startingVersion }, requestUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return axios
|
||||||
|
.get(requestUrl)
|
||||||
|
.then(res => res.data)
|
||||||
|
.catch(() => {
|
||||||
|
Flash(__('Something went wrong while fetching description changes. Please try again.'));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||||
export default () => {};
|
export default () => {};
|
||||||
|
|
|
@ -1,34 +1,9 @@
|
||||||
import { n__, s__, sprintf } from '~/locale';
|
|
||||||
import { DESCRIPTION_TYPE } from '../constants';
|
import { DESCRIPTION_TYPE } from '../constants';
|
||||||
|
|
||||||
/**
|
|
||||||
* Changes the description from a note, returns 'changed the description n number of times'
|
|
||||||
*/
|
|
||||||
export const changeDescriptionNote = (note, descriptionChangedTimes, timeDifferenceMinutes) => {
|
|
||||||
const descriptionNote = Object.assign({}, note);
|
|
||||||
|
|
||||||
descriptionNote.note_html = sprintf(
|
|
||||||
s__(`MergeRequest|
|
|
||||||
%{paragraphStart}changed the description %{descriptionChangedTimes} times %{timeDifferenceMinutes}%{paragraphEnd}`),
|
|
||||||
{
|
|
||||||
paragraphStart: '<p dir="auto">',
|
|
||||||
paragraphEnd: '</p>',
|
|
||||||
descriptionChangedTimes,
|
|
||||||
timeDifferenceMinutes: n__('within %d minute ', 'within %d minutes ', timeDifferenceMinutes),
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
descriptionNote.times_updated = descriptionChangedTimes;
|
|
||||||
|
|
||||||
return descriptionNote;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks the time difference between two notes from their 'created_at' dates
|
* Checks the time difference between two notes from their 'created_at' dates
|
||||||
* returns an integer
|
* returns an integer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const getTimeDifferenceMinutes = (noteBeggining, noteEnd) => {
|
export const getTimeDifferenceMinutes = (noteBeggining, noteEnd) => {
|
||||||
const descriptionNoteBegin = new Date(noteBeggining.created_at);
|
const descriptionNoteBegin = new Date(noteBeggining.created_at);
|
||||||
const descriptionNoteEnd = new Date(noteEnd.created_at);
|
const descriptionNoteEnd = new Date(noteEnd.created_at);
|
||||||
|
@ -57,7 +32,6 @@ export const isDescriptionSystemNote = note => note.system && note.note === DESC
|
||||||
export const collapseSystemNotes = notes => {
|
export const collapseSystemNotes = notes => {
|
||||||
let lastDescriptionSystemNote = null;
|
let lastDescriptionSystemNote = null;
|
||||||
let lastDescriptionSystemNoteIndex = -1;
|
let lastDescriptionSystemNoteIndex = -1;
|
||||||
let descriptionChangedTimes = 1;
|
|
||||||
|
|
||||||
return notes.slice(0).reduce((acc, currentNote) => {
|
return notes.slice(0).reduce((acc, currentNote) => {
|
||||||
const note = currentNote.notes[0];
|
const note = currentNote.notes[0];
|
||||||
|
@ -70,32 +44,24 @@ export const collapseSystemNotes = notes => {
|
||||||
} else if (lastDescriptionSystemNote) {
|
} else if (lastDescriptionSystemNote) {
|
||||||
const timeDifferenceMinutes = getTimeDifferenceMinutes(lastDescriptionSystemNote, note);
|
const timeDifferenceMinutes = getTimeDifferenceMinutes(lastDescriptionSystemNote, note);
|
||||||
|
|
||||||
// are they less than 10 minutes apart?
|
// are they less than 10 minutes apart from the same user?
|
||||||
if (timeDifferenceMinutes > 10) {
|
if (timeDifferenceMinutes > 10 || note.author.id !== lastDescriptionSystemNote.author.id) {
|
||||||
// reset counter
|
|
||||||
descriptionChangedTimes = 1;
|
|
||||||
// update the previous system note
|
// update the previous system note
|
||||||
lastDescriptionSystemNote = note;
|
lastDescriptionSystemNote = note;
|
||||||
lastDescriptionSystemNoteIndex = acc.length;
|
lastDescriptionSystemNoteIndex = acc.length;
|
||||||
} else {
|
} else {
|
||||||
// increase counter
|
// set the first version to fetch grouped system note versions
|
||||||
descriptionChangedTimes += 1;
|
note.start_description_version_id = lastDescriptionSystemNote.description_version_id;
|
||||||
|
|
||||||
// delete the previous one
|
// delete the previous one
|
||||||
acc.splice(lastDescriptionSystemNoteIndex, 1);
|
acc.splice(lastDescriptionSystemNoteIndex, 1);
|
||||||
|
|
||||||
// replace the text of the current system note with the collapsed note.
|
|
||||||
currentNote.notes.splice(
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
changeDescriptionNote(note, descriptionChangedTimes, timeDifferenceMinutes),
|
|
||||||
);
|
|
||||||
|
|
||||||
// update the previous system note index
|
// update the previous system note index
|
||||||
lastDescriptionSystemNoteIndex = acc.length;
|
lastDescriptionSystemNoteIndex = acc.length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
acc.push(currentNote);
|
acc.push(currentNote);
|
||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
|
@ -8,12 +8,13 @@ import {
|
||||||
GlModalDirective,
|
GlModalDirective,
|
||||||
GlEmptyState,
|
GlEmptyState,
|
||||||
} from '@gitlab/ui';
|
} from '@gitlab/ui';
|
||||||
import createFlash from '../../flash';
|
import createFlash from '~/flash';
|
||||||
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
|
import Tracking from '~/tracking';
|
||||||
import Icon from '../../vue_shared/components/icon.vue';
|
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||||
|
import Icon from '~/vue_shared/components/icon.vue';
|
||||||
import TableRegistry from './table_registry.vue';
|
import TableRegistry from './table_registry.vue';
|
||||||
import { errorMessages, errorMessagesTypes } from '../constants';
|
import { DELETE_REPO_ERROR_MESSAGE } from '../constants';
|
||||||
import { __ } from '../../locale';
|
import { __ } from '~/locale';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CollapsibeContainerRegisty',
|
name: 'CollapsibeContainerRegisty',
|
||||||
|
@ -30,6 +31,7 @@ export default {
|
||||||
GlTooltip: GlTooltipDirective,
|
GlTooltip: GlTooltipDirective,
|
||||||
GlModal: GlModalDirective,
|
GlModal: GlModalDirective,
|
||||||
},
|
},
|
||||||
|
mixins: [Tracking.mixin({})],
|
||||||
props: {
|
props: {
|
||||||
repo: {
|
repo: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -40,6 +42,10 @@ export default {
|
||||||
return {
|
return {
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
modalId: `confirm-repo-deletion-modal-${this.repo.id}`,
|
modalId: `confirm-repo-deletion-modal-${this.repo.id}`,
|
||||||
|
tracking: {
|
||||||
|
category: document.body.dataset.page,
|
||||||
|
label: 'registry_repository_delete',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -61,15 +67,13 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleDeleteRepository() {
|
handleDeleteRepository() {
|
||||||
|
this.track('confirm_delete', {});
|
||||||
return this.deleteItem(this.repo)
|
return this.deleteItem(this.repo)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
createFlash(__('This container registry has been scheduled for deletion.'), 'notice');
|
createFlash(__('This container registry has been scheduled for deletion.'), 'notice');
|
||||||
this.fetchRepos();
|
this.fetchRepos();
|
||||||
})
|
})
|
||||||
.catch(() => this.showError(errorMessagesTypes.DELETE_REPO));
|
.catch(() => createFlash(DELETE_REPO_ERROR_MESSAGE));
|
||||||
},
|
|
||||||
showError(message) {
|
|
||||||
createFlash(errorMessages[message]);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -97,10 +101,9 @@ export default {
|
||||||
v-gl-modal="modalId"
|
v-gl-modal="modalId"
|
||||||
:title="s__('ContainerRegistry|Remove repository')"
|
:title="s__('ContainerRegistry|Remove repository')"
|
||||||
:aria-label="s__('ContainerRegistry|Remove repository')"
|
:aria-label="s__('ContainerRegistry|Remove repository')"
|
||||||
data-track-event="click_button"
|
|
||||||
data-track-label="registry_repository_delete"
|
|
||||||
class="js-remove-repo btn-inverted"
|
class="js-remove-repo btn-inverted"
|
||||||
variant="danger"
|
variant="danger"
|
||||||
|
@click="track('click_button', {})"
|
||||||
>
|
>
|
||||||
<icon name="remove" />
|
<icon name="remove" />
|
||||||
</gl-button>
|
</gl-button>
|
||||||
|
@ -124,7 +127,13 @@ export default {
|
||||||
class="mx-auto my-0"
|
class="mx-auto my-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<gl-modal :modal-id="modalId" ok-variant="danger" @ok="handleDeleteRepository">
|
<gl-modal
|
||||||
|
ref="deleteModal"
|
||||||
|
:modal-id="modalId"
|
||||||
|
ok-variant="danger"
|
||||||
|
@ok="handleDeleteRepository"
|
||||||
|
@cancel="track('cancel_delete', {})"
|
||||||
|
>
|
||||||
<template v-slot:modal-title>{{ s__('ContainerRegistry|Remove repository') }}</template>
|
<template v-slot:modal-title>{{ s__('ContainerRegistry|Remove repository') }}</template>
|
||||||
<p
|
<p
|
||||||
v-html="
|
v-html="
|
||||||
|
|
|
@ -1,20 +1,15 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapActions, mapGetters } from 'vuex';
|
import { mapActions, mapGetters } from 'vuex';
|
||||||
import {
|
import { GlButton, GlFormCheckbox, GlTooltipDirective, GlModal } from '@gitlab/ui';
|
||||||
GlButton,
|
import Tracking from '~/tracking';
|
||||||
GlFormCheckbox,
|
import { n__, s__, sprintf } from '~/locale';
|
||||||
GlTooltipDirective,
|
import createFlash from '~/flash';
|
||||||
GlModal,
|
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||||
GlModalDirective,
|
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
|
||||||
} from '@gitlab/ui';
|
import Icon from '~/vue_shared/components/icon.vue';
|
||||||
import { n__, s__, sprintf } from '../../locale';
|
import timeagoMixin from '~/vue_shared/mixins/timeago';
|
||||||
import createFlash from '../../flash';
|
import { numberToHumanSize } from '~/lib/utils/number_utils';
|
||||||
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
|
import { FETCH_REGISTRY_ERROR_MESSAGE, DELETE_REGISTRY_ERROR_MESSAGE } from '../constants';
|
||||||
import TablePagination from '../../vue_shared/components/pagination/table_pagination.vue';
|
|
||||||
import Icon from '../../vue_shared/components/icon.vue';
|
|
||||||
import timeagoMixin from '../../vue_shared/mixins/timeago';
|
|
||||||
import { errorMessages, errorMessagesTypes } from '../constants';
|
|
||||||
import { numberToHumanSize } from '../../lib/utils/number_utils';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -27,7 +22,6 @@ export default {
|
||||||
},
|
},
|
||||||
directives: {
|
directives: {
|
||||||
GlTooltip: GlTooltipDirective,
|
GlTooltip: GlTooltipDirective,
|
||||||
GlModal: GlModalDirective,
|
|
||||||
},
|
},
|
||||||
mixins: [timeagoMixin],
|
mixins: [timeagoMixin],
|
||||||
props: {
|
props: {
|
||||||
|
@ -65,12 +59,21 @@ export default {
|
||||||
this.itemsToBeDeleted.length === 0 ? 1 : this.itemsToBeDeleted.length,
|
this.itemsToBeDeleted.length === 0 ? 1 : this.itemsToBeDeleted.length,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
isMultiDelete() {
|
||||||
mounted() {
|
return this.itemsToBeDeleted.length > 1;
|
||||||
this.$refs.deleteModal.$refs.modal.$on('hide', this.removeModalEvents);
|
},
|
||||||
|
tracking() {
|
||||||
|
return {
|
||||||
|
property: this.repo.name,
|
||||||
|
label: this.isMultiDelete ? 'bulk_registry_tag_delete' : 'registry_tag_delete',
|
||||||
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(['fetchList', 'deleteItem', 'multiDeleteItems']),
|
...mapActions(['fetchList', 'deleteItem', 'multiDeleteItems']),
|
||||||
|
track(action) {
|
||||||
|
Tracking.event(document.body.dataset.page, action, this.tracking);
|
||||||
|
},
|
||||||
setModalDescription(itemIndex = -1) {
|
setModalDescription(itemIndex = -1) {
|
||||||
if (itemIndex === -1) {
|
if (itemIndex === -1) {
|
||||||
this.modalDescription = sprintf(
|
this.modalDescription = sprintf(
|
||||||
|
@ -92,17 +95,11 @@ export default {
|
||||||
formatSize(size) {
|
formatSize(size) {
|
||||||
return numberToHumanSize(size);
|
return numberToHumanSize(size);
|
||||||
},
|
},
|
||||||
removeModalEvents() {
|
|
||||||
this.$refs.deleteModal.$refs.modal.$off('ok');
|
|
||||||
},
|
|
||||||
deleteSingleItem(index) {
|
deleteSingleItem(index) {
|
||||||
this.setModalDescription(index);
|
this.setModalDescription(index);
|
||||||
this.itemsToBeDeleted = [index];
|
this.itemsToBeDeleted = [index];
|
||||||
|
this.track('click_button');
|
||||||
this.$refs.deleteModal.$refs.modal.$once('ok', () => {
|
this.$refs.deleteModal.show();
|
||||||
this.removeModalEvents();
|
|
||||||
this.handleSingleDelete(this.repo.list[index]);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
deleteMultipleItems() {
|
deleteMultipleItems() {
|
||||||
this.itemsToBeDeleted = [...this.selectedItems];
|
this.itemsToBeDeleted = [...this.selectedItems];
|
||||||
|
@ -111,17 +108,14 @@ export default {
|
||||||
} else if (this.selectedItems.length > 1) {
|
} else if (this.selectedItems.length > 1) {
|
||||||
this.setModalDescription();
|
this.setModalDescription();
|
||||||
}
|
}
|
||||||
|
this.track('click_button');
|
||||||
this.$refs.deleteModal.$refs.modal.$once('ok', () => {
|
this.$refs.deleteModal.show();
|
||||||
this.removeModalEvents();
|
|
||||||
this.handleMultipleDelete();
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
handleSingleDelete(itemToDelete) {
|
handleSingleDelete(itemToDelete) {
|
||||||
this.itemsToBeDeleted = [];
|
this.itemsToBeDeleted = [];
|
||||||
this.deleteItem(itemToDelete)
|
this.deleteItem(itemToDelete)
|
||||||
.then(() => this.fetchList({ repo: this.repo }))
|
.then(() => this.fetchList({ repo: this.repo }))
|
||||||
.catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY));
|
.catch(() => createFlash(DELETE_REGISTRY_ERROR_MESSAGE));
|
||||||
},
|
},
|
||||||
handleMultipleDelete() {
|
handleMultipleDelete() {
|
||||||
const { itemsToBeDeleted } = this;
|
const { itemsToBeDeleted } = this;
|
||||||
|
@ -134,19 +128,16 @@ export default {
|
||||||
items: itemsToBeDeleted.map(x => this.repo.list[x].tag),
|
items: itemsToBeDeleted.map(x => this.repo.list[x].tag),
|
||||||
})
|
})
|
||||||
.then(() => this.fetchList({ repo: this.repo }))
|
.then(() => this.fetchList({ repo: this.repo }))
|
||||||
.catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY));
|
.catch(() => createFlash(DELETE_REGISTRY_ERROR_MESSAGE));
|
||||||
} else {
|
} else {
|
||||||
this.showError(errorMessagesTypes.DELETE_REGISTRY);
|
createFlash(DELETE_REGISTRY_ERROR_MESSAGE);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onPageChange(pageNumber) {
|
onPageChange(pageNumber) {
|
||||||
this.fetchList({ repo: this.repo, page: pageNumber }).catch(() =>
|
this.fetchList({ repo: this.repo, page: pageNumber }).catch(() =>
|
||||||
this.showError(errorMessagesTypes.FETCH_REGISTRY),
|
createFlash(FETCH_REGISTRY_ERROR_MESSAGE),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
showError(message) {
|
|
||||||
createFlash(errorMessages[message]);
|
|
||||||
},
|
|
||||||
onSelectAllChange() {
|
onSelectAllChange() {
|
||||||
if (this.selectAllChecked) {
|
if (this.selectAllChecked) {
|
||||||
this.deselectAll();
|
this.deselectAll();
|
||||||
|
@ -179,6 +170,15 @@ export default {
|
||||||
canDeleteRow(item) {
|
canDeleteRow(item) {
|
||||||
return item && item.canDelete && !this.isDeleteDisabled;
|
return item && item.canDelete && !this.isDeleteDisabled;
|
||||||
},
|
},
|
||||||
|
onDeletionConfirmed() {
|
||||||
|
this.track('confirm_delete');
|
||||||
|
if (this.isMultiDelete) {
|
||||||
|
this.handleMultipleDelete();
|
||||||
|
} else {
|
||||||
|
const index = this.itemsToBeDeleted[0];
|
||||||
|
this.handleSingleDelete(this.repo.list[index]);
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -202,12 +202,10 @@ export default {
|
||||||
<th>
|
<th>
|
||||||
<gl-button
|
<gl-button
|
||||||
v-if="canDeleteRepo"
|
v-if="canDeleteRepo"
|
||||||
|
ref="bulkDeleteButton"
|
||||||
v-gl-tooltip
|
v-gl-tooltip
|
||||||
v-gl-modal="modalId"
|
|
||||||
:disabled="!selectedItems || selectedItems.length === 0"
|
:disabled="!selectedItems || selectedItems.length === 0"
|
||||||
class="js-delete-registry float-right"
|
class="float-right"
|
||||||
data-track-event="click_button"
|
|
||||||
data-track-label="bulk_registry_tag_delete"
|
|
||||||
variant="danger"
|
variant="danger"
|
||||||
:title="s__('ContainerRegistry|Remove selected tags')"
|
:title="s__('ContainerRegistry|Remove selected tags')"
|
||||||
:aria-label="s__('ContainerRegistry|Remove selected tags')"
|
:aria-label="s__('ContainerRegistry|Remove selected tags')"
|
||||||
|
@ -259,11 +257,8 @@ export default {
|
||||||
<td class="content action-buttons">
|
<td class="content action-buttons">
|
||||||
<gl-button
|
<gl-button
|
||||||
v-if="canDeleteRow(item)"
|
v-if="canDeleteRow(item)"
|
||||||
v-gl-modal="modalId"
|
|
||||||
:title="s__('ContainerRegistry|Remove tag')"
|
:title="s__('ContainerRegistry|Remove tag')"
|
||||||
:aria-label="s__('ContainerRegistry|Remove tag')"
|
:aria-label="s__('ContainerRegistry|Remove tag')"
|
||||||
data-track-event="click_button"
|
|
||||||
data-track-label="registry_tag_delete"
|
|
||||||
variant="danger"
|
variant="danger"
|
||||||
class="js-delete-registry-row float-right btn-inverted btn-border-color btn-icon"
|
class="js-delete-registry-row float-right btn-inverted btn-border-color btn-icon"
|
||||||
@click="deleteSingleItem(index)"
|
@click="deleteSingleItem(index)"
|
||||||
|
@ -282,7 +277,13 @@ export default {
|
||||||
class="js-registry-pagination"
|
class="js-registry-pagination"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<gl-modal ref="deleteModal" :modal-id="modalId" ok-variant="danger">
|
<gl-modal
|
||||||
|
ref="deleteModal"
|
||||||
|
:modal-id="modalId"
|
||||||
|
ok-variant="danger"
|
||||||
|
@ok="onDeletionConfirmed"
|
||||||
|
@cancel="track('cancel_delete')"
|
||||||
|
>
|
||||||
<template v-slot:modal-title>{{ modalAction }}</template>
|
<template v-slot:modal-title>{{ modalAction }}</template>
|
||||||
<template v-slot:modal-ok>{{ modalAction }}</template>
|
<template v-slot:modal-ok>{{ modalAction }}</template>
|
||||||
<p v-html="modalDescription"></p>
|
<p v-html="modalDescription"></p>
|
||||||
|
|
|
@ -1,15 +1,8 @@
|
||||||
import { __ } from '../locale';
|
import { __ } from '../locale';
|
||||||
|
|
||||||
export const errorMessagesTypes = {
|
export const FETCH_REGISTRY_ERROR_MESSAGE = __(
|
||||||
FETCH_REGISTRY: 'FETCH_REGISTRY',
|
'Something went wrong while fetching the registry list.',
|
||||||
FETCH_REPOS: 'FETCH_REPOS',
|
);
|
||||||
DELETE_REPO: 'DELETE_REPO',
|
export const FETCH_REPOS_ERROR_MESSAGE = __('Something went wrong while fetching the projects.');
|
||||||
DELETE_REGISTRY: 'DELETE_REGISTRY',
|
export const DELETE_REPO_ERROR_MESSAGE = __('Something went wrong on our end.');
|
||||||
};
|
export const DELETE_REGISTRY_ERROR_MESSAGE = __('Something went wrong on our end.');
|
||||||
|
|
||||||
export const errorMessages = {
|
|
||||||
[errorMessagesTypes.FETCH_REGISTRY]: __('Something went wrong while fetching the registry list.'),
|
|
||||||
[errorMessagesTypes.FETCH_REPOS]: __('Something went wrong while fetching the projects.'),
|
|
||||||
[errorMessagesTypes.DELETE_REPO]: __('Something went wrong on our end.'),
|
|
||||||
[errorMessagesTypes.DELETE_REGISTRY]: __('Something went wrong on our end.'),
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import axios from '~/lib/utils/axios_utils';
|
import axios from '~/lib/utils/axios_utils';
|
||||||
import createFlash from '~/flash';
|
import createFlash from '~/flash';
|
||||||
import * as types from './mutation_types';
|
import * as types from './mutation_types';
|
||||||
import { errorMessages, errorMessagesTypes } from '../constants';
|
import { FETCH_REPOS_ERROR_MESSAGE, FETCH_REGISTRY_ERROR_MESSAGE } from '../constants';
|
||||||
|
|
||||||
export const fetchRepos = ({ commit, state }) => {
|
export const fetchRepos = ({ commit, state }) => {
|
||||||
commit(types.TOGGLE_MAIN_LOADING);
|
commit(types.TOGGLE_MAIN_LOADING);
|
||||||
|
@ -14,7 +14,7 @@ export const fetchRepos = ({ commit, state }) => {
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
commit(types.TOGGLE_MAIN_LOADING);
|
commit(types.TOGGLE_MAIN_LOADING);
|
||||||
createFlash(errorMessages[errorMessagesTypes.FETCH_REPOS]);
|
createFlash(FETCH_REPOS_ERROR_MESSAGE);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ export const fetchList = ({ commit }, { repo, page }) => {
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
|
commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
|
||||||
createFlash(errorMessages[errorMessagesTypes.FETCH_REGISTRY]);
|
createFlash(FETCH_REGISTRY_ERROR_MESSAGE);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,33 +1,31 @@
|
||||||
import * as types from './mutation_types';
|
import * as types from './mutation_types';
|
||||||
import { parseIntPagination, normalizeHeaders } from '../../lib/utils/common_utils';
|
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
[types.SET_MAIN_ENDPOINT](state, endpoint) {
|
[types.SET_MAIN_ENDPOINT](state, endpoint) {
|
||||||
Object.assign(state, { endpoint });
|
state.endpoint = endpoint;
|
||||||
},
|
},
|
||||||
|
|
||||||
[types.SET_IS_DELETE_DISABLED](state, isDeleteDisabled) {
|
[types.SET_IS_DELETE_DISABLED](state, isDeleteDisabled) {
|
||||||
Object.assign(state, { isDeleteDisabled });
|
state.isDeleteDisabled = isDeleteDisabled;
|
||||||
},
|
},
|
||||||
|
|
||||||
[types.SET_REPOS_LIST](state, list) {
|
[types.SET_REPOS_LIST](state, list) {
|
||||||
Object.assign(state, {
|
state.repos = list.map(el => ({
|
||||||
repos: list.map(el => ({
|
canDelete: Boolean(el.destroy_path),
|
||||||
canDelete: Boolean(el.destroy_path),
|
destroyPath: el.destroy_path,
|
||||||
destroyPath: el.destroy_path,
|
id: el.id,
|
||||||
id: el.id,
|
isLoading: false,
|
||||||
isLoading: false,
|
list: [],
|
||||||
list: [],
|
location: el.location,
|
||||||
location: el.location,
|
name: el.path,
|
||||||
name: el.path,
|
tagsPath: el.tags_path,
|
||||||
tagsPath: el.tags_path,
|
projectId: el.project_id,
|
||||||
projectId: el.project_id,
|
}));
|
||||||
})),
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
[types.TOGGLE_MAIN_LOADING](state) {
|
[types.TOGGLE_MAIN_LOADING](state) {
|
||||||
Object.assign(state, { isLoading: !state.isLoading });
|
state.isLoading = !state.isLoading;
|
||||||
},
|
},
|
||||||
|
|
||||||
[types.SET_REGISTRY_LIST](state, { repo, resp, headers }) {
|
[types.SET_REGISTRY_LIST](state, { repo, resp, headers }) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* eslint-disable func-names, consistent-return, no-var, one-var, no-else-return, class-methods-use-this */
|
/* eslint-disable func-names, consistent-return, one-var, no-else-return, class-methods-use-this */
|
||||||
|
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import { visitUrl } from './lib/utils/url_utility';
|
import { visitUrl } from './lib/utils/url_utility';
|
||||||
|
@ -9,9 +9,8 @@ export default class TreeView {
|
||||||
// Code browser tree slider
|
// Code browser tree slider
|
||||||
// Make the entire tree-item row clickable, but not if clicking another link (like a commit message)
|
// Make the entire tree-item row clickable, but not if clicking another link (like a commit message)
|
||||||
$('.tree-content-holder .tree-item').on('click', function(e) {
|
$('.tree-content-holder .tree-item').on('click', function(e) {
|
||||||
var $clickedEl, path;
|
const $clickedEl = $(e.target);
|
||||||
$clickedEl = $(e.target);
|
const path = $('.tree-item-file-name a', this).attr('href');
|
||||||
path = $('.tree-item-file-name a', this).attr('href');
|
|
||||||
if (!$clickedEl.is('a') && !$clickedEl.is('.str-truncated')) {
|
if (!$clickedEl.is('a') && !$clickedEl.is('.str-truncated')) {
|
||||||
if (e.metaKey || e.which === 2) {
|
if (e.metaKey || e.which === 2) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -26,11 +25,10 @@ export default class TreeView {
|
||||||
}
|
}
|
||||||
|
|
||||||
initKeyNav() {
|
initKeyNav() {
|
||||||
var li, liSelected;
|
const li = $('tr.tree-item');
|
||||||
li = $('tr.tree-item');
|
let liSelected = null;
|
||||||
liSelected = null;
|
|
||||||
return $('body').keydown(e => {
|
return $('body').keydown(e => {
|
||||||
var next, path;
|
let next, path;
|
||||||
if ($('input:focus').length > 0 && (e.which === 38 || e.which === 40)) {
|
if ($('input:focus').length > 0 && (e.which === 38 || e.which === 40)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,9 +17,11 @@
|
||||||
* />
|
* />
|
||||||
*/
|
*/
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters, mapActions } from 'vuex';
|
||||||
|
import { GlSkeletonLoading } from '@gitlab/ui';
|
||||||
import noteHeader from '~/notes/components/note_header.vue';
|
import noteHeader from '~/notes/components/note_header.vue';
|
||||||
import Icon from '~/vue_shared/components/icon.vue';
|
import Icon from '~/vue_shared/components/icon.vue';
|
||||||
|
import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history';
|
||||||
import TimelineEntryItem from './timeline_entry_item.vue';
|
import TimelineEntryItem from './timeline_entry_item.vue';
|
||||||
import { spriteIcon } from '../../../lib/utils/common_utils';
|
import { spriteIcon } from '../../../lib/utils/common_utils';
|
||||||
import initMRPopovers from '~/mr_popover/';
|
import initMRPopovers from '~/mr_popover/';
|
||||||
|
@ -32,7 +34,9 @@ export default {
|
||||||
Icon,
|
Icon,
|
||||||
noteHeader,
|
noteHeader,
|
||||||
TimelineEntryItem,
|
TimelineEntryItem,
|
||||||
|
GlSkeletonLoading,
|
||||||
},
|
},
|
||||||
|
mixins: [descriptionVersionHistoryMixin],
|
||||||
props: {
|
props: {
|
||||||
note: {
|
note: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -75,13 +79,16 @@ export default {
|
||||||
mounted() {
|
mounted() {
|
||||||
initMRPopovers(this.$el.querySelectorAll('.gfm-merge_request'));
|
initMRPopovers(this.$el.querySelectorAll('.gfm-merge_request'));
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(['fetchDescriptionVersion']),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<timeline-entry-item
|
<timeline-entry-item
|
||||||
:id="noteAnchorId"
|
:id="noteAnchorId"
|
||||||
:class="{ target: isTargetNote }"
|
:class="{ target: isTargetNote, 'pr-0': shouldShowDescriptionVersion }"
|
||||||
class="note system-note note-wrapper"
|
class="note system-note note-wrapper"
|
||||||
>
|
>
|
||||||
<div class="timeline-icon" v-html="iconHtml"></div>
|
<div class="timeline-icon" v-html="iconHtml"></div>
|
||||||
|
@ -89,14 +96,18 @@ export default {
|
||||||
<div class="note-header">
|
<div class="note-header">
|
||||||
<note-header :author="note.author" :created-at="note.created_at" :note-id="note.id">
|
<note-header :author="note.author" :created-at="note.created_at" :note-id="note.id">
|
||||||
<span v-html="actionTextHtml"></span>
|
<span v-html="actionTextHtml"></span>
|
||||||
|
<template v-if="canSeeDescriptionVersion" slot="extra-controls">
|
||||||
|
·
|
||||||
|
<button type="button" class="btn-blank btn-link" @click="toggleDescriptionVersion">
|
||||||
|
{{ __('Compare with previous version') }}
|
||||||
|
<icon :name="descriptionVersionToggleIcon" :size="12" class="append-left-5" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
</note-header>
|
</note-header>
|
||||||
</div>
|
</div>
|
||||||
<div class="note-body">
|
<div class="note-body">
|
||||||
<div
|
<div
|
||||||
:class="{
|
:class="{ 'system-note-commit-list': hasMoreCommits, 'hide-shade': expanded }"
|
||||||
'system-note-commit-list': hasMoreCommits,
|
|
||||||
'hide-shade': expanded,
|
|
||||||
}"
|
|
||||||
class="note-text md"
|
class="note-text md"
|
||||||
v-html="note.note_html"
|
v-html="note.note_html"
|
||||||
></div>
|
></div>
|
||||||
|
@ -106,6 +117,12 @@ export default {
|
||||||
<span>{{ __('Toggle commit list') }}</span>
|
<span>{{ __('Toggle commit list') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="shouldShowDescriptionVersion" class="description-version pt-2">
|
||||||
|
<pre v-if="isLoadingDescriptionVersion" class="loading-state">
|
||||||
|
<gl-skeleton-loading />
|
||||||
|
</pre>
|
||||||
|
<pre v-else class="wrapper mt-2" v-html="descriptionVersion"></pre>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</timeline-entry-item>
|
</timeline-entry-item>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
$notification-box-shadow-color: rgba(0, 0, 0, 0.25);
|
$notification-box-shadow-color: rgba(0, 0, 0, 0.25);
|
||||||
|
|
||||||
.flash-container {
|
.flash-container {
|
||||||
margin-top: 10px;
|
margin: 0;
|
||||||
margin-bottom: $gl-padding;
|
margin-bottom: $gl-padding;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -41,6 +41,7 @@ $notification-box-shadow-color: rgba(0, 0, 0, 0.25);
|
||||||
.flash-success,
|
.flash-success,
|
||||||
.flash-warning {
|
.flash-warning {
|
||||||
padding: $gl-padding $gl-padding-32 $gl-padding ($gl-padding + $gl-padding-4);
|
padding: $gl-padding $gl-padding-32 $gl-padding ($gl-padding + $gl-padding-4);
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
.container-fluid,
|
.container-fluid,
|
||||||
.container-fluid.container-limited {
|
.container-fluid.container-limited {
|
||||||
|
|
|
@ -310,6 +310,17 @@ $note-form-margin-left: 72px;
|
||||||
.note-body {
|
.note-body {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
.description-version {
|
||||||
|
pre {
|
||||||
|
max-height: $dropdown-max-height-lg;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
|
||||||
|
&.loading-state {
|
||||||
|
height: 94px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.system-note-commit-list-toggler {
|
.system-note-commit-list-toggler {
|
||||||
color: $blue-600;
|
color: $blue-600;
|
||||||
padding: 10px 0 0;
|
padding: 10px 0 0;
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class PrometheusMetricsFinder
|
||||||
|
ACCEPTED_PARAMS = [
|
||||||
|
:project,
|
||||||
|
:group,
|
||||||
|
:title,
|
||||||
|
:y_label,
|
||||||
|
:identifier,
|
||||||
|
:id,
|
||||||
|
:common,
|
||||||
|
:ordered
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
# Cautiously preferring a memoized class method over a constant
|
||||||
|
# so that the DB connection is accessed after the class is loaded.
|
||||||
|
def self.indexes
|
||||||
|
@indexes ||= PrometheusMetric
|
||||||
|
.connection
|
||||||
|
.indexes(:prometheus_metrics)
|
||||||
|
.map { |index| index.columns.map(&:to_sym) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(params = {})
|
||||||
|
@params = params.slice(*ACCEPTED_PARAMS)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [PrometheusMetric, PrometheusMetric::ActiveRecord_Relation]
|
||||||
|
def execute
|
||||||
|
validate_params!
|
||||||
|
|
||||||
|
metrics = by_project(::PrometheusMetric.all)
|
||||||
|
metrics = by_group(metrics)
|
||||||
|
metrics = by_title(metrics)
|
||||||
|
metrics = by_y_label(metrics)
|
||||||
|
metrics = by_common(metrics)
|
||||||
|
metrics = by_ordered(metrics)
|
||||||
|
metrics = by_identifier(metrics)
|
||||||
|
metrics = by_id(metrics)
|
||||||
|
|
||||||
|
metrics
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
attr_reader :params
|
||||||
|
|
||||||
|
def by_project(metrics)
|
||||||
|
return metrics unless params[:project]
|
||||||
|
|
||||||
|
metrics.for_project(params[:project])
|
||||||
|
end
|
||||||
|
|
||||||
|
def by_group(metrics)
|
||||||
|
return metrics unless params[:group]
|
||||||
|
|
||||||
|
metrics.for_group(params[:group])
|
||||||
|
end
|
||||||
|
|
||||||
|
def by_title(metrics)
|
||||||
|
return metrics unless params[:title]
|
||||||
|
|
||||||
|
metrics.for_title(params[:title])
|
||||||
|
end
|
||||||
|
|
||||||
|
def by_y_label(metrics)
|
||||||
|
return metrics unless params[:y_label]
|
||||||
|
|
||||||
|
metrics.for_y_label(params[:y_label])
|
||||||
|
end
|
||||||
|
|
||||||
|
def by_common(metrics)
|
||||||
|
return metrics unless params[:common]
|
||||||
|
|
||||||
|
metrics.common
|
||||||
|
end
|
||||||
|
|
||||||
|
def by_ordered(metrics)
|
||||||
|
return metrics unless params[:ordered]
|
||||||
|
|
||||||
|
metrics.ordered
|
||||||
|
end
|
||||||
|
|
||||||
|
def by_identifier(metrics)
|
||||||
|
return metrics unless params[:identifier]
|
||||||
|
|
||||||
|
metrics.for_identifier(params[:identifier])
|
||||||
|
end
|
||||||
|
|
||||||
|
def by_id(metrics)
|
||||||
|
return metrics unless params[:id]
|
||||||
|
|
||||||
|
metrics.id_in(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_params!
|
||||||
|
validate_params_present!
|
||||||
|
validate_id_params!
|
||||||
|
validate_indexes!
|
||||||
|
end
|
||||||
|
|
||||||
|
# Ensure all provided params are supported
|
||||||
|
def validate_params_present!
|
||||||
|
raise ArgumentError, "Please provide one or more of: #{ACCEPTED_PARAMS}" if params.blank?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Protect against the caller "finding" the wrong metric
|
||||||
|
def validate_id_params!
|
||||||
|
raise ArgumentError, 'Only one of :identifier, :id is permitted' if params[:identifier] && params[:id]
|
||||||
|
raise ArgumentError, ':identifier must be scoped to a :project or :common' if params[:identifier] && !(params[:project] || params[:common])
|
||||||
|
end
|
||||||
|
|
||||||
|
# Protect against unaccounted-for, complex/slow queries.
|
||||||
|
# This is not a hard and fast rule, but is meant to encourage
|
||||||
|
# mindful inclusion of new queries.
|
||||||
|
def validate_indexes!
|
||||||
|
indexable_params = params.except(:ordered, :id, :project).keys
|
||||||
|
indexable_params << :project_id if params[:project]
|
||||||
|
indexable_params.sort!
|
||||||
|
|
||||||
|
raise ArgumentError, "An index should exist for params: #{indexable_params}" unless appropriate_index?(indexable_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def appropriate_index?(indexable_params)
|
||||||
|
return true if indexable_params.blank?
|
||||||
|
|
||||||
|
self.class.indexes.any? { |index| (index - indexable_params).empty? }
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,6 +10,10 @@ class DescriptionVersion < ApplicationRecord
|
||||||
%i(issue merge_request).freeze
|
%i(issue merge_request).freeze
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def issuable
|
||||||
|
issue || merge_request
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def exactly_one_issuable
|
def exactly_one_issuable
|
||||||
|
|
|
@ -14,7 +14,13 @@ class PrometheusMetric < ApplicationRecord
|
||||||
validates :project, presence: true, unless: :common?
|
validates :project, presence: true, unless: :common?
|
||||||
validates :project, absence: true, if: :common?
|
validates :project, absence: true, if: :common?
|
||||||
|
|
||||||
|
scope :for_project, -> (project) { where(project: project) }
|
||||||
|
scope :for_group, -> (group) { where(group: group) }
|
||||||
|
scope :for_title, -> (title) { where(title: title) }
|
||||||
|
scope :for_y_label, -> (y_label) { where(y_label: y_label) }
|
||||||
|
scope :for_identifier, -> (identifier) { where(identifier: identifier) }
|
||||||
scope :common, -> { where(common: true) }
|
scope :common, -> { where(common: true) }
|
||||||
|
scope :ordered, -> { reorder(created_at: :asc) }
|
||||||
|
|
||||||
def priority
|
def priority
|
||||||
group_details(group).fetch(:priority)
|
group_details(group).fetch(:priority)
|
||||||
|
|
|
@ -79,3 +79,5 @@ class NoteEntity < API::Entities::Note
|
||||||
request.current_user
|
request.current_user
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
NoteEntity.prepend_if_ee('EE::NoteEntity')
|
||||||
|
|
|
@ -77,15 +77,14 @@ module Metrics
|
||||||
# There may be multiple metrics, but they should be
|
# There may be multiple metrics, but they should be
|
||||||
# displayed in a single panel/chart.
|
# displayed in a single panel/chart.
|
||||||
# @return [ActiveRecord::AssociationRelation<PromtheusMetric>]
|
# @return [ActiveRecord::AssociationRelation<PromtheusMetric>]
|
||||||
# rubocop: disable CodeReuse/ActiveRecord
|
|
||||||
def metrics
|
def metrics
|
||||||
project.prometheus_metrics.where(
|
PrometheusMetricsFinder.new(
|
||||||
|
project: project,
|
||||||
group: group_key,
|
group: group_key,
|
||||||
title: title,
|
title: title,
|
||||||
y_label: y_label
|
y_label: y_label
|
||||||
)
|
).execute
|
||||||
end
|
end
|
||||||
# rubocop: enable CodeReuse/ActiveRecord
|
|
||||||
|
|
||||||
# Returns a symbol representing the group that
|
# Returns a symbol representing the group that
|
||||||
# the dashboard's group title belongs to.
|
# the dashboard's group title belongs to.
|
||||||
|
|
|
@ -152,7 +152,7 @@
|
||||||
- email = " (#{@user.unconfirmed_email})"
|
- email = " (#{@user.unconfirmed_email})"
|
||||||
%p This user has an unconfirmed email address#{email}. You may force a confirmation.
|
%p This user has an unconfirmed email address#{email}. You may force a confirmation.
|
||||||
%br
|
%br
|
||||||
= link_to 'Confirm user', confirm_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' }
|
= link_to 'Confirm user', confirm_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?', qa_selector: 'confirm_user_button' }
|
||||||
|
|
||||||
= render_if_exists 'admin/users/user_detail_note'
|
= render_if_exists 'admin/users/user_detail_note'
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
- model = local_assigns.fetch(:model)
|
- model = local_assigns.fetch(:model)
|
||||||
|
|
||||||
- form = local_assigns.fetch(:form)
|
- form = local_assigns.fetch(:form)
|
||||||
|
- placeholder = model.is_a?(MergeRequest) ? _('Describe the goal of the changes and what reviewers should be aware of.') : _('Write a comment or drag your files here…')
|
||||||
- supports_quick_actions = model.new_record?
|
- supports_quick_actions = model.new_record?
|
||||||
|
|
||||||
- if supports_quick_actions
|
- if supports_quick_actions
|
||||||
|
@ -16,7 +17,7 @@
|
||||||
= render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do
|
= render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do
|
||||||
= render 'projects/zen', f: form, attr: :description,
|
= render 'projects/zen', f: form, attr: :description,
|
||||||
classes: 'note-textarea qa-issuable-form-description rspec-issuable-form-description',
|
classes: 'note-textarea qa-issuable-form-description rspec-issuable-form-description',
|
||||||
placeholder: "Write a comment or drag your files here…",
|
placeholder: placeholder,
|
||||||
supports_quick_actions: supports_quick_actions
|
supports_quick_actions: supports_quick_actions
|
||||||
= render 'shared/notes/hints', supports_quick_actions: supports_quick_actions
|
= render 'shared/notes/hints', supports_quick_actions: supports_quick_actions
|
||||||
.clearfix
|
.clearfix
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
- link_text = source.is_a?(Group) ? _('Leave group') : _('Leave project')
|
- link_text = source.is_a?(Group) ? _('Leave group') : _('Leave project')
|
||||||
= link_to link_text, polymorphic_path([:leave, source, :members]),
|
= link_to link_text, polymorphic_path([:leave, source, :members]),
|
||||||
method: :delete,
|
method: :delete,
|
||||||
data: { confirm: leave_confirmation_message(source) },
|
data: { confirm: leave_confirmation_message(source), qa_selector: 'leave_group_link' },
|
||||||
class: 'access-request-link js-leave-link'
|
class: 'access-request-link js-leave-link'
|
||||||
- elsif requester = source.requesters.find_by(user_id: current_user.id) # rubocop: disable CodeReuse/ActiveRecord
|
- elsif requester = source.requesters.find_by(user_id: current_user.id) # rubocop: disable CodeReuse/ActiveRecord
|
||||||
= link_to _('Withdraw Access Request'), polymorphic_path([:leave, source, :members]),
|
= link_to _('Withdraw Access Request'), polymorphic_path([:leave, source, :members]),
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Fix query validation in custom metrics form
|
||||||
|
merge_request: 18769
|
||||||
|
author:
|
||||||
|
type: fixed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Improve merge request description placeholder
|
||||||
|
merge_request: 20032
|
||||||
|
author: Jacopo Beschi @jacopo-beschi
|
||||||
|
type: changed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add event tracking to container registry
|
||||||
|
merge_request: 19772
|
||||||
|
author:
|
||||||
|
type: changed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Fix broken monitor cluster health dashboard
|
||||||
|
merge_request: 20120
|
||||||
|
author:
|
||||||
|
type: fixed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Move margin-top from flash container to flash
|
||||||
|
merge_request: 20211
|
||||||
|
author:
|
||||||
|
type: other
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Remove var from bootstrap_jquery_spec.js
|
||||||
|
merge_request: 20089
|
||||||
|
author: Lee Tickett
|
||||||
|
type: other
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Remove var from issue.js
|
||||||
|
merge_request: 20098
|
||||||
|
author: Lee Tickett
|
||||||
|
type: other
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Remove var from labels_select.js
|
||||||
|
merge_request: 20153
|
||||||
|
author: Lee Tickett
|
||||||
|
type: other
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Remove var from tree.js
|
||||||
|
merge_request: 20103
|
||||||
|
author: Lee Tickett
|
||||||
|
type: other
|
|
@ -0,0 +1,77 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Gitlab::Seeder::Users
|
||||||
|
include ActionView::Helpers::NumberHelper
|
||||||
|
|
||||||
|
RANDOM_USERS_COUNT = 20
|
||||||
|
MASS_USERS_COUNT = ENV['CI'] ? 10 : 1_000_000
|
||||||
|
MASS_INSERT_USERNAME_START = 'mass_insert_user_'
|
||||||
|
|
||||||
|
attr_reader :opts
|
||||||
|
|
||||||
|
def initialize(opts = {})
|
||||||
|
@opts = opts
|
||||||
|
end
|
||||||
|
|
||||||
|
def seed!
|
||||||
|
Sidekiq::Testing.inline! do
|
||||||
|
create_mass_users!
|
||||||
|
create_random_users!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def create_mass_users!
|
||||||
|
encrypted_password = Devise::Encryptor.digest(User, '12345678')
|
||||||
|
|
||||||
|
Gitlab::Seeder.with_mass_insert(MASS_USERS_COUNT, User) do
|
||||||
|
ActiveRecord::Base.connection.execute <<~SQL
|
||||||
|
INSERT INTO users (username, name, email, confirmed_at, projects_limit, encrypted_password)
|
||||||
|
SELECT
|
||||||
|
'#{MASS_INSERT_USERNAME_START}' || seq,
|
||||||
|
'Seed user ' || seq,
|
||||||
|
'seed_user' || seq || '@example.com',
|
||||||
|
to_timestamp(seq),
|
||||||
|
#{MASS_USERS_COUNT},
|
||||||
|
'#{encrypted_password}'
|
||||||
|
FROM generate_series(1, #{MASS_USERS_COUNT}) AS seq
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
|
||||||
|
relation = User.where(admin: false)
|
||||||
|
Gitlab::Seeder.with_mass_insert(relation.count, Namespace) do
|
||||||
|
ActiveRecord::Base.connection.execute <<~SQL
|
||||||
|
INSERT INTO namespaces (name, path, owner_id)
|
||||||
|
SELECT
|
||||||
|
username,
|
||||||
|
username,
|
||||||
|
id
|
||||||
|
FROM users WHERE NOT admin
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_random_users!
|
||||||
|
RANDOM_USERS_COUNT.times do |i|
|
||||||
|
begin
|
||||||
|
User.create!(
|
||||||
|
username: FFaker::Internet.user_name,
|
||||||
|
name: FFaker::Name.name,
|
||||||
|
email: FFaker::Internet.email,
|
||||||
|
confirmed_at: DateTime.now,
|
||||||
|
password: '12345678'
|
||||||
|
)
|
||||||
|
|
||||||
|
print '.'
|
||||||
|
rescue ActiveRecord::RecordInvalid
|
||||||
|
print 'F'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Gitlab::Seeder.quiet do
|
||||||
|
users = Gitlab::Seeder::Users.new
|
||||||
|
users.seed!
|
||||||
|
end
|
|
@ -1,137 +1,210 @@
|
||||||
require './spec/support/sidekiq'
|
require './spec/support/sidekiq'
|
||||||
|
|
||||||
# rubocop:disable Rails/Output
|
class Gitlab::Seeder::Projects
|
||||||
|
include ActionView::Helpers::NumberHelper
|
||||||
|
|
||||||
Sidekiq::Testing.inline! do
|
PROJECT_URLS = %w[
|
||||||
Gitlab::Seeder.quiet do
|
https://gitlab.com/gitlab-org/gitlab-test.git
|
||||||
Gitlab::Seeder.without_gitaly_timeout do
|
https://gitlab.com/gitlab-org/gitlab-shell.git
|
||||||
project_urls = %w[
|
https://gitlab.com/gnuwget/wget2.git
|
||||||
https://gitlab.com/gitlab-org/gitlab-test.git
|
https://gitlab.com/Commit451/LabCoat.git
|
||||||
https://gitlab.com/gitlab-org/gitlab-shell.git
|
https://github.com/jashkenas/underscore.git
|
||||||
https://gitlab.com/gnuwget/wget2.git
|
https://github.com/flightjs/flight.git
|
||||||
https://gitlab.com/Commit451/LabCoat.git
|
https://github.com/twitter/typeahead.js.git
|
||||||
https://github.com/jashkenas/underscore.git
|
https://github.com/h5bp/html5-boilerplate.git
|
||||||
https://github.com/flightjs/flight.git
|
https://github.com/google/material-design-lite.git
|
||||||
https://github.com/twitter/typeahead.js.git
|
https://github.com/jlevy/the-art-of-command-line.git
|
||||||
https://github.com/h5bp/html5-boilerplate.git
|
https://github.com/FreeCodeCamp/freecodecamp.git
|
||||||
https://github.com/google/material-design-lite.git
|
https://github.com/google/deepdream.git
|
||||||
https://github.com/jlevy/the-art-of-command-line.git
|
https://github.com/jtleek/datasharing.git
|
||||||
https://github.com/FreeCodeCamp/freecodecamp.git
|
https://github.com/WebAssembly/design.git
|
||||||
https://github.com/google/deepdream.git
|
https://github.com/airbnb/javascript.git
|
||||||
https://github.com/jtleek/datasharing.git
|
https://github.com/tessalt/echo-chamber-js.git
|
||||||
https://github.com/WebAssembly/design.git
|
https://github.com/atom/atom.git
|
||||||
https://github.com/airbnb/javascript.git
|
https://github.com/mattermost/mattermost-server.git
|
||||||
https://github.com/tessalt/echo-chamber-js.git
|
https://github.com/purifycss/purifycss.git
|
||||||
https://github.com/atom/atom.git
|
https://github.com/facebook/nuclide.git
|
||||||
https://github.com/mattermost/mattermost-server.git
|
https://github.com/wbkd/awesome-d3.git
|
||||||
https://github.com/purifycss/purifycss.git
|
https://github.com/kilimchoi/engineering-blogs.git
|
||||||
https://github.com/facebook/nuclide.git
|
https://github.com/gilbarbara/logos.git
|
||||||
https://github.com/wbkd/awesome-d3.git
|
https://github.com/reduxjs/redux.git
|
||||||
https://github.com/kilimchoi/engineering-blogs.git
|
https://github.com/awslabs/s2n.git
|
||||||
https://github.com/gilbarbara/logos.git
|
https://github.com/arkency/reactjs_koans.git
|
||||||
https://github.com/reduxjs/redux.git
|
https://github.com/twbs/bootstrap.git
|
||||||
https://github.com/awslabs/s2n.git
|
https://github.com/chjj/ttystudio.git
|
||||||
https://github.com/arkency/reactjs_koans.git
|
https://github.com/MostlyAdequate/mostly-adequate-guide.git
|
||||||
https://github.com/twbs/bootstrap.git
|
https://github.com/octocat/Spoon-Knife.git
|
||||||
https://github.com/chjj/ttystudio.git
|
https://github.com/opencontainers/runc.git
|
||||||
https://github.com/MostlyAdequate/mostly-adequate-guide.git
|
https://github.com/googlesamples/android-topeka.git
|
||||||
https://github.com/octocat/Spoon-Knife.git
|
]
|
||||||
https://github.com/opencontainers/runc.git
|
LARGE_PROJECT_URLS = %w[
|
||||||
https://github.com/googlesamples/android-topeka.git
|
https://github.com/torvalds/linux.git
|
||||||
]
|
https://gitlab.gnome.org/GNOME/gimp.git
|
||||||
|
https://gitlab.gnome.org/GNOME/gnome-mud.git
|
||||||
|
https://gitlab.com/fdroid/fdroidclient.git
|
||||||
|
https://gitlab.com/inkscape/inkscape.git
|
||||||
|
https://github.com/gnachman/iTerm2.git
|
||||||
|
]
|
||||||
|
# Consider altering MASS_USERS_COUNT for less
|
||||||
|
# users with projects.
|
||||||
|
MASS_PROJECTS_COUNT_PER_USER = {
|
||||||
|
private: 3, # 3m projects +
|
||||||
|
internal: 1, # 1m projects +
|
||||||
|
public: 1 # 1m projects = 5m total
|
||||||
|
}
|
||||||
|
MASS_INSERT_NAME_START = 'mass_insert_project_'
|
||||||
|
|
||||||
large_project_urls = %w[
|
def seed!
|
||||||
https://github.com/torvalds/linux.git
|
Sidekiq::Testing.inline! do
|
||||||
https://gitlab.gnome.org/GNOME/gimp.git
|
create_real_projects!
|
||||||
https://gitlab.gnome.org/GNOME/gnome-mud.git
|
create_large_projects!
|
||||||
https://gitlab.com/fdroid/fdroidclient.git
|
create_mass_projects!
|
||||||
https://gitlab.com/inkscape/inkscape.git
|
end
|
||||||
https://github.com/gnachman/iTerm2.git
|
end
|
||||||
]
|
|
||||||
|
|
||||||
def create_project(url, force_latest_storage: false)
|
private
|
||||||
group_path, project_path = url.split('/')[-2..-1]
|
|
||||||
|
|
||||||
group = Group.find_by(path: group_path)
|
def create_real_projects!
|
||||||
|
# You can specify how many projects you need during seed execution
|
||||||
|
size = ENV['SIZE'].present? ? ENV['SIZE'].to_i : 8
|
||||||
|
|
||||||
unless group
|
PROJECT_URLS.first(size).each_with_index do |url, i|
|
||||||
group = Group.new(
|
create_real_project!(url, force_latest_storage: i.even?)
|
||||||
name: group_path.titleize,
|
end
|
||||||
path: group_path
|
end
|
||||||
)
|
|
||||||
group.description = FFaker::Lorem.sentence
|
|
||||||
group.save!
|
|
||||||
|
|
||||||
group.add_owner(User.first)
|
def create_large_projects!
|
||||||
end
|
return unless ENV['LARGE_PROJECTS'].present?
|
||||||
|
|
||||||
project_path.gsub!(".git", "")
|
LARGE_PROJECT_URLS.each(&method(:create_real_project!))
|
||||||
|
|
||||||
params = {
|
if ENV['FORK'].present?
|
||||||
import_url: url,
|
puts "\nGenerating forks"
|
||||||
namespace_id: group.id,
|
|
||||||
name: project_path.titleize,
|
|
||||||
description: FFaker::Lorem.sentence,
|
|
||||||
visibility_level: Gitlab::VisibilityLevel.values.sample,
|
|
||||||
skip_disk_validation: true
|
|
||||||
}
|
|
||||||
|
|
||||||
if force_latest_storage
|
project_name = ENV['FORK'] == 'true' ? 'torvalds/linux' : ENV['FORK']
|
||||||
params[:storage_version] = Project::LATEST_STORAGE_VERSION
|
|
||||||
end
|
|
||||||
|
|
||||||
project = nil
|
project = Project.find_by_full_path(project_name)
|
||||||
|
|
||||||
Sidekiq::Worker.skipping_transaction_check do
|
User.offset(1).first(5).each do |user|
|
||||||
project = Projects::CreateService.new(User.first, params).execute
|
new_project = ::Projects::ForkService.new(project, user).execute
|
||||||
|
|
||||||
# Seed-Fu runs this entire fixture in a transaction, so the `after_commit`
|
if new_project.valid? && (new_project.valid_repo? || new_project.import_state.scheduled?)
|
||||||
# hook won't run until after the fixture is loaded. That is too late
|
|
||||||
# since the Sidekiq::Testing block has already exited. Force clearing
|
|
||||||
# the `after_commit` queue to ensure the job is run now.
|
|
||||||
project.send(:_run_after_commit_queue)
|
|
||||||
project.import_state.send(:_run_after_commit_queue)
|
|
||||||
end
|
|
||||||
|
|
||||||
if project.valid? && project.valid_repo?
|
|
||||||
print '.'
|
print '.'
|
||||||
else
|
else
|
||||||
puts project.errors.full_messages
|
new_project.errors.full_messages.each do |error|
|
||||||
print 'F'
|
puts "#{new_project.full_path}: #{error}"
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# You can specify how many projects you need during seed execution
|
|
||||||
size = ENV['SIZE'].present? ? ENV['SIZE'].to_i : 8
|
|
||||||
|
|
||||||
project_urls.first(size).each_with_index do |url, i|
|
|
||||||
create_project(url, force_latest_storage: i.even?)
|
|
||||||
end
|
|
||||||
|
|
||||||
if ENV['LARGE_PROJECTS'].present?
|
|
||||||
large_project_urls.each(&method(:create_project))
|
|
||||||
|
|
||||||
if ENV['FORK'].present?
|
|
||||||
puts "\nGenerating forks"
|
|
||||||
|
|
||||||
project_name = ENV['FORK'] == 'true' ? 'torvalds/linux' : ENV['FORK']
|
|
||||||
|
|
||||||
project = Project.find_by_full_path(project_name)
|
|
||||||
|
|
||||||
User.offset(1).first(5).each do |user|
|
|
||||||
new_project = Projects::ForkService.new(project, user).execute
|
|
||||||
|
|
||||||
if new_project.valid? && (new_project.valid_repo? || new_project.import_state.scheduled?)
|
|
||||||
print '.'
|
|
||||||
else
|
|
||||||
new_project.errors.full_messages.each do |error|
|
|
||||||
puts "#{new_project.full_path}: #{error}"
|
|
||||||
end
|
|
||||||
print 'F'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
print 'F'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def create_real_project!(url, force_latest_storage: false)
|
||||||
|
group_path, project_path = url.split('/')[-2..-1]
|
||||||
|
|
||||||
|
group = Group.find_by(path: group_path)
|
||||||
|
|
||||||
|
unless group
|
||||||
|
group = Group.new(
|
||||||
|
name: group_path.titleize,
|
||||||
|
path: group_path
|
||||||
|
)
|
||||||
|
group.description = FFaker::Lorem.sentence
|
||||||
|
group.save!
|
||||||
|
|
||||||
|
group.add_owner(User.first)
|
||||||
|
end
|
||||||
|
|
||||||
|
project_path.gsub!(".git", "")
|
||||||
|
|
||||||
|
params = {
|
||||||
|
import_url: url,
|
||||||
|
namespace_id: group.id,
|
||||||
|
name: project_path.titleize,
|
||||||
|
description: FFaker::Lorem.sentence,
|
||||||
|
visibility_level: Gitlab::VisibilityLevel.values.sample,
|
||||||
|
skip_disk_validation: true
|
||||||
|
}
|
||||||
|
|
||||||
|
if force_latest_storage
|
||||||
|
params[:storage_version] = Project::LATEST_STORAGE_VERSION
|
||||||
|
end
|
||||||
|
|
||||||
|
project = nil
|
||||||
|
|
||||||
|
Sidekiq::Worker.skipping_transaction_check do
|
||||||
|
project = ::Projects::CreateService.new(User.first, params).execute
|
||||||
|
|
||||||
|
# Seed-Fu runs this entire fixture in a transaction, so the `after_commit`
|
||||||
|
# hook won't run until after the fixture is loaded. That is too late
|
||||||
|
# since the Sidekiq::Testing block has already exited. Force clearing
|
||||||
|
# the `after_commit` queue to ensure the job is run now.
|
||||||
|
project.send(:_run_after_commit_queue)
|
||||||
|
project.import_state.send(:_run_after_commit_queue)
|
||||||
|
end
|
||||||
|
|
||||||
|
if project.valid? && project.valid_repo?
|
||||||
|
print '.'
|
||||||
|
else
|
||||||
|
puts project.errors.full_messages
|
||||||
|
print 'F'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_mass_projects!
|
||||||
|
projects_per_user_count = MASS_PROJECTS_COUNT_PER_USER.values.sum
|
||||||
|
visibility_per_user = ['private'] * MASS_PROJECTS_COUNT_PER_USER.fetch(:private) +
|
||||||
|
['internal'] * MASS_PROJECTS_COUNT_PER_USER.fetch(:internal) +
|
||||||
|
['public'] * MASS_PROJECTS_COUNT_PER_USER.fetch(:public)
|
||||||
|
visibility_level_per_user = visibility_per_user.map { |visibility| Gitlab::VisibilityLevel.level_value(visibility) }
|
||||||
|
|
||||||
|
visibility_per_user = visibility_per_user.join(',')
|
||||||
|
visibility_level_per_user = visibility_level_per_user.join(',')
|
||||||
|
|
||||||
|
Gitlab::Seeder.with_mass_insert(User.count * projects_per_user_count, "Projects and relations") do
|
||||||
|
ActiveRecord::Base.connection.execute <<~SQL
|
||||||
|
INSERT INTO projects (name, path, creator_id, namespace_id, visibility_level, created_at, updated_at)
|
||||||
|
SELECT
|
||||||
|
'Seed project ' || seq || ' ' || ('{#{visibility_per_user}}'::text[])[seq] AS project_name,
|
||||||
|
'mass_insert_project_' || ('{#{visibility_per_user}}'::text[])[seq] || '_' || seq AS project_path,
|
||||||
|
u.id AS user_id,
|
||||||
|
n.id AS namespace_id,
|
||||||
|
('{#{visibility_level_per_user}}'::int[])[seq] AS visibility_level,
|
||||||
|
NOW() AS created_at,
|
||||||
|
NOW() AS updated_at
|
||||||
|
FROM users u
|
||||||
|
CROSS JOIN generate_series(1, #{projects_per_user_count}) AS seq
|
||||||
|
JOIN namespaces n ON n.owner_id=u.id
|
||||||
|
SQL
|
||||||
|
|
||||||
|
ActiveRecord::Base.connection.execute <<~SQL
|
||||||
|
INSERT INTO project_features (project_id, merge_requests_access_level, issues_access_level, wiki_access_level,
|
||||||
|
pages_access_level)
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
#{ProjectFeature::ENABLED} AS merge_requests_access_level,
|
||||||
|
#{ProjectFeature::ENABLED} AS issues_access_level,
|
||||||
|
#{ProjectFeature::ENABLED} AS wiki_access_level,
|
||||||
|
#{ProjectFeature::ENABLED} AS pages_access_level
|
||||||
|
FROM projects ON CONFLICT (project_id) DO NOTHING;
|
||||||
|
SQL
|
||||||
|
|
||||||
|
ActiveRecord::Base.connection.execute <<~SQL
|
||||||
|
INSERT INTO routes (source_id, source_type, name, path)
|
||||||
|
SELECT
|
||||||
|
p.id,
|
||||||
|
'Project',
|
||||||
|
u.name || ' / ' || p.name,
|
||||||
|
u.username || '/' || p.path
|
||||||
|
FROM projects p JOIN users u ON u.id=p.creator_id
|
||||||
|
ON CONFLICT (source_type, source_id) DO NOTHING;
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Gitlab::Seeder.quiet do
|
||||||
|
projects = Gitlab::Seeder::Projects.new
|
||||||
|
projects.seed!
|
||||||
end
|
end
|
||||||
|
|
|
@ -43,7 +43,7 @@ Gitlab::Seeder.quiet do
|
||||||
end
|
end
|
||||||
|
|
||||||
puts "\nGenerating project labels"
|
puts "\nGenerating project labels"
|
||||||
Project.all.find_each do |project|
|
Project.not_mass_generated.find_each do |project|
|
||||||
Gitlab::Seeder::ProjectLabels.new(project).seed!
|
Gitlab::Seeder::ProjectLabels.new(project).seed!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
require './spec/support/sidekiq'
|
|
||||||
|
|
||||||
Gitlab::Seeder.quiet do
|
|
||||||
20.times do |i|
|
|
||||||
begin
|
|
||||||
User.create!(
|
|
||||||
username: FFaker::Internet.user_name,
|
|
||||||
name: FFaker::Name.name,
|
|
||||||
email: FFaker::Internet.email,
|
|
||||||
confirmed_at: DateTime.now,
|
|
||||||
password: '12345678'
|
|
||||||
)
|
|
||||||
|
|
||||||
print '.'
|
|
||||||
rescue ActiveRecord::RecordInvalid
|
|
||||||
print 'F'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
5.times do |i|
|
|
||||||
begin
|
|
||||||
User.create!(
|
|
||||||
username: "user#{i}",
|
|
||||||
name: "User #{i}",
|
|
||||||
email: "user#{i}@example.com",
|
|
||||||
confirmed_at: DateTime.now,
|
|
||||||
password: '12345678'
|
|
||||||
)
|
|
||||||
print '.'
|
|
||||||
rescue ActiveRecord::RecordInvalid
|
|
||||||
print 'F'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -3,7 +3,7 @@ require './spec/support/sidekiq'
|
||||||
Sidekiq::Testing.inline! do
|
Sidekiq::Testing.inline! do
|
||||||
Gitlab::Seeder.quiet do
|
Gitlab::Seeder.quiet do
|
||||||
Group.all.each do |group|
|
Group.all.each do |group|
|
||||||
User.all.sample(4).each do |user|
|
User.not_mass_generated.sample(4).each do |user|
|
||||||
if group.add_user(user, Gitlab::Access.values.sample).persisted?
|
if group.add_user(user, Gitlab::Access.values.sample).persisted?
|
||||||
print '.'
|
print '.'
|
||||||
else
|
else
|
||||||
|
@ -12,8 +12,8 @@ Sidekiq::Testing.inline! do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Project.all.each do |project|
|
Project.not_mass_generated.each do |project|
|
||||||
User.all.sample(4).each do |user|
|
User.not_mass_generated.sample(4).each do |user|
|
||||||
if project.add_role(user, Gitlab::Access.sym_options.keys.sample)
|
if project.add_role(user, Gitlab::Access.sym_options.keys.sample)
|
||||||
print '.'
|
print '.'
|
||||||
else
|
else
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
require './spec/support/sidekiq'
|
require './spec/support/sidekiq'
|
||||||
|
|
||||||
Gitlab::Seeder.quiet do
|
Gitlab::Seeder.quiet do
|
||||||
Project.all.each do |project|
|
Project.not_mass_generated.each do |project|
|
||||||
5.times do |i|
|
5.times do |i|
|
||||||
milestone_params = {
|
milestone_params = {
|
||||||
title: "v#{i}.0",
|
title: "v#{i}.0",
|
||||||
|
|
|
@ -4,7 +4,13 @@ Gitlab::Seeder.quiet do
|
||||||
# Limit the number of merge requests per project to avoid long seeds
|
# Limit the number of merge requests per project to avoid long seeds
|
||||||
MAX_NUM_MERGE_REQUESTS = 10
|
MAX_NUM_MERGE_REQUESTS = 10
|
||||||
|
|
||||||
Project.non_archived.with_merge_requests_enabled.reject(&:empty_repo?).each do |project|
|
projects = Project
|
||||||
|
.non_archived
|
||||||
|
.with_merge_requests_enabled
|
||||||
|
.not_mass_generated
|
||||||
|
.reject(&:empty_repo?)
|
||||||
|
|
||||||
|
projects.each do |project|
|
||||||
branches = project.repository.branch_names.sample(MAX_NUM_MERGE_REQUESTS * 2)
|
branches = project.repository.branch_names.sample(MAX_NUM_MERGE_REQUESTS * 2)
|
||||||
|
|
||||||
branches.each do |branch_name|
|
branches.each do |branch_name|
|
||||||
|
|
|
@ -9,7 +9,7 @@ Sidekiq::Testing.disable! do
|
||||||
# that it falls under `Sidekiq::Testing.disable!`.
|
# that it falls under `Sidekiq::Testing.disable!`.
|
||||||
Key.skip_callback(:commit, :after, :add_to_shell)
|
Key.skip_callback(:commit, :after, :add_to_shell)
|
||||||
|
|
||||||
User.first(10).each do |user|
|
User.not_mass_generated.first(10).each do |user|
|
||||||
key = "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt#{user.id + 100}6k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0="
|
key = "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt#{user.id + 100}6k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0="
|
||||||
|
|
||||||
key = user.keys.create(
|
key = user.keys.create(
|
||||||
|
|
|
@ -25,7 +25,7 @@ end
|
||||||
eos
|
eos
|
||||||
|
|
||||||
50.times do |i|
|
50.times do |i|
|
||||||
user = User.all.sample
|
user = User.not_mass_generated.sample
|
||||||
|
|
||||||
PersonalSnippet.seed(:id, [{
|
PersonalSnippet.seed(:id, [{
|
||||||
id: i,
|
id: i,
|
||||||
|
|
|
@ -214,7 +214,7 @@ class Gitlab::Seeder::Pipelines
|
||||||
end
|
end
|
||||||
|
|
||||||
Gitlab::Seeder.quiet do
|
Gitlab::Seeder.quiet do
|
||||||
Project.all.sample(5).each do |project|
|
Project.not_mass_generated.sample(5).each do |project|
|
||||||
project_builds = Gitlab::Seeder::Pipelines.new(project)
|
project_builds = Gitlab::Seeder::Pipelines.new(project)
|
||||||
project_builds.seed!
|
project_builds.seed!
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,7 @@ require './spec/support/sidekiq'
|
||||||
Gitlab::Seeder.quiet do
|
Gitlab::Seeder.quiet do
|
||||||
admin_user = User.find(1)
|
admin_user = User.find(1)
|
||||||
|
|
||||||
Project.all.each do |project|
|
Project.not_mass_generated.each do |project|
|
||||||
params = {
|
params = {
|
||||||
name: 'master'
|
name: 'master'
|
||||||
}
|
}
|
||||||
|
|
|
@ -217,7 +217,7 @@ Gitlab::Seeder.quiet do
|
||||||
flag = 'SEED_CYCLE_ANALYTICS'
|
flag = 'SEED_CYCLE_ANALYTICS'
|
||||||
|
|
||||||
if ENV[flag]
|
if ENV[flag]
|
||||||
Project.find_each do |project|
|
Project.not_mass_generated.find_each do |project|
|
||||||
# This seed naively assumes that every project has a repository, and every
|
# This seed naively assumes that every project has a repository, and every
|
||||||
# repository has a `master` branch, which may be the case for a pristine
|
# repository has a `master` branch, which may be the case for a pristine
|
||||||
# GDK seed, but is almost never true for a GDK that's actually had
|
# GDK seed, but is almost never true for a GDK that's actually had
|
||||||
|
|
|
@ -67,7 +67,7 @@ class Gitlab::Seeder::Environments
|
||||||
end
|
end
|
||||||
|
|
||||||
Gitlab::Seeder.quiet do
|
Gitlab::Seeder.quiet do
|
||||||
Project.all.sample(5).each do |project|
|
Project.not_mass_generated.sample(5).each do |project|
|
||||||
project_environments = Gitlab::Seeder::Environments.new(project)
|
project_environments = Gitlab::Seeder::Environments.new(project)
|
||||||
project_environments.seed!
|
project_environments.seed!
|
||||||
end
|
end
|
||||||
|
|
|
@ -22,7 +22,7 @@ module Db
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.random_user
|
def self.random_user
|
||||||
User.find(User.pluck(:id).sample)
|
User.find(User.not_mass_generated.pluck(:id).sample)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,8 +2,8 @@ require './spec/support/sidekiq'
|
||||||
|
|
||||||
Sidekiq::Testing.inline! do
|
Sidekiq::Testing.inline! do
|
||||||
Gitlab::Seeder.quiet do
|
Gitlab::Seeder.quiet do
|
||||||
User.all.sample(10).each do |user|
|
User.not_mass_generated.sample(10).each do |user|
|
||||||
source_project = Project.public_only.sample
|
source_project = Project.not_mass_generated.public_only.sample
|
||||||
|
|
||||||
##
|
##
|
||||||
# 03_project.rb might not have created a public project because
|
# 03_project.rb might not have created a public project because
|
||||||
|
|
|
@ -18,7 +18,9 @@ You can read more about the Docker Registry at
|
||||||
|
|
||||||
**Omnibus GitLab installations**
|
**Omnibus GitLab installations**
|
||||||
|
|
||||||
All you have to do is configure the domain name under which the Container
|
If you are using the Omnibus GitLab built in [Let's Encrypt integration](https://docs.gitlab.com/omnibus/settings/ssl.html#lets-encrypt-integration), as of GitLab 12.5, the Container Registry will be automatically enabled on port 5050 of the default domain.
|
||||||
|
|
||||||
|
If you would like to use a separate domain, all you have to do is configure the domain name under which the Container
|
||||||
Registry will listen to. Read
|
Registry will listen to. Read
|
||||||
[#container-registry-domain-configuration](#container-registry-domain-configuration)
|
[#container-registry-domain-configuration](#container-registry-domain-configuration)
|
||||||
and pick one of the two options that fits your case.
|
and pick one of the two options that fits your case.
|
||||||
|
|
|
@ -1219,6 +1219,10 @@ type Epic implements Noteable {
|
||||||
hasIssues: Boolean!
|
hasIssues: Boolean!
|
||||||
id: ID!
|
id: ID!
|
||||||
iid: ID!
|
iid: ID!
|
||||||
|
|
||||||
|
"""
|
||||||
|
A list of issues associated with the epic
|
||||||
|
"""
|
||||||
issues(
|
issues(
|
||||||
"""
|
"""
|
||||||
Returns the elements in the list that come after the specified cursor.
|
Returns the elements in the list that come after the specified cursor.
|
||||||
|
|
|
@ -3751,7 +3751,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "issues",
|
"name": "issues",
|
||||||
"description": null,
|
"description": "A list of issues associated with the epic",
|
||||||
"args": [
|
"args": [
|
||||||
{
|
{
|
||||||
"name": "after",
|
"name": "after",
|
||||||
|
|
|
@ -19,7 +19,7 @@ If you just want to delete everything and start over with an empty DB (~1 minute
|
||||||
|
|
||||||
- `bundle exec rake db:reset RAILS_ENV=development`
|
- `bundle exec rake db:reset RAILS_ENV=development`
|
||||||
|
|
||||||
If you just want to delete everything and start over with dummy data (~40 minutes). This also does `db:reset` and runs DB-specific migrations:
|
If you just want to delete everything and start over with dummy data (~4 minutes). This also does `db:reset` and runs DB-specific migrations:
|
||||||
|
|
||||||
- `bundle exec rake dev:setup RAILS_ENV=development`
|
- `bundle exec rake dev:setup RAILS_ENV=development`
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,14 @@ The `setup` task is an alias for `gitlab:setup`.
|
||||||
This tasks calls `db:reset` to create the database, and calls `db:seed_fu` to seed the database.
|
This tasks calls `db:reset` to create the database, and calls `db:seed_fu` to seed the database.
|
||||||
Note: `db:setup` calls `db:seed` but this does nothing.
|
Note: `db:setup` calls `db:seed` but this does nothing.
|
||||||
|
|
||||||
|
### Env variables
|
||||||
|
|
||||||
|
**MASS_INSERT**: Create millions of users (2m), projects (5m) and its
|
||||||
|
relations. It's highly recommended to run the seed with it to catch slow queries
|
||||||
|
while developing. Expect the process to take up to 20 extra minutes.
|
||||||
|
|
||||||
|
**LARGE_PROJECTS**: Create large projects (through import) from a predefined set of urls.
|
||||||
|
|
||||||
### Seeding issues for all or a given project
|
### Seeding issues for all or a given project
|
||||||
|
|
||||||
You can seed issues for all or a given project with the `gitlab:seed:issues`
|
You can seed issues for all or a given project with the `gitlab:seed:issues`
|
||||||
|
|
|
@ -37,7 +37,7 @@ The results are sorted by the severity of the vulnerability:
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
To run a Dependency Scanning job, you need GitLab Runner with the
|
To run a Dependency Scanning job, by default, you need GitLab Runner with the
|
||||||
[`docker`](https://docs.gitlab.com/runner/executors/docker.html#use-docker-in-docker-with-privileged-mode) or
|
[`docker`](https://docs.gitlab.com/runner/executors/docker.html#use-docker-in-docker-with-privileged-mode) or
|
||||||
[`kubernetes`](https://docs.gitlab.com/runner/install/kubernetes.html#running-privileged-containers-for-the-runners)
|
[`kubernetes`](https://docs.gitlab.com/runner/install/kubernetes.html#running-privileged-containers-for-the-runners)
|
||||||
executor running in privileged mode. If you're using the shared Runners on GitLab.com,
|
executor running in privileged mode. If you're using the shared Runners on GitLab.com,
|
||||||
|
@ -47,6 +47,8 @@ CAUTION: **Caution:**
|
||||||
If you use your own Runners, make sure that the Docker version you have installed
|
If you use your own Runners, make sure that the Docker version you have installed
|
||||||
is **not** `19.03.00`. See [troubleshooting information](#error-response-from-daemon-error-processing-tar-file-docker-tar-relocation-error) for details.
|
is **not** `19.03.00`. See [troubleshooting information](#error-response-from-daemon-error-processing-tar-file-docker-tar-relocation-error) for details.
|
||||||
|
|
||||||
|
Privileged mode is not necessary if you've [disabled Docker in Docker for Dependency Scanning](#disabling-docker-in-docker-for-dependency-scanning)
|
||||||
|
|
||||||
## Supported languages and package managers
|
## Supported languages and package managers
|
||||||
|
|
||||||
The following languages and dependency managers are supported.
|
The following languages and dependency managers are supported.
|
||||||
|
@ -133,6 +135,7 @@ using environment variables.
|
||||||
| `DS_PYTHON_VERSION` | Version of Python. If set to 2, dependencies are installed using Python 2.7 instead of Python 3.6. ([Introduced](https://gitlab.com/gitlab-org/gitlab/issues/12296) in GitLab 12.1)| |
|
| `DS_PYTHON_VERSION` | Version of Python. If set to 2, dependencies are installed using Python 2.7 instead of Python 3.6. ([Introduced](https://gitlab.com/gitlab-org/gitlab/issues/12296) in GitLab 12.1)| |
|
||||||
| `DS_PIP_DEPENDENCY_PATH` | Path to load Python pip dependencies from. ([Introduced](https://gitlab.com/gitlab-org/gitlab/issues/12412) in GitLab 12.2) | |
|
| `DS_PIP_DEPENDENCY_PATH` | Path to load Python pip dependencies from. ([Introduced](https://gitlab.com/gitlab-org/gitlab/issues/12412) in GitLab 12.2) | |
|
||||||
| `DS_DEFAULT_ANALYZERS` | Override the names of the official default images. Read more about [customizing analyzers](analyzers.md). | |
|
| `DS_DEFAULT_ANALYZERS` | Override the names of the official default images. Read more about [customizing analyzers](analyzers.md). | |
|
||||||
|
| `DS_DISABLE_DIND` | Disable Docker in Docker and run analyzers [individually](#disabling-docker-in-docker-for-dependency-scanning).| |
|
||||||
| `DS_PULL_ANALYZER_IMAGES` | Pull the images from the Docker registry (set to `0` to disable). | |
|
| `DS_PULL_ANALYZER_IMAGES` | Pull the images from the Docker registry (set to `0` to disable). | |
|
||||||
| `DS_EXCLUDED_PATHS` | Exclude vulnerabilities from output based on the paths. A comma-separated list of patterns. Patterns can be globs, file or folder paths. Parent directories will also match patterns. | `DS_EXCLUDED_PATHS=doc,spec` |
|
| `DS_EXCLUDED_PATHS` | Exclude vulnerabilities from output based on the paths. A comma-separated list of patterns. Patterns can be globs, file or folder paths. Parent directories will also match patterns. | `DS_EXCLUDED_PATHS=doc,spec` |
|
||||||
| `DS_DOCKER_CLIENT_NEGOTIATION_TIMEOUT` | Time limit for Docker client negotiation. Timeouts are parsed using Go's [`ParseDuration`](https://golang.org/pkg/time/#ParseDuration). Valid time units are `ns`, `us` (or `µs`), `ms`, `s`, `m`, `h`. For example, `300ms`, `1.5h`, or `2h45m`. | |
|
| `DS_DOCKER_CLIENT_NEGOTIATION_TIMEOUT` | Time limit for Docker client negotiation. Timeouts are parsed using Go's [`ParseDuration`](https://golang.org/pkg/time/#ParseDuration). Valid time units are `ns`, `us` (or `µs`), `ms`, `s`, `m`, `h`. For example, `300ms`, `1.5h`, or `2h45m`. | |
|
||||||
|
@ -168,6 +171,23 @@ so that you don't have to expose your private data in `.gitlab-ci.yml` (e.g., ad
|
||||||
</settings>
|
</settings>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Disabling Docker in Docker for Dependency Scanning
|
||||||
|
|
||||||
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/12487) in GitLab Ultimate 12.5.
|
||||||
|
|
||||||
|
You can avoid the need for Docker in Docker by running the individual analyzers.
|
||||||
|
This does not require running the executor in privileged mode. For example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
include:
|
||||||
|
template: Dependency-Scanning.gitlab-ci.yml
|
||||||
|
|
||||||
|
variables:
|
||||||
|
DS_DISABLE_DIND: "true"
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create individual `<analyzer-name>-dependency_scanning` jobs for each analyzer that runs in your CI/CD pipeline.
|
||||||
|
|
||||||
## Interacting with the vulnerabilities
|
## Interacting with the vulnerabilities
|
||||||
|
|
||||||
Once a vulnerability is found, you can interact with it. Read more on how to
|
Once a vulnerability is found, you can interact with it. Read more on how to
|
||||||
|
|
|
@ -9,12 +9,16 @@ module Gitlab
|
||||||
def instrument(_type, field)
|
def instrument(_type, field)
|
||||||
service = AuthorizeFieldService.new(field)
|
service = AuthorizeFieldService.new(field)
|
||||||
|
|
||||||
if service.authorizations?
|
if service.authorizations? && !resolver_skips_authorizations?(field)
|
||||||
field.redefine { resolve(service.authorized_resolve) }
|
field.redefine { resolve(service.authorized_resolve) }
|
||||||
else
|
else
|
||||||
field
|
field
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def resolver_skips_authorizations?(field)
|
||||||
|
field.metadata[:resolver].try(:skip_authorizations?)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,6 +8,10 @@ module Gitlab
|
||||||
ActiveRecord::Relation,
|
ActiveRecord::Relation,
|
||||||
Gitlab::Graphql::Connections::Keyset::Connection
|
Gitlab::Graphql::Connections::Keyset::Connection
|
||||||
)
|
)
|
||||||
|
GraphQL::Relay::BaseConnection.register_connection_implementation(
|
||||||
|
Gitlab::Graphql::FilterableArray,
|
||||||
|
Gitlab::Graphql::Connections::FilterableArrayConnection
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Gitlab
|
||||||
|
module Graphql
|
||||||
|
module Connections
|
||||||
|
# FilterableArrayConnection is useful especially for lazy-loaded values.
|
||||||
|
# It allows us to call a callback only on the slice of array being
|
||||||
|
# rendered in the "after loaded" phase. For example we can check
|
||||||
|
# permissions only on a small subset of items.
|
||||||
|
class FilterableArrayConnection < GraphQL::Relay::ArrayConnection
|
||||||
|
def paged_nodes
|
||||||
|
@filtered_nodes ||= nodes.filter_callback.call(super)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,14 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Gitlab
|
||||||
|
module Graphql
|
||||||
|
class FilterableArray < Array
|
||||||
|
attr_reader :filter_callback
|
||||||
|
|
||||||
|
def initialize(filter_callback, *args)
|
||||||
|
super(args)
|
||||||
|
@filter_callback = filter_callback
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -9,7 +9,7 @@ module Gitlab
|
||||||
# find a corresponding database record. If found,
|
# find a corresponding database record. If found,
|
||||||
# includes the record's id in the dashboard config.
|
# includes the record's id in the dashboard config.
|
||||||
def transform!
|
def transform!
|
||||||
common_metrics = ::PrometheusMetric.common
|
common_metrics = ::PrometheusMetricsFinder.new(common: true).execute
|
||||||
|
|
||||||
for_metrics do |metric|
|
for_metrics do |metric|
|
||||||
metric_record = common_metrics.find { |m| m.identifier == metric[:id] }
|
metric_record = common_metrics.find { |m| m.identifier == metric[:id] }
|
||||||
|
|
|
@ -9,7 +9,7 @@ module Gitlab
|
||||||
# config. If there are no project-specific metrics,
|
# config. If there are no project-specific metrics,
|
||||||
# this will have no effect.
|
# this will have no effect.
|
||||||
def transform!
|
def transform!
|
||||||
project.prometheus_metrics.each do |project_metric|
|
PrometheusMetricsFinder.new(project: project).execute.each do |project_metric|
|
||||||
group = find_or_create_panel_group(dashboard[:panel_groups], project_metric)
|
group = find_or_create_panel_group(dashboard[:panel_groups], project_metric)
|
||||||
panel = find_or_create_panel(group[:panels], project_metric)
|
panel = find_or_create_panel(group[:panels], project_metric)
|
||||||
find_or_create_metric(panel[:metrics], project_metric)
|
find_or_create_metric(panel[:metrics], project_metric)
|
||||||
|
|
|
@ -11,13 +11,15 @@ module Gitlab
|
||||||
validates :name, :priority, :metrics, presence: true
|
validates :name, :priority, :metrics, presence: true
|
||||||
|
|
||||||
def self.common_metrics
|
def self.common_metrics
|
||||||
all_groups = ::PrometheusMetric.common.group_by(&:group_title).map do |name, metrics|
|
all_groups = ::PrometheusMetricsFinder.new(common: true).execute
|
||||||
MetricGroup.new(
|
.group_by(&:group_title)
|
||||||
name: name,
|
.map do |name, metrics|
|
||||||
priority: metrics.map(&:priority).max,
|
MetricGroup.new(
|
||||||
metrics: metrics.map(&:to_query_metric)
|
name: name,
|
||||||
)
|
priority: metrics.map(&:priority).max,
|
||||||
end
|
metrics: metrics.map(&:to_query_metric)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
all_groups.sort_by(&:priority).reverse
|
all_groups.sort_by(&:priority).reverse
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,11 +7,14 @@ module Gitlab
|
||||||
include QueryAdditionalMetrics
|
include QueryAdditionalMetrics
|
||||||
|
|
||||||
def query(serverless_function_id)
|
def query(serverless_function_id)
|
||||||
PrometheusMetric
|
PrometheusMetricsFinder
|
||||||
.find_by_identifier(:system_metrics_knative_function_invocation_count)
|
.new(identifier: :system_metrics_knative_function_invocation_count, common: true)
|
||||||
.to_query_metric.tap do |q|
|
.execute
|
||||||
q.queries[0][:result] = run_query(q.queries[0][:query_range], context(serverless_function_id))
|
.first
|
||||||
end
|
.to_query_metric
|
||||||
|
.tap do |q|
|
||||||
|
q.queries[0][:result] = run_query(q.queries[0][:query_range], context(serverless_function_id))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
|
@ -14,7 +14,71 @@ end
|
||||||
|
|
||||||
module Gitlab
|
module Gitlab
|
||||||
class Seeder
|
class Seeder
|
||||||
|
extend ActionView::Helpers::NumberHelper
|
||||||
|
|
||||||
|
ESTIMATED_INSERT_PER_MINUTE = 2_000_000
|
||||||
|
MASS_INSERT_ENV = 'MASS_INSERT'
|
||||||
|
|
||||||
|
module ProjectSeed
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
scope :not_mass_generated, -> do
|
||||||
|
where.not("path LIKE '#{Gitlab::Seeder::Projects::MASS_INSERT_NAME_START}%'")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module UserSeed
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
scope :not_mass_generated, -> do
|
||||||
|
where.not("username LIKE '#{Gitlab::Seeder::Users::MASS_INSERT_USERNAME_START}%'")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.with_mass_insert(size, model)
|
||||||
|
humanized_model_name = model.is_a?(String) ? model : model.model_name.human.pluralize(size)
|
||||||
|
|
||||||
|
if !ENV[MASS_INSERT_ENV] && !ENV['CI']
|
||||||
|
puts "\nSkipping mass insertion for #{humanized_model_name}."
|
||||||
|
puts "Consider running the seed with #{MASS_INSERT_ENV}=1"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
humanized_size = number_with_delimiter(size)
|
||||||
|
estimative = estimated_time_message(size)
|
||||||
|
|
||||||
|
puts "\nCreating #{humanized_size} #{humanized_model_name}."
|
||||||
|
puts estimative
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
puts "\n#{number_with_delimiter(size)} #{humanized_model_name} created!"
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.estimated_time_message(size)
|
||||||
|
estimated_minutes = (size.to_f / ESTIMATED_INSERT_PER_MINUTE).round
|
||||||
|
humanized_minutes = 'minute'.pluralize(estimated_minutes)
|
||||||
|
|
||||||
|
if estimated_minutes.zero?
|
||||||
|
"Rough estimated time: less than a minute ⏰"
|
||||||
|
else
|
||||||
|
"Rough estimated time: #{estimated_minutes} #{humanized_minutes} ⏰"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def self.quiet
|
def self.quiet
|
||||||
|
# Disable database insertion logs so speed isn't limited by ability to print to console
|
||||||
|
old_logger = ActiveRecord::Base.logger
|
||||||
|
ActiveRecord::Base.logger = nil
|
||||||
|
|
||||||
|
# Additional seed logic for models.
|
||||||
|
Project.include(ProjectSeed)
|
||||||
|
User.include(UserSeed)
|
||||||
|
|
||||||
mute_notifications
|
mute_notifications
|
||||||
mute_mailer
|
mute_mailer
|
||||||
|
|
||||||
|
@ -23,6 +87,7 @@ module Gitlab
|
||||||
yield
|
yield
|
||||||
|
|
||||||
SeedFu.quiet = false
|
SeedFu.quiet = false
|
||||||
|
ActiveRecord::Base.logger = old_logger
|
||||||
puts "\nOK".color(:green)
|
puts "\nOK".color(:green)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,10 @@ namespace :dev do
|
||||||
task setup: :environment do
|
task setup: :environment do
|
||||||
ENV['force'] = 'yes'
|
ENV['force'] = 'yes'
|
||||||
Rake::Task["gitlab:setup"].invoke
|
Rake::Task["gitlab:setup"].invoke
|
||||||
|
|
||||||
|
# Make sure DB statistics are up to date.
|
||||||
|
ActiveRecord::Base.connection.execute('ANALYZE')
|
||||||
|
|
||||||
Rake::Task["gitlab:shell:setup"].invoke
|
Rake::Task["gitlab:shell:setup"].invoke
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ namespace :gitlab do
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
else
|
else
|
||||||
Project.find_each
|
Project.not_mass_generated.find_each
|
||||||
end
|
end
|
||||||
|
|
||||||
projects.each do |project|
|
projects.each do |project|
|
||||||
|
|
|
@ -4386,6 +4386,9 @@ msgstr ""
|
||||||
msgid "Compare changes with the merge request target branch"
|
msgid "Compare changes with the merge request target branch"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Compare with previous version"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "CompareBranches|%{source_branch} and %{target_branch} are the same."
|
msgid "CompareBranches|%{source_branch} and %{target_branch} are the same."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -5683,6 +5686,9 @@ msgstr ""
|
||||||
msgid "Descending"
|
msgid "Descending"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Describe the goal of the changes and what reviewers should be aware of."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Description"
|
msgid "Description"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -10711,9 +10717,6 @@ msgstr ""
|
||||||
msgid "MergeRequests|started a thread on commit %{linkStart}%{commitDisplay}%{linkEnd}"
|
msgid "MergeRequests|started a thread on commit %{linkStart}%{commitDisplay}%{linkEnd}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "MergeRequest| %{paragraphStart}changed the description %{descriptionChangedTimes} times %{timeDifferenceMinutes}%{paragraphEnd}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "MergeRequest|Error dismissing suggestion popover. Please try again."
|
msgid "MergeRequest|Error dismissing suggestion popover. Please try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -10858,6 +10861,9 @@ msgstr ""
|
||||||
msgid "Metrics|Used if the query returns a single series. If it returns multiple series, their legend labels will be picked up from the response."
|
msgid "Metrics|Used if the query returns a single series. If it returns multiple series, their legend labels will be picked up from the response."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Metrics|Validating query"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Metrics|Y-axis label"
|
msgid "Metrics|Y-axis label"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -15926,6 +15932,9 @@ msgstr ""
|
||||||
msgid "Something went wrong while fetching comments. Please try again."
|
msgid "Something went wrong while fetching comments. Please try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Something went wrong while fetching description changes. Please try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Something went wrong while fetching group member contributions"
|
msgid "Something went wrong while fetching group member contributions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -21193,10 +21202,5 @@ msgstr ""
|
||||||
msgid "with %{additions} additions, %{deletions} deletions."
|
msgid "with %{additions} additions, %{deletions} deletions."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "within %d minute "
|
|
||||||
msgid_plural "within %d minutes "
|
|
||||||
msgstr[0] ""
|
|
||||||
msgstr[1] ""
|
|
||||||
|
|
||||||
msgid "yaml invalid"
|
msgid "yaml invalid"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
|
@ -10,9 +10,19 @@ module QA
|
||||||
element :impersonate_user_link
|
element :impersonate_user_link
|
||||||
end
|
end
|
||||||
|
|
||||||
|
view 'app/views/admin/users/show.html.haml' do
|
||||||
|
element :confirm_user_button
|
||||||
|
end
|
||||||
|
|
||||||
def click_impersonate_user
|
def click_impersonate_user
|
||||||
click_element(:impersonate_user_link)
|
click_element(:impersonate_user_link)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def confirm_user
|
||||||
|
accept_confirm do
|
||||||
|
click_element :confirm_user_button
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,6 +18,10 @@ module QA
|
||||||
element :no_result_text, 'No groups or projects matched your search' # rubocop:disable QA/ElementWithPattern
|
element :no_result_text, 'No groups or projects matched your search' # rubocop:disable QA/ElementWithPattern
|
||||||
end
|
end
|
||||||
|
|
||||||
|
view 'app/views/shared/members/_access_request_links.html.haml' do
|
||||||
|
element :leave_group_link
|
||||||
|
end
|
||||||
|
|
||||||
def click_subgroup(name)
|
def click_subgroup(name)
|
||||||
click_link name
|
click_link name
|
||||||
end
|
end
|
||||||
|
@ -42,6 +46,12 @@ module QA
|
||||||
click_element :new_in_group_button
|
click_element :new_in_group_button
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def leave_group
|
||||||
|
accept_alert do
|
||||||
|
click_element :leave_group_link
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def select_kind(kind)
|
def select_kind(kind)
|
||||||
|
|
|
@ -64,12 +64,11 @@ module QA
|
||||||
end
|
end
|
||||||
|
|
||||||
def visit!
|
def visit!
|
||||||
Runtime::Logger.debug("Visiting #{web_url}")
|
Runtime::Logger.debug(%Q[Visiting #{self.class.name} at "#{web_url}"]) if Runtime::Env.debug?
|
||||||
|
|
||||||
Support::Retrier.retry_until do
|
Support::Retrier.retry_until do
|
||||||
visit(web_url)
|
visit(web_url)
|
||||||
|
wait { current_url.include?(URI.parse(web_url).path.split('/').last || web_url) }
|
||||||
wait { current_url == web_url }
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,10 @@ module QA
|
||||||
post Runtime::API::Request.new(api_client, api_members_path).url, { user_id: user.id, access_level: access_level }
|
post Runtime::API::Request.new(api_client, api_members_path).url, { user_id: user.id, access_level: access_level }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def list_members
|
||||||
|
JSON.parse(get(Runtime::API::Request.new(api_client, api_members_path).url).body)
|
||||||
|
end
|
||||||
|
|
||||||
def api_members_path
|
def api_members_path
|
||||||
"#{api_get_path}/members"
|
"#{api_get_path}/members"
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,6 +7,8 @@ module QA
|
||||||
# creating it if it doesn't yet exist.
|
# creating it if it doesn't yet exist.
|
||||||
#
|
#
|
||||||
class Sandbox < Base
|
class Sandbox < Base
|
||||||
|
include Members
|
||||||
|
|
||||||
attr_accessor :path
|
attr_accessor :path
|
||||||
|
|
||||||
attribute :id
|
attribute :id
|
||||||
|
|
|
@ -57,13 +57,13 @@ module QA
|
||||||
|
|
||||||
Capybara.register_driver QA::Runtime::Env.browser do |app|
|
Capybara.register_driver QA::Runtime::Env.browser do |app|
|
||||||
capabilities = Selenium::WebDriver::Remote::Capabilities.send(QA::Runtime::Env.browser,
|
capabilities = Selenium::WebDriver::Remote::Capabilities.send(QA::Runtime::Env.browser,
|
||||||
# This enables access to logs with `page.driver.manage.get_log(:browser)`
|
# This enables access to logs with `page.driver.manage.get_log(:browser)`
|
||||||
loggingPrefs: {
|
loggingPrefs: {
|
||||||
browser: "ALL",
|
browser: "ALL",
|
||||||
client: "ALL",
|
client: "ALL",
|
||||||
driver: "ALL",
|
driver: "ALL",
|
||||||
server: "ALL"
|
server: "ALL"
|
||||||
})
|
})
|
||||||
|
|
||||||
if QA::Runtime::Env.accept_insecure_certs?
|
if QA::Runtime::Env.accept_insecure_certs?
|
||||||
capabilities['acceptInsecureCerts'] = true
|
capabilities['acceptInsecureCerts'] = true
|
||||||
|
|
|
@ -19,6 +19,28 @@ module QA
|
||||||
set_feature(key, false)
|
set_feature(key, false)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def remove(key)
|
||||||
|
request = Runtime::API::Request.new(api_client, "/features/#{key}")
|
||||||
|
response = delete(request.url)
|
||||||
|
unless response.code == QA::Support::Api::HTTP_STATUS_NO_CONTENT
|
||||||
|
raise SetFeatureError, "Deleting feature flag #{key} failed with `#{response}`."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def enable_and_verify(key)
|
||||||
|
Support::Retrier.retry_on_exception(sleep_interval: 2) do
|
||||||
|
enable(key)
|
||||||
|
|
||||||
|
is_enabled = false
|
||||||
|
|
||||||
|
QA::Support::Waiter.wait(interval: 1) do
|
||||||
|
is_enabled = enabled?(key)
|
||||||
|
end
|
||||||
|
|
||||||
|
raise SetFeatureError, "#{key} was not enabled!" unless is_enabled
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def enabled?(key)
|
def enabled?(key)
|
||||||
feature = JSON.parse(get_features).find { |flag| flag["name"] == key }
|
feature = JSON.parse(get_features).find { |flag| flag["name"] == key }
|
||||||
feature && feature["state"] == "on"
|
feature && feature["state"] == "on"
|
||||||
|
|
|
@ -8,7 +8,9 @@ module QA
|
||||||
|
|
||||||
Page::Main::Login.perform(&:sign_in_with_saml)
|
Page::Main::Login.perform(&:sign_in_with_saml)
|
||||||
|
|
||||||
Vendor::SAMLIdp::Page::Login.perform(&:login)
|
Vendor::SAMLIdp::Page::Login.perform do |login_page|
|
||||||
|
login_page.login('user1', 'user1pass')
|
||||||
|
end
|
||||||
|
|
||||||
expect(page).to have_content('Welcome to GitLab')
|
expect(page).to have_content('Welcome to GitLab')
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,18 +7,22 @@ module QA
|
||||||
module SAMLIdp
|
module SAMLIdp
|
||||||
module Page
|
module Page
|
||||||
class Login < Page::Base
|
class Login < Page::Base
|
||||||
def login
|
def login(username, password)
|
||||||
fill_in 'username', with: 'user1'
|
QA::Runtime::Logger.debug("Logging into SAMLIdp with username: #{username} and password:#{password}") if QA::Runtime::Env.debug?
|
||||||
fill_in 'password', with: 'user1pass'
|
|
||||||
|
fill_in 'username', with: username
|
||||||
|
fill_in 'password', with: password
|
||||||
click_on 'Login'
|
click_on 'Login'
|
||||||
end
|
end
|
||||||
|
|
||||||
def login_if_required
|
def login_if_required(username, password)
|
||||||
login if login_required?
|
login(username, password) if login_required?
|
||||||
end
|
end
|
||||||
|
|
||||||
def login_required?
|
def login_required?
|
||||||
page.has_text?('Enter your username and password')
|
login_required = page.has_text?('Enter your username and password')
|
||||||
|
QA::Runtime::Logger.debug("login_required: #{login_required}") if QA::Runtime::Env.debug?
|
||||||
|
login_required
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -20,7 +20,7 @@ RSpec.configure do |config|
|
||||||
QA::Specs::Helpers::Quarantine.configure_rspec
|
QA::Specs::Helpers::Quarantine.configure_rspec
|
||||||
|
|
||||||
config.before do |example|
|
config.before do |example|
|
||||||
QA::Runtime::Logger.debug("Starting test: #{example.full_description}") if QA::Runtime::Env.debug?
|
QA::Runtime::Logger.debug("\nStarting test: #{example.full_description}\n") if QA::Runtime::Env.debug?
|
||||||
end
|
end
|
||||||
|
|
||||||
config.after(:context) do
|
config.after(:context) do
|
||||||
|
|
|
@ -25,6 +25,11 @@ describe "User creates a merge request", :js do
|
||||||
|
|
||||||
click_button("Compare branches")
|
click_button("Compare branches")
|
||||||
|
|
||||||
|
page.within('.merge-request-form') do
|
||||||
|
expect(page.find('#merge_request_title')['placeholder']).to eq 'Title'
|
||||||
|
expect(page.find('#merge_request_description')['placeholder']).to eq 'Describe the goal of the changes and what reviewers should be aware of.'
|
||||||
|
end
|
||||||
|
|
||||||
fill_in("Title", with: title)
|
fill_in("Title", with: title)
|
||||||
click_button("Submit merge request")
|
click_button("Submit merge request")
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,144 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe PrometheusMetricsFinder do
|
||||||
|
describe '#execute' do
|
||||||
|
let(:finder) { described_class.new(params) }
|
||||||
|
let(:params) { {} }
|
||||||
|
|
||||||
|
subject { finder.execute }
|
||||||
|
|
||||||
|
context 'with params' do
|
||||||
|
let_it_be(:project) { create(:project) }
|
||||||
|
let_it_be(:project_metric) { create(:prometheus_metric, project: project) }
|
||||||
|
let_it_be(:common_metric) { create(:prometheus_metric, :common) }
|
||||||
|
let_it_be(:unique_metric) do
|
||||||
|
create(
|
||||||
|
:prometheus_metric,
|
||||||
|
:common,
|
||||||
|
title: 'Unique title',
|
||||||
|
y_label: 'Unique y_label',
|
||||||
|
group: :kubernetes,
|
||||||
|
identifier: 'identifier',
|
||||||
|
created_at: 5.minutes.ago
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with appropriate indexes' do
|
||||||
|
before do
|
||||||
|
allow_any_instance_of(described_class).to receive(:appropriate_index?).and_return(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with project' do
|
||||||
|
let(:params) { { project: project } }
|
||||||
|
|
||||||
|
it { is_expected.to eq([project_metric]) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with group' do
|
||||||
|
let(:params) { { group: project_metric.group } }
|
||||||
|
|
||||||
|
it { is_expected.to contain_exactly(common_metric, project_metric) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with title' do
|
||||||
|
let(:params) { { title: project_metric.title } }
|
||||||
|
|
||||||
|
it { is_expected.to contain_exactly(project_metric, common_metric) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with y_label' do
|
||||||
|
let(:params) { { y_label: project_metric.y_label } }
|
||||||
|
|
||||||
|
it { is_expected.to contain_exactly(project_metric, common_metric) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with common' do
|
||||||
|
let(:params) { { common: true } }
|
||||||
|
|
||||||
|
it { is_expected.to contain_exactly(common_metric, unique_metric) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with ordered' do
|
||||||
|
let(:params) { { ordered: true } }
|
||||||
|
|
||||||
|
it { is_expected.to eq([unique_metric, project_metric, common_metric]) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with indentifier' do
|
||||||
|
let(:params) { { identifier: unique_metric.identifier } }
|
||||||
|
|
||||||
|
it 'raises an error' do
|
||||||
|
expect { subject }.to raise_error(
|
||||||
|
ArgumentError,
|
||||||
|
':identifier must be scoped to a :project or :common'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with common' do
|
||||||
|
let(:params) { { identifier: unique_metric.identifier, common: true } }
|
||||||
|
|
||||||
|
it { is_expected.to contain_exactly(unique_metric) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with id' do
|
||||||
|
let(:params) { { id: 14, identifier: 'string' } }
|
||||||
|
|
||||||
|
it 'raises an error' do
|
||||||
|
expect { subject }.to raise_error(
|
||||||
|
ArgumentError,
|
||||||
|
'Only one of :identifier, :id is permitted'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with id' do
|
||||||
|
let(:params) { { id: common_metric.id } }
|
||||||
|
|
||||||
|
it { is_expected.to contain_exactly(common_metric) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with multiple params' do
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
group: project_metric.group,
|
||||||
|
title: project_metric.title,
|
||||||
|
y_label: project_metric.y_label,
|
||||||
|
common: true,
|
||||||
|
ordered: true
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to contain_exactly(common_metric) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without an appropriate index' do
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
title: project_metric.title,
|
||||||
|
ordered: true
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises an error' do
|
||||||
|
expect { subject }.to raise_error(
|
||||||
|
ArgumentError,
|
||||||
|
'An index should exist for params: [:title]'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without params' do
|
||||||
|
it 'raises an error' do
|
||||||
|
expect { subject }.to raise_error(
|
||||||
|
ArgumentError,
|
||||||
|
'Please provide one or more of: [:project, :group, :title, :y_label, :identifier, :id, :common, :ordered]'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -81,6 +81,17 @@ describe('monitor helper', () => {
|
||||||
expect(result.name).toEqual('brpop, brpop');
|
expect(result.name).toEqual('brpop, brpop');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('supports hyphenated template variables', () => {
|
||||||
|
const config = { ...defaultConfig, name: 'expired - {{ test-attribute }}' };
|
||||||
|
|
||||||
|
const [result] = monitorHelper.makeDataSeries(
|
||||||
|
[{ metric: { 'test-attribute': 'test-attribute-value' }, values: series }],
|
||||||
|
config,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.name).toEqual('expired - test-attribute-value');
|
||||||
|
});
|
||||||
|
|
||||||
it('updates multiple series names from templates', () => {
|
it('updates multiple series names from templates', () => {
|
||||||
const config = {
|
const config = {
|
||||||
...defaultConfig,
|
...defaultConfig,
|
||||||
|
|
|
@ -1094,8 +1094,9 @@ export const collapsedSystemNotes = [
|
||||||
noteable_type: 'Issue',
|
noteable_type: 'Issue',
|
||||||
resolvable: false,
|
resolvable: false,
|
||||||
noteable_iid: 12,
|
noteable_iid: 12,
|
||||||
|
start_description_version_id: undefined,
|
||||||
note: 'changed the description',
|
note: 'changed the description',
|
||||||
note_html: ' <p dir="auto">changed the description 2 times within 1 minute </p>',
|
note_html: '<p dir="auto">changed the description</p>',
|
||||||
current_user: { can_edit: false, can_award_emoji: true },
|
current_user: { can_edit: false, can_award_emoji: true },
|
||||||
resolved: false,
|
resolved: false,
|
||||||
resolved_by: null,
|
resolved_by: null,
|
||||||
|
@ -1106,7 +1107,6 @@ export const collapsedSystemNotes = [
|
||||||
'/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_905&user_id=1',
|
'/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_905&user_id=1',
|
||||||
human_access: 'Owner',
|
human_access: 'Owner',
|
||||||
path: '/gitlab-org/gitlab-shell/notes/905',
|
path: '/gitlab-org/gitlab-shell/notes/905',
|
||||||
times_updated: 2,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
individual_note: true,
|
individual_note: true,
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Vuex from 'vuex';
|
import Vuex from 'vuex';
|
||||||
import { mount, createLocalVue } from '@vue/test-utils';
|
import { mount, createLocalVue } from '@vue/test-utils';
|
||||||
import collapsibleComponent from '~/registry/components/collapsible_container.vue';
|
|
||||||
import { repoPropsData } from '../mock_data';
|
|
||||||
import createFlash from '~/flash';
|
import createFlash from '~/flash';
|
||||||
|
import Tracking from '~/tracking';
|
||||||
|
import collapsibleComponent from '~/registry/components/collapsible_container.vue';
|
||||||
import * as getters from '~/registry/stores/getters';
|
import * as getters from '~/registry/stores/getters';
|
||||||
|
import { repoPropsData } from '../mock_data';
|
||||||
|
|
||||||
jest.mock('~/flash.js');
|
jest.mock('~/flash.js');
|
||||||
|
|
||||||
|
@ -16,9 +17,10 @@ describe('collapsible registry container', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
let store;
|
let store;
|
||||||
|
|
||||||
const findDeleteBtn = w => w.find('.js-remove-repo');
|
const findDeleteBtn = (w = wrapper) => w.find('.js-remove-repo');
|
||||||
const findContainerImageTags = w => w.find('.container-image-tags');
|
const findContainerImageTags = (w = wrapper) => w.find('.container-image-tags');
|
||||||
const findToggleRepos = w => w.findAll('.js-toggle-repo');
|
const findToggleRepos = (w = wrapper) => w.findAll('.js-toggle-repo');
|
||||||
|
const findDeleteModal = (w = wrapper) => w.find({ ref: 'deleteModal' });
|
||||||
|
|
||||||
const mountWithStore = config => mount(collapsibleComponent, { ...config, store, localVue });
|
const mountWithStore = config => mount(collapsibleComponent, { ...config, store, localVue });
|
||||||
|
|
||||||
|
@ -124,4 +126,45 @@ describe('collapsible registry container', () => {
|
||||||
expect(deleteBtn.exists()).toBe(false);
|
expect(deleteBtn.exists()).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('tracking', () => {
|
||||||
|
const category = 'mock_page';
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(Tracking, 'event');
|
||||||
|
wrapper.vm.deleteItem = jest.fn().mockResolvedValue();
|
||||||
|
wrapper.vm.fetchRepos = jest.fn();
|
||||||
|
wrapper.setData({
|
||||||
|
tracking: {
|
||||||
|
...wrapper.vm.tracking,
|
||||||
|
category,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('send an event when delete button is clicked', () => {
|
||||||
|
const deleteBtn = findDeleteBtn();
|
||||||
|
deleteBtn.trigger('click');
|
||||||
|
expect(Tracking.event).toHaveBeenCalledWith(category, 'click_button', {
|
||||||
|
label: 'registry_repository_delete',
|
||||||
|
category,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('send an event when cancel is pressed on modal', () => {
|
||||||
|
const deleteModal = findDeleteModal();
|
||||||
|
deleteModal.vm.$emit('cancel');
|
||||||
|
expect(Tracking.event).toHaveBeenCalledWith(category, 'cancel_delete', {
|
||||||
|
label: 'registry_repository_delete',
|
||||||
|
category,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('send an event when confirm is clicked on modal', () => {
|
||||||
|
const deleteModal = findDeleteModal();
|
||||||
|
deleteModal.vm.$emit('ok');
|
||||||
|
|
||||||
|
expect(Tracking.event).toHaveBeenCalledWith(category, 'confirm_delete', {
|
||||||
|
label: 'registry_repository_delete',
|
||||||
|
category,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Vuex from 'vuex';
|
import Vuex from 'vuex';
|
||||||
import tableRegistry from '~/registry/components/table_registry.vue';
|
|
||||||
import { mount, createLocalVue } from '@vue/test-utils';
|
import { mount, createLocalVue } from '@vue/test-utils';
|
||||||
|
import createFlash from '~/flash';
|
||||||
|
import Tracking from '~/tracking';
|
||||||
|
import tableRegistry from '~/registry/components/table_registry.vue';
|
||||||
import { repoPropsData } from '../mock_data';
|
import { repoPropsData } from '../mock_data';
|
||||||
import * as getters from '~/registry/stores/getters';
|
import * as getters from '~/registry/stores/getters';
|
||||||
|
|
||||||
|
jest.mock('~/flash');
|
||||||
|
|
||||||
const [firstImage, secondImage] = repoPropsData.list;
|
const [firstImage, secondImage] = repoPropsData.list;
|
||||||
|
|
||||||
const localVue = createLocalVue();
|
const localVue = createLocalVue();
|
||||||
|
@ -15,11 +19,12 @@ describe('table registry', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
let store;
|
let store;
|
||||||
|
|
||||||
const findSelectAllCheckbox = w => w.find('.js-select-all-checkbox > input');
|
const findSelectAllCheckbox = (w = wrapper) => w.find('.js-select-all-checkbox > input');
|
||||||
const findSelectCheckboxes = w => w.findAll('.js-select-checkbox > input');
|
const findSelectCheckboxes = (w = wrapper) => w.findAll('.js-select-checkbox > input');
|
||||||
const findDeleteButton = w => w.find('.js-delete-registry');
|
const findDeleteButton = (w = wrapper) => w.find({ ref: 'bulkDeleteButton' });
|
||||||
const findDeleteButtonsRow = w => w.findAll('.js-delete-registry-row');
|
const findDeleteButtonsRow = (w = wrapper) => w.findAll('.js-delete-registry-row');
|
||||||
const findPagination = w => w.find('.js-registry-pagination');
|
const findPagination = (w = wrapper) => w.find('.js-registry-pagination');
|
||||||
|
const findDeleteModal = (w = wrapper) => w.find({ ref: 'deleteModal' });
|
||||||
const bulkDeletePath = 'path';
|
const bulkDeletePath = 'path';
|
||||||
|
|
||||||
const mountWithStore = config => mount(tableRegistry, { ...config, store, localVue });
|
const mountWithStore = config => mount(tableRegistry, { ...config, store, localVue });
|
||||||
|
@ -139,7 +144,7 @@ describe('table registry', () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
wrapper.vm.handleMultipleDelete();
|
wrapper.vm.handleMultipleDelete();
|
||||||
expect(wrapper.vm.showError).toHaveBeenCalled();
|
expect(createFlash).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -169,6 +174,27 @@ describe('table registry', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('modal event handlers', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper.vm.handleSingleDelete = jest.fn();
|
||||||
|
wrapper.vm.handleMultipleDelete = jest.fn();
|
||||||
|
});
|
||||||
|
it('on ok when one item is selected should call singleDelete', () => {
|
||||||
|
wrapper.setData({ itemsToBeDeleted: [0] });
|
||||||
|
wrapper.vm.onDeletionConfirmed();
|
||||||
|
|
||||||
|
expect(wrapper.vm.handleSingleDelete).toHaveBeenCalledWith(repoPropsData.list[0]);
|
||||||
|
expect(wrapper.vm.handleMultipleDelete).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
it('on ok when multiple items are selected should call muultiDelete', () => {
|
||||||
|
wrapper.setData({ itemsToBeDeleted: [0, 1, 2] });
|
||||||
|
wrapper.vm.onDeletionConfirmed();
|
||||||
|
|
||||||
|
expect(wrapper.vm.handleMultipleDelete).toHaveBeenCalled();
|
||||||
|
expect(wrapper.vm.handleSingleDelete).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('pagination', () => {
|
describe('pagination', () => {
|
||||||
const repo = {
|
const repo = {
|
||||||
repoPropsData,
|
repoPropsData,
|
||||||
|
@ -265,4 +291,83 @@ describe('table registry', () => {
|
||||||
expect(deleteBtns.length).toBe(0);
|
expect(deleteBtns.length).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('event tracking', () => {
|
||||||
|
const mockPageName = 'mock_page';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(Tracking, 'event');
|
||||||
|
wrapper.vm.handleSingleDelete = jest.fn();
|
||||||
|
wrapper.vm.handleMultipleDelete = jest.fn();
|
||||||
|
document.body.dataset.page = mockPageName;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
document.body.dataset.page = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('single tag delete', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper.setData({ itemsToBeDeleted: [0] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('send an event when delete button is clicked', () => {
|
||||||
|
const deleteBtn = findDeleteButtonsRow();
|
||||||
|
deleteBtn.at(0).trigger('click');
|
||||||
|
expect(Tracking.event).toHaveBeenCalledWith(mockPageName, 'click_button', {
|
||||||
|
label: 'registry_tag_delete',
|
||||||
|
property: 'foo',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('send an event when cancel is pressed on modal', () => {
|
||||||
|
const deleteModal = findDeleteModal();
|
||||||
|
deleteModal.vm.$emit('cancel');
|
||||||
|
expect(Tracking.event).toHaveBeenCalledWith(mockPageName, 'cancel_delete', {
|
||||||
|
label: 'registry_tag_delete',
|
||||||
|
property: 'foo',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('send an event when confirm is clicked on modal', () => {
|
||||||
|
const deleteModal = findDeleteModal();
|
||||||
|
deleteModal.vm.$emit('ok');
|
||||||
|
|
||||||
|
expect(Tracking.event).toHaveBeenCalledWith(mockPageName, 'confirm_delete', {
|
||||||
|
label: 'registry_tag_delete',
|
||||||
|
property: 'foo',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('bulk tag delete', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const items = [0, 1, 2];
|
||||||
|
wrapper.setData({ itemsToBeDeleted: items, selectedItems: items });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('send an event when delete button is clicked', () => {
|
||||||
|
const deleteBtn = findDeleteButton();
|
||||||
|
deleteBtn.vm.$emit('click');
|
||||||
|
expect(Tracking.event).toHaveBeenCalledWith(mockPageName, 'click_button', {
|
||||||
|
label: 'bulk_registry_tag_delete',
|
||||||
|
property: 'foo',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('send an event when cancel is pressed on modal', () => {
|
||||||
|
const deleteModal = findDeleteModal();
|
||||||
|
deleteModal.vm.$emit('cancel');
|
||||||
|
expect(Tracking.event).toHaveBeenCalledWith(mockPageName, 'cancel_delete', {
|
||||||
|
label: 'bulk_registry_tag_delete',
|
||||||
|
property: 'foo',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('send an event when confirm is clicked on modal', () => {
|
||||||
|
const deleteModal = findDeleteModal();
|
||||||
|
deleteModal.vm.$emit('ok');
|
||||||
|
|
||||||
|
expect(Tracking.event).toHaveBeenCalledWith(mockPageName, 'confirm_delete', {
|
||||||
|
label: 'bulk_registry_tag_delete',
|
||||||
|
property: 'foo',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -57,7 +57,7 @@ describe('system note component', () => {
|
||||||
// we need to strip them because they break layout of commit lists in system notes:
|
// we need to strip them because they break layout of commit lists in system notes:
|
||||||
// https://gitlab.com/gitlab-org/gitlab-foss/uploads/b07a10670919254f0220d3ff5c1aa110/jqzI.png
|
// https://gitlab.com/gitlab-org/gitlab-foss/uploads/b07a10670919254f0220d3ff5c1aa110/jqzI.png
|
||||||
it('removes wrapping paragraph from note HTML', () => {
|
it('removes wrapping paragraph from note HTML', () => {
|
||||||
expect(vm.$el.querySelector('.system-note-message').innerHTML).toEqual('<span>closed</span>');
|
expect(vm.$el.querySelector('.system-note-message').innerHTML).toContain('<span>closed</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should initMRPopovers onMount', () => {
|
it('should initMRPopovers onMount', () => {
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
/* eslint-disable no-var */
|
|
||||||
|
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import '~/commons/bootstrap';
|
import '~/commons/bootstrap';
|
||||||
|
|
||||||
|
@ -10,15 +8,13 @@ describe('Bootstrap jQuery extensions', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds the disabled attribute', function() {
|
it('adds the disabled attribute', function() {
|
||||||
var $input;
|
const $input = $('input').first();
|
||||||
$input = $('input').first();
|
|
||||||
$input.disable();
|
$input.disable();
|
||||||
|
|
||||||
expect($input).toHaveAttr('disabled', 'disabled');
|
expect($input).toHaveAttr('disabled', 'disabled');
|
||||||
});
|
});
|
||||||
return it('adds the disabled class', function() {
|
return it('adds the disabled class', function() {
|
||||||
var $input;
|
const $input = $('input').first();
|
||||||
$input = $('input').first();
|
|
||||||
$input.disable();
|
$input.disable();
|
||||||
|
|
||||||
expect($input).toHaveClass('disabled');
|
expect($input).toHaveClass('disabled');
|
||||||
|
@ -30,15 +26,13 @@ describe('Bootstrap jQuery extensions', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('removes the disabled attribute', function() {
|
it('removes the disabled attribute', function() {
|
||||||
var $input;
|
const $input = $('input').first();
|
||||||
$input = $('input').first();
|
|
||||||
$input.enable();
|
$input.enable();
|
||||||
|
|
||||||
expect($input).not.toHaveAttr('disabled');
|
expect($input).not.toHaveAttr('disabled');
|
||||||
});
|
});
|
||||||
return it('removes the disabled class', function() {
|
return it('removes the disabled class', function() {
|
||||||
var $input;
|
const $input = $('input').first();
|
||||||
$input = $('input').first();
|
|
||||||
$input.enable();
|
$input.enable();
|
||||||
|
|
||||||
expect($input).not.toHaveClass('disabled');
|
expect($input).not.toHaveClass('disabled');
|
||||||
|
|
|
@ -122,6 +122,32 @@ describe('Dashboard', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('cluster health', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
beforeEach(done => {
|
||||||
|
wrapper = shallowMount(DashboardComponent, {
|
||||||
|
localVue,
|
||||||
|
sync: false,
|
||||||
|
propsData: { ...propsData, hasMetrics: true },
|
||||||
|
store,
|
||||||
|
});
|
||||||
|
|
||||||
|
// all_dashboards is not defined in health dashboards
|
||||||
|
wrapper.vm.$store.commit(`monitoringDashboard/${types.SET_ALL_DASHBOARDS}`, undefined);
|
||||||
|
wrapper.vm.$nextTick(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders correctly', () => {
|
||||||
|
expect(wrapper.isVueInstance()).toBe(true);
|
||||||
|
expect(wrapper.exists()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('requests information to the server', () => {
|
describe('requests information to the server', () => {
|
||||||
let spy;
|
let spy;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
|
@ -144,7 +144,19 @@ describe('Monitoring mutations', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('SET_ALL_DASHBOARDS', () => {
|
describe('SET_ALL_DASHBOARDS', () => {
|
||||||
it('stores the dashboards loaded from the git repository', () => {
|
it('stores `undefined` dashboards as an empty array', () => {
|
||||||
|
mutations[types.SET_ALL_DASHBOARDS](stateCopy, undefined);
|
||||||
|
|
||||||
|
expect(stateCopy.allDashboards).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stores `null` dashboards as an empty array', () => {
|
||||||
|
mutations[types.SET_ALL_DASHBOARDS](stateCopy, null);
|
||||||
|
|
||||||
|
expect(stateCopy.allDashboards).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stores dashboards loaded from the git repository', () => {
|
||||||
mutations[types.SET_ALL_DASHBOARDS](stateCopy, dashboardGitResponse);
|
mutations[types.SET_ALL_DASHBOARDS](stateCopy, dashboardGitResponse);
|
||||||
|
|
||||||
expect(stateCopy.allDashboards).toEqual(dashboardGitResponse);
|
expect(stateCopy.allDashboards).toEqual(dashboardGitResponse);
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import {
|
import {
|
||||||
isDescriptionSystemNote,
|
isDescriptionSystemNote,
|
||||||
changeDescriptionNote,
|
|
||||||
getTimeDifferenceMinutes,
|
getTimeDifferenceMinutes,
|
||||||
collapseSystemNotes,
|
collapseSystemNotes,
|
||||||
} from '~/notes/stores/collapse_utils';
|
} from '~/notes/stores/collapse_utils';
|
||||||
|
@ -24,15 +23,6 @@ describe('Collapse utils', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('changes the description to contain the number of changed times', () => {
|
|
||||||
const changedNote = changeDescriptionNote(mockSystemNote, 3, 5);
|
|
||||||
|
|
||||||
expect(changedNote.times_updated).toEqual(3);
|
|
||||||
expect(changedNote.note_html.trim()).toContain(
|
|
||||||
'<p dir="auto">changed the description 3 times within 5 minutes </p>',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('gets the time difference between two notes', () => {
|
it('gets the time difference between two notes', () => {
|
||||||
const anotherSystemNote = {
|
const anotherSystemNote = {
|
||||||
created_at: '2018-05-14T21:33:00.000Z',
|
created_at: '2018-05-14T21:33:00.000Z',
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Gitlab::Graphql::Connections::FilterableArrayConnection do
|
||||||
|
let(:callback) { proc { |nodes| nodes } }
|
||||||
|
let(:all_nodes) { Gitlab::Graphql::FilterableArray.new(callback, 1, 2, 3, 4, 5) }
|
||||||
|
let(:arguments) { {} }
|
||||||
|
subject(:connection) do
|
||||||
|
described_class.new(all_nodes, arguments, max_page_size: 3)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#paged_nodes' do
|
||||||
|
let(:paged_nodes) { subject.paged_nodes }
|
||||||
|
|
||||||
|
it_behaves_like "connection with paged nodes"
|
||||||
|
|
||||||
|
context 'when callback filters some nodes' do
|
||||||
|
let(:callback) { proc { |nodes| nodes[1..-1] } }
|
||||||
|
|
||||||
|
it 'does not return filtered elements' do
|
||||||
|
expect(subject.paged_nodes).to contain_exactly(all_nodes[1], all_nodes[2])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -240,38 +240,16 @@ describe Gitlab::Graphql::Connections::Keyset::Connection do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#paged_nodes' do
|
describe '#paged_nodes' do
|
||||||
let!(:projects) { create_list(:project, 5) }
|
let_it_be(:all_nodes) { create_list(:project, 5) }
|
||||||
|
let(:paged_nodes) { subject.paged_nodes }
|
||||||
|
|
||||||
it 'returns the collection limited to max page size' do
|
it_behaves_like "connection with paged nodes"
|
||||||
expect(subject.paged_nodes.size).to eq(3)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'is a loaded memoized array' do
|
|
||||||
expect(subject.paged_nodes).to be_an(Array)
|
|
||||||
expect(subject.paged_nodes.object_id).to eq(subject.paged_nodes.object_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when `first` is passed' do
|
|
||||||
let(:arguments) { { first: 2 } }
|
|
||||||
|
|
||||||
it 'returns only the first elements' do
|
|
||||||
expect(subject.paged_nodes).to contain_exactly(projects.first, projects.second)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when `last` is passed' do
|
|
||||||
let(:arguments) { { last: 2 } }
|
|
||||||
|
|
||||||
it 'returns only the last elements' do
|
|
||||||
expect(subject.paged_nodes).to contain_exactly(projects[3], projects[4])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when both are passed' do
|
context 'when both are passed' do
|
||||||
let(:arguments) { { first: 2, last: 2 } }
|
let(:arguments) { { first: 2, last: 2 } }
|
||||||
|
|
||||||
it 'raises an error' do
|
it 'raises an error' do
|
||||||
expect { subject.paged_nodes }.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
|
expect { paged_nodes }.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -13,14 +13,19 @@ describe Gitlab::Prometheus::Queries::KnativeInvocationQuery do
|
||||||
|
|
||||||
context 'verify queries' do
|
context 'verify queries' do
|
||||||
before do
|
before do
|
||||||
allow(PrometheusMetric).to receive(:find_by_identifier).and_return(create(:prometheus_metric, query: prometheus_istio_query('test-name', 'test-ns')))
|
create(:prometheus_metric,
|
||||||
allow(client).to receive(:query_range)
|
:common,
|
||||||
|
identifier: :system_metrics_knative_function_invocation_count,
|
||||||
|
query: 'sum(ceil(rate(istio_requests_total{destination_service_namespace="%{kube_namespace}", destination_app=~"%{function_name}.*"}[1m])*60))')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'has the query, but no data' do
|
it 'has the query, but no data' do
|
||||||
results = subject.query(serverless_func.id)
|
expect(client).to receive(:query_range).with(
|
||||||
|
'sum(ceil(rate(istio_requests_total{destination_service_namespace="test-ns", destination_app=~"test-name.*"}[1m])*60))',
|
||||||
|
hash_including(:start, :stop)
|
||||||
|
)
|
||||||
|
|
||||||
expect(results.queries[0][:query_range]).to eql('floor(sum(rate(istio_revision_request_count{destination_configuration="test-name", destination_namespace="test-ns"}[1m])*30))')
|
subject.query(serverless_func.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -37,9 +37,12 @@ module GraphqlHelpers
|
||||||
# BatchLoader::GraphQL returns a wrapper, so we need to :sync in order
|
# BatchLoader::GraphQL returns a wrapper, so we need to :sync in order
|
||||||
# to get the actual values
|
# to get the actual values
|
||||||
def batch_sync(max_queries: nil, &blk)
|
def batch_sync(max_queries: nil, &blk)
|
||||||
result = batch(max_queries: nil, &blk)
|
wrapper = proc do
|
||||||
|
lazy_vals = yield
|
||||||
|
lazy_vals.is_a?(Array) ? lazy_vals.map(&:sync) : lazy_vals&.sync
|
||||||
|
end
|
||||||
|
|
||||||
result.is_a?(Array) ? result.map(&:sync) : result&.sync
|
batch(max_queries: max_queries, &wrapper)
|
||||||
end
|
end
|
||||||
|
|
||||||
def graphql_query_for(name, attributes = {}, fields = nil)
|
def graphql_query_for(name, attributes = {}, fields = nil)
|
||||||
|
@ -157,7 +160,13 @@ module GraphqlHelpers
|
||||||
|
|
||||||
def attributes_to_graphql(attributes)
|
def attributes_to_graphql(attributes)
|
||||||
attributes.map do |name, value|
|
attributes.map do |name, value|
|
||||||
"#{GraphqlHelpers.fieldnamerize(name.to_s)}: \"#{value}\""
|
value_str = if value.is_a?(Array)
|
||||||
|
'["' + value.join('","') + '"]'
|
||||||
|
else
|
||||||
|
"\"#{value}\""
|
||||||
|
end
|
||||||
|
|
||||||
|
"#{GraphqlHelpers.fieldnamerize(name.to_s)}: #{value_str}"
|
||||||
end.join(", ")
|
end.join(", ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -282,6 +291,12 @@ module GraphqlHelpers
|
||||||
def allow_high_graphql_recursion
|
def allow_high_graphql_recursion
|
||||||
allow_any_instance_of(Gitlab::Graphql::QueryAnalyzers::RecursionAnalyzer).to receive(:recursion_threshold).and_return 1000
|
allow_any_instance_of(Gitlab::Graphql::QueryAnalyzers::RecursionAnalyzer).to receive(:recursion_threshold).and_return 1000
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def node_array(data, extract_attribute = nil)
|
||||||
|
data.map do |item|
|
||||||
|
extract_attribute ? item['node'][extract_attribute] : item['node']
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# This warms our schema, doing this as part of loading the helpers to avoid
|
# This warms our schema, doing this as part of loading the helpers to avoid
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
RSpec.shared_examples 'connection with paged nodes' do
|
||||||
|
it 'returns the collection limited to max page size' do
|
||||||
|
expect(paged_nodes.size).to eq(3)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is a loaded memoized array' do
|
||||||
|
expect(paged_nodes).to be_an(Array)
|
||||||
|
expect(paged_nodes.object_id).to eq(paged_nodes.object_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when `first` is passed' do
|
||||||
|
let(:arguments) { { first: 2 } }
|
||||||
|
|
||||||
|
it 'returns only the first elements' do
|
||||||
|
expect(paged_nodes).to contain_exactly(all_nodes.first, all_nodes.second)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when `last` is passed' do
|
||||||
|
let(:arguments) { { last: 2 } }
|
||||||
|
|
||||||
|
it 'returns only the last elements' do
|
||||||
|
expect(paged_nodes).to contain_exactly(all_nodes[3], all_nodes[4])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue