Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
e69aae81ea
commit
03fbe61813
47 changed files with 1468 additions and 469 deletions
|
@ -9,6 +9,7 @@ require:
|
|||
inherit_from:
|
||||
- .rubocop_todo.yml
|
||||
- ./rubocop/rubocop-migrations.yml
|
||||
- ./rubocop/rubocop-usage-data.yml
|
||||
|
||||
inherit_mode:
|
||||
merge:
|
||||
|
|
|
@ -14,7 +14,7 @@ export default () => {
|
|||
new ProtectedTagEditList();
|
||||
initDeployKeys();
|
||||
initSettingsPanels();
|
||||
new ProtectedBranchCreate();
|
||||
new ProtectedBranchCreate({ hasLicense: false });
|
||||
new ProtectedBranchEditList();
|
||||
new DueDateSelectors();
|
||||
fileUpload('.js-choose-file', '.js-object-map-input');
|
||||
|
|
524
app/assets/javascripts/projects/settings/access_dropdown.js
Normal file
524
app/assets/javascripts/projects/settings/access_dropdown.js
Normal file
|
@ -0,0 +1,524 @@
|
|||
/* 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>
|
||||
`;
|
||||
}
|
||||
}
|
13
app/assets/javascripts/projects/settings/constants.js
Normal file
13
app/assets/javascripts/projects/settings/constants.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
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;
|
18
app/assets/javascripts/protected_branches/constants.js
Normal file
18
app/assets/javascripts/protected_branches/constants.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
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;
|
|
@ -1,28 +0,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();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,41 +1,62 @@
|
|||
import $ from 'jquery';
|
||||
import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown';
|
||||
import CreateItemDropdown from '../create_item_dropdown';
|
||||
import AccessorUtilities from '../lib/utils/accessor';
|
||||
import AccessDropdown from '~/projects/settings/access_dropdown';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
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';
|
||||
|
||||
export default class ProtectedBranchCreate {
|
||||
constructor() {
|
||||
constructor(options) {
|
||||
this.hasLicense = options.hasLicense;
|
||||
|
||||
this.$form = $('.js-new-protected-branch');
|
||||
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
|
||||
this.currentProjectUserDefaults = {};
|
||||
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() {
|
||||
const $allowedToMergeDropdown = this.$form.find('.js-allowed-to-merge');
|
||||
const $allowedToPushDropdown = this.$form.find('.js-allowed-to-push');
|
||||
const $protectedBranchDropdown = this.$form.find('.js-protected-branch-select');
|
||||
|
||||
// Cache callback
|
||||
this.onSelectCallback = this.onSelect.bind(this);
|
||||
|
||||
// Allowed to Merge dropdown
|
||||
this.protectedBranchMergeAccessDropdown = new ProtectedBranchAccessDropdown({
|
||||
this[`${ACCESS_LEVELS.MERGE}_dropdown`] = new AccessDropdown({
|
||||
$dropdown: $allowedToMergeDropdown,
|
||||
data: gon.merge_access_levels,
|
||||
accessLevelsData: gon.merge_access_levels,
|
||||
onSelect: this.onSelectCallback,
|
||||
accessLevel: ACCESS_LEVELS.MERGE,
|
||||
hasLicense: this.hasLicense,
|
||||
});
|
||||
|
||||
// Allowed to Push dropdown
|
||||
this.protectedBranchPushAccessDropdown = new ProtectedBranchAccessDropdown({
|
||||
this[`${ACCESS_LEVELS.PUSH}_dropdown`] = new AccessDropdown({
|
||||
$dropdown: $allowedToPushDropdown,
|
||||
data: gon.push_access_levels,
|
||||
accessLevelsData: gon.push_access_levels,
|
||||
onSelect: this.onSelectCallback,
|
||||
accessLevel: ACCESS_LEVELS.PUSH,
|
||||
hasLicense: this.hasLicense,
|
||||
});
|
||||
|
||||
this.createItemDropdown = new CreateItemDropdown({
|
||||
$dropdown: $protectedBranchDropdown,
|
||||
$dropdown: this.$form.find('.js-protected-branch-select'),
|
||||
defaultToggleLabel: __('Protected Branch'),
|
||||
fieldName: 'protected_branch[name]',
|
||||
onSelect: this.onSelectCallback,
|
||||
|
@ -43,26 +64,66 @@ export default class ProtectedBranchCreate {
|
|||
});
|
||||
}
|
||||
|
||||
// This will run after clicked callback
|
||||
// Enable submit button after selecting an option
|
||||
onSelect() {
|
||||
// Enable submit button
|
||||
const $branchInput = this.$form.find('input[name="protected_branch[name]"]');
|
||||
const $allowedToMergeInput = this.$form.find(
|
||||
'input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]',
|
||||
);
|
||||
const $allowedToPushInput = this.$form.find(
|
||||
'input[name="protected_branch[push_access_levels_attributes][0][access_level]"]',
|
||||
);
|
||||
const completedForm = !(
|
||||
$branchInput.val() &&
|
||||
$allowedToMergeInput.length &&
|
||||
$allowedToPushInput.length
|
||||
const $allowedToMerge = this[`${ACCESS_LEVELS.MERGE}_dropdown`].getSelectedItems();
|
||||
const $allowedToPush = this[`${ACCESS_LEVELS.PUSH}_dropdown`].getSelectedItems();
|
||||
const toggle = !(
|
||||
this.$form.find('input[name="protected_branch[name]"]').val() &&
|
||||
$allowedToMerge.length &&
|
||||
$allowedToPush.length
|
||||
);
|
||||
|
||||
this.$form.find('input[type="submit"]').prop('disabled', completedForm);
|
||||
this.$form.find('input[type="submit"]').attr('disabled', toggle);
|
||||
}
|
||||
|
||||
static getProtectedBranches(term, callback) {
|
||||
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')));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,78 +1,165 @@
|
|||
import flash from '../flash';
|
||||
import axios from '../lib/utils/axios_utils';
|
||||
import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown';
|
||||
import { find } from 'lodash';
|
||||
import AccessDropdown from '~/projects/settings/access_dropdown';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import Flash from '~/flash';
|
||||
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
export default class ProtectedBranchEdit {
|
||||
constructor(options) {
|
||||
this.hasLicense = options.hasLicense;
|
||||
|
||||
this.$wraps = {};
|
||||
this.hasChanges = false;
|
||||
this.$wrap = options.$wrap;
|
||||
this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge');
|
||||
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.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() {
|
||||
// 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,
|
||||
data: gon.merge_access_levels,
|
||||
onSelect: this.onSelectCallback,
|
||||
onSelect: this.onSelectOption.bind(this),
|
||||
onHide: this.onDropdownHide.bind(this),
|
||||
hasLicense: this.hasLicense,
|
||||
});
|
||||
|
||||
// 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,
|
||||
data: gon.push_access_levels,
|
||||
onSelect: this.onSelectCallback,
|
||||
onSelect: this.onSelectOption.bind(this),
|
||||
onHide: this.onDropdownHide.bind(this),
|
||||
hasLicense: this.hasLicense,
|
||||
});
|
||||
}
|
||||
|
||||
onSelect() {
|
||||
const $allowedToMergeInput = this.$wrap.find(
|
||||
`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`,
|
||||
);
|
||||
const $allowedToPushInput = this.$wrap.find(
|
||||
`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`,
|
||||
);
|
||||
onSelectOption() {
|
||||
this.hasChanges = true;
|
||||
}
|
||||
|
||||
// Do not update if one dropdown has not selected any option
|
||||
if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return;
|
||||
onDropdownHide() {
|
||||
if (!this.hasChanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$allowedToMergeDropdown.disable();
|
||||
this.$allowedToPushDropdown.disable();
|
||||
this.hasChanges = true;
|
||||
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
|
||||
.patch(this.$wrap.data('url'), {
|
||||
protected_branch: {
|
||||
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(),
|
||||
},
|
||||
],
|
||||
},
|
||||
protected_branch: formData,
|
||||
})
|
||||
.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.$allowedToPushDropdown.enable();
|
||||
})
|
||||
.catch(() => {
|
||||
this.$allowedToMergeDropdown.enable();
|
||||
this.$allowedToPushDropdown.enable();
|
||||
|
||||
flash(
|
||||
__('Failed to update branch!'),
|
||||
'alert',
|
||||
document.querySelector('.js-protected-branches-list'),
|
||||
);
|
||||
Flash(__('Failed to update branch!'));
|
||||
});
|
||||
}
|
||||
|
||||
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 {
|
|||
this.$wrap.find('.js-protected-branch-edit-form').each((i, el) => {
|
||||
new ProtectedBranchEdit({
|
||||
$wrap: $(el),
|
||||
hasLicense: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,3 +1,13 @@
|
|||
<script>
|
||||
export default {
|
||||
props: {
|
||||
mergeRequestsIllustrationPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<router-view />
|
||||
<router-view :merge-requests-illustration-path="mergeRequestsIllustrationPath" />
|
||||
</template>
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
<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';
|
|||
import createApolloProvider from './graphql';
|
||||
|
||||
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 returnUrl = el.dataset.returnUrl || null;
|
||||
|
||||
|
@ -26,7 +33,11 @@ const initStaticSiteEditor = el => {
|
|||
App,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('app');
|
||||
return createElement('app', {
|
||||
props: {
|
||||
mergeRequestsIllustrationPath,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,12 +1,21 @@
|
|||
<script>
|
||||
import { GlEmptyState, GlButton } from '@gitlab/ui';
|
||||
import { s__, __, sprintf } from '~/locale';
|
||||
|
||||
import savedContentMetaQuery from '../graphql/queries/saved_content_meta.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';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SavedChangesMessage,
|
||||
GlEmptyState,
|
||||
GlButton,
|
||||
},
|
||||
props: {
|
||||
mergeRequestsIllustrationPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
savedContentMeta: {
|
||||
|
@ -16,20 +25,65 @@ export default {
|
|||
query: appDataQuery,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
updatedFileDescription() {
|
||||
const { sourcePath } = this.appData;
|
||||
|
||||
return sprintf(s__('Update %{sourcePath} file'), { sourcePath });
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (!this.savedContentMeta) {
|
||||
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>
|
||||
<template>
|
||||
<div v-if="savedContentMeta" class="container">
|
||||
<saved-changes-message
|
||||
:branch="savedContentMeta.branch"
|
||||
:commit="savedContentMeta.commit"
|
||||
:merge-request="savedContentMeta.mergeRequest"
|
||||
:return-url="appData.returnUrl"
|
||||
/>
|
||||
<div
|
||||
v-if="savedContentMeta"
|
||||
class="container gl-flex-grow-1 gl-display-flex gl-flex-direction-column"
|
||||
>
|
||||
<div class="gl-fixed gl-left-0 gl-right-0 gl-border-b-solid gl-border-b-1 gl-border-b-gray-100">
|
||||
<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>
|
||||
</template>
|
||||
|
|
|
@ -586,5 +586,16 @@ const fileNameIcons = {
|
|||
};
|
||||
|
||||
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 @@
|
|||
%td
|
||||
.float-right
|
||||
- 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')
|
||||
- if can?(current_user, :update_build, job)
|
||||
- if job.active?
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
- add_page_startup_api_call discussions_path(@issue)
|
||||
|
||||
- @gfm_form = true
|
||||
|
||||
- content_for :note_actions do
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
- can_reopen_issue = can?(current_user, :reopen_issue, @issue)
|
||||
- can_report_spam = @issue.submittable_as_spam_by?(current_user)
|
||||
- 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 "projects/issues/alert_moved_from_service_desk", issue: @issue
|
||||
|
@ -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 } }
|
||||
|
||||
- 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.
|
||||
|
||||
.content-block.emoji-block.emoji-block-sticky
|
||||
|
|
|
@ -1 +1 @@
|
|||
#static-site-editor{ data: @config.payload }
|
||||
#static-site-editor{ data: @config.payload.merge({ merge_requests_illustration_path: image_path('illustrations/merge_requests.svg') }) }
|
||||
|
|
5
changelogs/unreleased/216868-improve-success-screen.yml
Normal file
5
changelogs/unreleased/216868-improve-success-screen.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Improve the IA and styling of the Success screen in the Static Site Editor
|
||||
merge_request: 37475
|
||||
author:
|
||||
type: changed
|
5
changelogs/unreleased/225888-fix-download-icon.yml
Normal file
5
changelogs/unreleased/225888-fix-download-icon.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix misalignment of download icon on jobs page
|
||||
merge_request: 37966
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Improve performance of Banzai reference filters
|
||||
merge_request: 37465
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Make file icons extension detection be case-insensitive
|
||||
merge_request: 37817
|
||||
author:
|
||||
type: fixed
|
5
changelogs/unreleased/doc-iam-role-ambiguity.yml
Normal file
5
changelogs/unreleased/doc-iam-role-ambiguity.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Adds clarifying documentation on EKS IAM roles
|
||||
merge_request: 37870
|
||||
author:
|
||||
type: added
|
|
@ -13,6 +13,7 @@ Gitlab.ee do
|
|||
Elasticsearch::Model::Adapter::Multiple::Records.prepend GemExtensions::Elasticsearch::Model::Adapter::Multiple::Records
|
||||
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::Records.prepend GemExtensions::Elasticsearch::Model::Adapter::ActiveRecord::Records
|
||||
Elasticsearch::Model::Client::InstanceMethods.prepend GemExtensions::Elasticsearch::Model::Client
|
||||
Elasticsearch::Model::Client::ClassMethods.prepend GemExtensions::Elasticsearch::Model::Client
|
||||
Elasticsearch::Model::ClassMethods.prepend GemExtensions::Elasticsearch::Model::Client
|
||||
|
|
|
@ -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.
|
||||
> - 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
|
||||
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
|
|||
the archive and at this point there is one directory, a couple files, and
|
||||
one HTML file that you can view directly online when
|
||||
[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)
|
||||
|
||||
|
|
|
@ -7,24 +7,6 @@ type: reference
|
|||
|
||||
# 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
|
||||
[pipeline](../pipelines/index.md), you must:
|
||||
|
||||
|
@ -49,7 +31,11 @@ something.
|
|||
It's also common to use pipelines to automatically deploy
|
||||
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:
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
NOTE: **Note:**
|
||||
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
|
||||
[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
|
||||
request is created. In return, Jenkins shows the pipeline status on merge requests widgets and
|
||||
|
|
|
@ -965,6 +965,7 @@ documentation:
|
|||
|
||||
- [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)
|
||||
- [Azure AKS](https://docs.cilium.io/en/stable/gettingstarted/k8s-install-aks/#deploy-cilium)
|
||||
|
||||
You can customize Cilium's Helm variables by defining the
|
||||
`.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:
|
|||
1. Click **Add Kubernetes cluster**.
|
||||
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.
|
||||
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. From the left panel, select **Roles**.
|
||||
1. Click **Create role**.
|
||||
|
@ -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.
|
||||
- **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.
|
||||
- **Role name** - Select the [IAM role](https://docs.aws.amazon.com/eks/latest/userguide/service_IAM_role.html)
|
||||
to allow Amazon EKS and the Kubernetes control plane to manage AWS resources on your behalf. This IAM role is separate
|
||||
to the IAM role created above, you will need to create it if it does not yet exist.
|
||||
- **Role name** - Select the **EKS IAM role** you created earlier to allow Amazon EKS
|
||||
and the Kubernetes control plane to manage AWS resources on your behalf.
|
||||
|
||||
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)
|
||||
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)
|
||||
|
@ -194,10 +205,10 @@ If the `Cluster` resource failed with the error
|
|||
the role specified in **Role name** is not configured correctly.
|
||||
|
||||
NOTE: **Note:**
|
||||
This role should not be the same as the one created above. If you don't have an
|
||||
existing
|
||||
[EKS cluster IAM role](https://docs.aws.amazon.com/eks/latest/userguide/service_IAM_role.html),
|
||||
you must create one.
|
||||
This role should be the role you created by following the
|
||||
[EKS cluster IAM role](https://docs.aws.amazon.com/eks/latest/userguide/service_IAM_role.html) guide.
|
||||
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.
|
||||
|
||||
## Existing EKS cluster
|
||||
|
||||
|
|
|
@ -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).
|
||||
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),
|
||||
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
|
||||
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:
|
|||
- Connect the cluster to GitLab.
|
||||
- Set up one or more runners.
|
||||
- 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.
|
||||
- Install Prometheus to get statistics and metrics in the
|
||||
[threat monitoring](../../application_security/threat_monitoring/)
|
||||
|
@ -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
|
||||
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.
|
||||
|
||||
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
|
|||
```
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
## Connect the cluster to GitLab
|
||||
|
|
|
@ -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.
|
||||
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:
|
||||
|
||||
|
@ -87,6 +87,19 @@ admin.confirmed_at = Time.zone.now
|
|||
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?
|
||||
|
||||
LDAP users should NOT be affected.
|
||||
|
|
|
@ -25,14 +25,12 @@ module Banzai
|
|||
def initialize(doc, context = nil, result = nil)
|
||||
super
|
||||
|
||||
if update_nodes_enabled?
|
||||
@new_nodes = {}
|
||||
@nodes = self.result[:reference_filter_nodes]
|
||||
end
|
||||
@new_nodes = {}
|
||||
@nodes = self.result[:reference_filter_nodes]
|
||||
end
|
||||
|
||||
def call_and_update_nodes
|
||||
update_nodes_enabled? ? with_update_nodes { call } : call
|
||||
with_update_nodes { call }
|
||||
end
|
||||
|
||||
# Returns a data attribute String to attach to a reference link
|
||||
|
@ -165,11 +163,7 @@ module Banzai
|
|||
end
|
||||
|
||||
def replace_text_with_html(node, index, html)
|
||||
if update_nodes_enabled?
|
||||
replace_and_update_new_nodes(node, index, html)
|
||||
else
|
||||
node.replace(html)
|
||||
end
|
||||
replace_and_update_new_nodes(node, index, html)
|
||||
end
|
||||
|
||||
def replace_and_update_new_nodes(node, index, html)
|
||||
|
@ -209,10 +203,6 @@ module Banzai
|
|||
end
|
||||
result[:reference_filter_nodes] = nodes
|
||||
end
|
||||
|
||||
def update_nodes_enabled?
|
||||
Feature.enabled?(:update_nodes_for_banzai_reference_filter, project)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
# alt_usage_data { Gitlab::VERSION }
|
||||
# redis_usage_data(Gitlab::UsageDataCounters::WikiPageCounter)
|
||||
# redis_usage_data { ::Gitlab::UsageCounters::PodLogs.usage_totals[:total] }
|
||||
|
||||
module Gitlab
|
||||
class UsageData
|
||||
BATCH_SIZE = 100
|
||||
|
@ -84,9 +83,11 @@ module Gitlab
|
|||
auto_devops_enabled: count(::ProjectAutoDevops.enabled),
|
||||
auto_devops_disabled: count(::ProjectAutoDevops.disabled),
|
||||
deploy_keys: count(DeployKey),
|
||||
# rubocop: disable UsageData/LargeTable:
|
||||
deployments: deployment_count(Deployment),
|
||||
successful_deployments: deployment_count(Deployment.success),
|
||||
failed_deployments: deployment_count(Deployment.failed),
|
||||
# rubocop: enable UsageData/LargeTable:
|
||||
environments: count(::Environment),
|
||||
clusters: count(::Clusters::Cluster),
|
||||
clusters_enabled: count(::Clusters::Cluster.enabled),
|
||||
|
@ -171,9 +172,11 @@ module Gitlab
|
|||
def system_usage_data_monthly
|
||||
{
|
||||
counts_monthly: {
|
||||
# rubocop: disable UsageData/LargeTable:
|
||||
deployments: deployment_count(Deployment.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)),
|
||||
# rubocop: enable UsageData/LargeTable:
|
||||
personal_snippets: count(PersonalSnippet.where(last_28_days_time_period)),
|
||||
project_snippets: count(ProjectSnippet.where(last_28_days_time_period))
|
||||
}.tap do |data|
|
||||
|
@ -332,14 +335,18 @@ module Gitlab
|
|||
finish = ::Project.maximum(:id)
|
||||
|
||||
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
|
||||
# rubocop: enable UsageData/LargeTable
|
||||
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|
|
||||
::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)
|
||||
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_older_than_unset] = distinct_count(base.where(older_than: nil), :project_id, start: start, finish: finish)
|
||||
|
@ -350,9 +357,11 @@ module Gitlab
|
|||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def services_usage
|
||||
# rubocop: disable UsageData/LargeTable:
|
||||
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))
|
||||
end.merge(jira_usage).merge(jira_import_usage)
|
||||
# rubocop: enable UsageData/LargeTable:
|
||||
end
|
||||
|
||||
def jira_usage
|
||||
|
@ -365,6 +374,7 @@ module Gitlab
|
|||
projects_jira_active: 0
|
||||
}
|
||||
|
||||
# rubocop: disable UsageData/LargeTable:
|
||||
JiraService.active.includes(:jira_tracker_data).find_in_batches(batch_size: BATCH_SIZE) do |services|
|
||||
counts = services.group_by do |service|
|
||||
# TODO: Simplify as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
|
||||
|
@ -376,21 +386,24 @@ module Gitlab
|
|||
results[:projects_jira_cloud_active] += counts[:cloud].size if counts[:cloud]
|
||||
results[:projects_jira_active] += services.size
|
||||
end
|
||||
|
||||
# rubocop: enable UsageData/LargeTable:
|
||||
results
|
||||
rescue ActiveRecord::StatementInvalid
|
||||
{ projects_jira_server_active: FALLBACK, projects_jira_cloud_active: FALLBACK, projects_jira_active: FALLBACK }
|
||||
end
|
||||
|
||||
# rubocop: disable UsageData/LargeTable
|
||||
def successful_deployments_with_cluster(scope)
|
||||
scope
|
||||
.joins(cluster: :deployments)
|
||||
.merge(Clusters::Cluster.enabled)
|
||||
.merge(Deployment.success)
|
||||
end
|
||||
# rubocop: enable UsageData/LargeTable
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
def jira_import_usage
|
||||
# rubocop: disable UsageData/LargeTable
|
||||
finished_jira_imports = JiraImportState.finished
|
||||
|
||||
{
|
||||
|
@ -398,6 +411,7 @@ module Gitlab
|
|||
jira_imports_projects_count: distinct_count(finished_jira_imports, :project_id),
|
||||
jira_imports_total_imported_issues_count: alt_usage_data { JiraImportState.finished_imports_count }
|
||||
}
|
||||
# rubocop: enable UsageData/LargeTable
|
||||
end
|
||||
|
||||
def user_preferences_usage
|
||||
|
@ -406,13 +420,8 @@ module Gitlab
|
|||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def merge_requests_users(time_period)
|
||||
query =
|
||||
Event
|
||||
.where(target_type: Event::TARGET_TYPES[:merge_request].to_s)
|
||||
.where(time_period)
|
||||
|
||||
distinct_count(
|
||||
query,
|
||||
Event.where(target_type: Event::TARGET_TYPES[:merge_request].to_s).where(time_period),
|
||||
:author_id,
|
||||
start: user_minimum_id,
|
||||
finish: user_maximum_id
|
||||
|
@ -450,6 +459,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
# rubocop: disable UsageData/LargeTable
|
||||
def usage_activity_by_stage_configure(time_period)
|
||||
{
|
||||
clusters_applications_cert_managers: cluster_applications_user_distinct_count(::Clusters::Applications::CertManager, time_period),
|
||||
|
@ -470,6 +480,7 @@ module Gitlab
|
|||
project_clusters_enabled: clusters_user_distinct_count(::Clusters::Cluster.enabled.project_type, time_period)
|
||||
}
|
||||
end
|
||||
# rubocop: enable UsageData/LargeTable
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
|
@ -628,8 +639,9 @@ module Gitlab
|
|||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def service_desk_counts
|
||||
# rubocop: disable UsageData/LargeTable:
|
||||
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_issues: count(
|
||||
|
|
|
@ -22720,6 +22720,15 @@ msgstr ""
|
|||
msgid "Static Application Security Testing (SAST)"
|
||||
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."
|
||||
msgstr ""
|
||||
|
||||
|
@ -22741,34 +22750,19 @@ msgstr ""
|
|||
msgid "StaticSiteEditor|Static site editor"
|
||||
msgstr ""
|
||||
|
||||
msgid "StaticSiteEditor|Success!"
|
||||
msgstr ""
|
||||
|
||||
msgid "StaticSiteEditor|Summary of changes"
|
||||
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 ""
|
||||
|
||||
msgid "StaticSiteEditor|To see your changes live you will need to do the following things:"
|
||||
msgstr ""
|
||||
|
||||
msgid "StaticSiteEditor|Update %{sourcePath} file"
|
||||
msgstr ""
|
||||
|
||||
msgid "StaticSiteEditor|View documentation"
|
||||
msgstr ""
|
||||
|
||||
msgid "StaticSiteEditor|View merge request"
|
||||
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."
|
||||
msgid "StaticSiteEditor|Your merge request has been created"
|
||||
msgstr ""
|
||||
|
||||
msgid "Statistics"
|
||||
|
@ -25689,6 +25683,9 @@ msgstr ""
|
|||
msgid "Update"
|
||||
msgstr ""
|
||||
|
||||
msgid "Update %{sourcePath} file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Update all"
|
||||
msgstr ""
|
||||
|
||||
|
@ -26425,6 +26422,9 @@ msgstr ""
|
|||
msgid "View log"
|
||||
msgstr ""
|
||||
|
||||
msgid "View merge request"
|
||||
msgstr ""
|
||||
|
||||
msgid "View open merge request"
|
||||
msgstr ""
|
||||
|
||||
|
|
87
rubocop/cop/usage_data/large_table.rb
Normal file
87
rubocop/cop/usage_data/large_table.rb
Normal file
|
@ -0,0 +1,87 @@
|
|||
# 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
|
32
rubocop/rubocop-usage-data.yml
Normal file
32
rubocop/rubocop-usage-data.yml
Normal file
|
@ -0,0 +1,32 @@
|
|||
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
|
140
spec/frontend/projects/settings/access_dropdown_spec.js
Normal file
140
spec/frontend/projects/settings/access_dropdown_spec.js
Normal file
|
@ -0,0 +1,140 @@
|
|||
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);
|
||||
});
|
||||
});
|
||||
});
|
34
spec/frontend/static_site_editor/components/app_spec.js
Normal file
34
spec/frontend/static_site_editor/components/app_spec.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
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,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,56 +0,0 @@
|
|||
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);
|
||||
});
|
||||
});
|
|
@ -1,17 +1,12 @@
|
|||
import Vuex from 'vuex';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlEmptyState, GlButton } from '@gitlab/ui';
|
||||
import Success from '~/static_site_editor/pages/success.vue';
|
||||
import SavedChangesMessage from '~/static_site_editor/components/saved_changes_message.vue';
|
||||
import { savedContentMeta, returnUrl } from '../mock_data';
|
||||
import { savedContentMeta, returnUrl, sourcePath } from '../mock_data';
|
||||
import { HOME_ROUTE } from '~/static_site_editor/router/constants';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
|
||||
localVue.use(Vuex);
|
||||
|
||||
describe('static_site_editor/pages/success', () => {
|
||||
const mergeRequestsIllustrationPath = 'illustrations/merge_requests.svg';
|
||||
let wrapper;
|
||||
let store;
|
||||
let router;
|
||||
|
||||
const buildRouter = () => {
|
||||
|
@ -22,16 +17,22 @@ describe('static_site_editor/pages/success', () => {
|
|||
|
||||
const buildWrapper = (data = {}) => {
|
||||
wrapper = shallowMount(Success, {
|
||||
localVue,
|
||||
store,
|
||||
mocks: {
|
||||
$router: router,
|
||||
},
|
||||
stubs: {
|
||||
GlEmptyState,
|
||||
GlButton,
|
||||
},
|
||||
propsData: {
|
||||
mergeRequestsIllustrationPath,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
savedContentMeta,
|
||||
appData: {
|
||||
returnUrl,
|
||||
sourcePath,
|
||||
},
|
||||
...data,
|
||||
};
|
||||
|
@ -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(() => {
|
||||
buildRouter();
|
||||
|
@ -50,29 +52,50 @@ describe('static_site_editor/pages/success', () => {
|
|||
wrapper = null;
|
||||
});
|
||||
|
||||
it('renders saved changes message', () => {
|
||||
it('renders empty state with a link to the created merge request', () => {
|
||||
buildWrapper();
|
||||
|
||||
expect(findSavedChangesMessage().exists()).toBe(true);
|
||||
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('passes returnUrl to the saved changes message', () => {
|
||||
it('displays merge request instructions in the empty state', () => {
|
||||
buildWrapper();
|
||||
|
||||
expect(findSavedChangesMessage().props('returnUrl')).toBe(returnUrl);
|
||||
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 saved content metadata to the saved changes message', () => {
|
||||
it('displays return to site button', () => {
|
||||
buildWrapper();
|
||||
|
||||
expect(findSavedChangesMessage().props('branch')).toBe(savedContentMeta.branch);
|
||||
expect(findSavedChangesMessage().props('commit')).toBe(savedContentMeta.commit);
|
||||
expect(findSavedChangesMessage().props('mergeRequest')).toBe(savedContentMeta.mergeRequest);
|
||||
expect(findReturnUrlButton().text()).toBe('Return to site');
|
||||
expect(findReturnUrlButton().attributes().href).toBe(returnUrl);
|
||||
});
|
||||
|
||||
it('displays source path', () => {
|
||||
buildWrapper();
|
||||
|
||||
expect(wrapper.text()).toContain(`Update ${sourcePath} file`);
|
||||
});
|
||||
|
||||
it('redirects to the HOME route when content has not been submitted', () => {
|
||||
buildWrapper({ savedContentMeta: null });
|
||||
|
||||
expect(router.push).toHaveBeenCalledWith(HOME_ROUTE);
|
||||
expect(wrapper.html()).toBe('');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -36,6 +36,9 @@ describe('File Icon component', () => {
|
|||
fileName | iconName
|
||||
${'test.js'} | ${'javascript'}
|
||||
${'test.png'} | ${'image'}
|
||||
${'test.PNG'} | ${'image'}
|
||||
${'.npmrc'} | ${'npm'}
|
||||
${'.Npmrc'} | ${'file'}
|
||||
${'webpack.js'} | ${'webpack'}
|
||||
`('should render a $iconName icon based on file ending', ({ fileName, iconName }) => {
|
||||
createComponent({ fileName });
|
||||
|
|
|
@ -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]] })
|
||||
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
|
||||
|
||||
|
@ -198,49 +184,20 @@ RSpec.describe Banzai::Filter::ReferenceFilter do
|
|||
end
|
||||
|
||||
describe "#call_and_update_nodes" do
|
||||
context "with update_nodes_for_banzai_reference_filter feature flag enabled" do
|
||||
include_context 'new nodes'
|
||||
let(:document) { Nokogiri::HTML.fragment('<a href="foo">foo</a>') }
|
||||
let(:filter) { described_class.new(document, project: project) }
|
||||
include_context 'new nodes'
|
||||
let(:document) { Nokogiri::HTML.fragment('<a href="foo">foo</a>') }
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
document = Nokogiri::HTML.fragment('<a href="foo">foo</a>')
|
||||
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).not_to receive(:with_update_nodes)
|
||||
expect(filter).not_to receive(:update_nodes!)
|
||||
|
||||
filter.call_and_update_nodes
|
||||
|
||||
expect(filter.nodes).to eq(nodes)
|
||||
expect(filter.result[:reference_filter_nodes]).to be nil
|
||||
end
|
||||
expect(filter.result[:reference_filter_nodes]).to eq(expected_nodes)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -251,10 +208,6 @@ RSpec.describe Banzai::Filter::ReferenceFilter do
|
|||
|
||||
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
|
||||
expect_next_instance_of(described_class) do |filter|
|
||||
expect(filter).to receive(:call_and_update_nodes).and_call_original
|
||||
|
@ -267,26 +220,5 @@ RSpec.describe Banzai::Filter::ReferenceFilter do
|
|||
|
||||
expect(result[:reference_filter_nodes]).to eq(expected_nodes)
|
||||
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
|
||||
|
|
|
@ -30,34 +30,6 @@ RSpec.describe Banzai::Pipeline::GfmPipeline do
|
|||
described_class.call(markdown, project: project)
|
||||
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
|
||||
it 'links an internal issue if it exists' do
|
||||
issue = create(:issue, project: project)
|
||||
|
|
|
@ -1,22 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe DesignManagement::DesignPolicy do
|
||||
include DesignManagementTestHelpers
|
||||
|
||||
include_context 'ProjectPolicy context'
|
||||
|
||||
let(:guest_design_abilities) { %i[read_design] }
|
||||
let(:developer_design_abilities) do
|
||||
%i[create_design destroy_design]
|
||||
end
|
||||
let(:developer_design_abilities) { %i[create_design destroy_design] }
|
||||
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) }
|
||||
|
||||
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
|
||||
context "for owners" do
|
||||
let(:current_user) { owner }
|
||||
|
@ -71,11 +81,11 @@ RSpec.describe DesignManagement::DesignPolicy do
|
|||
context "for admins" do
|
||||
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) }
|
||||
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_disallowed(*developer_design_abilities) }
|
||||
end
|
||||
|
@ -122,7 +132,7 @@ RSpec.describe DesignManagement::DesignPolicy do
|
|||
it_behaves_like "design abilities available for members"
|
||||
|
||||
context "for guests in private projects" do
|
||||
let(:project) { create(:project, :private) }
|
||||
let_it_be(:project) { create(:project, :private) }
|
||||
let(:current_user) { guest }
|
||||
|
||||
it { is_expected.to be_allowed(*guest_design_abilities) }
|
||||
|
@ -137,7 +147,7 @@ RSpec.describe DesignManagement::DesignPolicy do
|
|||
end
|
||||
|
||||
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"
|
||||
|
||||
|
@ -155,26 +165,24 @@ RSpec.describe DesignManagement::DesignPolicy do
|
|||
end
|
||||
|
||||
context "when the issue is locked" do
|
||||
let_it_be(:issue) { create(:issue, :locked, project: project) }
|
||||
let(:current_user) { owner }
|
||||
let(:issue) { create(:issue, :locked, project: project) }
|
||||
|
||||
it_behaves_like "read-only design abilities"
|
||||
end
|
||||
|
||||
context "when the issue has moved" do
|
||||
let_it_be(:issue) { create(:issue, project: project, moved_to: create(:issue)) }
|
||||
let(:current_user) { owner }
|
||||
let(:issue) { create(:issue, project: project, moved_to: create(:issue)) }
|
||||
|
||||
it_behaves_like "read-only design abilities"
|
||||
end
|
||||
|
||||
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 }
|
||||
|
||||
before do
|
||||
project.update!(archived: true)
|
||||
end
|
||||
|
||||
it_behaves_like "read-only design abilities"
|
||||
end
|
||||
end
|
||||
|
|
90
spec/rubocop/cop/usage_data/large_table_spec.rb
Normal file
90
spec/rubocop/cop/usage_data/large_table_spec.rb
Normal file
|
@ -0,0 +1,90 @@
|
|||
# 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
|
|||
set_allowed_to('merge')
|
||||
set_allowed_to('push')
|
||||
end
|
||||
|
||||
def click_on_protect
|
||||
click_on "Protect"
|
||||
wait_for_requests
|
||||
end
|
||||
end
|
||||
|
|
|
@ -22,7 +22,7 @@ RSpec.shared_examples "protected branches > access control > CE" do
|
|||
end
|
||||
end
|
||||
|
||||
click_on "Protect"
|
||||
click_on_protect
|
||||
|
||||
expect(ProtectedBranch.count).to eq(1)
|
||||
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
|
|||
find(:link, 'No one').click
|
||||
end
|
||||
|
||||
click_on "Protect"
|
||||
click_on_protect
|
||||
|
||||
expect(ProtectedBranch.count).to eq(1)
|
||||
|
||||
|
@ -85,7 +85,7 @@ RSpec.shared_examples "protected branches > access control > CE" do
|
|||
find(:link, 'No one').click
|
||||
end
|
||||
|
||||
click_on "Protect"
|
||||
click_on_protect
|
||||
|
||||
expect(ProtectedBranch.count).to eq(1)
|
||||
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
|
|||
find(:link, 'No one').click
|
||||
end
|
||||
|
||||
click_on "Protect"
|
||||
click_on_protect
|
||||
|
||||
expect(ProtectedBranch.count).to eq(1)
|
||||
|
||||
|
|
Loading…
Reference in a new issue