提交 03fbe618 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 e69aae81
...@@ -9,6 +9,7 @@ require: ...@@ -9,6 +9,7 @@ require:
inherit_from: inherit_from:
- .rubocop_todo.yml - .rubocop_todo.yml
- ./rubocop/rubocop-migrations.yml - ./rubocop/rubocop-migrations.yml
- ./rubocop/rubocop-usage-data.yml
inherit_mode: inherit_mode:
merge: merge:
......
...@@ -14,7 +14,7 @@ export default () => { ...@@ -14,7 +14,7 @@ export default () => {
new ProtectedTagEditList(); new ProtectedTagEditList();
initDeployKeys(); initDeployKeys();
initSettingsPanels(); initSettingsPanels();
new ProtectedBranchCreate(); new ProtectedBranchCreate({ hasLicense: false });
new ProtectedBranchEditList(); new ProtectedBranchEditList();
new DueDateSelectors(); new DueDateSelectors();
fileUpload('.js-choose-file', '.js-object-map-input'); fileUpload('.js-choose-file', '.js-object-map-input');
......
/* eslint-disable no-underscore-dangle, class-methods-use-this */
import { escape, find, countBy } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import Flash from '~/flash';
import { n__, s__, __ } from '~/locale';
import { LEVEL_TYPES, LEVEL_ID_PROP, ACCESS_LEVEL_NONE } from './constants';
export default class AccessDropdown {
constructor(options) {
const { $dropdown, accessLevel, accessLevelsData, hasLicense = true } = options;
this.options = options;
this.hasLicense = hasLicense;
this.groups = [];
this.accessLevel = accessLevel;
this.accessLevelsData = accessLevelsData.roles;
this.$dropdown = $dropdown;
this.$wrap = this.$dropdown.closest(`.${this.accessLevel}-container`);
this.usersPath = '/-/autocomplete/users.json';
this.groupsPath = '/-/autocomplete/project_groups.json';
this.defaultLabel = this.$dropdown.data('defaultLabel');
this.setSelectedItems([]);
this.persistPreselectedItems();
this.noOneObj = this.accessLevelsData.find(level => level.id === ACCESS_LEVEL_NONE);
this.initDropdown();
}
initDropdown() {
const { onSelect, onHide } = this.options;
this.$dropdown.glDropdown({
data: this.getData.bind(this),
selectable: true,
filterable: true,
filterRemote: true,
multiSelect: this.$dropdown.hasClass('js-multiselect'),
renderRow: this.renderRow.bind(this),
toggleLabel: this.toggleLabel.bind(this),
hidden() {
if (onHide) {
onHide();
}
},
clicked: options => {
const { $el, e } = options;
const item = options.selectedObj;
e.preventDefault();
if (!this.hasLicense) {
// We're not multiselecting quite yet with FOSS:
// remove all preselected items before selecting this item
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37499
this.accessLevelsData.forEach(level => {
this.removeSelectedItem(level);
});
}
if ($el.is('.is-active')) {
if (this.noOneObj) {
if (item.id === this.noOneObj.id && this.hasLicense) {
// remove all others selected items
this.accessLevelsData.forEach(level => {
if (level.id !== item.id) {
this.removeSelectedItem(level);
}
});
// remove selected item visually
this.$wrap.find(`.item-${item.type}`).removeClass('is-active');
} else {
const $noOne = this.$wrap.find(
`.is-active.item-${item.type}[data-role-id="${this.noOneObj.id}"]`,
);
if ($noOne.length) {
$noOne.removeClass('is-active');
this.removeSelectedItem(this.noOneObj);
}
}
}
// make element active right away
$el.addClass(`is-active item-${item.type}`);
// Add "No one"
this.addSelectedItem(item);
} else {
this.removeSelectedItem(item);
}
if (onSelect) {
onSelect(item, $el, this);
}
},
});
this.$dropdown.find('.dropdown-toggle-text').text(this.toggleLabel());
}
persistPreselectedItems() {
const itemsToPreselect = this.$dropdown.data('preselectedItems');
if (!itemsToPreselect || !itemsToPreselect.length) {
return;
}
const persistedItems = itemsToPreselect.map(item => {
const persistedItem = { ...item };
persistedItem.persisted = true;
return persistedItem;
});
this.setSelectedItems(persistedItems);
}
setSelectedItems(items = []) {
this.items = items;
}
getSelectedItems() {
return this.items.filter(item => !item._destroy);
}
getAllSelectedItems() {
return this.items;
}
// Return dropdown as input data ready to submit
getInputData() {
const selectedItems = this.getAllSelectedItems();
const accessLevels = selectedItems.map(item => {
const obj = {};
if (typeof item.id !== 'undefined') {
obj.id = item.id;
}
if (typeof item._destroy !== 'undefined') {
obj._destroy = item._destroy;
}
if (item.type === LEVEL_TYPES.ROLE) {
obj.access_level = item.access_level;
} else if (item.type === LEVEL_TYPES.USER) {
obj.user_id = item.user_id;
} else if (item.type === LEVEL_TYPES.GROUP) {
obj.group_id = item.group_id;
}
return obj;
});
return accessLevels;
}
addSelectedItem(selectedItem) {
let itemToAdd = {};
let index = -1;
let alreadyAdded = false;
const selectedItems = this.getAllSelectedItems();
// Compare IDs based on selectedItem.type
selectedItems.forEach((item, i) => {
let comparator;
switch (selectedItem.type) {
case LEVEL_TYPES.ROLE:
comparator = LEVEL_ID_PROP.ROLE;
// If the item already exists, just use it
if (item[comparator] === selectedItem.id) {
alreadyAdded = true;
}
break;
case LEVEL_TYPES.GROUP:
comparator = LEVEL_ID_PROP.GROUP;
break;
case LEVEL_TYPES.USER:
comparator = LEVEL_ID_PROP.USER;
break;
default:
break;
}
if (selectedItem.id === item[comparator]) {
index = i;
}
});
if (alreadyAdded) {
return;
}
if (index !== -1 && selectedItems[index]._destroy) {
delete selectedItems[index]._destroy;
return;
}
itemToAdd.type = selectedItem.type;
if (selectedItem.type === LEVEL_TYPES.USER) {
itemToAdd = {
user_id: selectedItem.id,
name: selectedItem.name || '_name1',
username: selectedItem.username || '_username1',
avatar_url: selectedItem.avatar_url || '_avatar_url1',
type: LEVEL_TYPES.USER,
};
} else if (selectedItem.type === LEVEL_TYPES.ROLE) {
itemToAdd = {
access_level: selectedItem.id,
type: LEVEL_TYPES.ROLE,
};
} else if (selectedItem.type === LEVEL_TYPES.GROUP) {
itemToAdd = {
group_id: selectedItem.id,
type: LEVEL_TYPES.GROUP,
};
}
this.items.push(itemToAdd);
}
removeSelectedItem(itemToDelete) {
let index = -1;
const selectedItems = this.getAllSelectedItems();
// To find itemToDelete on selectedItems, first we need the index
selectedItems.every((item, i) => {
if (item.type !== itemToDelete.type) {
return true;
}
if (item.type === LEVEL_TYPES.USER && item.user_id === itemToDelete.id) {
index = i;
} else if (item.type === LEVEL_TYPES.ROLE && item.access_level === itemToDelete.id) {
index = i;
} else if (item.type === LEVEL_TYPES.GROUP && item.group_id === itemToDelete.id) {
index = i;
}
// Break once we have index set
return !(index > -1);
});
// if ItemToDelete is not really selected do nothing
if (index === -1) {
return;
}
if (selectedItems[index].persisted) {
// If we toggle an item that has been already marked with _destroy
if (selectedItems[index]._destroy) {
delete selectedItems[index]._destroy;
} else {
selectedItems[index]._destroy = '1';
}
} else {
selectedItems.splice(index, 1);
}
}
toggleLabel() {
const currentItems = this.getSelectedItems();
const $dropdownToggleText = this.$dropdown.find('.dropdown-toggle-text');
if (currentItems.length === 0) {
$dropdownToggleText.addClass('is-default');
return this.defaultLabel;
}
$dropdownToggleText.removeClass('is-default');
if (currentItems.length === 1 && currentItems[0].type === LEVEL_TYPES.ROLE) {
const roleData = this.accessLevelsData.find(data => data.id === currentItems[0].access_level);
return roleData.text;
}
const labelPieces = [];
const counts = countBy(currentItems, item => item.type);
if (counts[LEVEL_TYPES.ROLE] > 0) {
labelPieces.push(n__('1 role', '%d roles', counts[LEVEL_TYPES.ROLE]));
}
if (counts[LEVEL_TYPES.USER] > 0) {
labelPieces.push(n__('1 user', '%d users', counts[LEVEL_TYPES.USER]));
}
if (counts[LEVEL_TYPES.GROUP] > 0) {
labelPieces.push(n__('1 group', '%d groups', counts[LEVEL_TYPES.GROUP]));
}
return labelPieces.join(', ');
}
getData(query, callback) {
if (this.hasLicense) {
Promise.all([
this.getUsers(query),
this.groupsData ? Promise.resolve(this.groupsData) : this.getGroups(),
])
.then(([usersResponse, groupsResponse]) => {
this.groupsData = groupsResponse;
callback(this.consolidateData(usersResponse.data, groupsResponse.data));
})
.catch(() => Flash(__('Failed to load groups & users.')));
} else {
callback(this.consolidateData());
}
}
consolidateData(usersResponse = [], groupsResponse = []) {
let consolidatedData = [];
// ID property is handled differently locally from the server
//
// For Groups
// In dropdown: `id`
// For submit: `group_id`
//
// For Roles
// In dropdown: `id`
// For submit: `access_level`
//
// For Users
// In dropdown: `id`
// For submit: `user_id`
/*
* Build roles
*/
const roles = this.accessLevelsData.map(level => {
/* eslint-disable no-param-reassign */
// This re-assignment is intentional as
// level.type property is being used in removeSelectedItem()
// for comparision, and accessLevelsData is provided by
// gon.create_access_levels which doesn't have `type` included.
// See this discussion https://gitlab.com/gitlab-org/gitlab/merge_requests/1629#note_31285823
level.type = LEVEL_TYPES.ROLE;
return level;
});
if (roles.length) {
consolidatedData = consolidatedData.concat(
[{ type: 'header', content: s__('AccessDropdown|Roles') }],
roles,
);
}
if (this.hasLicense) {
const map = [];
const selectedItems = this.getSelectedItems();
/*
* Build groups
*/
const groups = groupsResponse.map(group => ({
...group,
type: LEVEL_TYPES.GROUP,
}));
/*
* Build users
*/
const users = selectedItems
.filter(item => item.type === LEVEL_TYPES.USER)
.map(item => {
// Save identifiers for easy-checking more later
map.push(LEVEL_TYPES.USER + item.user_id);
return {
id: item.user_id,
name: item.name,
username: item.username,
avatar_url: item.avatar_url,
type: LEVEL_TYPES.USER,
};
});
// Has to be checked against server response
// because the selected item can be in filter results
usersResponse.forEach(response => {
// Add is it has not been added
if (map.indexOf(LEVEL_TYPES.USER + response.id) === -1) {
const user = { ...response };
user.type = LEVEL_TYPES.USER;
users.push(user);
}
});
if (groups.length) {
if (roles.length) {
consolidatedData = consolidatedData.concat([{ type: 'divider' }]);
}
consolidatedData = consolidatedData.concat(
[{ type: 'header', content: s__('AccessDropdown|Groups') }],
groups,
);
}
if (users.length) {
consolidatedData = consolidatedData.concat(
[{ type: 'divider' }],
[{ type: 'header', content: s__('AccessDropdown|Users') }],
users,
);
}
}
return consolidatedData;
}
getUsers(query) {
return axios.get(this.buildUrl(gon.relative_url_root, this.usersPath), {
params: {
search: query,
per_page: 20,
active: true,
project_id: gon.current_project_id,
push_code: true,
},
});
}
getGroups() {
return axios.get(this.buildUrl(gon.relative_url_root, this.groupsPath), {
params: {
project_id: gon.current_project_id,
},
});
}
buildUrl(urlRoot, url) {
let newUrl;
if (urlRoot != null) {
newUrl = urlRoot.replace(/\/$/, '') + url;
}
return newUrl;
}
renderRow(item) {
let criteria = {};
let groupRowEl;
// Dectect if the current item is already saved so we can add
// the `is-active` class so the item looks as marked
switch (item.type) {
case LEVEL_TYPES.USER:
criteria = { user_id: item.id };
break;
case LEVEL_TYPES.ROLE:
criteria = { access_level: item.id };
break;
case LEVEL_TYPES.GROUP:
criteria = { group_id: item.id };
break;
default:
break;
}
const isActive = find(this.getSelectedItems(), criteria) ? 'is-active' : '';
switch (item.type) {
case LEVEL_TYPES.USER:
groupRowEl = this.userRowHtml(item, isActive);
break;
case LEVEL_TYPES.ROLE:
groupRowEl = this.roleRowHtml(item, isActive);
break;
case LEVEL_TYPES.GROUP:
groupRowEl = this.groupRowHtml(item, isActive);
break;
default:
groupRowEl = '';
break;
}
return groupRowEl;
}
userRowHtml(user, isActive) {
const isActiveClass = isActive || '';
return `
<li>
<a href="#" class="${isActiveClass}">
<img src="${user.avatar_url}" class="avatar avatar-inline" width="30">
<strong class="dropdown-menu-user-full-name">${escape(user.name)}</strong>
<span class="dropdown-menu-user-username">${user.username}</span>
</a>
</li>
`;
}
groupRowHtml(group, isActive) {
const isActiveClass = isActive || '';
const avatarEl = group.avatar_url
? `<img src="${group.avatar_url}" class="avatar avatar-inline" width="30">`
: '';
return `
<li>
<a href="#" class="${isActiveClass}">
${avatarEl}
<span class="dropdown-menu-group-groupname">${group.name}</span>
</a>
</li>
`;
}
roleRowHtml(role, isActive) {
const isActiveClass = isActive || '';
return `
<li>
<a href="#" class="${isActiveClass} item-${role.type}" data-role-id="${role.id}">
${role.text}
</a>
</li>
`;
}
}
export const LEVEL_TYPES = {
ROLE: 'role',
USER: 'user',
GROUP: 'group',
};
export const LEVEL_ID_PROP = {
ROLE: 'access_level',
USER: 'user_id',
GROUP: 'group_id',
};
export const ACCESS_LEVEL_NONE = 0;
export const ACCESS_LEVELS = {
MERGE: 'merge_access_levels',
PUSH: 'push_access_levels',
};
export const LEVEL_TYPES = {
ROLE: 'role',
USER: 'user',
GROUP: 'group',
};
export const LEVEL_ID_PROP = {
ROLE: 'access_level',
USER: 'user_id',
GROUP: 'group_id',
};
export const ACCESS_LEVEL_NONE = 0;
import { __ } from '~/locale';
export default class ProtectedBranchAccessDropdown {
constructor(options) {
this.options = options;
this.initDropdown();
}
initDropdown() {
const { $dropdown, data, onSelect } = this.options;
$dropdown.glDropdown({
data,
selectable: true,
inputId: $dropdown.data('inputId'),
fieldName: $dropdown.data('fieldName'),
toggleLabel(item, $el) {
if ($el.is('.is-active')) {
return item.text;
}
return __('Select');
},
clicked(options) {
options.e.preventDefault();
onSelect();
},
});
}
}
import $ from 'jquery'; import $ from 'jquery';
import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown'; import AccessDropdown from '~/projects/settings/access_dropdown';
import CreateItemDropdown from '../create_item_dropdown'; import axios from '~/lib/utils/axios_utils';
import AccessorUtilities from '../lib/utils/accessor'; import AccessorUtilities from '~/lib/utils/accessor';
import Flash from '~/flash';
import CreateItemDropdown from '~/create_item_dropdown';
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
import { __ } from '~/locale'; import { __ } from '~/locale';
export default class ProtectedBranchCreate { export default class ProtectedBranchCreate {
constructor() { constructor(options) {
this.hasLicense = options.hasLicense;
this.$form = $('.js-new-protected-branch'); this.$form = $('.js-new-protected-branch');
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe(); this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
this.currentProjectUserDefaults = {}; this.currentProjectUserDefaults = {};
this.buildDropdowns(); this.buildDropdowns();
this.$codeOwnerToggle = this.$form.find('.js-code-owner-toggle');
this.bindEvents();
}
bindEvents() {
if (this.hasLicense) {
this.$codeOwnerToggle.on('click', this.onCodeOwnerToggleClick.bind(this));
}
this.$form.on('submit', this.onFormSubmit.bind(this));
}
onCodeOwnerToggleClick() {
this.$codeOwnerToggle.toggleClass('is-checked');
} }
buildDropdowns() { buildDropdowns() {
const $allowedToMergeDropdown = this.$form.find('.js-allowed-to-merge'); const $allowedToMergeDropdown = this.$form.find('.js-allowed-to-merge');
const $allowedToPushDropdown = this.$form.find('.js-allowed-to-push'); const $allowedToPushDropdown = this.$form.find('.js-allowed-to-push');
const $protectedBranchDropdown = this.$form.find('.js-protected-branch-select');
// Cache callback // Cache callback
this.onSelectCallback = this.onSelect.bind(this); this.onSelectCallback = this.onSelect.bind(this);
// Allowed to Merge dropdown // Allowed to Merge dropdown
this.protectedBranchMergeAccessDropdown = new ProtectedBranchAccessDropdown({ this[`${ACCESS_LEVELS.MERGE}_dropdown`] = new AccessDropdown({
$dropdown: $allowedToMergeDropdown, $dropdown: $allowedToMergeDropdown,
data: gon.merge_access_levels, accessLevelsData: gon.merge_access_levels,
onSelect: this.onSelectCallback, onSelect: this.onSelectCallback,
accessLevel: ACCESS_LEVELS.MERGE,
hasLicense: this.hasLicense,
}); });
// Allowed to Push dropdown // Allowed to Push dropdown
this.protectedBranchPushAccessDropdown = new ProtectedBranchAccessDropdown({ this[`${ACCESS_LEVELS.PUSH}_dropdown`] = new AccessDropdown({
$dropdown: $allowedToPushDropdown, $dropdown: $allowedToPushDropdown,
data: gon.push_access_levels, accessLevelsData: gon.push_access_levels,
onSelect: this.onSelectCallback, onSelect: this.onSelectCallback,
accessLevel: ACCESS_LEVELS.PUSH,
hasLicense: this.hasLicense,
}); });
this.createItemDropdown = new CreateItemDropdown({ this.createItemDropdown = new CreateItemDropdown({
$dropdown: $protectedBranchDropdown, $dropdown: this.$form.find('.js-protected-branch-select'),
defaultToggleLabel: __('Protected Branch'), defaultToggleLabel: __('Protected Branch'),
fieldName: 'protected_branch[name]', fieldName: 'protected_branch[name]',
onSelect: this.onSelectCallback, onSelect: this.onSelectCallback,
...@@ -43,26 +64,66 @@ export default class ProtectedBranchCreate { ...@@ -43,26 +64,66 @@ export default class ProtectedBranchCreate {
}); });
} }
// This will run after clicked callback // Enable submit button after selecting an option
onSelect() { onSelect() {
// Enable submit button const $allowedToMerge = this[`${ACCESS_LEVELS.MERGE}_dropdown`].getSelectedItems();
const $branchInput = this.$form.find('input[name="protected_branch[name]"]'); const $allowedToPush = this[`${ACCESS_LEVELS.PUSH}_dropdown`].getSelectedItems();
const $allowedToMergeInput = this.$form.find( const toggle = !(
'input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]', this.$form.find('input[name="protected_branch[name]"]').val() &&
); $allowedToMerge.length &&
const $allowedToPushInput = this.$form.find( $allowedToPush.length
'input[name="protected_branch[push_access_levels_attributes][0][access_level]"]',
);
const completedForm = !(
$branchInput.val() &&
$allowedToMergeInput.length &&
$allowedToPushInput.length
); );
this.$form.find('input[type="submit"]').prop('disabled', completedForm); this.$form.find('input[type="submit"]').attr('disabled', toggle);
} }
static getProtectedBranches(term, callback) { static getProtectedBranches(term, callback) {
callback(gon.open_branches); callback(gon.open_branches);
} }
getFormData() {
const formData = {
authenticity_token: this.$form.find('input[name="authenticity_token"]').val(),
protected_branch: {
name: this.$form.find('input[name="protected_branch[name]"]').val(),
code_owner_approval_required: this.$codeOwnerToggle.hasClass('is-checked'),
},
};
Object.keys(ACCESS_LEVELS).forEach(level => {
const accessLevel = ACCESS_LEVELS[level];
const selectedItems = this[`${accessLevel}_dropdown`].getSelectedItems();
const levelAttributes = [];
selectedItems.forEach(item => {
if (item.type === LEVEL_TYPES.USER) {
levelAttributes.push({
user_id: item.user_id,
});
} else if (item.type === LEVEL_TYPES.ROLE) {
levelAttributes.push({
access_level: item.access_level,
});
} else if (item.type === LEVEL_TYPES.GROUP) {
levelAttributes.push({
group_id: item.group_id,
});
}
});
formData.protected_branch[`${accessLevel}_attributes`] = levelAttributes;
});
return formData;
}
onFormSubmit(e) {
e.preventDefault();
axios[this.$form.attr('method')](this.$form.attr('action'), this.getFormData())
.then(() => {
window.location.reload();
})
.catch(() => Flash(__('Failed to protect the branch')));
}
} }
import flash from '../flash'; import { find } from 'lodash';
import axios from '../lib/utils/axios_utils'; import AccessDropdown from '~/projects/settings/access_dropdown';
import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown'; import axios from '~/lib/utils/axios_utils';
import Flash from '~/flash';
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
import { __ } from '~/locale'; import { __ } from '~/locale';
export default class ProtectedBranchEdit { export default class ProtectedBranchEdit {
constructor(options) { constructor(options) {
this.hasLicense = options.hasLicense;
this.$wraps = {};
this.hasChanges = false;
this.$wrap = options.$wrap; this.$wrap = options.$wrap;
this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge'); this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge');
this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push'); this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push');
this.onSelectCallback = this.onSelect.bind(this); this.$codeOwnerToggle = this.$wrap.find('.js-code-owner-toggle');
this.$wraps[ACCESS_LEVELS.MERGE] = this.$allowedToMergeDropdown.closest(
`.${ACCESS_LEVELS.MERGE}-container`,
);
this.$wraps[ACCESS_LEVELS.PUSH] = this.$allowedToPushDropdown.closest(
`.${ACCESS_LEVELS.PUSH}-container`,
);
this.buildDropdowns(); this.buildDropdowns();
this.bindEvents();
}
bindEvents() {
if (this.hasLicense) {
this.$codeOwnerToggle.on('click', this.onCodeOwnerToggleClick.bind(this));
}
}
onCodeOwnerToggleClick() {
this.$codeOwnerToggle.toggleClass('is-checked');
this.$codeOwnerToggle.prop('disabled', true);
const formData = {
code_owner_approval_required: this.$codeOwnerToggle.hasClass('is-checked'),
};
this.updateCodeOwnerApproval(formData);
}
updateCodeOwnerApproval(formData) {
axios
.patch(this.$wrap.data('url'), {
protected_branch: formData,
})
.then(() => {
this.$codeOwnerToggle.prop('disabled', false);
})
.catch(() => {
Flash(__('Failed to update branch!'));
});
} }
buildDropdowns() { buildDropdowns() {
// Allowed to merge dropdown // Allowed to merge dropdown
this.protectedBranchAccessDropdown = new ProtectedBranchAccessDropdown({ this[`${ACCESS_LEVELS.MERGE}_dropdown`] = new AccessDropdown({
accessLevel: ACCESS_LEVELS.MERGE,
accessLevelsData: gon.merge_access_levels,
$dropdown: this.$allowedToMergeDropdown, $dropdown: this.$allowedToMergeDropdown,
data: gon.merge_access_levels, onSelect: this.onSelectOption.bind(this),
onSelect: this.onSelectCallback, onHide: this.onDropdownHide.bind(this),
hasLicense: this.hasLicense,
}); });
// Allowed to push dropdown // Allowed to push dropdown
this.protectedBranchAccessDropdown = new ProtectedBranchAccessDropdown({ this[`${ACCESS_LEVELS.PUSH}_dropdown`] = new AccessDropdown({
accessLevel: ACCESS_LEVELS.PUSH,
accessLevelsData: gon.push_access_levels,
$dropdown: this.$allowedToPushDropdown, $dropdown: this.$allowedToPushDropdown,
data: gon.push_access_levels, onSelect: this.onSelectOption.bind(this),
onSelect: this.onSelectCallback, onHide: this.onDropdownHide.bind(this),
hasLicense: this.hasLicense,
}); });
} }
onSelect() { onSelectOption() {
const $allowedToMergeInput = this.$wrap.find( this.hasChanges = true;
`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`, }
);
const $allowedToPushInput = this.$wrap.find(
`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`,
);
// Do not update if one dropdown has not selected any option onDropdownHide() {
if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return; if (!this.hasChanges) {
return;
}
this.$allowedToMergeDropdown.disable(); this.hasChanges = true;
this.$allowedToPushDropdown.disable(); this.updatePermissions();
}
updatePermissions() {
const formData = Object.keys(ACCESS_LEVELS).reduce((acc, level) => {
const accessLevelName = ACCESS_LEVELS[level];
const inputData = this[`${accessLevelName}_dropdown`].getInputData(accessLevelName);
acc[`${accessLevelName}_attributes`] = inputData;
return acc;
}, {});
axios axios
.patch(this.$wrap.data('url'), { .patch(this.$wrap.data('url'), {
protected_branch: { protected_branch: formData,
merge_access_levels_attributes: [
{
id: this.$allowedToMergeDropdown.data('accessLevelId'),
access_level: $allowedToMergeInput.val(),
},
],
push_access_levels_attributes: [
{
id: this.$allowedToPushDropdown.data('accessLevelId'),
access_level: $allowedToPushInput.val(),
},
],
},
}) })
.then(() => { .then(({ data }) => {
this.hasChanges = false;
Object.keys(ACCESS_LEVELS).forEach(level => {
const accessLevelName = ACCESS_LEVELS[level];
// The data coming from server will be the new persisted *state* for each dropdown
this.setSelectedItemsToDropdown(data[accessLevelName], `${accessLevelName}_dropdown`);
});
this.$allowedToMergeDropdown.enable(); this.$allowedToMergeDropdown.enable();
this.$allowedToPushDropdown.enable(); this.$allowedToPushDropdown.enable();
}) })
.catch(() => { .catch(() => {
this.$allowedToMergeDropdown.enable(); this.$allowedToMergeDropdown.enable();
this.$allowedToPushDropdown.enable(); this.$allowedToPushDropdown.enable();
Flash(__('Failed to update branch!'));
flash(
__('Failed to update branch!'),
'alert',
document.querySelector('.js-protected-branches-list'),
);
}); });
} }
setSelectedItemsToDropdown(items = [], dropdownName) {
const itemsToAdd = items.map(currentItem => {
if (currentItem.user_id) {
// Do this only for users for now
// get the current data for selected items
const selectedItems = this[dropdownName].getSelectedItems();
const currentSelectedItem = find(selectedItems, {
user_id: currentItem.user_id,
});
return {
id: currentItem.id,
user_id: currentItem.user_id,
type: LEVEL_TYPES.USER,
persisted: true,
name: currentSelectedItem.name,
username: currentSelectedItem.username,
avatar_url: currentSelectedItem.avatar_url,
};
} else if (currentItem.group_id) {
return {
id: currentItem.id,
group_id: currentItem.group_id,
type: LEVEL_TYPES.GROUP,
persisted: true,
};
}
return {
id: currentItem.id,
access_level: currentItem.access_level,
type: LEVEL_TYPES.ROLE,
persisted: true,
};
});
this[dropdownName].setSelectedItems(itemsToAdd);
}
} }
...@@ -13,6 +13,7 @@ export default class ProtectedBranchEditList { ...@@ -13,6 +13,7 @@ export default class ProtectedBranchEditList {
this.$wrap.find('.js-protected-branch-edit-form').each((i, el) => { this.$wrap.find('.js-protected-branch-edit-form').each((i, el) => {
new ProtectedBranchEdit({ new ProtectedBranchEdit({
$wrap: $(el), $wrap: $(el),
hasLicense: false,
}); });
}); });
} }
......
<script>
export default {
props: {
mergeRequestsIllustrationPath: {
type: String,
required: true,
},
},
};
</script>
<template> <template>
<router-view /> <router-view :merge-requests-illustration-path="mergeRequestsIllustrationPath" />
</template> </template>
<script>
import { isString } from 'lodash';
import { GlLink, GlButton } from '@gitlab/ui';
const validateUrlAndLabel = value => isString(value.label) && isString(value.url);
export default {
components: {
GlLink,
GlButton,
},
props: {
branch: {
type: Object,
required: true,
validator: validateUrlAndLabel,
},
commit: {
type: Object,
required: true,
validator: validateUrlAndLabel,
},
mergeRequest: {
type: Object,
required: true,
validator: validateUrlAndLabel,
},
returnUrl: {
type: String,
required: false,
default: '',
},
},
};
</script>
<template>
<div>
<div class="border-bottom pb-4">
<h3>{{ s__('StaticSiteEditor|Success!') }}</h3>
<p>
{{
s__(
'StaticSiteEditor|Your changes have been submitted and a merge request has been created. The changes won’t be visible on the site until the merge request has been accepted.',
)
}}
</p>
<div class="d-flex justify-content-end">
<gl-button v-if="returnUrl" ref="returnToSiteButton" :href="returnUrl">{{
s__('StaticSiteEditor|Return to site')
}}</gl-button>
<gl-button ref="mergeRequestButton" class="ml-2" :href="mergeRequest.url" variant="success">
{{ s__('StaticSiteEditor|View merge request') }}
</gl-button>
</div>
</div>
<div class="pt-2">
<h4>{{ s__('StaticSiteEditor|Summary of changes') }}</h4>
<ul>
<li>
{{ s__('StaticSiteEditor|You created a new branch:') }}
<gl-link ref="branchLink" :href="branch.url">{{ branch.label }}</gl-link>
</li>
<li>
{{ s__('StaticSiteEditor|You created a merge request:') }}
<gl-link ref="mergeRequestLink" :href="mergeRequest.url">{{
mergeRequest.label
}}</gl-link>
</li>
<li>
{{ s__('StaticSiteEditor|You added a commit:') }}
<gl-link ref="commitLink" :href="commit.url">{{ commit.label }}</gl-link>
</li>
</ul>
</div>
</div>
</template>
...@@ -5,7 +5,14 @@ import createRouter from './router'; ...@@ -5,7 +5,14 @@ import createRouter from './router';
import createApolloProvider from './graphql'; import createApolloProvider from './graphql';
const initStaticSiteEditor = el => { const initStaticSiteEditor = el => {
const { isSupportedContent, path: sourcePath, baseUrl, namespace, project } = el.dataset; const {
isSupportedContent,
path: sourcePath,
baseUrl,
namespace,
project,
mergeRequestsIllustrationPath,
} = el.dataset;
const { current_username: username } = window.gon; const { current_username: username } = window.gon;
const returnUrl = el.dataset.returnUrl || null; const returnUrl = el.dataset.returnUrl || null;
...@@ -26,7 +33,11 @@ const initStaticSiteEditor = el => { ...@@ -26,7 +33,11 @@ const initStaticSiteEditor = el => {
App, App,
}, },
render(createElement) { render(createElement) {
return createElement('app'); return createElement('app', {
props: {
mergeRequestsIllustrationPath,
},
});
}, },
}); });
}; };
......
<script> <script>
import { GlEmptyState, GlButton } from '@gitlab/ui';
import { s__, __, sprintf } from '~/locale';
import savedContentMetaQuery from '../graphql/queries/saved_content_meta.query.graphql'; import savedContentMetaQuery from '../graphql/queries/saved_content_meta.query.graphql';
import appDataQuery from '../graphql/queries/app_data.query.graphql'; import appDataQuery from '../graphql/queries/app_data.query.graphql';
import SavedChangesMessage from '../components/saved_changes_message.vue';
import { HOME_ROUTE } from '../router/constants'; import { HOME_ROUTE } from '../router/constants';
export default { export default {
components: { components: {
SavedChangesMessage, GlEmptyState,
GlButton,
},
props: {
mergeRequestsIllustrationPath: {
type: String,
required: true,
},
}, },
apollo: { apollo: {
savedContentMeta: { savedContentMeta: {
...@@ -16,20 +25,65 @@ export default { ...@@ -16,20 +25,65 @@ export default {
query: appDataQuery, query: appDataQuery,
}, },
}, },
computed: {
updatedFileDescription() {
const { sourcePath } = this.appData;
return sprintf(s__('Update %{sourcePath} file'), { sourcePath });
},
},
created() { created() {
if (!this.savedContentMeta) { if (!this.savedContentMeta) {
this.$router.push(HOME_ROUTE); this.$router.push(HOME_ROUTE);
} }
}, },
title: s__('StaticSiteEditor|Your merge request has been created'),
primaryButtonText: __('View merge request'),
returnToSiteBtnText: s__('StaticSiteEditor|Return to site'),
mergeRequestInstructionsHeading: s__(
'StaticSiteEditor|To see your changes live you will need to do the following things:',
),
addTitleInstruction: s__('StaticSiteEditor|1. Add a clear title to describe the change.'),
addDescriptionInstruction: s__(
'StaticSiteEditor|2. Add a description to explain why the change is being made.',
),
assignMergeRequestInstruction: s__(
'StaticSiteEditor|3. Assign a person to review and accept the merge request.',
),
}; };
</script> </script>
<template> <template>
<div v-if="savedContentMeta" class="container"> <div
<saved-changes-message v-if="savedContentMeta"
:branch="savedContentMeta.branch" class="container gl-flex-grow-1 gl-display-flex gl-flex-direction-column"
:commit="savedContentMeta.commit" >
:merge-request="savedContentMeta.mergeRequest" <div class="gl-fixed gl-left-0 gl-right-0 gl-border-b-solid gl-border-b-1 gl-border-b-gray-100">
:return-url="appData.returnUrl" <div class="container gl-py-4">
/> <gl-button
v-if="appData.returnUrl"
ref="returnToSiteButton"
class="gl-mr-5"
:href="appData.returnUrl"
>{{ $options.returnToSiteBtnText }}</gl-button
>
<strong>
{{ updatedFileDescription }}
</strong>
</div>
</div>
<gl-empty-state
class="gl-my-9"
:primary-button-text="$options.primaryButtonText"
:title="$options.title"
:primary-button-link="savedContentMeta.mergeRequest.url"
:svg-path="mergeRequestsIllustrationPath"
>
<template #description>
<p>{{ $options.mergeRequestInstructionsHeading }}</p>
<p>{{ $options.addTitleInstruction }}</p>
<p>{{ $options.addDescriptionInstruction }}</p>
<p>{{ $options.assignMergeRequestInstruction }}</p>
</template>
</gl-empty-state>
</div> </div>
</template> </template>
...@@ -586,5 +586,16 @@ const fileNameIcons = { ...@@ -586,5 +586,16 @@ const fileNameIcons = {
}; };
export default function getIconForFile(name) { export default function getIconForFile(name) {
return fileNameIcons[name] || fileExtensionIcons[name ? name.split('.').pop() : ''] || ''; return (
fileNameIcons[name] ||
fileExtensionIcons[
name
? name
.split('.')
.pop()
.toLowerCase()
: ''
] ||
''
);
} }
...@@ -97,7 +97,7 @@ ...@@ -97,7 +97,7 @@
%td %td
.float-right .float-right
- if can?(current_user, :read_build, job) && job.artifacts? - if can?(current_user, :read_build, job) && job.artifacts?
= link_to download_project_job_artifacts_path(job.project, job), rel: 'nofollow', download: '', title: _('Download artifacts'), class: 'btn btn-build' do = link_to download_project_job_artifacts_path(job.project, job), rel: 'nofollow', download: '', title: _('Download artifacts'), class: 'btn btn-build gl-button btn-icon btn-svg' do
= sprite_icon('download') = sprite_icon('download')
- if can?(current_user, :update_build, job) - if can?(current_user, :update_build, job)
- if job.active? - if job.active?
......
- add_page_startup_api_call discussions_path(@issue)
- @gfm_form = true - @gfm_form = true
- content_for :note_actions do - content_for :note_actions do
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
- can_reopen_issue = can?(current_user, :reopen_issue, @issue) - can_reopen_issue = can?(current_user, :reopen_issue, @issue)
- can_report_spam = @issue.submittable_as_spam_by?(current_user) - can_report_spam = @issue.submittable_as_spam_by?(current_user)
- can_create_issue = show_new_issue_link?(@project) - can_create_issue = show_new_issue_link?(@project)
- related_branches_path = related_branches_project_issue_path(@project, @issue)
= render_if_exists "projects/issues/alert_blocked", issue: @issue, current_user: current_user = render_if_exists "projects/issues/alert_blocked", issue: @issue, current_user: current_user
= render "projects/issues/alert_moved_from_service_desk", issue: @issue = render "projects/issues/alert_moved_from_service_desk", issue: @issue
...@@ -82,7 +83,8 @@ ...@@ -82,7 +83,8 @@
#js-related-merge-requests{ data: { endpoint: expose_path(api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: @issue.iid)), project_namespace: @project.namespace.path, project_path: @project.path } } #js-related-merge-requests{ data: { endpoint: expose_path(api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: @issue.iid)), project_namespace: @project.namespace.path, project_path: @project.path } }
- if can?(current_user, :download_code, @project) - if can?(current_user, :download_code, @project)
#related-branches{ data: { url: related_branches_project_issue_path(@project, @issue) } } - add_page_startup_api_call related_branches_path
#related-branches{ data: { url: related_branches_path } }
-# This element is filled in using JavaScript. -# This element is filled in using JavaScript.
.content-block.emoji-block.emoji-block-sticky .content-block.emoji-block.emoji-block-sticky
......
#static-site-editor{ data: @config.payload } #static-site-editor{ data: @config.payload.merge({ merge_requests_illustration_path: image_path('illustrations/merge_requests.svg') }) }
---
title: Improve the IA and styling of the Success screen in the Static Site Editor
merge_request: 37475
author:
type: changed
---
title: Fix misalignment of download icon on jobs page
merge_request: 37966
author:
type: other
---
title: Improve performance of Banzai reference filters
merge_request: 37465
author:
type: performance
---
title: Make file icons extension detection be case-insensitive
merge_request: 37817
author:
type: fixed
---
title: Adds clarifying documentation on EKS IAM roles
merge_request: 37870
author:
type: added
...@@ -13,6 +13,7 @@ Gitlab.ee do ...@@ -13,6 +13,7 @@ Gitlab.ee do
Elasticsearch::Model::Adapter::Multiple::Records.prepend GemExtensions::Elasticsearch::Model::Adapter::Multiple::Records Elasticsearch::Model::Adapter::Multiple::Records.prepend GemExtensions::Elasticsearch::Model::Adapter::Multiple::Records
Elasticsearch::Model::Indexing::InstanceMethods.prepend GemExtensions::Elasticsearch::Model::Indexing::InstanceMethods Elasticsearch::Model::Indexing::InstanceMethods.prepend GemExtensions::Elasticsearch::Model::Indexing::InstanceMethods
Elasticsearch::Model::Adapter::ActiveRecord::Importing.prepend GemExtensions::Elasticsearch::Model::Adapter::ActiveRecord::Importing Elasticsearch::Model::Adapter::ActiveRecord::Importing.prepend GemExtensions::Elasticsearch::Model::Adapter::ActiveRecord::Importing
Elasticsearch::Model::Adapter::ActiveRecord::Records.prepend GemExtensions::Elasticsearch::Model::Adapter::ActiveRecord::Records
Elasticsearch::Model::Client::InstanceMethods.prepend GemExtensions::Elasticsearch::Model::Client Elasticsearch::Model::Client::InstanceMethods.prepend GemExtensions::Elasticsearch::Model::Client
Elasticsearch::Model::Client::ClassMethods.prepend GemExtensions::Elasticsearch::Model::Client Elasticsearch::Model::Client::ClassMethods.prepend GemExtensions::Elasticsearch::Model::Client
Elasticsearch::Model::ClassMethods.prepend GemExtensions::Elasticsearch::Model::Client Elasticsearch::Model::ClassMethods.prepend GemExtensions::Elasticsearch::Model::Client
......
...@@ -294,7 +294,7 @@ marked as Satisfied. ...@@ -294,7 +294,7 @@ marked as Satisfied.
> - From GitLab 9.2, PDFs, images, videos, and other formats can be previewed directly in the job artifacts browser without the need to download them. > - From GitLab 9.2, PDFs, images, videos, and other formats can be previewed directly in the job artifacts browser without the need to download them.
> - Introduced in [GitLab 10.1](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/14399), HTML files in a public project can be previewed directly in a new tab without the need to download them when [GitLab Pages](../../administration/pages/index.md) is enabled. The same applies for textual formats (currently supported extensions: `.txt`, `.json`, and `.log`). > - Introduced in [GitLab 10.1](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/14399), HTML files in a public project can be previewed directly in a new tab without the need to download them when [GitLab Pages](../../administration/pages/index.md) is enabled. The same applies for textual formats (currently supported extensions: `.txt`, `.json`, and `.log`).
> - Introduced in [GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/16675), artifacts in private projects can be previewed when [GitLab Pages access control](../../administration/pages/index.md#access-control) is enabled. > - Introduced in [GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/16675), artifacts in internal and private projects can be previewed when [GitLab Pages access control](../../administration/pages/index.md#access-control) is enabled.
After a job finishes, if you visit the job's specific page, there are three After a job finishes, if you visit the job's specific page, there are three
buttons. You can download the artifacts archive or browse its contents, whereas buttons. You can download the artifacts archive or browse its contents, whereas
...@@ -311,6 +311,8 @@ Below you can see what browsing looks like. In this case we have browsed inside ...@@ -311,6 +311,8 @@ Below you can see what browsing looks like. In this case we have browsed inside
the archive and at this point there is one directory, a couple files, and the archive and at this point there is one directory, a couple files, and
one HTML file that you can view directly online when one HTML file that you can view directly online when
[GitLab Pages](../../administration/pages/index.md) is enabled (opens in a new tab). [GitLab Pages](../../administration/pages/index.md) is enabled (opens in a new tab).
Select artifacts in internal and private projects can only be previewed when
[GitLab Pages access control](../../administration/pages/index.md#access-control) is enabled.
![Job artifacts browser](img/job_artifacts_browser.png) ![Job artifacts browser](img/job_artifacts_browser.png)
......
...@@ -7,24 +7,6 @@ type: reference ...@@ -7,24 +7,6 @@ type: reference
# Getting started with GitLab CI/CD # Getting started with GitLab CI/CD
NOTE: **Note:**
Starting from version 8.0, GitLab [Continuous Integration](https://about.gitlab.com/stages-devops-lifecycle/continuous-integration/) (CI)
is fully integrated into GitLab itself and is [enabled](../enable_or_disable_ci.md) by default on all
projects.
NOTE: **Note:**
Please keep in mind that only project Maintainers and Admin users have
the permissions to access a project's settings.
NOTE: **Note:**
Coming over to GitLab from Jenkins? Check out our [reference](../jenkins/index.md)
for converting your pre-existing pipelines over to our format.
NOTE: **Note:**
There are a few different [basic pipeline architectures](../pipelines/pipeline_architectures.md)
that you can consider for use in your project. You may want to familiarize
yourself with these prior to getting started.
GitLab offers a [continuous integration](https://about.gitlab.com/stages-devops-lifecycle/continuous-integration/) service. For each commit or push to trigger your CI GitLab offers a [continuous integration](https://about.gitlab.com/stages-devops-lifecycle/continuous-integration/) service. For each commit or push to trigger your CI
[pipeline](../pipelines/index.md), you must: [pipeline](../pipelines/index.md), you must:
...@@ -49,7 +31,11 @@ something. ...@@ -49,7 +31,11 @@ something.
It's also common to use pipelines to automatically deploy It's also common to use pipelines to automatically deploy
tested code to staging and production environments. tested code to staging and production environments.
--- If you're already familiar with general CI/CD concepts, you can review which
[pipeline architectures](../pipelines/pipeline_architectures.md) can be used
in your projects. If you're coming over to GitLab from Jenkins, you can check out
our [reference](../migration/jenkins.md) for converting your pre-existing pipelines
over to our format.
This guide assumes that you have: This guide assumes that you have:
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
NOTE: **Note:** NOTE: **Note:**
This documentation focuses only on how to **configure** a Jenkins *integration* with This documentation focuses only on how to **configure** a Jenkins *integration* with
GitLab. Learn how to **migrate** from Jenkins to GitLab CI/CD in our GitLab. Learn how to **migrate** from Jenkins to GitLab CI/CD in our
[Migrating from Jenkins](../ci/jenkins/index.md) documentation. [Migrating from Jenkins](../ci/migration/jenkins.md) documentation.
From GitLab, you can trigger a Jenkins build when you push code to a repository, or when a merge From GitLab, you can trigger a Jenkins build when you push code to a repository, or when a merge
request is created. In return, Jenkins shows the pipeline status on merge requests widgets and request is created. In return, Jenkins shows the pipeline status on merge requests widgets and
......
...@@ -965,6 +965,7 @@ documentation: ...@@ -965,6 +965,7 @@ documentation:
- [Google GKE](https://docs.cilium.io/en/stable/gettingstarted/k8s-install-gke/#deploy-cilium) - [Google GKE](https://docs.cilium.io/en/stable/gettingstarted/k8s-install-gke/#deploy-cilium)
- [AWS EKS](https://docs.cilium.io/en/stable/gettingstarted/k8s-install-eks/#deploy-cilium) - [AWS EKS](https://docs.cilium.io/en/stable/gettingstarted/k8s-install-eks/#deploy-cilium)
- [Azure AKS](https://docs.cilium.io/en/stable/gettingstarted/k8s-install-aks/#deploy-cilium)
You can customize Cilium's Helm variables by defining the You can customize Cilium's Helm variables by defining the
`.gitlab/managed-apps/cilium/values.yaml` file in your cluster `.gitlab/managed-apps/cilium/values.yaml` file in your cluster
......
...@@ -62,6 +62,11 @@ To create and add a new Kubernetes cluster to your project, group, or instance: ...@@ -62,6 +62,11 @@ To create and add a new Kubernetes cluster to your project, group, or instance:
1. Click **Add Kubernetes cluster**. 1. Click **Add Kubernetes cluster**.
1. Under the **Create new cluster** tab, click **Amazon EKS**. You will be provided with an 1. Under the **Create new cluster** tab, click **Amazon EKS**. You will be provided with an
`Account ID` and `External ID` to use in the next step. `Account ID` and `External ID` to use in the next step.
1. In the [IAM Management Console](https://console.aws.amazon.com/iam/home), create an EKS management IAM role.
To do so, follow the [Amazon EKS cluster IAM role](https://docs.aws.amazon.com/eks/latest/userguide/service_IAM_role.html) instructions
to create a IAM role suitable for managing the AWS EKS cluster's resources on your behalf.
In addition to the policies that guide suggests, you must also include the `AmazonEKSServicePolicy`
policy for this role in order for GitLab to manage the EKS cluster correctly.
1. In the [IAM Management Console](https://console.aws.amazon.com/iam/home), create an IAM role: 1. In the [IAM Management Console](https://console.aws.amazon.com/iam/home), create an IAM role:
1. From the left panel, select **Roles**. 1. From the left panel, select **Roles**.
1. Click **Create role**. 1. Click **Create role**.
...@@ -137,9 +142,15 @@ To create and add a new Kubernetes cluster to your project, group, or instance: ...@@ -137,9 +142,15 @@ To create and add a new Kubernetes cluster to your project, group, or instance:
- **Kubernetes cluster name** - The name you wish to give the cluster. - **Kubernetes cluster name** - The name you wish to give the cluster.
- **Environment scope** - The [associated environment](index.md#setting-the-environment-scope-premium) to this cluster. - **Environment scope** - The [associated environment](index.md#setting-the-environment-scope-premium) to this cluster.
- **Kubernetes version** - The Kubernetes version to use. Currently the only version supported is 1.14. - **Kubernetes version** - The Kubernetes version to use. Currently the only version supported is 1.14.
- **Role name** - Select the [IAM role](https://docs.aws.amazon.com/eks/latest/userguide/service_IAM_role.html) - **Role name** - Select the **EKS IAM role** you created earlier to allow Amazon EKS
to allow Amazon EKS and the Kubernetes control plane to manage AWS resources on your behalf. This IAM role is separate and the Kubernetes control plane to manage AWS resources on your behalf.
to the IAM role created above, you will need to create it if it does not yet exist.
NOTE: **Note:**
This IAM role is _not_ the IAM role you created in the previous step. It should be
the one you created much earlier by following the
[Amazon EKS cluster IAM role](https://docs.aws.amazon.com/eks/latest/userguide/service_IAM_role.html)
guide.
- **Region** - The [region](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html) - **Region** - The [region](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html)
in which the cluster will be created. in which the cluster will be created.
- **Key pair name** - Select the [key pair](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html) - **Key pair name** - Select the [key pair](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html)
...@@ -194,10 +205,10 @@ If the `Cluster` resource failed with the error ...@@ -194,10 +205,10 @@ If the `Cluster` resource failed with the error
the role specified in **Role name** is not configured correctly. the role specified in **Role name** is not configured correctly.
NOTE: **Note:** NOTE: **Note:**
This role should not be the same as the one created above. If you don't have an This role should be the role you created by following the
existing [EKS cluster IAM role](https://docs.aws.amazon.com/eks/latest/userguide/service_IAM_role.html) guide.
[EKS cluster IAM role](https://docs.aws.amazon.com/eks/latest/userguide/service_IAM_role.html), In addition to the policies that guide suggests, you must also include the
you must create one. `AmazonEKSServicePolicy` policy for this role in order for GitLab to manage the EKS cluster correctly.
## Existing EKS cluster ## Existing EKS cluster
......
...@@ -9,7 +9,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w ...@@ -9,7 +9,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
GitLab makes it easy to secure applications deployed in [connected Kubernetes clusters](index.md). GitLab makes it easy to secure applications deployed in [connected Kubernetes clusters](index.md).
You can benefit from the protection of a [Web Application Firewall](../../../topics/web_application_firewall/quick_start_guide.md), You can benefit from the protection of a [Web Application Firewall](../../../topics/web_application_firewall/quick_start_guide.md),
[Network Policies](../../../topics/autodevops/stages.md#network-policy), [Network Policies](../../../topics/autodevops/stages.md#network-policy),
or even [Container Host Security](../../clusters/applications.md#install-falco-using-gitlab-cicd). and [Container Host Security](../../clusters/applications.md#install-falco-using-gitlab-cicd).
This page contains full end-to-end steps and instructions to connect your cluster to GitLab and This page contains full end-to-end steps and instructions to connect your cluster to GitLab and
install these features, whether or not your applications are deployed through GitLab CI/CD. If you install these features, whether or not your applications are deployed through GitLab CI/CD. If you
...@@ -25,7 +25,7 @@ At a high level, the required steps include the following: ...@@ -25,7 +25,7 @@ At a high level, the required steps include the following:
- Connect the cluster to GitLab. - Connect the cluster to GitLab.
- Set up one or more runners. - Set up one or more runners.
- Set up a cluster management project. - Set up a cluster management project.
- Install a Web Application Firewall, Network Policies, and/or Container Host - Install a Web Application Firewall, and/or Network Policies, and/or Container Host
Security. Security.
- Install Prometheus to get statistics and metrics in the - Install Prometheus to get statistics and metrics in the
[threat monitoring](../../application_security/threat_monitoring/) [threat monitoring](../../application_security/threat_monitoring/)
...@@ -57,7 +57,7 @@ uses Sidekiq (a background processing service) to facilitate this. ...@@ -57,7 +57,7 @@ uses Sidekiq (a background processing service) to facilitate this.
``` ```
Although this installation method is easier because it's a point-and-click action in the user Although this installation method is easier because it's a point-and-click action in the user
interface, it's inflexible and hard to debug. When something goes wrong, you can't see the interface, it's inflexible and harder to debug. If something goes wrong, you can't see the
deployment logs. The Web Application Firewall feature uses this installation method. deployment logs. The Web Application Firewall feature uses this installation method.
However, the next generation of GitLab Managed Apps V2 ([CI/CD-based GitLab Managed Apps](https://gitlab.com/groups/gitlab-org/-/epics/2103)) However, the next generation of GitLab Managed Apps V2 ([CI/CD-based GitLab Managed Apps](https://gitlab.com/groups/gitlab-org/-/epics/2103))
...@@ -75,10 +75,10 @@ sequenceDiagram ...@@ -75,10 +75,10 @@ sequenceDiagram
``` ```
Debugging is easier because you have access to the raw logs of these jobs (the Helm Tiller output is Debugging is easier because you have access to the raw logs of these jobs (the Helm Tiller output is
available as an artifact in case of failure) and the flexibility is much better. Since these available as an artifact in case of failure), and the flexibility is much better. Since these
deployments are only triggered when a pipeline is running (most likely when there's a new commit in deployments are only triggered when a pipeline is running (most likely when there's a new commit in
the cluster management repository), every action has a paper trail and follows the classic merge the cluster management repository), every action has a paper trail and follows the classic merge
request workflow (approvals, merge, deploy). The Network Policy (Cilium) Managed App and Container request workflow (approvals, merge, deploy). The Network Policy (Cilium) Managed App, and Container
Host Security (Falco) are deployed with this model. Host Security (Falco) are deployed with this model.
## Connect the cluster to GitLab ## Connect the cluster to GitLab
......
...@@ -66,7 +66,7 @@ Your account has been blocked. Fatal: Could not read from remote repository ...@@ -66,7 +66,7 @@ Your account has been blocked. Fatal: Could not read from remote repository
You can assure your users that they have not been [Blocked](admin_area/blocking_unblocking_users.md) by an administrator. You can assure your users that they have not been [Blocked](admin_area/blocking_unblocking_users.md) by an administrator.
When affected users see this message, they must confirm their email address before they can commit code. When affected users see this message, they must confirm their email address before they can commit code.
## What do I need to know as an administrator of a GitLab Self-Managed Instance? ## What do I need to know as an administrator of a GitLab self-managed Instance?
You have the following options to help your users: You have the following options to help your users:
...@@ -87,6 +87,19 @@ admin.confirmed_at = Time.zone.now ...@@ -87,6 +87,19 @@ admin.confirmed_at = Time.zone.now
admin.save! admin.save!
``` ```
## How do I force-confirm all users on my self-managed instance?
If you are an administrator and would like to force-confirm all users on your system, sign in to your GitLab
instance with a [Rails console session](../administration/troubleshooting/navigating_gitlab_via_rails_console.md#starting-a-rails-console-session).
Once connected, run the following commands to confirm all user accounts:
```ruby
User.where('LENGTH(confirmation_token) = 32').where(confirmed_at: nil).find_each { |u| u.confirmed_at = Time.now; u.save }
```
CAUTION: **Caution:**
The command described in this section may activate users who have not properly confirmed their email addresses.
## What about LDAP users? ## What about LDAP users?
LDAP users should NOT be affected. LDAP users should NOT be affected.
...@@ -25,14 +25,12 @@ module Banzai ...@@ -25,14 +25,12 @@ module Banzai
def initialize(doc, context = nil, result = nil) def initialize(doc, context = nil, result = nil)
super super
if update_nodes_enabled? @new_nodes = {}
@new_nodes = {} @nodes = self.result[:reference_filter_nodes]
@nodes = self.result[:reference_filter_nodes]
end
end end
def call_and_update_nodes def call_and_update_nodes
update_nodes_enabled? ? with_update_nodes { call } : call with_update_nodes { call }
end end
# Returns a data attribute String to attach to a reference link # Returns a data attribute String to attach to a reference link
...@@ -165,11 +163,7 @@ module Banzai ...@@ -165,11 +163,7 @@ module Banzai
end end
def replace_text_with_html(node, index, html) def replace_text_with_html(node, index, html)
if update_nodes_enabled? replace_and_update_new_nodes(node, index, html)
replace_and_update_new_nodes(node, index, html)
else
node.replace(html)
end
end end
def replace_and_update_new_nodes(node, index, html) def replace_and_update_new_nodes(node, index, html)
...@@ -209,10 +203,6 @@ module Banzai ...@@ -209,10 +203,6 @@ module Banzai
end end
result[:reference_filter_nodes] = nodes result[:reference_filter_nodes] = nodes
end end
def update_nodes_enabled?
Feature.enabled?(:update_nodes_for_banzai_reference_filter, project)
end
end end
end end
end end
...@@ -10,7 +10,6 @@ ...@@ -10,7 +10,6 @@
# alt_usage_data { Gitlab::VERSION } # alt_usage_data { Gitlab::VERSION }
# redis_usage_data(Gitlab::UsageDataCounters::WikiPageCounter) # redis_usage_data(Gitlab::UsageDataCounters::WikiPageCounter)
# redis_usage_data { ::Gitlab::UsageCounters::PodLogs.usage_totals[:total] } # redis_usage_data { ::Gitlab::UsageCounters::PodLogs.usage_totals[:total] }
module Gitlab module Gitlab
class UsageData class UsageData
BATCH_SIZE = 100 BATCH_SIZE = 100
...@@ -84,9 +83,11 @@ module Gitlab ...@@ -84,9 +83,11 @@ module Gitlab
auto_devops_enabled: count(::ProjectAutoDevops.enabled), auto_devops_enabled: count(::ProjectAutoDevops.enabled),
auto_devops_disabled: count(::ProjectAutoDevops.disabled), auto_devops_disabled: count(::ProjectAutoDevops.disabled),
deploy_keys: count(DeployKey), deploy_keys: count(DeployKey),
# rubocop: disable UsageData/LargeTable:
deployments: deployment_count(Deployment), deployments: deployment_count(Deployment),
successful_deployments: deployment_count(Deployment.success), successful_deployments: deployment_count(Deployment.success),
failed_deployments: deployment_count(Deployment.failed), failed_deployments: deployment_count(Deployment.failed),
# rubocop: enable UsageData/LargeTable:
environments: count(::Environment), environments: count(::Environment),
clusters: count(::Clusters::Cluster), clusters: count(::Clusters::Cluster),
clusters_enabled: count(::Clusters::Cluster.enabled), clusters_enabled: count(::Clusters::Cluster.enabled),
...@@ -171,9 +172,11 @@ module Gitlab ...@@ -171,9 +172,11 @@ module Gitlab
def system_usage_data_monthly def system_usage_data_monthly
{ {
counts_monthly: { counts_monthly: {
# rubocop: disable UsageData/LargeTable:
deployments: deployment_count(Deployment.where(last_28_days_time_period)), deployments: deployment_count(Deployment.where(last_28_days_time_period)),
successful_deployments: deployment_count(Deployment.success.where(last_28_days_time_period)), successful_deployments: deployment_count(Deployment.success.where(last_28_days_time_period)),
failed_deployments: deployment_count(Deployment.failed.where(last_28_days_time_period)), failed_deployments: deployment_count(Deployment.failed.where(last_28_days_time_period)),
# rubocop: enable UsageData/LargeTable:
personal_snippets: count(PersonalSnippet.where(last_28_days_time_period)), personal_snippets: count(PersonalSnippet.where(last_28_days_time_period)),
project_snippets: count(ProjectSnippet.where(last_28_days_time_period)) project_snippets: count(ProjectSnippet.where(last_28_days_time_period))
}.tap do |data| }.tap do |data|
...@@ -332,14 +335,18 @@ module Gitlab ...@@ -332,14 +335,18 @@ module Gitlab
finish = ::Project.maximum(:id) finish = ::Project.maximum(:id)
results[:projects_with_expiration_policy_disabled] = distinct_count(::ContainerExpirationPolicy.where(enabled: false), :project_id, start: start, finish: finish) results[:projects_with_expiration_policy_disabled] = distinct_count(::ContainerExpirationPolicy.where(enabled: false), :project_id, start: start, finish: finish)
# rubocop: disable UsageData/LargeTable
base = ::ContainerExpirationPolicy.active base = ::ContainerExpirationPolicy.active
# rubocop: enable UsageData/LargeTable
results[:projects_with_expiration_policy_enabled] = distinct_count(base, :project_id, start: start, finish: finish) results[:projects_with_expiration_policy_enabled] = distinct_count(base, :project_id, start: start, finish: finish)
# rubocop: disable UsageData/LargeTable
%i[keep_n cadence older_than].each do |option| %i[keep_n cadence older_than].each do |option|
::ContainerExpirationPolicy.public_send("#{option}_options").keys.each do |value| # rubocop: disable GitlabSecurity/PublicSend ::ContainerExpirationPolicy.public_send("#{option}_options").keys.each do |value| # rubocop: disable GitlabSecurity/PublicSend
results["projects_with_expiration_policy_enabled_with_#{option}_set_to_#{value}".to_sym] = distinct_count(base.where(option => value), :project_id, start: start, finish: finish) results["projects_with_expiration_policy_enabled_with_#{option}_set_to_#{value}".to_sym] = distinct_count(base.where(option => value), :project_id, start: start, finish: finish)
end end
end end
# rubocop: enable UsageData/LargeTable
results[:projects_with_expiration_policy_enabled_with_keep_n_unset] = distinct_count(base.where(keep_n: nil), :project_id, start: start, finish: finish) results[:projects_with_expiration_policy_enabled_with_keep_n_unset] = distinct_count(base.where(keep_n: nil), :project_id, start: start, finish: finish)
results[:projects_with_expiration_policy_enabled_with_older_than_unset] = distinct_count(base.where(older_than: nil), :project_id, start: start, finish: finish) results[:projects_with_expiration_policy_enabled_with_older_than_unset] = distinct_count(base.where(older_than: nil), :project_id, start: start, finish: finish)
...@@ -350,9 +357,11 @@ module Gitlab ...@@ -350,9 +357,11 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def services_usage def services_usage
# rubocop: disable UsageData/LargeTable:
Service.available_services_names.without('jira').each_with_object({}) do |service_name, response| Service.available_services_names.without('jira').each_with_object({}) do |service_name, response|
response["projects_#{service_name}_active".to_sym] = count(Service.active.where(template: false, type: "#{service_name}_service".camelize)) response["projects_#{service_name}_active".to_sym] = count(Service.active.where(template: false, type: "#{service_name}_service".camelize))
end.merge(jira_usage).merge(jira_import_usage) end.merge(jira_usage).merge(jira_import_usage)
# rubocop: enable UsageData/LargeTable:
end end
def jira_usage def jira_usage
...@@ -365,6 +374,7 @@ module Gitlab ...@@ -365,6 +374,7 @@ module Gitlab
projects_jira_active: 0 projects_jira_active: 0
} }
# rubocop: disable UsageData/LargeTable:
JiraService.active.includes(:jira_tracker_data).find_in_batches(batch_size: BATCH_SIZE) do |services| JiraService.active.includes(:jira_tracker_data).find_in_batches(batch_size: BATCH_SIZE) do |services|
counts = services.group_by do |service| counts = services.group_by do |service|
# TODO: Simplify as part of https://gitlab.com/gitlab-org/gitlab/issues/29404 # TODO: Simplify as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
...@@ -376,21 +386,24 @@ module Gitlab ...@@ -376,21 +386,24 @@ module Gitlab
results[:projects_jira_cloud_active] += counts[:cloud].size if counts[:cloud] results[:projects_jira_cloud_active] += counts[:cloud].size if counts[:cloud]
results[:projects_jira_active] += services.size results[:projects_jira_active] += services.size
end end
# rubocop: enable UsageData/LargeTable:
results results
rescue ActiveRecord::StatementInvalid rescue ActiveRecord::StatementInvalid
{ projects_jira_server_active: FALLBACK, projects_jira_cloud_active: FALLBACK, projects_jira_active: FALLBACK } { projects_jira_server_active: FALLBACK, projects_jira_cloud_active: FALLBACK, projects_jira_active: FALLBACK }
end end
# rubocop: disable UsageData/LargeTable
def successful_deployments_with_cluster(scope) def successful_deployments_with_cluster(scope)
scope scope
.joins(cluster: :deployments) .joins(cluster: :deployments)
.merge(Clusters::Cluster.enabled) .merge(Clusters::Cluster.enabled)
.merge(Deployment.success) .merge(Deployment.success)
end end
# rubocop: enable UsageData/LargeTable
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def jira_import_usage def jira_import_usage
# rubocop: disable UsageData/LargeTable
finished_jira_imports = JiraImportState.finished finished_jira_imports = JiraImportState.finished
{ {
...@@ -398,6 +411,7 @@ module Gitlab ...@@ -398,6 +411,7 @@ module Gitlab
jira_imports_projects_count: distinct_count(finished_jira_imports, :project_id), jira_imports_projects_count: distinct_count(finished_jira_imports, :project_id),
jira_imports_total_imported_issues_count: alt_usage_data { JiraImportState.finished_imports_count } jira_imports_total_imported_issues_count: alt_usage_data { JiraImportState.finished_imports_count }
} }
# rubocop: enable UsageData/LargeTable
end end
def user_preferences_usage def user_preferences_usage
...@@ -406,13 +420,8 @@ module Gitlab ...@@ -406,13 +420,8 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def merge_requests_users(time_period) def merge_requests_users(time_period)
query =
Event
.where(target_type: Event::TARGET_TYPES[:merge_request].to_s)
.where(time_period)
distinct_count( distinct_count(
query, Event.where(target_type: Event::TARGET_TYPES[:merge_request].to_s).where(time_period),
:author_id, :author_id,
start: user_minimum_id, start: user_minimum_id,
finish: user_maximum_id finish: user_maximum_id
...@@ -450,6 +459,7 @@ module Gitlab ...@@ -450,6 +459,7 @@ module Gitlab
end end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
# rubocop: disable UsageData/LargeTable
def usage_activity_by_stage_configure(time_period) def usage_activity_by_stage_configure(time_period)
{ {
clusters_applications_cert_managers: cluster_applications_user_distinct_count(::Clusters::Applications::CertManager, time_period), clusters_applications_cert_managers: cluster_applications_user_distinct_count(::Clusters::Applications::CertManager, time_period),
...@@ -470,6 +480,7 @@ module Gitlab ...@@ -470,6 +480,7 @@ module Gitlab
project_clusters_enabled: clusters_user_distinct_count(::Clusters::Cluster.enabled.project_type, time_period) project_clusters_enabled: clusters_user_distinct_count(::Clusters::Cluster.enabled.project_type, time_period)
} }
end end
# rubocop: enable UsageData/LargeTable
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
...@@ -628,8 +639,9 @@ module Gitlab ...@@ -628,8 +639,9 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def service_desk_counts def service_desk_counts
# rubocop: disable UsageData/LargeTable:
projects_with_service_desk = ::Project.where(service_desk_enabled: true) projects_with_service_desk = ::Project.where(service_desk_enabled: true)
# rubocop: enable UsageData/LargeTable:
{ {
service_desk_enabled_projects: count(projects_with_service_desk), service_desk_enabled_projects: count(projects_with_service_desk),
service_desk_issues: count( service_desk_issues: count(
......
...@@ -22720,6 +22720,15 @@ msgstr "" ...@@ -22720,6 +22720,15 @@ msgstr ""
msgid "Static Application Security Testing (SAST)" msgid "Static Application Security Testing (SAST)"
msgstr "" msgstr ""
msgid "StaticSiteEditor|1. Add a clear title to describe the change."
msgstr ""
msgid "StaticSiteEditor|2. Add a description to explain why the change is being made."
msgstr ""
msgid "StaticSiteEditor|3. Assign a person to review and accept the merge request."
msgstr ""
msgid "StaticSiteEditor|An error occurred while submitting your changes." msgid "StaticSiteEditor|An error occurred while submitting your changes."
msgstr "" msgstr ""
...@@ -22741,13 +22750,10 @@ msgstr "" ...@@ -22741,13 +22750,10 @@ msgstr ""
msgid "StaticSiteEditor|Static site editor" msgid "StaticSiteEditor|Static site editor"
msgstr "" msgstr ""
msgid "StaticSiteEditor|Success!" msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
msgstr "" msgstr ""
msgid "StaticSiteEditor|Summary of changes" msgid "StaticSiteEditor|To see your changes live you will need to do the following things:"
msgstr ""
msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
msgstr "" msgstr ""
msgid "StaticSiteEditor|Update %{sourcePath} file" msgid "StaticSiteEditor|Update %{sourcePath} file"
...@@ -22756,19 +22762,7 @@ msgstr "" ...@@ -22756,19 +22762,7 @@ msgstr ""
msgid "StaticSiteEditor|View documentation" msgid "StaticSiteEditor|View documentation"
msgstr "" msgstr ""
msgid "StaticSiteEditor|View merge request" msgid "StaticSiteEditor|Your merge request has been created"
msgstr ""
msgid "StaticSiteEditor|You added a commit:"
msgstr ""
msgid "StaticSiteEditor|You created a merge request:"
msgstr ""
msgid "StaticSiteEditor|You created a new branch:"
msgstr ""
msgid "StaticSiteEditor|Your changes have been submitted and a merge request has been created. The changes won’t be visible on the site until the merge request has been accepted."
msgstr "" msgstr ""
msgid "Statistics" msgid "Statistics"
...@@ -25689,6 +25683,9 @@ msgstr "" ...@@ -25689,6 +25683,9 @@ msgstr ""
msgid "Update" msgid "Update"
msgstr "" msgstr ""
msgid "Update %{sourcePath} file"
msgstr ""
msgid "Update all" msgid "Update all"
msgstr "" msgstr ""
...@@ -26425,6 +26422,9 @@ msgstr "" ...@@ -26425,6 +26422,9 @@ msgstr ""
msgid "View log" msgid "View log"
msgstr "" msgstr ""
msgid "View merge request"
msgstr ""
msgid "View open merge request" msgid "View open merge request"
msgstr "" msgstr ""
......
# frozen_string_literal: true
module RuboCop
module Cop
module UsageData
class LargeTable < RuboCop::Cop::Cop
# This cop checks that batch count and distinct_count are used in usage_data.rb files in metrics based on ActiveRecord models.
#
# @example
#
# # bad
# Issue.count
# List.assignee.count
# ::Ci::Pipeline.auto_devops_source.count
# ZoomMeeting.distinct.count(:issue_id)
#
# # Good
# count(Issue)
# count(List.assignee)
# count(::Ci::Pipeline.auto_devops_source)
# distinct_count(ZoomMeeting, :issue_id)
MSG = 'Use one of the %{count_methods} methods for counting on %{class_name}'
# Match one level const as Issue, Gitlab
def_node_matcher :one_level_node, <<~PATTERN
(send
(const {nil? cbase} $...)
$...)
PATTERN
# Match two level const as ::Clusters::Cluster, ::Ci::Pipeline
def_node_matcher :two_level_node, <<~PATTERN
(send
(const
(const {nil? cbase} $...)
$...)
$...)
PATTERN
def on_send(node)
one_level_matches = one_level_node(node)
two_level_matches = two_level_node(node)
return unless Array(one_level_matches).any? || Array(two_level_matches).any?
if one_level_matches
class_name = one_level_matches[0].first
method_used = one_level_matches[1]&.first
else
class_name = "#{two_level_matches[0].first}::#{two_level_matches[1].first}".to_sym
method_used = two_level_matches[2]&.first
end
return if non_related?(class_name) || allowed_methods.include?(method_used)
counters_used = node.ancestors.any? { |ancestor| allowed_method?(ancestor) }
unless counters_used
add_offense(node, location: :expression, message: format(MSG, count_methods: count_methods.join(', '), class_name: class_name))
end
end
private
def count_methods
cop_config['CountMethods'] || []
end
def allowed_methods
cop_config['AllowedMethods'] || []
end
def non_related_classes
cop_config['NonRelatedClasses'] || []
end
def non_related?(class_name)
non_related_classes.include?(class_name)
end
def allowed_method?(ancestor)
ancestor.send_type? && !ancestor.dot? && count_methods.include?(ancestor.method_name)
end
end
end
end
end
UsageData/LargeTable:
Enabled: true
Include:
- 'lib/gitlab/usage_data.rb'
- 'ee/lib/ee/gitlab/usage_data.rb'
NonRelatedClasses:
- :Feature
- :Gitlab
- :Gitlab::AppLogger
- :Gitlab::Auth
- :Gitlab::CurrentSettings
- :Gitlab::Database
- :Gitlab::ErrorTracking
- :Gitlab::Geo
- :Gitlab::Git
- :Gitlab::IncomingEmail
- :Gitlab::Metrics
- :Gitlab::Runtime
- :Gitaly::Server
- :Gitlab::UsageData
- :License
- :Rails
- :Time
- :SECURE_PRODUCT_TYPES
- :Settings
CountMethods:
- :count
- :distinct_count
AllowedMethods:
- :arel_table
- :minimum
- :maximum
import $ from 'jquery';
import '~/gl_dropdown';
import AccessDropdown from '~/projects/settings/access_dropdown';
import { LEVEL_TYPES } from '~/projects/settings/constants';
describe('AccessDropdown', () => {
const defaultLabel = 'dummy default label';
let dropdown;
beforeEach(() => {
setFixtures(`
<div id="dummy-dropdown">
<span class="dropdown-toggle-text"></span>
</div>
`);
const $dropdown = $('#dummy-dropdown');
$dropdown.data('defaultLabel', defaultLabel);
const options = {
$dropdown,
accessLevelsData: {
roles: [
{
id: 42,
text: 'Dummy Role',
},
],
},
};
dropdown = new AccessDropdown(options);
});
describe('toggleLabel', () => {
let $dropdownToggleText;
const dummyItems = [
{ type: LEVEL_TYPES.ROLE, access_level: 42 },
{ type: LEVEL_TYPES.USER },
{ type: LEVEL_TYPES.USER },
{ type: LEVEL_TYPES.GROUP },
{ type: LEVEL_TYPES.GROUP },
{ type: LEVEL_TYPES.GROUP },
];
beforeEach(() => {
$dropdownToggleText = $('.dropdown-toggle-text');
});
it('displays number of items', () => {
dropdown.setSelectedItems(dummyItems);
$dropdownToggleText.addClass('is-default');
const label = dropdown.toggleLabel();
expect(label).toBe('1 role, 2 users, 3 groups');
expect($dropdownToggleText).not.toHaveClass('is-default');
});
describe('without selected items', () => {
beforeEach(() => {
dropdown.setSelectedItems([]);
});
it('falls back to default label', () => {
const label = dropdown.toggleLabel();
expect(label).toBe(defaultLabel);
expect($dropdownToggleText).toHaveClass('is-default');
});
});
describe('with only role', () => {
beforeEach(() => {
dropdown.setSelectedItems(dummyItems.filter(item => item.type === LEVEL_TYPES.ROLE));
$dropdownToggleText.addClass('is-default');
});
it('displays the role name', () => {
const label = dropdown.toggleLabel();
expect(label).toBe('Dummy Role');
expect($dropdownToggleText).not.toHaveClass('is-default');
});
});
describe('with only users', () => {
beforeEach(() => {
dropdown.setSelectedItems(dummyItems.filter(item => item.type === LEVEL_TYPES.USER));
$dropdownToggleText.addClass('is-default');
});
it('displays number of users', () => {
const label = dropdown.toggleLabel();
expect(label).toBe('2 users');
expect($dropdownToggleText).not.toHaveClass('is-default');
});
});
describe('with only groups', () => {
beforeEach(() => {
dropdown.setSelectedItems(dummyItems.filter(item => item.type === LEVEL_TYPES.GROUP));
$dropdownToggleText.addClass('is-default');
});
it('displays number of groups', () => {
const label = dropdown.toggleLabel();
expect(label).toBe('3 groups');
expect($dropdownToggleText).not.toHaveClass('is-default');
});
});
describe('with users and groups', () => {
beforeEach(() => {
const selectedTypes = [LEVEL_TYPES.GROUP, LEVEL_TYPES.USER];
dropdown.setSelectedItems(dummyItems.filter(item => selectedTypes.includes(item.type)));
$dropdownToggleText.addClass('is-default');
});
it('displays number of groups', () => {
const label = dropdown.toggleLabel();
expect(label).toBe('2 users, 3 groups');
expect($dropdownToggleText).not.toHaveClass('is-default');
});
});
});
describe('userRowHtml', () => {
it('escapes users name', () => {
const user = {
avatar_url: '',
name: '<img src=x onerror=alert(document.domain)>',
username: 'test',
};
const template = dropdown.userRowHtml(user);
expect(template).not.toContain(user.name);
});
});
});
import { shallowMount } from '@vue/test-utils';
import App from '~/static_site_editor/components/app.vue';
describe('static_site_editor/components/app', () => {
const mergeRequestsIllustrationPath = 'illustrations/merge_requests.svg';
const RouterView = {
template: '<div></div>',
};
let wrapper;
const buildWrapper = () => {
wrapper = shallowMount(App, {
stubs: {
RouterView,
},
propsData: {
mergeRequestsIllustrationPath,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('passes merge request illustration path to the router view component', () => {
buildWrapper();
expect(wrapper.find(RouterView).attributes()).toMatchObject({
'merge-requests-illustration-path': mergeRequestsIllustrationPath,
});
});
});
import { shallowMount } from '@vue/test-utils';
import SavedChangesMessage from '~/static_site_editor/components/saved_changes_message.vue';
import { returnUrl, savedContentMeta } from '../mock_data';
describe('~/static_site_editor/components/saved_changes_message.vue', () => {
let wrapper;
const { branch, commit, mergeRequest } = savedContentMeta;
const props = {
branch,
commit,
mergeRequest,
returnUrl,
};
const findReturnToSiteButton = () => wrapper.find({ ref: 'returnToSiteButton' });
const findMergeRequestButton = () => wrapper.find({ ref: 'mergeRequestButton' });
const findBranchLink = () => wrapper.find({ ref: 'branchLink' });
const findCommitLink = () => wrapper.find({ ref: 'commitLink' });
const findMergeRequestLink = () => wrapper.find({ ref: 'mergeRequestLink' });
beforeEach(() => {
wrapper = shallowMount(SavedChangesMessage, {
propsData: props,
});
});
afterEach(() => {
wrapper.destroy();
});
it.each`
text | findEl | url
${'Return to site'} | ${findReturnToSiteButton} | ${props.returnUrl}
${'View merge request'} | ${findMergeRequestButton} | ${props.mergeRequest.url}
`('renders "$text" button link', ({ text, findEl, url }) => {
const btn = findEl();
expect(btn.exists()).toBe(true);
expect(btn.text()).toBe(text);
expect(btn.attributes('href')).toBe(url);
});
it.each`
desc | findEl | prop
${'branch'} | ${findBranchLink} | ${props.branch}
${'commit'} | ${findCommitLink} | ${props.commit}
${'merge request'} | ${findMergeRequestLink} | ${props.mergeRequest}
`('renders $desc link', ({ findEl, prop }) => {
const el = findEl();
expect(el.exists()).toBe(true);
expect(el.text()).toBe(prop.label);
expect(el.attributes('href')).toBe(prop.url);
});
});
import Vuex from 'vuex'; import { shallowMount } from '@vue/test-utils';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { GlEmptyState, GlButton } from '@gitlab/ui';
import Success from '~/static_site_editor/pages/success.vue'; import Success from '~/static_site_editor/pages/success.vue';
import SavedChangesMessage from '~/static_site_editor/components/saved_changes_message.vue'; import { savedContentMeta, returnUrl, sourcePath } from '../mock_data';
import { savedContentMeta, returnUrl } from '../mock_data';
import { HOME_ROUTE } from '~/static_site_editor/router/constants'; import { HOME_ROUTE } from '~/static_site_editor/router/constants';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('static_site_editor/pages/success', () => { describe('static_site_editor/pages/success', () => {
const mergeRequestsIllustrationPath = 'illustrations/merge_requests.svg';
let wrapper; let wrapper;
let store;
let router; let router;
const buildRouter = () => { const buildRouter = () => {
...@@ -22,16 +17,22 @@ describe('static_site_editor/pages/success', () => { ...@@ -22,16 +17,22 @@ describe('static_site_editor/pages/success', () => {
const buildWrapper = (data = {}) => { const buildWrapper = (data = {}) => {
wrapper = shallowMount(Success, { wrapper = shallowMount(Success, {
localVue,
store,
mocks: { mocks: {
$router: router, $router: router,
}, },
stubs: {
GlEmptyState,
GlButton,
},
propsData: {
mergeRequestsIllustrationPath,
},
data() { data() {
return { return {
savedContentMeta, savedContentMeta,
appData: { appData: {
returnUrl, returnUrl,
sourcePath,
}, },
...data, ...data,
}; };
...@@ -39,7 +40,8 @@ describe('static_site_editor/pages/success', () => { ...@@ -39,7 +40,8 @@ describe('static_site_editor/pages/success', () => {
}); });
}; };
const findSavedChangesMessage = () => wrapper.find(SavedChangesMessage); const findEmptyState = () => wrapper.find(GlEmptyState);
const findReturnUrlButton = () => wrapper.find(GlButton);
beforeEach(() => { beforeEach(() => {
buildRouter(); buildRouter();
...@@ -50,29 +52,50 @@ describe('static_site_editor/pages/success', () => { ...@@ -50,29 +52,50 @@ describe('static_site_editor/pages/success', () => {
wrapper = null; wrapper = null;
}); });
it('renders saved changes message', () => { it('renders empty state with a link to the created merge request', () => {
buildWrapper();
expect(findEmptyState().exists()).toBe(true);
expect(findEmptyState().props()).toMatchObject({
primaryButtonText: 'View merge request',
primaryButtonLink: savedContentMeta.mergeRequest.url,
title: 'Your merge request has been created',
svgPath: mergeRequestsIllustrationPath,
});
});
it('displays merge request instructions in the empty state', () => {
buildWrapper(); buildWrapper();
expect(findSavedChangesMessage().exists()).toBe(true); expect(findEmptyState().text()).toContain(
'To see your changes live you will need to do the following things:',
);
expect(findEmptyState().text()).toContain('1. Add a clear title to describe the change.');
expect(findEmptyState().text()).toContain(
'2. Add a description to explain why the change is being made.',
);
expect(findEmptyState().text()).toContain(
'3. Assign a person to review and accept the merge request.',
);
}); });
it('passes returnUrl to the saved changes message', () => { it('displays return to site button', () => {
buildWrapper(); buildWrapper();
expect(findSavedChangesMessage().props('returnUrl')).toBe(returnUrl); expect(findReturnUrlButton().text()).toBe('Return to site');
expect(findReturnUrlButton().attributes().href).toBe(returnUrl);
}); });
it('passes saved content metadata to the saved changes message', () => { it('displays source path', () => {
buildWrapper(); buildWrapper();
expect(findSavedChangesMessage().props('branch')).toBe(savedContentMeta.branch); expect(wrapper.text()).toContain(`Update ${sourcePath} file`);
expect(findSavedChangesMessage().props('commit')).toBe(savedContentMeta.commit);
expect(findSavedChangesMessage().props('mergeRequest')).toBe(savedContentMeta.mergeRequest);
}); });
it('redirects to the HOME route when content has not been submitted', () => { it('redirects to the HOME route when content has not been submitted', () => {
buildWrapper({ savedContentMeta: null }); buildWrapper({ savedContentMeta: null });
expect(router.push).toHaveBeenCalledWith(HOME_ROUTE); expect(router.push).toHaveBeenCalledWith(HOME_ROUTE);
expect(wrapper.html()).toBe('');
}); });
}); });
...@@ -36,6 +36,9 @@ describe('File Icon component', () => { ...@@ -36,6 +36,9 @@ describe('File Icon component', () => {
fileName | iconName fileName | iconName
${'test.js'} | ${'javascript'} ${'test.js'} | ${'javascript'}
${'test.png'} | ${'image'} ${'test.png'} | ${'image'}
${'test.PNG'} | ${'image'}
${'.npmrc'} | ${'npm'}
${'.Npmrc'} | ${'file'}
${'webpack.js'} | ${'webpack'} ${'webpack.js'} | ${'webpack'}
`('should render a $iconName icon based on file ending', ({ fileName, iconName }) => { `('should render a $iconName icon based on file ending', ({ fileName, iconName }) => {
createComponent({ fileName }); createComponent({ fileName });
......
...@@ -110,20 +110,6 @@ RSpec.describe Banzai::Filter::ReferenceFilter do ...@@ -110,20 +110,6 @@ RSpec.describe Banzai::Filter::ReferenceFilter do
expect(filter.instance_variable_get(:@new_nodes)).to eq({ index => [filter.each_node.to_a[index]] }) expect(filter.instance_variable_get(:@new_nodes)).to eq({ index => [filter.each_node.to_a[index]] })
end end
context "with update_nodes_for_banzai_reference_filter feature flag disabled" do
before do
stub_feature_flags(update_nodes_for_banzai_reference_filter: false)
end
it 'does not call replace_and_update_new_nodes' do
expect(filter).not_to receive(:replace_and_update_new_nodes).with(filter.nodes[index], index, html)
filter.send(method_name, *args) do
html
end
end
end
end end
end end
...@@ -198,49 +184,20 @@ RSpec.describe Banzai::Filter::ReferenceFilter do ...@@ -198,49 +184,20 @@ RSpec.describe Banzai::Filter::ReferenceFilter do
end end
describe "#call_and_update_nodes" do describe "#call_and_update_nodes" do
context "with update_nodes_for_banzai_reference_filter feature flag enabled" do include_context 'new nodes'
include_context 'new nodes' let(:document) { Nokogiri::HTML.fragment('<a href="foo">foo</a>') }
let(:document) { Nokogiri::HTML.fragment('<a href="foo">foo</a>') } let(:filter) { described_class.new(document, project: project) }
let(:filter) { described_class.new(document, project: project) }
before do
stub_feature_flags(update_nodes_for_banzai_reference_filter: true)
end
it "updates all new nodes", :aggregate_failures do
filter.instance_variable_set('@nodes', nodes)
expect(filter).to receive(:call) { filter.instance_variable_set('@new_nodes', new_nodes) }
expect(filter).to receive(:with_update_nodes).and_call_original
expect(filter).to receive(:update_nodes!).and_call_original
filter.call_and_update_nodes
expect(filter.result[:reference_filter_nodes]).to eq(expected_nodes)
end
end
context "with update_nodes_for_banzai_reference_filter feature flag disabled" do
include_context 'new nodes'
before do
stub_feature_flags(update_nodes_for_banzai_reference_filter: false)
end
it "does not change nodes", :aggregate_failures do it "updates all new nodes", :aggregate_failures do
document = Nokogiri::HTML.fragment('<a href="foo">foo</a>') filter.instance_variable_set('@nodes', nodes)
filter = described_class.new(document, project: project)
filter.instance_variable_set('@nodes', nodes)
expect(filter).to receive(:call) { filter.instance_variable_set('@new_nodes', new_nodes) } expect(filter).to receive(:call) { filter.instance_variable_set('@new_nodes', new_nodes) }
expect(filter).not_to receive(:with_update_nodes) expect(filter).to receive(:with_update_nodes).and_call_original
expect(filter).not_to receive(:update_nodes!) expect(filter).to receive(:update_nodes!).and_call_original
filter.call_and_update_nodes filter.call_and_update_nodes
expect(filter.nodes).to eq(nodes) expect(filter.result[:reference_filter_nodes]).to eq(expected_nodes)
expect(filter.result[:reference_filter_nodes]).to be nil
end
end end
end end
...@@ -251,10 +208,6 @@ RSpec.describe Banzai::Filter::ReferenceFilter do ...@@ -251,10 +208,6 @@ RSpec.describe Banzai::Filter::ReferenceFilter do
let(:result) { { reference_filter_nodes: nodes } } let(:result) { { reference_filter_nodes: nodes } }
before do
stub_feature_flags(update_nodes_for_banzai_reference_filter: true)
end
it "updates all nodes", :aggregate_failures do it "updates all nodes", :aggregate_failures do
expect_next_instance_of(described_class) do |filter| expect_next_instance_of(described_class) do |filter|
expect(filter).to receive(:call_and_update_nodes).and_call_original expect(filter).to receive(:call_and_update_nodes).and_call_original
...@@ -267,26 +220,5 @@ RSpec.describe Banzai::Filter::ReferenceFilter do ...@@ -267,26 +220,5 @@ RSpec.describe Banzai::Filter::ReferenceFilter do
expect(result[:reference_filter_nodes]).to eq(expected_nodes) expect(result[:reference_filter_nodes]).to eq(expected_nodes)
end end
context "with update_nodes_for_banzai_reference_filter feature flag disabled" do
let(:result) { {} }
before do
stub_feature_flags(update_nodes_for_banzai_reference_filter: false)
end
it "updates all nodes", :aggregate_failures do
expect_next_instance_of(described_class) do |filter|
expect(filter).to receive(:call_and_update_nodes).and_call_original
expect(filter).not_to receive(:with_update_nodes)
expect(filter).to receive(:call) { filter.instance_variable_set('@new_nodes', new_nodes) }
expect(filter).not_to receive(:update_nodes!)
end
described_class.call(document, { project: project }, result)
expect(result[:reference_filter_nodes]).to be nil
end
end
end end
end end
...@@ -30,34 +30,6 @@ RSpec.describe Banzai::Pipeline::GfmPipeline do ...@@ -30,34 +30,6 @@ RSpec.describe Banzai::Pipeline::GfmPipeline do
described_class.call(markdown, project: project) described_class.call(markdown, project: project)
end end
context "with update_nodes_for_banzai_reference_filter feature flag disabled" do
before do
stub_feature_flags(update_nodes_for_banzai_reference_filter: false)
end
context 'when shorthand pattern #ISSUE_ID is used' do
it 'links an internal issues and doesnt store nodes in result[:reference_filter_nodes]', :aggregate_failures do
issue = create(:issue, project: project)
markdown = "text #{issue.to_reference(project, full: true)}"
result = described_class.call(markdown, project: project)
link = result[:output].css('a').first
expect(link['href']).to eq(Gitlab::Routing.url_helpers.project_issue_path(project, issue))
expect(result[:reference_filter_nodes]).to eq nil
end
end
it 'execute :each_node for each reference_filter', :aggregate_failures do
issue = create(:issue, project: project)
markdown = "text #{issue.to_reference(project, full: true)}"
described_class.reference_filters do |reference_filter|
expect_any_instance_of(reference_filter).to receive(:each_node).once
end
described_class.call(markdown, project: project)
end
end
context 'when shorthand pattern #ISSUE_ID is used' do context 'when shorthand pattern #ISSUE_ID is used' do
it 'links an internal issue if it exists' do it 'links an internal issue if it exists' do
issue = create(:issue, project: project) issue = create(:issue, project: project)
......
# frozen_string_literal: true # frozen_string_literal: true
require 'spec_helper' require "spec_helper"
RSpec.describe DesignManagement::DesignPolicy do RSpec.describe DesignManagement::DesignPolicy do
include DesignManagementTestHelpers include DesignManagementTestHelpers
include_context 'ProjectPolicy context'
let(:guest_design_abilities) { %i[read_design] } let(:guest_design_abilities) { %i[read_design] }
let(:developer_design_abilities) do let(:developer_design_abilities) { %i[create_design destroy_design] }
%i[create_design destroy_design]
end
let(:design_abilities) { guest_design_abilities + developer_design_abilities } let(:design_abilities) { guest_design_abilities + developer_design_abilities }
let(:issue) { create(:issue, project: project) } let_it_be(:guest) { create(:user) }
let_it_be(:reporter) { create(:user) }
let_it_be(:developer) { create(:user) }
let_it_be(:maintainer) { create(:user) }
let_it_be(:owner) { create(:user) }
let_it_be(:admin) { create(:admin) }
let_it_be(:project) { create(:project, :public, namespace: owner.namespace) }
let_it_be(:issue) { create(:issue, project: project) }
let(:design) { create(:design, issue: issue) } let(:design) { create(:design, issue: issue) }
subject(:design_policy) { described_class.new(current_user, design) } subject(:design_policy) { described_class.new(current_user, design) }
before_all do
project.add_guest(guest)
project.add_maintainer(maintainer)
project.add_developer(developer)
project.add_reporter(reporter)
end
shared_examples_for "design abilities not available" do shared_examples_for "design abilities not available" do
context "for owners" do context "for owners" do
let(:current_user) { owner } let(:current_user) { owner }
...@@ -71,11 +81,11 @@ RSpec.describe DesignManagement::DesignPolicy do ...@@ -71,11 +81,11 @@ RSpec.describe DesignManagement::DesignPolicy do
context "for admins" do context "for admins" do
let(:current_user) { admin } let(:current_user) { admin }
context 'when admin mode enabled', :enable_admin_mode do context "when admin mode enabled", :enable_admin_mode do
it { is_expected.to be_allowed(*design_abilities) } it { is_expected.to be_allowed(*design_abilities) }
end end
context 'when admin mode disabled' do context "when admin mode disabled" do
it { is_expected.to be_allowed(*guest_design_abilities) } it { is_expected.to be_allowed(*guest_design_abilities) }
it { is_expected.to be_disallowed(*developer_design_abilities) } it { is_expected.to be_disallowed(*developer_design_abilities) }
end end
...@@ -122,7 +132,7 @@ RSpec.describe DesignManagement::DesignPolicy do ...@@ -122,7 +132,7 @@ RSpec.describe DesignManagement::DesignPolicy do
it_behaves_like "design abilities available for members" it_behaves_like "design abilities available for members"
context "for guests in private projects" do context "for guests in private projects" do
let(:project) { create(:project, :private) } let_it_be(:project) { create(:project, :private) }
let(:current_user) { guest } let(:current_user) { guest }
it { is_expected.to be_allowed(*guest_design_abilities) } it { is_expected.to be_allowed(*guest_design_abilities) }
...@@ -137,7 +147,7 @@ RSpec.describe DesignManagement::DesignPolicy do ...@@ -137,7 +147,7 @@ RSpec.describe DesignManagement::DesignPolicy do
end end
context "when the issue is confidential" do context "when the issue is confidential" do
let(:issue) { create(:issue, :confidential, project: project) } let_it_be(:issue) { create(:issue, :confidential, project: project) }
it_behaves_like "design abilities available for members" it_behaves_like "design abilities available for members"
...@@ -155,26 +165,24 @@ RSpec.describe DesignManagement::DesignPolicy do ...@@ -155,26 +165,24 @@ RSpec.describe DesignManagement::DesignPolicy do
end end
context "when the issue is locked" do context "when the issue is locked" do
let_it_be(:issue) { create(:issue, :locked, project: project) }
let(:current_user) { owner } let(:current_user) { owner }
let(:issue) { create(:issue, :locked, project: project) }
it_behaves_like "read-only design abilities" it_behaves_like "read-only design abilities"
end end
context "when the issue has moved" do context "when the issue has moved" do
let_it_be(:issue) { create(:issue, project: project, moved_to: create(:issue)) }
let(:current_user) { owner } let(:current_user) { owner }
let(:issue) { create(:issue, project: project, moved_to: create(:issue)) }
it_behaves_like "read-only design abilities" it_behaves_like "read-only design abilities"
end end
context "when the project is archived" do context "when the project is archived" do
let_it_be(:project) { create(:project, :public, :archived) }
let_it_be(:issue) { create(:issue, project: project) }
let(:current_user) { owner } let(:current_user) { owner }
before do
project.update!(archived: true)
end
it_behaves_like "read-only design abilities" it_behaves_like "read-only design abilities"
end end
end end
......
# frozen_string_literal: true
require 'fast_spec_helper'
require 'rubocop'
require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/usage_data/large_table'
RSpec.describe RuboCop::Cop::UsageData::LargeTable, type: :rubocop do
include CopHelper
let(:large_tables) { %i[Rails Time] }
let(:count_methods) { %i[count distinct_count] }
let(:allowed_methods) { %i[minimum maximum] }
let(:config) do
RuboCop::Config.new('UsageData/LargeTable' => {
'NonRelatedClasses' => large_tables,
'CountMethods' => count_methods,
'AllowedMethods' => allowed_methods
})
end
subject(:cop) { described_class.new(config) }
context 'when in usage_data files' do
before do
allow(cop).to receive(:usage_data_files?).and_return(true)
end
context 'with large tables' do
context 'when calling Issue.count' do
it 'register an offence' do
inspect_source('Issue.count')
expect(cop.offenses.size).to eq(1)
end
end
context 'when calling Issue.active.count' do
it 'register an offence' do
inspect_source('Issue.active.count')
expect(cop.offenses.size).to eq(1)
end
end
context 'when calling count(Issue)' do
it 'does not register an offence' do
inspect_source('count(Issue)')
expect(cop.offenses).to be_empty
end
end
context 'when calling count(Ci::Build.active)' do
it 'does not register an offence' do
inspect_source('count(Ci::Build.active)')
expect(cop.offenses).to be_empty
end
end
context 'when calling Ci::Build.active.count' do
it 'register an offence' do
inspect_source('Ci::Build.active.count')
expect(cop.offenses.size).to eq(1)
end
end
context 'when using allowed methods' do
it 'does not register an offence' do
inspect_source('Issue.minimum')
expect(cop.offenses).to be_empty
end
end
end
context 'with non related class' do
it 'does not register an offence' do
inspect_source('Rails.count')
expect(cop.offenses).to be_empty
end
end
end
end
...@@ -27,4 +27,9 @@ module ProtectedBranchHelpers ...@@ -27,4 +27,9 @@ module ProtectedBranchHelpers
set_allowed_to('merge') set_allowed_to('merge')
set_allowed_to('push') set_allowed_to('push')
end end
def click_on_protect
click_on "Protect"
wait_for_requests
end
end end
...@@ -22,7 +22,7 @@ RSpec.shared_examples "protected branches > access control > CE" do ...@@ -22,7 +22,7 @@ RSpec.shared_examples "protected branches > access control > CE" do
end end
end end
click_on "Protect" click_on_protect
expect(ProtectedBranch.count).to eq(1) expect(ProtectedBranch.count).to eq(1)
expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to eq([access_type_id]) expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to eq([access_type_id])
...@@ -45,7 +45,7 @@ RSpec.shared_examples "protected branches > access control > CE" do ...@@ -45,7 +45,7 @@ RSpec.shared_examples "protected branches > access control > CE" do
find(:link, 'No one').click find(:link, 'No one').click
end end
click_on "Protect" click_on_protect
expect(ProtectedBranch.count).to eq(1) expect(ProtectedBranch.count).to eq(1)
...@@ -85,7 +85,7 @@ RSpec.shared_examples "protected branches > access control > CE" do ...@@ -85,7 +85,7 @@ RSpec.shared_examples "protected branches > access control > CE" do
find(:link, 'No one').click find(:link, 'No one').click
end end
click_on "Protect" click_on_protect
expect(ProtectedBranch.count).to eq(1) expect(ProtectedBranch.count).to eq(1)
expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to eq([access_type_id]) expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to eq([access_type_id])
...@@ -108,7 +108,7 @@ RSpec.shared_examples "protected branches > access control > CE" do ...@@ -108,7 +108,7 @@ RSpec.shared_examples "protected branches > access control > CE" do
find(:link, 'No one').click find(:link, 'No one').click
end end
click_on "Protect" click_on_protect
expect(ProtectedBranch.count).to eq(1) expect(ProtectedBranch.count).to eq(1)
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册