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/**
|
||||
.overcommit.yml
|
||||
.projections.json
|
||||
/qa/.rakeTasks
|
||||
|
|
1
Gemfile
1
Gemfile
|
@ -159,6 +159,7 @@ gem 'icalendar'
|
|||
|
||||
# Diffs
|
||||
gem 'diffy', '~> 3.1.0'
|
||||
gem 'diff_match_patch', '~> 0.1.0'
|
||||
|
||||
# Application server
|
||||
gem 'rack', '~> 2.0.7'
|
||||
|
|
|
@ -224,6 +224,7 @@ GEM
|
|||
railties
|
||||
rotp (~> 2.0)
|
||||
diff-lcs (1.3)
|
||||
diff_match_patch (0.1.0)
|
||||
diffy (3.1.0)
|
||||
discordrb-webhooks-blackst0ne (3.3.0)
|
||||
rest-client (~> 2.0)
|
||||
|
@ -1133,6 +1134,7 @@ DEPENDENCIES
|
|||
device_detector
|
||||
devise (~> 4.6)
|
||||
devise-two-factor (~> 3.0.0)
|
||||
diff_match_patch (~> 0.1.0)
|
||||
diffy (~> 3.1.0)
|
||||
discordrb-webhooks-blackst0ne (~> 3.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 {Object} defaultConfig - Default chart config values (e.g. lineStyle, name)
|
||||
* @returns {Array} The formatted values
|
||||
*/
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const makeDataSeries = (queryResults, defaultConfig) =>
|
||||
queryResults
|
||||
.map(result => {
|
||||
|
@ -19,10 +17,13 @@ export const makeDataSeries = (queryResults, defaultConfig) =>
|
|||
if (name) {
|
||||
series.name = `${defaultConfig.name}: ${name}`;
|
||||
} else {
|
||||
const template = _.template(defaultConfig.name, {
|
||||
interpolate: /\{\{(.+?)\}\}/g,
|
||||
series.name = defaultConfig.name;
|
||||
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 };
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* eslint-disable no-var, one-var, consistent-return */
|
||||
/* eslint-disable consistent-return */
|
||||
|
||||
import $ from 'jquery';
|
||||
import axios from './lib/utils/axios_utils';
|
||||
|
@ -91,18 +91,17 @@ export default class Issue {
|
|||
'click',
|
||||
'.js-issuable-actions a.btn-close, .js-issuable-actions a.btn-reopen',
|
||||
e => {
|
||||
var $button, shouldSubmit, url;
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
$button = $(e.currentTarget);
|
||||
shouldSubmit = $button.hasClass('btn-comment');
|
||||
const $button = $(e.currentTarget);
|
||||
const shouldSubmit = $button.hasClass('btn-comment');
|
||||
if (shouldSubmit) {
|
||||
Issue.submitNoteForm($button.closest('form'));
|
||||
}
|
||||
|
||||
this.disableCloseReopenButton($button);
|
||||
|
||||
url = $button.attr('href');
|
||||
const url = $button.attr('href');
|
||||
return axios
|
||||
.put(url)
|
||||
.then(({ data }) => {
|
||||
|
@ -139,16 +138,14 @@ export default class Issue {
|
|||
}
|
||||
|
||||
static submitNoteForm(form) {
|
||||
var noteText;
|
||||
noteText = form.find('textarea.js-note-text').val();
|
||||
const noteText = form.find('textarea.js-note-text').val();
|
||||
if (noteText && noteText.trim().length > 0) {
|
||||
return form.submit();
|
||||
}
|
||||
}
|
||||
|
||||
static initRelatedBranches() {
|
||||
var $container;
|
||||
$container = $('#related-branches');
|
||||
const $container = $('#related-branches');
|
||||
return axios
|
||||
.get($container.data('url'))
|
||||
.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 ListLabel */
|
||||
|
||||
|
@ -15,63 +15,39 @@ import { isScopedLabel } from '~/lib/utils/common_utils';
|
|||
|
||||
export default class LabelsSelect {
|
||||
constructor(els, options = {}) {
|
||||
var _this, $els;
|
||||
_this = this;
|
||||
const _this = this;
|
||||
|
||||
$els = $(els);
|
||||
let $els = $(els);
|
||||
|
||||
if (!els) {
|
||||
$els = $('.js-label-select');
|
||||
}
|
||||
|
||||
$els.each((i, dropdown) => {
|
||||
var $block,
|
||||
$dropdown,
|
||||
$form,
|
||||
$loading,
|
||||
$selectbox,
|
||||
$sidebarCollapsedValue,
|
||||
$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');
|
||||
const $dropdown = $(dropdown);
|
||||
const $dropdownContainer = $dropdown.closest('.labels-filter');
|
||||
const namespacePath = $dropdown.data('namespacePath');
|
||||
const projectPath = $dropdown.data('projectPath');
|
||||
const issueUpdateURL = $dropdown.data('issueUpdate');
|
||||
let selectedLabel = $dropdown.data('selected');
|
||||
if (selectedLabel != null && !$dropdown.hasClass('js-multiselect')) {
|
||||
selectedLabel = selectedLabel.split(',');
|
||||
}
|
||||
showNo = $dropdown.data('showNo');
|
||||
showAny = $dropdown.data('showAny');
|
||||
showMenuAbove = $dropdown.data('showMenuAbove');
|
||||
defaultLabel = $dropdown.data('defaultLabel') || __('Label');
|
||||
abilityName = $dropdown.data('abilityName');
|
||||
$selectbox = $dropdown.closest('.selectbox');
|
||||
$block = $selectbox.closest('.block');
|
||||
$form = $dropdown.closest('form, .js-issuable-update');
|
||||
$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span');
|
||||
$sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
|
||||
$value = $block.find('.value');
|
||||
$dropdownMenu = $dropdown.parent().find('.dropdown-menu');
|
||||
$loading = $block.find('.block-loading').fadeOut();
|
||||
fieldName = $dropdown.data('fieldName');
|
||||
initialSelected = $selectbox
|
||||
const showNo = $dropdown.data('showNo');
|
||||
const showAny = $dropdown.data('showAny');
|
||||
const showMenuAbove = $dropdown.data('showMenuAbove');
|
||||
const defaultLabel = $dropdown.data('defaultLabel') || __('Label');
|
||||
const abilityName = $dropdown.data('abilityName');
|
||||
const $selectbox = $dropdown.closest('.selectbox');
|
||||
const $block = $selectbox.closest('.block');
|
||||
const $form = $dropdown.closest('form, .js-issuable-update');
|
||||
const $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span');
|
||||
const $sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
|
||||
const $value = $block.find('.value');
|
||||
const $dropdownMenu = $dropdown.parent().find('.dropdown-menu');
|
||||
const $loading = $block.find('.block-loading').fadeOut();
|
||||
const fieldName = $dropdown.data('fieldName');
|
||||
let initialSelected = $selectbox
|
||||
.find(`input[name="${$dropdown.data('fieldName')}"]`)
|
||||
.map(function() {
|
||||
return this.value;
|
||||
|
@ -90,9 +66,8 @@ export default class LabelsSelect {
|
|||
);
|
||||
}
|
||||
|
||||
saveLabelData = function() {
|
||||
var data, selected;
|
||||
selected = $dropdown
|
||||
const saveLabelData = function() {
|
||||
const selected = $dropdown
|
||||
.closest('.selectbox')
|
||||
.find(`input[name='${fieldName}']`)
|
||||
.map(function() {
|
||||
|
@ -103,7 +78,7 @@ export default class LabelsSelect {
|
|||
if (_.isEqual(initialSelected, selected)) return;
|
||||
initialSelected = selected;
|
||||
|
||||
data = {};
|
||||
const data = {};
|
||||
data[abilityName] = {};
|
||||
data[abilityName].label_ids = selected;
|
||||
if (!selected.length) {
|
||||
|
@ -114,12 +89,13 @@ export default class LabelsSelect {
|
|||
axios
|
||||
.put(issueUpdateURL, data)
|
||||
.then(({ data }) => {
|
||||
var labelCount, template, labelTooltipTitle, labelTitles;
|
||||
let labelTooltipTitle;
|
||||
let template;
|
||||
$loading.fadeOut();
|
||||
$dropdown.trigger('loaded.gl.dropdown');
|
||||
$selectbox.hide();
|
||||
data.issueUpdateURL = issueUpdateURL;
|
||||
labelCount = 0;
|
||||
let labelCount = 0;
|
||||
if (data.labels.length && issueUpdateURL) {
|
||||
template = LabelsSelect.getLabelTemplate({
|
||||
labels: _.sortBy(data.labels, 'title'),
|
||||
|
@ -174,7 +150,7 @@ export default class LabelsSelect {
|
|||
$sidebarCollapsedValue.text(labelCount);
|
||||
|
||||
if (data.labels.length) {
|
||||
labelTitles = data.labels.map(label => label.title);
|
||||
let labelTitles = data.labels.map(label => label.title);
|
||||
|
||||
if (labelTitles.length > 5) {
|
||||
labelTitles = labelTitles.slice(0, 5);
|
||||
|
@ -199,13 +175,13 @@ export default class LabelsSelect {
|
|||
$dropdown.glDropdown({
|
||||
showMenuAbove,
|
||||
data(term, callback) {
|
||||
labelUrl = $dropdown.attr('data-labels');
|
||||
const labelUrl = $dropdown.attr('data-labels');
|
||||
axios
|
||||
.get(labelUrl)
|
||||
.then(res => {
|
||||
let { data } = res;
|
||||
if ($dropdown.hasClass('js-extra-options')) {
|
||||
var extraData = [];
|
||||
const extraData = [];
|
||||
if (showNo) {
|
||||
extraData.unshift({
|
||||
id: 0,
|
||||
|
@ -232,22 +208,14 @@ export default class LabelsSelect {
|
|||
.catch(() => flash(__('Error fetching labels.')));
|
||||
},
|
||||
renderRow(label) {
|
||||
var linkEl,
|
||||
listItemEl,
|
||||
colorEl,
|
||||
indeterminate,
|
||||
removesAll,
|
||||
selectedClass,
|
||||
i,
|
||||
marked,
|
||||
dropdownValue;
|
||||
let colorEl;
|
||||
|
||||
selectedClass = [];
|
||||
removesAll = label.id <= 0 || label.id == null;
|
||||
const selectedClass = [];
|
||||
const removesAll = label.id <= 0 || label.id == null;
|
||||
|
||||
if ($dropdown.hasClass('js-filter-bulk-update')) {
|
||||
indeterminate = $dropdown.data('indeterminate') || [];
|
||||
marked = $dropdown.data('marked') || [];
|
||||
const indeterminate = $dropdown.data('indeterminate') || [];
|
||||
const marked = $dropdown.data('marked') || [];
|
||||
|
||||
if (indeterminate.indexOf(label.id) !== -1) {
|
||||
selectedClass.push('is-indeterminate');
|
||||
|
@ -255,7 +223,7 @@ export default class LabelsSelect {
|
|||
|
||||
if (marked.indexOf(label.id) !== -1) {
|
||||
// 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) {
|
||||
selectedClass.splice(i, 1);
|
||||
}
|
||||
|
@ -263,7 +231,7 @@ export default class LabelsSelect {
|
|||
}
|
||||
} else {
|
||||
if (this.id(label)) {
|
||||
dropdownValue = this.id(label)
|
||||
const dropdownValue = this.id(label)
|
||||
.toString()
|
||||
.replace(/'/g, "\\'");
|
||||
|
||||
|
@ -287,7 +255,7 @@ export default class LabelsSelect {
|
|||
colorEl = '';
|
||||
}
|
||||
|
||||
linkEl = document.createElement('a');
|
||||
const linkEl = document.createElement('a');
|
||||
linkEl.href = '#';
|
||||
|
||||
// We need to identify which items are actually labels
|
||||
|
@ -300,7 +268,7 @@ export default class LabelsSelect {
|
|||
linkEl.className = selectedClass.join(' ');
|
||||
linkEl.innerHTML = `${colorEl} ${_.escape(label.title)}`;
|
||||
|
||||
listItemEl = document.createElement('li');
|
||||
const listItemEl = document.createElement('li');
|
||||
listItemEl.appendChild(linkEl);
|
||||
|
||||
return listItemEl;
|
||||
|
@ -312,12 +280,12 @@ export default class LabelsSelect {
|
|||
filterable: true,
|
||||
selected: $dropdown.data('selected') || [],
|
||||
toggleLabel(selected, el) {
|
||||
var $dropdownParent = $dropdown.parent();
|
||||
var $dropdownInputField = $dropdownParent.find('.dropdown-input-field');
|
||||
var isSelected = el !== null ? el.hasClass('is-active') : false;
|
||||
const $dropdownParent = $dropdown.parent();
|
||||
const $dropdownInputField = $dropdownParent.find('.dropdown-input-field');
|
||||
const isSelected = el !== null ? el.hasClass('is-active') : false;
|
||||
|
||||
var title = selected ? selected.title : null;
|
||||
var selectedLabels = this.selected;
|
||||
const title = selected ? selected.title : null;
|
||||
const selectedLabels = this.selected;
|
||||
|
||||
if ($dropdownInputField.length && $dropdownInputField.val().length) {
|
||||
$dropdownParent.find('.dropdown-input-clear').trigger('click');
|
||||
|
@ -329,7 +297,7 @@ export default class LabelsSelect {
|
|||
} else if (isSelected) {
|
||||
this.selected.push(title);
|
||||
} else if (!isSelected && title) {
|
||||
var index = this.selected.indexOf(title);
|
||||
const index = this.selected.indexOf(title);
|
||||
this.selected.splice(index, 1);
|
||||
}
|
||||
|
||||
|
@ -359,10 +327,9 @@ export default class LabelsSelect {
|
|||
}
|
||||
},
|
||||
hidden() {
|
||||
var isIssueIndex, isMRIndex, page;
|
||||
page = $('body').attr('data-page');
|
||||
isIssueIndex = page === 'projects:issues:index';
|
||||
isMRIndex = page === 'projects:merge_requests:index';
|
||||
const page = $('body').attr('data-page');
|
||||
const isIssueIndex = page === 'projects:issues:index';
|
||||
const isMRIndex = page === 'projects:merge_requests:index';
|
||||
$selectbox.hide();
|
||||
// display:block overrides the hide-collapse rule
|
||||
$value.removeAttr('style');
|
||||
|
@ -393,14 +360,13 @@ export default class LabelsSelect {
|
|||
const { $el, e, isMarking } = clickEvent;
|
||||
const label = clickEvent.selectedObj;
|
||||
|
||||
var isIssueIndex, isMRIndex, page, boardsModel;
|
||||
var fadeOutLoader = () => {
|
||||
const fadeOutLoader = () => {
|
||||
$loading.fadeOut();
|
||||
};
|
||||
|
||||
page = $('body').attr('data-page');
|
||||
isIssueIndex = page === 'projects:issues:index';
|
||||
isMRIndex = page === 'projects:merge_requests:index';
|
||||
const page = $('body').attr('data-page');
|
||||
const isIssueIndex = page === 'projects:issues:index';
|
||||
const isMRIndex = page === 'projects:merge_requests:index';
|
||||
|
||||
if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) {
|
||||
$dropdown
|
||||
|
@ -419,6 +385,7 @@ export default class LabelsSelect {
|
|||
return;
|
||||
}
|
||||
|
||||
let boardsModel;
|
||||
if ($dropdown.closest('.add-issues-modal').length) {
|
||||
boardsModel = ModalStore.store.filter;
|
||||
}
|
||||
|
@ -450,7 +417,7 @@ export default class LabelsSelect {
|
|||
}),
|
||||
);
|
||||
} else {
|
||||
var { labels } = boardsStore.detail.issue;
|
||||
let { labels } = boardsStore.detail.issue;
|
||||
labels = labels.filter(selectedLabel => selectedLabel.id !== label.id);
|
||||
boardsStore.detail.issue.labels = labels;
|
||||
}
|
||||
|
@ -578,16 +545,14 @@ export default class LabelsSelect {
|
|||
}
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
setDropdownData($dropdown, isMarking, value) {
|
||||
var i, markedIds, unmarkedIds, indeterminateIds;
|
||||
|
||||
markedIds = $dropdown.data('marked') || [];
|
||||
unmarkedIds = $dropdown.data('unmarked') || [];
|
||||
indeterminateIds = $dropdown.data('indeterminate') || [];
|
||||
const markedIds = $dropdown.data('marked') || [];
|
||||
const unmarkedIds = $dropdown.data('unmarked') || [];
|
||||
const indeterminateIds = $dropdown.data('indeterminate') || [];
|
||||
|
||||
if (isMarking) {
|
||||
markedIds.push(value);
|
||||
|
||||
i = indeterminateIds.indexOf(value);
|
||||
let i = indeterminateIds.indexOf(value);
|
||||
if (i > -1) {
|
||||
indeterminateIds.splice(i, 1);
|
||||
}
|
||||
|
@ -598,7 +563,7 @@ export default class LabelsSelect {
|
|||
}
|
||||
} else {
|
||||
// If marked item (not common) is unmarked
|
||||
i = markedIds.indexOf(value);
|
||||
const i = markedIds.indexOf(value);
|
||||
if (i > -1) {
|
||||
markedIds.splice(i, 1);
|
||||
}
|
||||
|
|
|
@ -84,7 +84,10 @@ export const fetchDashboard = ({ state, dispatch }, params) => {
|
|||
return backOffRequest(() => axios.get(state.dashboardEndpoint, { params }))
|
||||
.then(resp => resp.data)
|
||||
.then(response => {
|
||||
dispatch('receiveMetricsDashboardSuccess', { response, params });
|
||||
dispatch('receiveMetricsDashboardSuccess', {
|
||||
response,
|
||||
params,
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch('receiveMetricsDashboardFailure', error);
|
||||
|
|
|
@ -94,7 +94,7 @@ export default {
|
|||
state.emptyState = 'noData';
|
||||
},
|
||||
[types.SET_ALL_DASHBOARDS](state, dashboards) {
|
||||
state.allDashboards = dashboards;
|
||||
state.allDashboards = dashboards || [];
|
||||
},
|
||||
[types.SET_SHOW_ERROR_BANNER](state, enabled) {
|
||||
state.showErrorBanner = enabled;
|
||||
|
|
|
@ -101,6 +101,7 @@ export default {
|
|||
<time-ago-tooltip :time="createdAt" tooltip-placement="bottom" />
|
||||
</a>
|
||||
</template>
|
||||
<slot name="extra-controls"></slot>
|
||||
<i
|
||||
class="fa fa-spinner fa-spin editing-spinner"
|
||||
: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 sidebarTimeTrackingEventHub from '../../sidebar/event_hub';
|
||||
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 { __ } from '~/locale';
|
||||
import Api from '~/api';
|
||||
|
@ -475,5 +476,20 @@ export const convertToDiscussion = ({ commit }, noteId) =>
|
|||
export const removeConvertedDiscussion = ({ commit }, 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
|
||||
export default () => {};
|
||||
|
|
|
@ -1,34 +1,9 @@
|
|||
import { n__, s__, sprintf } from '~/locale';
|
||||
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
|
||||
* returns an integer
|
||||
*/
|
||||
|
||||
export const getTimeDifferenceMinutes = (noteBeggining, noteEnd) => {
|
||||
const descriptionNoteBegin = new Date(noteBeggining.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 => {
|
||||
let lastDescriptionSystemNote = null;
|
||||
let lastDescriptionSystemNoteIndex = -1;
|
||||
let descriptionChangedTimes = 1;
|
||||
|
||||
return notes.slice(0).reduce((acc, currentNote) => {
|
||||
const note = currentNote.notes[0];
|
||||
|
@ -70,32 +44,24 @@ export const collapseSystemNotes = notes => {
|
|||
} else if (lastDescriptionSystemNote) {
|
||||
const timeDifferenceMinutes = getTimeDifferenceMinutes(lastDescriptionSystemNote, note);
|
||||
|
||||
// are they less than 10 minutes apart?
|
||||
if (timeDifferenceMinutes > 10) {
|
||||
// reset counter
|
||||
descriptionChangedTimes = 1;
|
||||
// are they less than 10 minutes apart from the same user?
|
||||
if (timeDifferenceMinutes > 10 || note.author.id !== lastDescriptionSystemNote.author.id) {
|
||||
// update the previous system note
|
||||
lastDescriptionSystemNote = note;
|
||||
lastDescriptionSystemNoteIndex = acc.length;
|
||||
} else {
|
||||
// increase counter
|
||||
descriptionChangedTimes += 1;
|
||||
// set the first version to fetch grouped system note versions
|
||||
note.start_description_version_id = lastDescriptionSystemNote.description_version_id;
|
||||
|
||||
// delete the previous one
|
||||
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
|
||||
lastDescriptionSystemNoteIndex = acc.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
acc.push(currentNote);
|
||||
return acc;
|
||||
}, []);
|
||||
|
|
|
@ -8,12 +8,13 @@ import {
|
|||
GlModalDirective,
|
||||
GlEmptyState,
|
||||
} from '@gitlab/ui';
|
||||
import createFlash from '../../flash';
|
||||
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
|
||||
import Icon from '../../vue_shared/components/icon.vue';
|
||||
import createFlash from '~/flash';
|
||||
import Tracking from '~/tracking';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import TableRegistry from './table_registry.vue';
|
||||
import { errorMessages, errorMessagesTypes } from '../constants';
|
||||
import { __ } from '../../locale';
|
||||
import { DELETE_REPO_ERROR_MESSAGE } from '../constants';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
export default {
|
||||
name: 'CollapsibeContainerRegisty',
|
||||
|
@ -30,6 +31,7 @@ export default {
|
|||
GlTooltip: GlTooltipDirective,
|
||||
GlModal: GlModalDirective,
|
||||
},
|
||||
mixins: [Tracking.mixin({})],
|
||||
props: {
|
||||
repo: {
|
||||
type: Object,
|
||||
|
@ -40,6 +42,10 @@ export default {
|
|||
return {
|
||||
isOpen: false,
|
||||
modalId: `confirm-repo-deletion-modal-${this.repo.id}`,
|
||||
tracking: {
|
||||
category: document.body.dataset.page,
|
||||
label: 'registry_repository_delete',
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -61,15 +67,13 @@ export default {
|
|||
}
|
||||
},
|
||||
handleDeleteRepository() {
|
||||
this.track('confirm_delete', {});
|
||||
return this.deleteItem(this.repo)
|
||||
.then(() => {
|
||||
createFlash(__('This container registry has been scheduled for deletion.'), 'notice');
|
||||
this.fetchRepos();
|
||||
})
|
||||
.catch(() => this.showError(errorMessagesTypes.DELETE_REPO));
|
||||
},
|
||||
showError(message) {
|
||||
createFlash(errorMessages[message]);
|
||||
.catch(() => createFlash(DELETE_REPO_ERROR_MESSAGE));
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -97,10 +101,9 @@ export default {
|
|||
v-gl-modal="modalId"
|
||||
:title="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"
|
||||
variant="danger"
|
||||
@click="track('click_button', {})"
|
||||
>
|
||||
<icon name="remove" />
|
||||
</gl-button>
|
||||
|
@ -124,7 +127,13 @@ export default {
|
|||
class="mx-auto my-0"
|
||||
/>
|
||||
</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>
|
||||
<p
|
||||
v-html="
|
||||
|
|
|
@ -1,20 +1,15 @@
|
|||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import {
|
||||
GlButton,
|
||||
GlFormCheckbox,
|
||||
GlTooltipDirective,
|
||||
GlModal,
|
||||
GlModalDirective,
|
||||
} from '@gitlab/ui';
|
||||
import { n__, s__, sprintf } from '../../locale';
|
||||
import createFlash from '../../flash';
|
||||
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
|
||||
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';
|
||||
import { GlButton, GlFormCheckbox, GlTooltipDirective, GlModal } from '@gitlab/ui';
|
||||
import Tracking from '~/tracking';
|
||||
import { n__, s__, sprintf } from '~/locale';
|
||||
import createFlash from '~/flash';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
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 { numberToHumanSize } from '~/lib/utils/number_utils';
|
||||
import { FETCH_REGISTRY_ERROR_MESSAGE, DELETE_REGISTRY_ERROR_MESSAGE } from '../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -27,7 +22,6 @@ export default {
|
|||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
GlModal: GlModalDirective,
|
||||
},
|
||||
mixins: [timeagoMixin],
|
||||
props: {
|
||||
|
@ -65,12 +59,21 @@ export default {
|
|||
this.itemsToBeDeleted.length === 0 ? 1 : this.itemsToBeDeleted.length,
|
||||
);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.deleteModal.$refs.modal.$on('hide', this.removeModalEvents);
|
||||
isMultiDelete() {
|
||||
return this.itemsToBeDeleted.length > 1;
|
||||
},
|
||||
tracking() {
|
||||
return {
|
||||
property: this.repo.name,
|
||||
label: this.isMultiDelete ? 'bulk_registry_tag_delete' : 'registry_tag_delete',
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['fetchList', 'deleteItem', 'multiDeleteItems']),
|
||||
track(action) {
|
||||
Tracking.event(document.body.dataset.page, action, this.tracking);
|
||||
},
|
||||
setModalDescription(itemIndex = -1) {
|
||||
if (itemIndex === -1) {
|
||||
this.modalDescription = sprintf(
|
||||
|
@ -92,17 +95,11 @@ export default {
|
|||
formatSize(size) {
|
||||
return numberToHumanSize(size);
|
||||
},
|
||||
removeModalEvents() {
|
||||
this.$refs.deleteModal.$refs.modal.$off('ok');
|
||||
},
|
||||
deleteSingleItem(index) {
|
||||
this.setModalDescription(index);
|
||||
this.itemsToBeDeleted = [index];
|
||||
|
||||
this.$refs.deleteModal.$refs.modal.$once('ok', () => {
|
||||
this.removeModalEvents();
|
||||
this.handleSingleDelete(this.repo.list[index]);
|
||||
});
|
||||
this.track('click_button');
|
||||
this.$refs.deleteModal.show();
|
||||
},
|
||||
deleteMultipleItems() {
|
||||
this.itemsToBeDeleted = [...this.selectedItems];
|
||||
|
@ -111,17 +108,14 @@ export default {
|
|||
} else if (this.selectedItems.length > 1) {
|
||||
this.setModalDescription();
|
||||
}
|
||||
|
||||
this.$refs.deleteModal.$refs.modal.$once('ok', () => {
|
||||
this.removeModalEvents();
|
||||
this.handleMultipleDelete();
|
||||
});
|
||||
this.track('click_button');
|
||||
this.$refs.deleteModal.show();
|
||||
},
|
||||
handleSingleDelete(itemToDelete) {
|
||||
this.itemsToBeDeleted = [];
|
||||
this.deleteItem(itemToDelete)
|
||||
.then(() => this.fetchList({ repo: this.repo }))
|
||||
.catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY));
|
||||
.catch(() => createFlash(DELETE_REGISTRY_ERROR_MESSAGE));
|
||||
},
|
||||
handleMultipleDelete() {
|
||||
const { itemsToBeDeleted } = this;
|
||||
|
@ -134,19 +128,16 @@ export default {
|
|||
items: itemsToBeDeleted.map(x => this.repo.list[x].tag),
|
||||
})
|
||||
.then(() => this.fetchList({ repo: this.repo }))
|
||||
.catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY));
|
||||
.catch(() => createFlash(DELETE_REGISTRY_ERROR_MESSAGE));
|
||||
} else {
|
||||
this.showError(errorMessagesTypes.DELETE_REGISTRY);
|
||||
createFlash(DELETE_REGISTRY_ERROR_MESSAGE);
|
||||
}
|
||||
},
|
||||
onPageChange(pageNumber) {
|
||||
this.fetchList({ repo: this.repo, page: pageNumber }).catch(() =>
|
||||
this.showError(errorMessagesTypes.FETCH_REGISTRY),
|
||||
createFlash(FETCH_REGISTRY_ERROR_MESSAGE),
|
||||
);
|
||||
},
|
||||
showError(message) {
|
||||
createFlash(errorMessages[message]);
|
||||
},
|
||||
onSelectAllChange() {
|
||||
if (this.selectAllChecked) {
|
||||
this.deselectAll();
|
||||
|
@ -179,6 +170,15 @@ export default {
|
|||
canDeleteRow(item) {
|
||||
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>
|
||||
|
@ -202,12 +202,10 @@ export default {
|
|||
<th>
|
||||
<gl-button
|
||||
v-if="canDeleteRepo"
|
||||
ref="bulkDeleteButton"
|
||||
v-gl-tooltip
|
||||
v-gl-modal="modalId"
|
||||
:disabled="!selectedItems || selectedItems.length === 0"
|
||||
class="js-delete-registry float-right"
|
||||
data-track-event="click_button"
|
||||
data-track-label="bulk_registry_tag_delete"
|
||||
class="float-right"
|
||||
variant="danger"
|
||||
:title="s__('ContainerRegistry|Remove selected tags')"
|
||||
:aria-label="s__('ContainerRegistry|Remove selected tags')"
|
||||
|
@ -259,11 +257,8 @@ export default {
|
|||
<td class="content action-buttons">
|
||||
<gl-button
|
||||
v-if="canDeleteRow(item)"
|
||||
v-gl-modal="modalId"
|
||||
:title="s__('ContainerRegistry|Remove tag')"
|
||||
:aria-label="s__('ContainerRegistry|Remove tag')"
|
||||
data-track-event="click_button"
|
||||
data-track-label="registry_tag_delete"
|
||||
variant="danger"
|
||||
class="js-delete-registry-row float-right btn-inverted btn-border-color btn-icon"
|
||||
@click="deleteSingleItem(index)"
|
||||
|
@ -282,7 +277,13 @@ export default {
|
|||
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-ok>{{ modalAction }}</template>
|
||||
<p v-html="modalDescription"></p>
|
||||
|
|
|
@ -1,15 +1,8 @@
|
|||
import { __ } from '../locale';
|
||||
|
||||
export const errorMessagesTypes = {
|
||||
FETCH_REGISTRY: 'FETCH_REGISTRY',
|
||||
FETCH_REPOS: 'FETCH_REPOS',
|
||||
DELETE_REPO: 'DELETE_REPO',
|
||||
DELETE_REGISTRY: 'DELETE_REGISTRY',
|
||||
};
|
||||
|
||||
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.'),
|
||||
};
|
||||
export const FETCH_REGISTRY_ERROR_MESSAGE = __(
|
||||
'Something went wrong while fetching the registry list.',
|
||||
);
|
||||
export const FETCH_REPOS_ERROR_MESSAGE = __('Something went wrong while fetching the projects.');
|
||||
export const DELETE_REPO_ERROR_MESSAGE = __('Something went wrong on our end.');
|
||||
export const DELETE_REGISTRY_ERROR_MESSAGE = __('Something went wrong on our end.');
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import axios from '~/lib/utils/axios_utils';
|
||||
import createFlash from '~/flash';
|
||||
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 }) => {
|
||||
commit(types.TOGGLE_MAIN_LOADING);
|
||||
|
@ -14,7 +14,7 @@ export const fetchRepos = ({ commit, state }) => {
|
|||
})
|
||||
.catch(() => {
|
||||
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(() => {
|
||||
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 { parseIntPagination, normalizeHeaders } from '../../lib/utils/common_utils';
|
||||
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
|
||||
|
||||
export default {
|
||||
[types.SET_MAIN_ENDPOINT](state, endpoint) {
|
||||
Object.assign(state, { endpoint });
|
||||
state.endpoint = endpoint;
|
||||
},
|
||||
|
||||
[types.SET_IS_DELETE_DISABLED](state, isDeleteDisabled) {
|
||||
Object.assign(state, { isDeleteDisabled });
|
||||
state.isDeleteDisabled = isDeleteDisabled;
|
||||
},
|
||||
|
||||
[types.SET_REPOS_LIST](state, list) {
|
||||
Object.assign(state, {
|
||||
repos: list.map(el => ({
|
||||
canDelete: Boolean(el.destroy_path),
|
||||
destroyPath: el.destroy_path,
|
||||
id: el.id,
|
||||
isLoading: false,
|
||||
list: [],
|
||||
location: el.location,
|
||||
name: el.path,
|
||||
tagsPath: el.tags_path,
|
||||
projectId: el.project_id,
|
||||
})),
|
||||
});
|
||||
state.repos = list.map(el => ({
|
||||
canDelete: Boolean(el.destroy_path),
|
||||
destroyPath: el.destroy_path,
|
||||
id: el.id,
|
||||
isLoading: false,
|
||||
list: [],
|
||||
location: el.location,
|
||||
name: el.path,
|
||||
tagsPath: el.tags_path,
|
||||
projectId: el.project_id,
|
||||
}));
|
||||
},
|
||||
|
||||
[types.TOGGLE_MAIN_LOADING](state) {
|
||||
Object.assign(state, { isLoading: !state.isLoading });
|
||||
state.isLoading = !state.isLoading;
|
||||
},
|
||||
|
||||
[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 { visitUrl } from './lib/utils/url_utility';
|
||||
|
@ -9,9 +9,8 @@ export default class TreeView {
|
|||
// Code browser tree slider
|
||||
// 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) {
|
||||
var $clickedEl, path;
|
||||
$clickedEl = $(e.target);
|
||||
path = $('.tree-item-file-name a', this).attr('href');
|
||||
const $clickedEl = $(e.target);
|
||||
const path = $('.tree-item-file-name a', this).attr('href');
|
||||
if (!$clickedEl.is('a') && !$clickedEl.is('.str-truncated')) {
|
||||
if (e.metaKey || e.which === 2) {
|
||||
e.preventDefault();
|
||||
|
@ -26,11 +25,10 @@ export default class TreeView {
|
|||
}
|
||||
|
||||
initKeyNav() {
|
||||
var li, liSelected;
|
||||
li = $('tr.tree-item');
|
||||
liSelected = null;
|
||||
const li = $('tr.tree-item');
|
||||
let liSelected = null;
|
||||
return $('body').keydown(e => {
|
||||
var next, path;
|
||||
let next, path;
|
||||
if ($('input:focus').length > 0 && (e.which === 38 || e.which === 40)) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -17,9 +17,11 @@
|
|||
* />
|
||||
*/
|
||||
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 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 { spriteIcon } from '../../../lib/utils/common_utils';
|
||||
import initMRPopovers from '~/mr_popover/';
|
||||
|
@ -32,7 +34,9 @@ export default {
|
|||
Icon,
|
||||
noteHeader,
|
||||
TimelineEntryItem,
|
||||
GlSkeletonLoading,
|
||||
},
|
||||
mixins: [descriptionVersionHistoryMixin],
|
||||
props: {
|
||||
note: {
|
||||
type: Object,
|
||||
|
@ -75,13 +79,16 @@ export default {
|
|||
mounted() {
|
||||
initMRPopovers(this.$el.querySelectorAll('.gfm-merge_request'));
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['fetchDescriptionVersion']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<timeline-entry-item
|
||||
:id="noteAnchorId"
|
||||
:class="{ target: isTargetNote }"
|
||||
:class="{ target: isTargetNote, 'pr-0': shouldShowDescriptionVersion }"
|
||||
class="note system-note note-wrapper"
|
||||
>
|
||||
<div class="timeline-icon" v-html="iconHtml"></div>
|
||||
|
@ -89,14 +96,18 @@ export default {
|
|||
<div class="note-header">
|
||||
<note-header :author="note.author" :created-at="note.created_at" :note-id="note.id">
|
||||
<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>
|
||||
</div>
|
||||
<div class="note-body">
|
||||
<div
|
||||
:class="{
|
||||
'system-note-commit-list': hasMoreCommits,
|
||||
'hide-shade': expanded,
|
||||
}"
|
||||
:class="{ 'system-note-commit-list': hasMoreCommits, 'hide-shade': expanded }"
|
||||
class="note-text md"
|
||||
v-html="note.note_html"
|
||||
></div>
|
||||
|
@ -106,6 +117,12 @@ export default {
|
|||
<span>{{ __('Toggle commit list') }}</span>
|
||||
</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>
|
||||
</timeline-entry-item>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
$notification-box-shadow-color: rgba(0, 0, 0, 0.25);
|
||||
|
||||
.flash-container {
|
||||
margin-top: 10px;
|
||||
margin: 0;
|
||||
margin-bottom: $gl-padding;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
|
@ -41,6 +41,7 @@ $notification-box-shadow-color: rgba(0, 0, 0, 0.25);
|
|||
.flash-success,
|
||||
.flash-warning {
|
||||
padding: $gl-padding $gl-padding-32 $gl-padding ($gl-padding + $gl-padding-4);
|
||||
margin-top: 10px;
|
||||
|
||||
.container-fluid,
|
||||
.container-fluid.container-limited {
|
||||
|
|
|
@ -310,6 +310,17 @@ $note-form-margin-left: 72px;
|
|||
.note-body {
|
||||
overflow: hidden;
|
||||
|
||||
.description-version {
|
||||
pre {
|
||||
max-height: $dropdown-max-height-lg;
|
||||
white-space: pre-wrap;
|
||||
|
||||
&.loading-state {
|
||||
height: 94px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.system-note-commit-list-toggler {
|
||||
color: $blue-600;
|
||||
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
|
||||
end
|
||||
|
||||
def issuable
|
||||
issue || merge_request
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def exactly_one_issuable
|
||||
|
|
|
@ -14,7 +14,13 @@ class PrometheusMetric < ApplicationRecord
|
|||
validates :project, presence: true, unless: :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 :ordered, -> { reorder(created_at: :asc) }
|
||||
|
||||
def priority
|
||||
group_details(group).fetch(:priority)
|
||||
|
|
|
@ -79,3 +79,5 @@ class NoteEntity < API::Entities::Note
|
|||
request.current_user
|
||||
end
|
||||
end
|
||||
|
||||
NoteEntity.prepend_if_ee('EE::NoteEntity')
|
||||
|
|
|
@ -77,15 +77,14 @@ module Metrics
|
|||
# There may be multiple metrics, but they should be
|
||||
# displayed in a single panel/chart.
|
||||
# @return [ActiveRecord::AssociationRelation<PromtheusMetric>]
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def metrics
|
||||
project.prometheus_metrics.where(
|
||||
PrometheusMetricsFinder.new(
|
||||
project: project,
|
||||
group: group_key,
|
||||
title: title,
|
||||
y_label: y_label
|
||||
)
|
||||
).execute
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
# Returns a symbol representing the group that
|
||||
# the dashboard's group title belongs to.
|
||||
|
|
|
@ -152,7 +152,7 @@
|
|||
- email = " (#{@user.unconfirmed_email})"
|
||||
%p This user has an unconfirmed email address#{email}. You may force a confirmation.
|
||||
%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'
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
- model = local_assigns.fetch(:model)
|
||||
|
||||
- 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?
|
||||
|
||||
- if supports_quick_actions
|
||||
|
@ -16,7 +17,7 @@
|
|||
= render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do
|
||||
= render 'projects/zen', f: form, attr: :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
|
||||
= render 'shared/notes/hints', supports_quick_actions: supports_quick_actions
|
||||
.clearfix
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
- link_text = source.is_a?(Group) ? _('Leave group') : _('Leave project')
|
||||
= link_to link_text, polymorphic_path([:leave, source, :members]),
|
||||
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'
|
||||
- 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]),
|
||||
|
|
|
@ -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'
|
||||
|
||||
# rubocop:disable Rails/Output
|
||||
class Gitlab::Seeder::Projects
|
||||
include ActionView::Helpers::NumberHelper
|
||||
|
||||
Sidekiq::Testing.inline! do
|
||||
Gitlab::Seeder.quiet do
|
||||
Gitlab::Seeder.without_gitaly_timeout do
|
||||
project_urls = %w[
|
||||
https://gitlab.com/gitlab-org/gitlab-test.git
|
||||
https://gitlab.com/gitlab-org/gitlab-shell.git
|
||||
https://gitlab.com/gnuwget/wget2.git
|
||||
https://gitlab.com/Commit451/LabCoat.git
|
||||
https://github.com/jashkenas/underscore.git
|
||||
https://github.com/flightjs/flight.git
|
||||
https://github.com/twitter/typeahead.js.git
|
||||
https://github.com/h5bp/html5-boilerplate.git
|
||||
https://github.com/google/material-design-lite.git
|
||||
https://github.com/jlevy/the-art-of-command-line.git
|
||||
https://github.com/FreeCodeCamp/freecodecamp.git
|
||||
https://github.com/google/deepdream.git
|
||||
https://github.com/jtleek/datasharing.git
|
||||
https://github.com/WebAssembly/design.git
|
||||
https://github.com/airbnb/javascript.git
|
||||
https://github.com/tessalt/echo-chamber-js.git
|
||||
https://github.com/atom/atom.git
|
||||
https://github.com/mattermost/mattermost-server.git
|
||||
https://github.com/purifycss/purifycss.git
|
||||
https://github.com/facebook/nuclide.git
|
||||
https://github.com/wbkd/awesome-d3.git
|
||||
https://github.com/kilimchoi/engineering-blogs.git
|
||||
https://github.com/gilbarbara/logos.git
|
||||
https://github.com/reduxjs/redux.git
|
||||
https://github.com/awslabs/s2n.git
|
||||
https://github.com/arkency/reactjs_koans.git
|
||||
https://github.com/twbs/bootstrap.git
|
||||
https://github.com/chjj/ttystudio.git
|
||||
https://github.com/MostlyAdequate/mostly-adequate-guide.git
|
||||
https://github.com/octocat/Spoon-Knife.git
|
||||
https://github.com/opencontainers/runc.git
|
||||
https://github.com/googlesamples/android-topeka.git
|
||||
]
|
||||
PROJECT_URLS = %w[
|
||||
https://gitlab.com/gitlab-org/gitlab-test.git
|
||||
https://gitlab.com/gitlab-org/gitlab-shell.git
|
||||
https://gitlab.com/gnuwget/wget2.git
|
||||
https://gitlab.com/Commit451/LabCoat.git
|
||||
https://github.com/jashkenas/underscore.git
|
||||
https://github.com/flightjs/flight.git
|
||||
https://github.com/twitter/typeahead.js.git
|
||||
https://github.com/h5bp/html5-boilerplate.git
|
||||
https://github.com/google/material-design-lite.git
|
||||
https://github.com/jlevy/the-art-of-command-line.git
|
||||
https://github.com/FreeCodeCamp/freecodecamp.git
|
||||
https://github.com/google/deepdream.git
|
||||
https://github.com/jtleek/datasharing.git
|
||||
https://github.com/WebAssembly/design.git
|
||||
https://github.com/airbnb/javascript.git
|
||||
https://github.com/tessalt/echo-chamber-js.git
|
||||
https://github.com/atom/atom.git
|
||||
https://github.com/mattermost/mattermost-server.git
|
||||
https://github.com/purifycss/purifycss.git
|
||||
https://github.com/facebook/nuclide.git
|
||||
https://github.com/wbkd/awesome-d3.git
|
||||
https://github.com/kilimchoi/engineering-blogs.git
|
||||
https://github.com/gilbarbara/logos.git
|
||||
https://github.com/reduxjs/redux.git
|
||||
https://github.com/awslabs/s2n.git
|
||||
https://github.com/arkency/reactjs_koans.git
|
||||
https://github.com/twbs/bootstrap.git
|
||||
https://github.com/chjj/ttystudio.git
|
||||
https://github.com/MostlyAdequate/mostly-adequate-guide.git
|
||||
https://github.com/octocat/Spoon-Knife.git
|
||||
https://github.com/opencontainers/runc.git
|
||||
https://github.com/googlesamples/android-topeka.git
|
||||
]
|
||||
LARGE_PROJECT_URLS = %w[
|
||||
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[
|
||||
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
|
||||
]
|
||||
def seed!
|
||||
Sidekiq::Testing.inline! do
|
||||
create_real_projects!
|
||||
create_large_projects!
|
||||
create_mass_projects!
|
||||
end
|
||||
end
|
||||
|
||||
def create_project(url, force_latest_storage: false)
|
||||
group_path, project_path = url.split('/')[-2..-1]
|
||||
private
|
||||
|
||||
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
|
||||
group = Group.new(
|
||||
name: group_path.titleize,
|
||||
path: group_path
|
||||
)
|
||||
group.description = FFaker::Lorem.sentence
|
||||
group.save!
|
||||
PROJECT_URLS.first(size).each_with_index do |url, i|
|
||||
create_real_project!(url, force_latest_storage: i.even?)
|
||||
end
|
||||
end
|
||||
|
||||
group.add_owner(User.first)
|
||||
end
|
||||
def create_large_projects!
|
||||
return unless ENV['LARGE_PROJECTS'].present?
|
||||
|
||||
project_path.gsub!(".git", "")
|
||||
LARGE_PROJECT_URLS.each(&method(:create_real_project!))
|
||||
|
||||
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 ENV['FORK'].present?
|
||||
puts "\nGenerating forks"
|
||||
|
||||
if force_latest_storage
|
||||
params[:storage_version] = Project::LATEST_STORAGE_VERSION
|
||||
end
|
||||
project_name = ENV['FORK'] == 'true' ? 'torvalds/linux' : ENV['FORK']
|
||||
|
||||
project = nil
|
||||
project = Project.find_by_full_path(project_name)
|
||||
|
||||
Sidekiq::Worker.skipping_transaction_check do
|
||||
project = Projects::CreateService.new(User.first, params).execute
|
||||
User.offset(1).first(5).each do |user|
|
||||
new_project = ::Projects::ForkService.new(project, user).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?
|
||||
if new_project.valid? && (new_project.valid_repo? || new_project.import_state.scheduled?)
|
||||
print '.'
|
||||
else
|
||||
puts project.errors.full_messages
|
||||
print 'F'
|
||||
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
|
||||
new_project.errors.full_messages.each do |error|
|
||||
puts "#{new_project.full_path}: #{error}"
|
||||
end
|
||||
print 'F'
|
||||
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
|
||||
|
|
|
@ -43,7 +43,7 @@ Gitlab::Seeder.quiet do
|
|||
end
|
||||
|
||||
puts "\nGenerating project labels"
|
||||
Project.all.find_each do |project|
|
||||
Project.not_mass_generated.find_each do |project|
|
||||
Gitlab::Seeder::ProjectLabels.new(project).seed!
|
||||
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
|
||||
Gitlab::Seeder.quiet do
|
||||
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?
|
||||
print '.'
|
||||
else
|
||||
|
@ -12,8 +12,8 @@ Sidekiq::Testing.inline! do
|
|||
end
|
||||
end
|
||||
|
||||
Project.all.each do |project|
|
||||
User.all.sample(4).each do |user|
|
||||
Project.not_mass_generated.each do |project|
|
||||
User.not_mass_generated.sample(4).each do |user|
|
||||
if project.add_role(user, Gitlab::Access.sym_options.keys.sample)
|
||||
print '.'
|
||||
else
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
require './spec/support/sidekiq'
|
||||
|
||||
Gitlab::Seeder.quiet do
|
||||
Project.all.each do |project|
|
||||
Project.not_mass_generated.each do |project|
|
||||
5.times do |i|
|
||||
milestone_params = {
|
||||
title: "v#{i}.0",
|
||||
|
|
|
@ -4,7 +4,13 @@ Gitlab::Seeder.quiet do
|
|||
# Limit the number of merge requests per project to avoid long seeds
|
||||
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.each do |branch_name|
|
||||
|
|
|
@ -9,7 +9,7 @@ Sidekiq::Testing.disable! do
|
|||
# that it falls under `Sidekiq::Testing.disable!`.
|
||||
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 = user.keys.create(
|
||||
|
|
|
@ -25,7 +25,7 @@ end
|
|||
eos
|
||||
|
||||
50.times do |i|
|
||||
user = User.all.sample
|
||||
user = User.not_mass_generated.sample
|
||||
|
||||
PersonalSnippet.seed(:id, [{
|
||||
id: i,
|
||||
|
|
|
@ -214,7 +214,7 @@ class Gitlab::Seeder::Pipelines
|
|||
end
|
||||
|
||||
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.seed!
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@ require './spec/support/sidekiq'
|
|||
Gitlab::Seeder.quiet do
|
||||
admin_user = User.find(1)
|
||||
|
||||
Project.all.each do |project|
|
||||
Project.not_mass_generated.each do |project|
|
||||
params = {
|
||||
name: 'master'
|
||||
}
|
||||
|
|
|
@ -217,7 +217,7 @@ Gitlab::Seeder.quiet do
|
|||
flag = 'SEED_CYCLE_ANALYTICS'
|
||||
|
||||
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
|
||||
# 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
|
||||
|
|
|
@ -67,7 +67,7 @@ class Gitlab::Seeder::Environments
|
|||
end
|
||||
|
||||
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.seed!
|
||||
end
|
||||
|
|
|
@ -22,7 +22,7 @@ module Db
|
|||
end
|
||||
|
||||
def self.random_user
|
||||
User.find(User.pluck(:id).sample)
|
||||
User.find(User.not_mass_generated.pluck(:id).sample)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,8 +2,8 @@ require './spec/support/sidekiq'
|
|||
|
||||
Sidekiq::Testing.inline! do
|
||||
Gitlab::Seeder.quiet do
|
||||
User.all.sample(10).each do |user|
|
||||
source_project = Project.public_only.sample
|
||||
User.not_mass_generated.sample(10).each do |user|
|
||||
source_project = Project.not_mass_generated.public_only.sample
|
||||
|
||||
##
|
||||
# 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**
|
||||
|
||||
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
|
||||
[#container-registry-domain-configuration](#container-registry-domain-configuration)
|
||||
and pick one of the two options that fits your case.
|
||||
|
|
|
@ -1219,6 +1219,10 @@ type Epic implements Noteable {
|
|||
hasIssues: Boolean!
|
||||
id: ID!
|
||||
iid: ID!
|
||||
|
||||
"""
|
||||
A list of issues associated with the epic
|
||||
"""
|
||||
issues(
|
||||
"""
|
||||
Returns the elements in the list that come after the specified cursor.
|
||||
|
|
|
@ -3751,7 +3751,7 @@
|
|||
},
|
||||
{
|
||||
"name": "issues",
|
||||
"description": null,
|
||||
"description": "A list of issues associated with the epic",
|
||||
"args": [
|
||||
{
|
||||
"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`
|
||||
|
||||
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`
|
||||
|
||||
|
|
|
@ -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.
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
[`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,
|
||||
|
@ -47,6 +47,8 @@ CAUTION: **Caution:**
|
|||
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.
|
||||
|
||||
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
|
||||
|
||||
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_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_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_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`. | |
|
||||
|
@ -168,6 +171,23 @@ so that you don't have to expose your private data in `.gitlab-ci.yml` (e.g., ad
|
|||
</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
|
||||
|
||||
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)
|
||||
service = AuthorizeFieldService.new(field)
|
||||
|
||||
if service.authorizations?
|
||||
if service.authorizations? && !resolver_skips_authorizations?(field)
|
||||
field.redefine { resolve(service.authorized_resolve) }
|
||||
else
|
||||
field
|
||||
end
|
||||
end
|
||||
|
||||
def resolver_skips_authorizations?(field)
|
||||
field.metadata[:resolver].try(:skip_authorizations?)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,6 +8,10 @@ module Gitlab
|
|||
ActiveRecord::Relation,
|
||||
Gitlab::Graphql::Connections::Keyset::Connection
|
||||
)
|
||||
GraphQL::Relay::BaseConnection.register_connection_implementation(
|
||||
Gitlab::Graphql::FilterableArray,
|
||||
Gitlab::Graphql::Connections::FilterableArrayConnection
|
||||
)
|
||||
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,
|
||||
# includes the record's id in the dashboard config.
|
||||
def transform!
|
||||
common_metrics = ::PrometheusMetric.common
|
||||
common_metrics = ::PrometheusMetricsFinder.new(common: true).execute
|
||||
|
||||
for_metrics do |metric|
|
||||
metric_record = common_metrics.find { |m| m.identifier == metric[:id] }
|
||||
|
|
|
@ -9,7 +9,7 @@ module Gitlab
|
|||
# config. If there are no project-specific metrics,
|
||||
# this will have no effect.
|
||||
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)
|
||||
panel = find_or_create_panel(group[:panels], project_metric)
|
||||
find_or_create_metric(panel[:metrics], project_metric)
|
||||
|
|
|
@ -11,13 +11,15 @@ module Gitlab
|
|||
validates :name, :priority, :metrics, presence: true
|
||||
|
||||
def self.common_metrics
|
||||
all_groups = ::PrometheusMetric.common.group_by(&:group_title).map do |name, metrics|
|
||||
MetricGroup.new(
|
||||
name: name,
|
||||
priority: metrics.map(&:priority).max,
|
||||
metrics: metrics.map(&:to_query_metric)
|
||||
)
|
||||
end
|
||||
all_groups = ::PrometheusMetricsFinder.new(common: true).execute
|
||||
.group_by(&:group_title)
|
||||
.map do |name, metrics|
|
||||
MetricGroup.new(
|
||||
name: name,
|
||||
priority: metrics.map(&:priority).max,
|
||||
metrics: metrics.map(&:to_query_metric)
|
||||
)
|
||||
end
|
||||
|
||||
all_groups.sort_by(&:priority).reverse
|
||||
end
|
||||
|
|
|
@ -7,11 +7,14 @@ module Gitlab
|
|||
include QueryAdditionalMetrics
|
||||
|
||||
def query(serverless_function_id)
|
||||
PrometheusMetric
|
||||
.find_by_identifier(:system_metrics_knative_function_invocation_count)
|
||||
.to_query_metric.tap do |q|
|
||||
q.queries[0][:result] = run_query(q.queries[0][:query_range], context(serverless_function_id))
|
||||
end
|
||||
PrometheusMetricsFinder
|
||||
.new(identifier: :system_metrics_knative_function_invocation_count, common: true)
|
||||
.execute
|
||||
.first
|
||||
.to_query_metric
|
||||
.tap do |q|
|
||||
q.queries[0][:result] = run_query(q.queries[0][:query_range], context(serverless_function_id))
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
|
|
@ -14,7 +14,71 @@ end
|
|||
|
||||
module Gitlab
|
||||
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
|
||||
# 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_mailer
|
||||
|
||||
|
@ -23,6 +87,7 @@ module Gitlab
|
|||
yield
|
||||
|
||||
SeedFu.quiet = false
|
||||
ActiveRecord::Base.logger = old_logger
|
||||
puts "\nOK".color(:green)
|
||||
end
|
||||
|
||||
|
|
|
@ -5,6 +5,10 @@ namespace :dev do
|
|||
task setup: :environment do
|
||||
ENV['force'] = 'yes'
|
||||
Rake::Task["gitlab:setup"].invoke
|
||||
|
||||
# Make sure DB statistics are up to date.
|
||||
ActiveRecord::Base.connection.execute('ANALYZE')
|
||||
|
||||
Rake::Task["gitlab:shell:setup"].invoke
|
||||
end
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ namespace :gitlab do
|
|||
|
||||
[project]
|
||||
else
|
||||
Project.find_each
|
||||
Project.not_mass_generated.find_each
|
||||
end
|
||||
|
||||
projects.each do |project|
|
||||
|
|
|
@ -4386,6 +4386,9 @@ msgstr ""
|
|||
msgid "Compare changes with the merge request target branch"
|
||||
msgstr ""
|
||||
|
||||
msgid "Compare with previous version"
|
||||
msgstr ""
|
||||
|
||||
msgid "CompareBranches|%{source_branch} and %{target_branch} are the same."
|
||||
msgstr ""
|
||||
|
||||
|
@ -5683,6 +5686,9 @@ msgstr ""
|
|||
msgid "Descending"
|
||||
msgstr ""
|
||||
|
||||
msgid "Describe the goal of the changes and what reviewers should be aware of."
|
||||
msgstr ""
|
||||
|
||||
msgid "Description"
|
||||
msgstr ""
|
||||
|
||||
|
@ -10711,9 +10717,6 @@ msgstr ""
|
|||
msgid "MergeRequests|started a thread on commit %{linkStart}%{commitDisplay}%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "MergeRequest| %{paragraphStart}changed the description %{descriptionChangedTimes} times %{timeDifferenceMinutes}%{paragraphEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "MergeRequest|Error dismissing suggestion popover. Please try again."
|
||||
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."
|
||||
msgstr ""
|
||||
|
||||
msgid "Metrics|Validating query"
|
||||
msgstr ""
|
||||
|
||||
msgid "Metrics|Y-axis label"
|
||||
msgstr ""
|
||||
|
||||
|
@ -15926,6 +15932,9 @@ msgstr ""
|
|||
msgid "Something went wrong while fetching comments. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Something went wrong while fetching description changes. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Something went wrong while fetching group member contributions"
|
||||
msgstr ""
|
||||
|
||||
|
@ -21193,10 +21202,5 @@ msgstr ""
|
|||
msgid "with %{additions} additions, %{deletions} deletions."
|
||||
msgstr ""
|
||||
|
||||
msgid "within %d minute "
|
||||
msgid_plural "within %d minutes "
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "yaml invalid"
|
||||
msgstr ""
|
||||
|
|
|
@ -10,9 +10,19 @@ module QA
|
|||
element :impersonate_user_link
|
||||
end
|
||||
|
||||
view 'app/views/admin/users/show.html.haml' do
|
||||
element :confirm_user_button
|
||||
end
|
||||
|
||||
def click_impersonate_user
|
||||
click_element(:impersonate_user_link)
|
||||
end
|
||||
|
||||
def confirm_user
|
||||
accept_confirm do
|
||||
click_element :confirm_user_button
|
||||
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
|
||||
end
|
||||
|
||||
view 'app/views/shared/members/_access_request_links.html.haml' do
|
||||
element :leave_group_link
|
||||
end
|
||||
|
||||
def click_subgroup(name)
|
||||
click_link name
|
||||
end
|
||||
|
@ -42,6 +46,12 @@ module QA
|
|||
click_element :new_in_group_button
|
||||
end
|
||||
|
||||
def leave_group
|
||||
accept_alert do
|
||||
click_element :leave_group_link
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def select_kind(kind)
|
||||
|
|
|
@ -64,12 +64,11 @@ module QA
|
|||
end
|
||||
|
||||
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
|
||||
visit(web_url)
|
||||
|
||||
wait { current_url == web_url }
|
||||
wait { current_url.include?(URI.parse(web_url).path.split('/').last || web_url) }
|
||||
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 }
|
||||
end
|
||||
|
||||
def list_members
|
||||
JSON.parse(get(Runtime::API::Request.new(api_client, api_members_path).url).body)
|
||||
end
|
||||
|
||||
def api_members_path
|
||||
"#{api_get_path}/members"
|
||||
end
|
||||
|
|
|
@ -7,6 +7,8 @@ module QA
|
|||
# creating it if it doesn't yet exist.
|
||||
#
|
||||
class Sandbox < Base
|
||||
include Members
|
||||
|
||||
attr_accessor :path
|
||||
|
||||
attribute :id
|
||||
|
|
|
@ -57,13 +57,13 @@ module QA
|
|||
|
||||
Capybara.register_driver QA::Runtime::Env.browser do |app|
|
||||
capabilities = Selenium::WebDriver::Remote::Capabilities.send(QA::Runtime::Env.browser,
|
||||
# This enables access to logs with `page.driver.manage.get_log(:browser)`
|
||||
loggingPrefs: {
|
||||
browser: "ALL",
|
||||
client: "ALL",
|
||||
driver: "ALL",
|
||||
server: "ALL"
|
||||
})
|
||||
# This enables access to logs with `page.driver.manage.get_log(:browser)`
|
||||
loggingPrefs: {
|
||||
browser: "ALL",
|
||||
client: "ALL",
|
||||
driver: "ALL",
|
||||
server: "ALL"
|
||||
})
|
||||
|
||||
if QA::Runtime::Env.accept_insecure_certs?
|
||||
capabilities['acceptInsecureCerts'] = true
|
||||
|
|
|
@ -19,6 +19,28 @@ module QA
|
|||
set_feature(key, false)
|
||||
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)
|
||||
feature = JSON.parse(get_features).find { |flag| flag["name"] == key }
|
||||
feature && feature["state"] == "on"
|
||||
|
|
|
@ -8,7 +8,9 @@ module QA
|
|||
|
||||
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')
|
||||
end
|
||||
|
|
|
@ -7,18 +7,22 @@ module QA
|
|||
module SAMLIdp
|
||||
module Page
|
||||
class Login < Page::Base
|
||||
def login
|
||||
fill_in 'username', with: 'user1'
|
||||
fill_in 'password', with: 'user1pass'
|
||||
def login(username, password)
|
||||
QA::Runtime::Logger.debug("Logging into SAMLIdp with username: #{username} and password:#{password}") if QA::Runtime::Env.debug?
|
||||
|
||||
fill_in 'username', with: username
|
||||
fill_in 'password', with: password
|
||||
click_on 'Login'
|
||||
end
|
||||
|
||||
def login_if_required
|
||||
login if login_required?
|
||||
def login_if_required(username, password)
|
||||
login(username, password) if login_required?
|
||||
end
|
||||
|
||||
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
|
||||
|
|
|
@ -20,7 +20,7 @@ RSpec.configure do |config|
|
|||
QA::Specs::Helpers::Quarantine.configure_rspec
|
||||
|
||||
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
|
||||
|
||||
config.after(:context) do
|
||||
|
|
|
@ -25,6 +25,11 @@ describe "User creates a merge request", :js do
|
|||
|
||||
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)
|
||||
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');
|
||||
});
|
||||
|
||||
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', () => {
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
|
|
|
@ -1094,8 +1094,9 @@ export const collapsedSystemNotes = [
|
|||
noteable_type: 'Issue',
|
||||
resolvable: false,
|
||||
noteable_iid: 12,
|
||||
start_description_version_id: undefined,
|
||||
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 },
|
||||
resolved: false,
|
||||
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',
|
||||
human_access: 'Owner',
|
||||
path: '/gitlab-org/gitlab-shell/notes/905',
|
||||
times_updated: 2,
|
||||
},
|
||||
],
|
||||
individual_note: true,
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
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 Tracking from '~/tracking';
|
||||
import collapsibleComponent from '~/registry/components/collapsible_container.vue';
|
||||
import * as getters from '~/registry/stores/getters';
|
||||
import { repoPropsData } from '../mock_data';
|
||||
|
||||
jest.mock('~/flash.js');
|
||||
|
||||
|
@ -16,9 +17,10 @@ describe('collapsible registry container', () => {
|
|||
let wrapper;
|
||||
let store;
|
||||
|
||||
const findDeleteBtn = w => w.find('.js-remove-repo');
|
||||
const findContainerImageTags = w => w.find('.container-image-tags');
|
||||
const findToggleRepos = w => w.findAll('.js-toggle-repo');
|
||||
const findDeleteBtn = (w = wrapper) => w.find('.js-remove-repo');
|
||||
const findContainerImageTags = (w = wrapper) => w.find('.container-image-tags');
|
||||
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 });
|
||||
|
||||
|
@ -124,4 +126,45 @@ describe('collapsible registry container', () => {
|
|||
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 Vuex from 'vuex';
|
||||
import tableRegistry from '~/registry/components/table_registry.vue';
|
||||
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 * as getters from '~/registry/stores/getters';
|
||||
|
||||
jest.mock('~/flash');
|
||||
|
||||
const [firstImage, secondImage] = repoPropsData.list;
|
||||
|
||||
const localVue = createLocalVue();
|
||||
|
@ -15,11 +19,12 @@ describe('table registry', () => {
|
|||
let wrapper;
|
||||
let store;
|
||||
|
||||
const findSelectAllCheckbox = w => w.find('.js-select-all-checkbox > input');
|
||||
const findSelectCheckboxes = w => w.findAll('.js-select-checkbox > input');
|
||||
const findDeleteButton = w => w.find('.js-delete-registry');
|
||||
const findDeleteButtonsRow = w => w.findAll('.js-delete-registry-row');
|
||||
const findPagination = w => w.find('.js-registry-pagination');
|
||||
const findSelectAllCheckbox = (w = wrapper) => w.find('.js-select-all-checkbox > input');
|
||||
const findSelectCheckboxes = (w = wrapper) => w.findAll('.js-select-checkbox > input');
|
||||
const findDeleteButton = (w = wrapper) => w.find({ ref: 'bulkDeleteButton' });
|
||||
const findDeleteButtonsRow = (w = wrapper) => w.findAll('.js-delete-registry-row');
|
||||
const findPagination = (w = wrapper) => w.find('.js-registry-pagination');
|
||||
const findDeleteModal = (w = wrapper) => w.find({ ref: 'deleteModal' });
|
||||
const bulkDeletePath = 'path';
|
||||
|
||||
const mountWithStore = config => mount(tableRegistry, { ...config, store, localVue });
|
||||
|
@ -139,7 +144,7 @@ describe('table registry', () => {
|
|||
},
|
||||
});
|
||||
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', () => {
|
||||
const repo = {
|
||||
repoPropsData,
|
||||
|
@ -265,4 +291,83 @@ describe('table registry', () => {
|
|||
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:
|
||||
// https://gitlab.com/gitlab-org/gitlab-foss/uploads/b07a10670919254f0220d3ff5c1aa110/jqzI.png
|
||||
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', () => {
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
/* eslint-disable no-var */
|
||||
|
||||
import $ from 'jquery';
|
||||
import '~/commons/bootstrap';
|
||||
|
||||
|
@ -10,15 +8,13 @@ describe('Bootstrap jQuery extensions', function() {
|
|||
});
|
||||
|
||||
it('adds the disabled attribute', function() {
|
||||
var $input;
|
||||
$input = $('input').first();
|
||||
const $input = $('input').first();
|
||||
$input.disable();
|
||||
|
||||
expect($input).toHaveAttr('disabled', 'disabled');
|
||||
});
|
||||
return it('adds the disabled class', function() {
|
||||
var $input;
|
||||
$input = $('input').first();
|
||||
const $input = $('input').first();
|
||||
$input.disable();
|
||||
|
||||
expect($input).toHaveClass('disabled');
|
||||
|
@ -30,15 +26,13 @@ describe('Bootstrap jQuery extensions', function() {
|
|||
});
|
||||
|
||||
it('removes the disabled attribute', function() {
|
||||
var $input;
|
||||
$input = $('input').first();
|
||||
const $input = $('input').first();
|
||||
$input.enable();
|
||||
|
||||
expect($input).not.toHaveAttr('disabled');
|
||||
});
|
||||
return it('removes the disabled class', function() {
|
||||
var $input;
|
||||
$input = $('input').first();
|
||||
const $input = $('input').first();
|
||||
$input.enable();
|
||||
|
||||
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', () => {
|
||||
let spy;
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -144,7 +144,19 @@ describe('Monitoring mutations', () => {
|
|||
});
|
||||
|
||||
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);
|
||||
|
||||
expect(stateCopy.allDashboards).toEqual(dashboardGitResponse);
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import {
|
||||
isDescriptionSystemNote,
|
||||
changeDescriptionNote,
|
||||
getTimeDifferenceMinutes,
|
||||
collapseSystemNotes,
|
||||
} 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', () => {
|
||||
const anotherSystemNote = {
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
it_behaves_like "connection with paged nodes"
|
||||
|
||||
context 'when both are passed' do
|
||||
let(:arguments) { { first: 2, last: 2 } }
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -13,14 +13,19 @@ describe Gitlab::Prometheus::Queries::KnativeInvocationQuery do
|
|||
|
||||
context 'verify queries' do
|
||||
before do
|
||||
allow(PrometheusMetric).to receive(:find_by_identifier).and_return(create(:prometheus_metric, query: prometheus_istio_query('test-name', 'test-ns')))
|
||||
allow(client).to receive(:query_range)
|
||||
create(:prometheus_metric,
|
||||
: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
|
||||
|
||||
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
|
||||
|
|
|
@ -37,9 +37,12 @@ module GraphqlHelpers
|
|||
# BatchLoader::GraphQL returns a wrapper, so we need to :sync in order
|
||||
# to get the actual values
|
||||
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
|
||||
|
||||
def graphql_query_for(name, attributes = {}, fields = nil)
|
||||
|
@ -157,7 +160,13 @@ module GraphqlHelpers
|
|||
|
||||
def attributes_to_graphql(attributes)
|
||||
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
|
||||
|
||||
|
@ -282,6 +291,12 @@ module GraphqlHelpers
|
|||
def allow_high_graphql_recursion
|
||||
allow_any_instance_of(Gitlab::Graphql::QueryAnalyzers::RecursionAnalyzer).to receive(:recursion_threshold).and_return 1000
|
||||
end
|
||||
|
||||
def node_array(data, extract_attribute = nil)
|
||||
data.map do |item|
|
||||
extract_attribute ? item['node'][extract_attribute] : item['node']
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# 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