Commit 0cbec8f2 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge remote-tracking branch 'ee-com/master' into ce-to-ee

Signed-off-by: default avatarDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
parents 8a3d61ba cc3e7a47
...@@ -161,6 +161,7 @@ build-package: ...@@ -161,6 +161,7 @@ build-package:
variables: variables:
SETUP_DB: "false" SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false" USE_BUNDLE_INSTALL: "false"
EE_PACKAGE: "true"
stage: build stage: build
when: manual when: manual
script: script:
......
...@@ -87,6 +87,7 @@ $(() => { ...@@ -87,6 +87,7 @@ $(() => {
Store.rootPath = this.endpoint; Store.rootPath = this.endpoint;
this.filterManager = new FilteredSearchBoards(Store.filter, true, [(this.milestoneTitle ? 'milestone' : null)]); this.filterManager = new FilteredSearchBoards(Store.filter, true, [(this.milestoneTitle ? 'milestone' : null)]);
this.filterManager.setup();
// Listen for updateTokens event // Listen for updateTokens event
eventHub.$on('updateTokens', this.updateTokens); eventHub.$on('updateTokens', this.updateTokens);
......
...@@ -13,6 +13,7 @@ export default { ...@@ -13,6 +13,7 @@ export default {
FilteredSearchContainer.container = this.$el; FilteredSearchContainer.container = this.$el;
this.filteredSearch = new FilteredSearchBoards(this.store); this.filteredSearch = new FilteredSearchBoards(this.store);
this.filteredSearch.setup();
this.filteredSearch.removeTokens(); this.filteredSearch.removeTokens();
this.filteredSearch.handleInputPlaceholder(); this.filteredSearch.handleInputPlaceholder();
this.filteredSearch.toggleClearSearchButton(); this.filteredSearch.toggleClearSearchButton();
......
...@@ -42,9 +42,7 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { ...@@ -42,9 +42,7 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
this.filteredSearchInput.dispatchEvent(new Event('input')); this.filteredSearchInput.dispatchEvent(new Event('input'));
} }
canEdit(token) { canEdit(tokenName) {
const tokenName = token.querySelector('.name').textContent.trim();
return this.cantEdit.indexOf(tokenName) === -1; return this.cantEdit.indexOf(tokenName) === -1;
} }
} }
...@@ -130,7 +130,10 @@ import ApproversSelect from './approvers_select'; ...@@ -130,7 +130,10 @@ import ApproversSelect from './approvers_select';
case 'projects:merge_requests:index': case 'projects:merge_requests:index':
case 'projects:issues:index': case 'projects:issues:index':
if (gl.FilteredSearchManager && document.querySelector('.filtered-search')) { if (gl.FilteredSearchManager && document.querySelector('.filtered-search')) {
new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests'); const filteredSearchManager = new gl.FilteredSearchManager(
page === 'projects:issues:index' ? 'issues' : 'merge_requests',
);
filteredSearchManager.setup();
} }
Issuable.init(); Issuable.init();
new gl.IssuableBulkActions({ new gl.IssuableBulkActions({
......
...@@ -6,6 +6,7 @@ import eventHub from './event_hub'; ...@@ -6,6 +6,7 @@ import eventHub from './event_hub';
class FilteredSearchManager { class FilteredSearchManager {
constructor(page) { constructor(page) {
this.page = page;
this.container = FilteredSearchContainer.container; this.container = FilteredSearchContainer.container;
this.filteredSearchInput = this.container.querySelector('.filtered-search'); this.filteredSearchInput = this.container.querySelector('.filtered-search');
this.filteredSearchInputForm = this.filteredSearchInput.form; this.filteredSearchInputForm = this.filteredSearchInput.form;
...@@ -13,7 +14,7 @@ class FilteredSearchManager { ...@@ -13,7 +14,7 @@ class FilteredSearchManager {
this.tokensContainer = this.container.querySelector('.tokens-container'); this.tokensContainer = this.container.querySelector('.tokens-container');
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys; this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
if (page === 'issues' || page === 'boards') { if (this.page === 'issues' || this.page === 'boards') {
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeysIssuesEE; this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeysIssuesEE;
} }
...@@ -21,16 +22,18 @@ class FilteredSearchManager { ...@@ -21,16 +22,18 @@ class FilteredSearchManager {
isLocalStorageAvailable: RecentSearchesService.isAvailable(), isLocalStorageAvailable: RecentSearchesService.isAvailable(),
allowedKeys: this.filteredSearchTokenKeys.getKeys(), allowedKeys: this.filteredSearchTokenKeys.getKeys(),
}); });
const searchHistoryDropdownElement = document.querySelector('.js-filtered-search-history-dropdown'); this.searchHistoryDropdownElement = document.querySelector('.js-filtered-search-history-dropdown');
const projectPath = searchHistoryDropdownElement ? const projectPath = this.searchHistoryDropdownElement ?
searchHistoryDropdownElement.dataset.projectFullPath : 'project'; this.searchHistoryDropdownElement.dataset.projectFullPath : 'project';
let recentSearchesPagePrefix = 'issue-recent-searches'; let recentSearchesPagePrefix = 'issue-recent-searches';
if (page === 'merge_requests') { if (this.page === 'merge_requests') {
recentSearchesPagePrefix = 'merge-request-recent-searches'; recentSearchesPagePrefix = 'merge-request-recent-searches';
} }
const recentSearchesKey = `${projectPath}-${recentSearchesPagePrefix}`; const recentSearchesKey = `${projectPath}-${recentSearchesPagePrefix}`;
this.recentSearchesService = new RecentSearchesService(recentSearchesKey); this.recentSearchesService = new RecentSearchesService(recentSearchesKey);
}
setup() {
// Fetch recent searches from localStorage // Fetch recent searches from localStorage
this.fetchingRecentSearchesPromise = this.recentSearchesService.fetch() this.fetchingRecentSearchesPromise = this.recentSearchesService.fetch()
.catch((error) => { .catch((error) => {
...@@ -51,12 +54,12 @@ class FilteredSearchManager { ...@@ -51,12 +54,12 @@ class FilteredSearchManager {
if (this.filteredSearchInput) { if (this.filteredSearchInput) {
this.tokenizer = gl.FilteredSearchTokenizer; this.tokenizer = gl.FilteredSearchTokenizer;
this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '', this.tokenizer, page); this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '', this.tokenizer, this.page);
this.recentSearchesRoot = new RecentSearchesRoot( this.recentSearchesRoot = new RecentSearchesRoot(
this.recentSearchesStore, this.recentSearchesStore,
this.recentSearchesService, this.recentSearchesService,
searchHistoryDropdownElement, this.searchHistoryDropdownElement,
); );
this.recentSearchesRoot.init(); this.recentSearchesRoot.init();
...@@ -145,9 +148,9 @@ class FilteredSearchManager { ...@@ -145,9 +148,9 @@ class FilteredSearchManager {
if (e.keyCode === 8 || e.keyCode === 46) { if (e.keyCode === 8 || e.keyCode === 46) {
const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
if (this.filteredSearchInput.value === '' && lastVisualToken) { const sanitizedTokenName = lastVisualToken && lastVisualToken.querySelector('.name').textContent.trim();
if (this.canEdit && !this.canEdit(lastVisualToken)) return; const canEdit = sanitizedTokenName && this.canEdit && this.canEdit(sanitizedTokenName);
if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) {
this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial(); this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
gl.FilteredSearchVisualTokens.removeLastTokenPartial(); gl.FilteredSearchVisualTokens.removeLastTokenPartial();
} }
...@@ -246,10 +249,10 @@ class FilteredSearchManager { ...@@ -246,10 +249,10 @@ class FilteredSearchManager {
editToken(e) { editToken(e) {
const token = e.target.closest('.js-visual-token'); const token = e.target.closest('.js-visual-token');
const sanitizedTokenName = token.querySelector('.name').textContent.trim();
const canEdit = this.canEdit && this.canEdit(sanitizedTokenName);
if (this.canEdit && !this.canEdit(token)) return; if (token && canEdit) {
if (token) {
gl.FilteredSearchVisualTokens.editToken(token); gl.FilteredSearchVisualTokens.editToken(token);
this.tokenChange(); this.tokenChange();
} }
...@@ -399,7 +402,12 @@ class FilteredSearchManager { ...@@ -399,7 +402,12 @@ class FilteredSearchManager {
if (condition) { if (condition) {
hasFilteredSearch = true; hasFilteredSearch = true;
gl.FilteredSearchVisualTokens.addFilterVisualToken(condition.tokenKey, condition.value); const canEdit = this.canEdit && this.canEdit(condition.tokenKey);
gl.FilteredSearchVisualTokens.addFilterVisualToken(
condition.tokenKey,
condition.value,
canEdit,
);
} else { } else {
// Sanitize value since URL converts spaces into + // Sanitize value since URL converts spaces into +
// Replace before decode so that we know what was originally + versus the encoded + // Replace before decode so that we know what was originally + versus the encoded +
...@@ -418,18 +426,27 @@ class FilteredSearchManager { ...@@ -418,18 +426,27 @@ class FilteredSearchManager {
} }
hasFilteredSearch = true; hasFilteredSearch = true;
gl.FilteredSearchVisualTokens.addFilterVisualToken(sanitizedKey, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`); const canEdit = this.canEdit && this.canEdit(sanitizedKey);
gl.FilteredSearchVisualTokens.addFilterVisualToken(
sanitizedKey,
`${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`,
canEdit,
);
} else if (!match && keyParam === 'assignee_id') { } else if (!match && keyParam === 'assignee_id') {
const id = parseInt(value, 10); const id = parseInt(value, 10);
if (usernameParams[id]) { if (usernameParams[id]) {
hasFilteredSearch = true; hasFilteredSearch = true;
gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', `@${usernameParams[id]}`); const tokenName = 'assignee';
const canEdit = this.canEdit && this.canEdit(tokenName);
gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, canEdit);
} }
} else if (!match && keyParam === 'author_id') { } else if (!match && keyParam === 'author_id') {
const id = parseInt(value, 10); const id = parseInt(value, 10);
if (usernameParams[id]) { if (usernameParams[id]) {
hasFilteredSearch = true; hasFilteredSearch = true;
gl.FilteredSearchVisualTokens.addFilterVisualToken('author', `@${usernameParams[id]}`); const tokenName = 'author';
const canEdit = this.canEdit && this.canEdit(tokenName);
gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, canEdit);
} }
} else if (!match && keyParam === 'search') { } else if (!match && keyParam === 'search') {
hasFilteredSearch = true; hasFilteredSearch = true;
...@@ -524,6 +541,11 @@ class FilteredSearchManager { ...@@ -524,6 +541,11 @@ class FilteredSearchManager {
this.filteredSearchInput.dispatchEvent(new CustomEvent('input')); this.filteredSearchInput.dispatchEvent(new CustomEvent('input'));
this.search(); this.search();
} }
// eslint-disable-next-line class-methods-use-this
canEdit() {
return true;
}
} }
window.gl = window.gl || {}; window.gl = window.gl || {};
......
...@@ -36,15 +36,22 @@ class FilteredSearchVisualTokens { ...@@ -36,15 +36,22 @@ class FilteredSearchVisualTokens {
} }
} }
static createVisualTokenElementHTML() { static createVisualTokenElementHTML(canEdit = true) {
let removeTokenMarkup = '';
if (canEdit) {
removeTokenMarkup = `
<div class="remove-token" role="button">
<i class="fa fa-close"></i>
</div>
`;
}
return ` return `
<div class="selectable" role="button"> <div class="selectable" role="button">
<div class="name"></div> <div class="name"></div>
<div class="value-container"> <div class="value-container">
<div class="value"></div> <div class="value"></div>
<div class="remove-token" role="button"> ${removeTokenMarkup}
<i class="fa fa-close"></i>
</div>
</div> </div>
</div> </div>
`; `;
...@@ -84,13 +91,13 @@ class FilteredSearchVisualTokens { ...@@ -84,13 +91,13 @@ class FilteredSearchVisualTokens {
} }
} }
static addVisualTokenElement(name, value, isSearchTerm) { static addVisualTokenElement(name, value, isSearchTerm, canEdit) {
const li = document.createElement('li'); const li = document.createElement('li');
li.classList.add('js-visual-token'); li.classList.add('js-visual-token');
li.classList.add(isSearchTerm ? 'filtered-search-term' : 'filtered-search-token'); li.classList.add(isSearchTerm ? 'filtered-search-term' : 'filtered-search-token');
if (value) { if (value) {
li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML(); li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML(canEdit);
FilteredSearchVisualTokens.renderVisualTokenValue(li, name, value); FilteredSearchVisualTokens.renderVisualTokenValue(li, name, value);
} else { } else {
li.innerHTML = '<div class="name"></div>'; li.innerHTML = '<div class="name"></div>';
...@@ -114,20 +121,20 @@ class FilteredSearchVisualTokens { ...@@ -114,20 +121,20 @@ class FilteredSearchVisualTokens {
} }
} }
static addFilterVisualToken(tokenName, tokenValue) { static addFilterVisualToken(tokenName, tokenValue, canEdit) {
const { lastVisualToken, isLastVisualTokenValid } const { lastVisualToken, isLastVisualTokenValid }
= FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
const addVisualTokenElement = FilteredSearchVisualTokens.addVisualTokenElement; const addVisualTokenElement = FilteredSearchVisualTokens.addVisualTokenElement;
if (isLastVisualTokenValid) { if (isLastVisualTokenValid) {
addVisualTokenElement(tokenName, tokenValue, false); addVisualTokenElement(tokenName, tokenValue, false, canEdit);
} else { } else {
const previousTokenName = lastVisualToken.querySelector('.name').innerText; const previousTokenName = lastVisualToken.querySelector('.name').innerText;
const tokensContainer = FilteredSearchContainer.container.querySelector('.tokens-container'); const tokensContainer = FilteredSearchContainer.container.querySelector('.tokens-container');
tokensContainer.removeChild(lastVisualToken); tokensContainer.removeChild(lastVisualToken);
const value = tokenValue || tokenName; const value = tokenValue || tokenName;
addVisualTokenElement(previousTokenName, value, false); addVisualTokenElement(previousTokenName, value, false, canEdit);
} }
} }
......
...@@ -468,8 +468,8 @@ GitLabDropdown = (function() { ...@@ -468,8 +468,8 @@ GitLabDropdown = (function() {
// Process the data to make sure rendered data // Process the data to make sure rendered data
// matches the correct layout // matches the correct layout
if (this.fullData && hasMultiSelect && this.options.processData) { const inputValue = this.filterInput.val();
const inputValue = this.filterInput.val(); if (this.fullData && hasMultiSelect && this.options.processData && inputValue.length === 0) {
this.options.processData.call(this.options, inputValue, this.filteredFullData(), this.parseData.bind(this)); this.options.processData.call(this.options, inputValue, this.filteredFullData(), this.parseData.bind(this));
} }
...@@ -755,6 +755,12 @@ GitLabDropdown = (function() { ...@@ -755,6 +755,12 @@ GitLabDropdown = (function() {
$input.attr('id', this.options.inputId); $input.attr('id', this.options.inputId);
} }
if (this.options.multiSelect) {
Object.keys(selectedObject).forEach((attribute) => {
$input.attr(`data-${attribute}`, selectedObject[attribute]);
});
}
if (this.options.inputMeta) { if (this.options.inputMeta) {
$input.attr('data-meta', selectedObject[this.options.inputMeta]); $input.attr('data-meta', selectedObject[this.options.inputMeta]);
} }
......
...@@ -42,3 +42,13 @@ export function formatRelevantDigits(number) { ...@@ -42,3 +42,13 @@ export function formatRelevantDigits(number) {
export function bytesToKiB(number) { export function bytesToKiB(number) {
return number / BYTES_IN_KIB; return number / BYTES_IN_KIB;
} }
/**
* Utility function that calculates MiB of the given bytes.
*
* @param {Number} number bytes
* @return {Number} MiB
*/
export function bytesToMiB(number) {
return number / (BYTES_IN_KIB * BYTES_IN_KIB);
}
This diff is collapsed.
<script> <script>
/* global Flash */
import Visibility from 'visibilityjs';
import Poll from '../../../lib/utils/poll';
import PipelineService from '../../services/pipeline_service';
import PipelineStore from '../../stores/pipeline_store';
import stageColumnComponent from './stage_column_component.vue'; import stageColumnComponent from './stage_column_component.vue';
import loadingIcon from '../../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
import '../../../flash'; import '../../../flash';
export default { export default {
props: {
isLoading: {
type: Boolean,
required: true,
},
pipeline: {
type: Object,
required: true,
},
},
components: { components: {
stageColumnComponent, stageColumnComponent,
loadingIcon, loadingIcon,
}, },
data() { computed: {
const DOMdata = document.getElementById('js-pipeline-graph-vue').dataset; graph() {
const store = new PipelineStore(); return this.pipeline.details && this.pipeline.details.stages;
},
return {
isLoading: false,
endpoint: DOMdata.endpoint,
store,
state: store.state,
};
},
created() {
this.service = new PipelineService(this.endpoint);
const poll = new Poll({
resource: this.service,
method: 'getPipeline',
successCallback: this.successCallback,
errorCallback: this.errorCallback,
});
if (!Visibility.hidden()) {
this.isLoading = true;
poll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
poll.restart();
} else {
poll.stop();
}
});
}, },
methods: { methods: {
successCallback(response) {
const data = response.json();
this.isLoading = false;
this.store.storeGraph(data.details.stages);
},
errorCallback() {
this.isLoading = false;
return new Flash('An error occurred while fetching the pipeline.');
},
capitalizeStageName(name) { capitalizeStageName(name) {
return name.charAt(0).toUpperCase() + name.slice(1); return name.charAt(0).toUpperCase() + name.slice(1);
}, },
...@@ -101,7 +65,7 @@ ...@@ -101,7 +65,7 @@
v-if="!isLoading" v-if="!isLoading"
class="stage-column-list"> class="stage-column-list">
<stage-column-component <stage-column-component
v-for="(stage, index) in state.graph" v-for="(stage, index) in graph"
:title="capitalizeStageName(stage.name)" :title="capitalizeStageName(stage.name)"
:jobs="stage.groups" :jobs="stage.groups"
:key="stage.name" :key="stage.name"
......
import Vue from 'vue';
import pipelineGraph from './components/graph/graph_component.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#js-pipeline-graph-vue',
components: {
pipelineGraph,
},
render: createElement => createElement('pipeline-graph'),
}));
import Vue from 'vue';
import PipelinesMediator from './pipeline_details_mediatior';
import pipelineGraph from './components/graph/graph_component.vue';
document.addEventListener('DOMContentLoaded', () => {
const dataset = document.querySelector('.js-pipeline-details-vue').dataset;
const mediator = new PipelinesMediator({ endpoint: dataset.endpoint });
mediator.fetchPipeline();
const pipelineGraphApp = new Vue({
el: '#js-pipeline-graph-vue',
data() {
return {
mediator,
};
},
components: {
pipelineGraph,
},
render(createElement) {
return createElement('pipeline-graph', {
props: {
isLoading: this.mediator.state.isLoading,
pipeline: this.mediator.store.state.pipeline,
},
});
},
});
return pipelineGraphApp;
});
/* global Flash */
import Visibility from 'visibilityjs';
import Poll from '../lib/utils/poll';
import PipelineStore from './stores/pipeline_store';
import PipelineService from './services/pipeline_service';
export default class pipelinesMediator {
constructor(options = {}) {
this.options = options;
this.store = new PipelineStore();
this.service = new PipelineService(options.endpoint);
this.state = {};
this.state.isLoading = false;
}
fetchPipeline() {
this.poll = new Poll({
resource: this.service,
method: 'getPipeline',
successCallback: this.successCallback.bind(this),
errorCallback: this.errorCallback.bind(this),
});
if (!Visibility.hidden()) {
this.state.isLoading = true;
this.poll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
}
successCallback(response) {
const data = response.json();
this.state.isLoading = false;
this.store.storePipeline(data);
}
errorCallback() {
this.state.isLoading = false;
return new Flash('An error occurred while fetching the pipeline.');
}
}
...@@ -2,10 +2,10 @@ export default class PipelineStore { ...@@ -2,10 +2,10 @@ export default class PipelineStore {
constructor() { constructor() {
this.state = {}; this.state = {};
this.state.graph = []; this.state.pipeline = {};
} }
storeGraph(graph = []) { storePipeline(pipeline = {}) {
this.state.graph = graph; this.state.pipeline = pipeline;
} }
} }
...@@ -35,6 +35,7 @@ function UsersSelect(currentUser, els) { ...@@ -35,6 +35,7 @@ function UsersSelect(currentUser, els) {
options.showCurrentUser = $dropdown.data('current-user'); options.showCurrentUser = $dropdown.data('current-user');
options.todoFilter = $dropdown.data('todo-filter'); options.todoFilter = $dropdown.data('todo-filter');
options.todoStateFilter = $dropdown.data('todo-state-filter'); options.todoStateFilter = $dropdown.data('todo-state-filter');
options.perPage = $dropdown.data('per-page');
showNullUser = $dropdown.data('null-user'); showNullUser = $dropdown.data('null-user');
defaultNullUser = $dropdown.data('null-user-default'); defaultNullUser = $dropdown.data('null-user-default');
showMenuAbove = $dropdown.data('showMenuAbove'); showMenuAbove = $dropdown.data('showMenuAbove');
...@@ -214,7 +215,36 @@ function UsersSelect(currentUser, els) { ...@@ -214,7 +215,36 @@ function UsersSelect(currentUser, els) {
glDropdown.options.processData(term, users, callback); glDropdown.options.processData(term, users, callback);
}.bind(this)); }.bind(this));
}, },
processData: function(term, users, callback) { processData: function(term, data, callback) {
let users = data;
// Only show assigned user list when there is no search term
if ($dropdown.hasClass('js-multiselect') && term.length === 0) {
const selectedInputs = getSelectedUserInputs();
// Potential duplicate entries when dealing with issue board
// because issue board is also managed by vue
const selectedUsers = _.uniq(selectedInputs, false, a => a.value)
.filter((input) => {
const userId = parseInt(input.value, 10);
const inUsersArray = users.find(u => u.id === userId);
return !inUsersArray && userId !== 0;
})
.map((input) => {
const userId = parseInt(input.value, 10);
const { avatarUrl, avatar_url, name, username } = input.dataset;
return {
avatar_url: avatarUrl || avatar_url,
id: userId,
name,
username,
};
});
users = data.concat(selectedUsers);
}
let anyUser; let anyUser;
let index; let index;
let j; let j;
...@@ -645,7 +675,7 @@ UsersSelect.prototype.users = function(query, options, callback) { ...@@ -645,7 +675,7 @@ UsersSelect.prototype.users = function(query, options, callback) {
url: url, url: url,
data: { data: {
search: query, search: query,
per_page: 20, per_page: options.perPage || 20,
active: true, active: true,
project_id: options.projectId || null, project_id: options.projectId || null,
group_id: options.groupId || null, group_id: options.groupId || null,
......
import statusCodes from '~/lib/utils/http_status'; import statusCodes from '~/lib/utils/http_status';
import { bytesToMiB } from '~/lib/utils/number_utils';
import MemoryGraph from '../../vue_shared/components/memory_graph'; import MemoryGraph from '../../vue_shared/components/memory_graph';
import MRWidgetService from '../services/mr_widget_service'; import MRWidgetService from '../services/mr_widget_service';
...@@ -9,8 +11,8 @@ export default { ...@@ -9,8 +11,8 @@ export default {
}, },
data() { data() {
return { return {
// memoryFrom: 0, memoryFrom: 0,
// memoryTo: 0, memoryTo: 0,
memoryMetrics: [], memoryMetrics: [],
deploymentTime: 0, deploymentTime: 0,
hasMetrics: false, hasMetrics: false,
...@@ -35,18 +37,38 @@ export default { ...@@ -35,18 +37,38 @@ export default {
shouldShowMetricsUnavailable() { shouldShowMetricsUnavailable() {
return !this.loadingMetrics && !this.hasMetrics && !this.loadFailed; return !this.loadingMetrics && !this.hasMetrics && !this.loadFailed;
}, },
memoryChangeType() {
const memoryTo = Number(this.memoryTo);
const memoryFrom = Number(this.memoryFrom);
if (memoryTo > memoryFrom) {
return 'increased';
} else if (memoryTo < memoryFrom) {
return 'decreased';
}
return 'unchanged';
},
}, },
methods: { methods: {
getMegabytes(bytesString) {
const valueInBytes = Number(bytesString).toFixed(2);
return (bytesToMiB(valueInBytes)).toFixed(2);
},
computeGraphData(metrics, deploymentTime) { computeGraphData(metrics, deploymentTime) {
this.loadingMetrics = false; this.loadingMetrics = false;
const { memory_values } = metrics; const { memory_before, memory_after, memory_values } = metrics;
// if (memory_previous.length > 0) {
// this.memoryFrom = Number(memory_previous[0].value[1]).toFixed(2); // Both `memory_before` and `memory_after` objects
// } // have peculiar structure where accessing only a specific
// // index yeilds correct value that we can use to show memory delta.
// if (memory_current.length > 0) { if (memory_before.length > 0) {
// this.memoryTo = Number(memory_current[0].value[1]).toFixed(2); this.memoryFrom = this.getMegabytes(memory_before[0].value[1]);
// } }
if (memory_after.length > 0) {
this.memoryTo = this.getMegabytes(memory_after[0].value[1]);
}
if (memory_values.length > 0) { if (memory_values.length > 0) {
this.hasMetrics = true; this.hasMetrics = true;
...@@ -102,7 +124,7 @@ export default { ...@@ -102,7 +124,7 @@ export default {
<p <p
v-if="shouldShowMemoryGraph" v-if="shouldShowMemoryGraph"
class="usage-info js-usage-info"> class="usage-info js-usage-info">
Deployment memory usage: Memory usage <b>{{memoryChangeType}}</b> from {{memoryFrom}}MB to {{memoryTo}}MB
</p> </p>
<p <p
v-if="shouldShowLoadFailure" v-if="shouldShowLoadFailure"
......
...@@ -104,6 +104,22 @@ ...@@ -104,6 +104,22 @@
padding: 2px 7px; padding: 2px 7px;
} }
.name {
background-color: $filter-name-resting-color;
color: $filter-name-text-color;
border-radius: 2px 0 0 2px;
margin-right: 1px;
text-transform: capitalize;
}
.value-container {
background-color: $white-normal;
color: $filter-value-text-color;
border-radius: 0 2px 2px 0;
margin-right: 5px;
padding-right: 8px;
}
.value { .value {
padding-right: 0; padding-right: 0;
} }
...@@ -111,7 +127,7 @@ ...@@ -111,7 +127,7 @@
.remove-token { .remove-token {
display: inline-block; display: inline-block;
padding-left: 4px; padding-left: 4px;
padding-right: 8px; padding-right: 0;
.fa-close { .fa-close {
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
...@@ -132,21 +148,6 @@ ...@@ -132,21 +148,6 @@
} }
} }
.name {
background-color: $filter-name-resting-color;
color: $filter-name-text-color;
border-radius: 2px 0 0 2px;
margin-right: 1px;
text-transform: capitalize;
}
.value-container {
background-color: $white-normal;
color: $filter-value-text-color;
border-radius: 0 2px 2px 0;
margin-right: 5px;
}
.selected { .selected {
.name { .name {
background-color: $filter-name-selected-color; background-color: $filter-name-selected-color;
......
...@@ -72,7 +72,7 @@ ...@@ -72,7 +72,7 @@
height: 100%; height: 100%;
top: 0; top: 0;
background: $white-light; background: $white-light;
z-index: 100; z-index: 500;
.boards-list { .boards-list {
height: calc(100vh - 50px); height: calc(100vh - 50px);
......
...@@ -9,7 +9,7 @@ class AutocompleteController < ApplicationController ...@@ -9,7 +9,7 @@ class AutocompleteController < ApplicationController
@users = @users.where.not(id: params[:skip_users]) if params[:skip_users].present? @users = @users.where.not(id: params[:skip_users]) if params[:skip_users].present?
@users = @users.active @users = @users.active
@users = @users.reorder(:name) @users = @users.reorder(:name)
@users = load_users_by_ability || @users.page(params[:page]) @users = load_users_by_ability || @users.page(params[:page]).per(params[:per_page])
if params[:todo_filter].present? && current_user if params[:todo_filter].present? && current_user
@users = @users.todo_authors(current_user.id, params[:todo_state_filter]) @users = @users.todo_authors(current_user.id, params[:todo_state_filter])
...@@ -63,7 +63,7 @@ class AutocompleteController < ApplicationController ...@@ -63,7 +63,7 @@ class AutocompleteController < ApplicationController
@users.to_a @users.to_a
.select { |user| user.can?(ability, @project) } .select { |user| user.can?(ability, @project) }
.take(Kaminari.config.default_per_page) .take(params[:per_page]&.to_i || Kaminari.config.default_per_page)
end end
def find_users def find_users
......
...@@ -85,6 +85,12 @@ module ProjectsHelper ...@@ -85,6 +85,12 @@ module ProjectsHelper
@nav_tabs ||= get_project_nav_tabs(@project, current_user) @nav_tabs ||= get_project_nav_tabs(@project, current_user)
end end
def project_search_tabs?(tab)
abilities = Array(search_tab_ability_map[tab])
abilities.any? { |ability| can?(current_user, ability, @project) }
end
def project_nav_tab?(name) def project_nav_tab?(name)
project_nav_tabs.include? name project_nav_tabs.include? name
end end
...@@ -204,7 +210,17 @@ module ProjectsHelper ...@@ -204,7 +210,17 @@ module ProjectsHelper
nav_tabs << :container_registry nav_tabs << :container_registry
end end
tab_ability_map = { tab_ability_map.each do |tab, ability|
if can?(current_user, ability, project)
nav_tabs << tab
end
end
nav_tabs.flatten
end
def tab_ability_map
{
environments: :read_environment, environments: :read_environment,
milestones: :read_milestone, milestones: :read_milestone,
pipelines: :read_pipeline, pipelines: :read_pipeline,
...@@ -216,14 +232,15 @@ module ProjectsHelper ...@@ -216,14 +232,15 @@ module ProjectsHelper
team: :read_project_member, team: :read_project_member,
wiki: :read_wiki wiki: :read_wiki
} }
end
tab_ability_map.each do |tab, ability| def search_tab_ability_map
if can?(current_user, ability, project) @search_tab_ability_map ||= tab_ability_map.merge(
nav_tabs << tab blobs: :download_code,
end commits: :download_code,
end merge_requests: :read_merge_request,
notes: [:read_merge_request, :download_code, :read_issue, :read_project_snippet]
nav_tabs.flatten )
end end
def project_lfs_status(project) def project_lfs_status(project)
......
...@@ -41,7 +41,7 @@ module SearchHelper ...@@ -41,7 +41,7 @@ module SearchHelper
def find_project_for_blob(blob) def find_project_for_blob(blob)
Project.find(blob['_parent']) Project.find(blob['_parent'])
end end
private private
# Autocomplete results for various settings pages # Autocomplete results for various settings pages
......
...@@ -21,10 +21,12 @@ module EE ...@@ -21,10 +21,12 @@ module EE
prepended do prepended do
has_one :namespace_statistics, dependent: :destroy has_one :namespace_statistics, dependent: :destroy
scope :with_plan, -> { where.not(plan: [nil, '']) }
delegate :shared_runners_minutes, :shared_runners_seconds, :shared_runners_seconds_last_reset, delegate :shared_runners_minutes, :shared_runners_seconds, :shared_runners_seconds_last_reset,
to: :namespace_statistics, allow_nil: true to: :namespace_statistics, allow_nil: true
validates :plan, inclusion: { in: EE_PLANS.keys }, allow_nil: true validates :plan, inclusion: { in: EE_PLANS.keys }, allow_blank: true
end end
# Checks features (i.e. https://about.gitlab.com/products/) availabily # Checks features (i.e. https://about.gitlab.com/products/) availabily
...@@ -58,7 +60,7 @@ module EE ...@@ -58,7 +60,7 @@ module EE
def plans def plans
@ancestors_plans ||= @ancestors_plans ||=
if parent_id if parent_id
ancestors.where.not(plan: nil).reorder(nil).pluck('DISTINCT plan') + [plan] ancestors.with_plan.reorder(nil).pluck('DISTINCT plan') + [plan]
else else
[plan] [plan]
end end
......
...@@ -182,6 +182,8 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated ...@@ -182,6 +182,8 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end end
def user_can_push_to_source_branch? def user_can_push_to_source_branch?
return false unless source_branch_exists?
::Gitlab::UserAccess ::Gitlab::UserAccess
.new(current_user, project: source_project) .new(current_user, project: source_project)
.can_push_to_branch?(source_branch) .can_push_to_branch?(source_branch)
......
...@@ -90,7 +90,7 @@ module MergeRequests ...@@ -90,7 +90,7 @@ module MergeRequests
target_project.reset_approvals_on_push && target_project.reset_approvals_on_push &&
merge_request.rebase_commit_sha != @newrev merge_request.rebase_commit_sha != @newrev
merge_request.approvals.destroy_all merge_request.approvals.delete_all
end end
end end
end end
......
...@@ -16,6 +16,7 @@ module MergeRequests ...@@ -16,6 +16,7 @@ module MergeRequests
if params[:force_remove_source_branch].present? if params[:force_remove_source_branch].present?
merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch) merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch)
end end
old_approvers = merge_request.overall_approvers.to_a old_approvers = merge_request.overall_approvers.to_a
handle_wip_event(merge_request) handle_wip_event(merge_request)
...@@ -48,6 +49,7 @@ module MergeRequests ...@@ -48,6 +49,7 @@ module MergeRequests
create_branch_change_note(merge_request, 'target', create_branch_change_note(merge_request, 'target',
merge_request.previous_changes['target_branch'].first, merge_request.previous_changes['target_branch'].first,
merge_request.target_branch) merge_request.target_branch)
reset_approvals(merge_request)
end end
if merge_request.previous_changes.include?('milestone_id') if merge_request.previous_changes.include?('milestone_id')
...@@ -111,6 +113,14 @@ module MergeRequests ...@@ -111,6 +113,14 @@ module MergeRequests
private private
def reset_approvals(merge_request)
target_project = merge_request.target_project
if target_project.approvals_before_merge.nonzero? && target_project.reset_approvals_on_push
merge_request.approvals.delete_all
end
end
def handle_wip_event(merge_request) def handle_wip_event(merge_request)
if wip_event = params.delete(:wip_event) if wip_event = params.delete(:wip_event)
# We update the title that is provided in the params or we use the mr title # We update the title that is provided in the params or we use the mr title
......
...@@ -12,7 +12,7 @@ class SearchService ...@@ -12,7 +12,7 @@ class SearchService
@project = @project =
if params[:project_id].present? if params[:project_id].present?
the_project = Project.find_by(id: params[:project_id]) the_project = Project.find_by(id: params[:project_id])
can?(current_user, :download_code, the_project) ? the_project : nil can?(current_user, :read_project, the_project) ? the_project : nil
else else
nil nil
end end
......
.form-group .form-group
= f.label :plan, class: 'control-label' = f.label :plan, class: 'control-label'
.col-sm-10 .col-sm-10
= f.select :plan, options_for_select(Namespace::EE_PLANS.keys.map { |plan| [plan.titleize, plan] }, f.object.plan), {}, class: 'form-control' = f.select :plan, options_for_select(Namespace::EE_PLANS.keys.map { |plan| [plan.titleize, plan] }, f.object.plan),
{ include_blank: 'No plan' },
class: 'form-control'
...@@ -14,7 +14,10 @@ ...@@ -14,7 +14,10 @@
name: "issue[assignee_ids][]", name: "issue[assignee_ids][]",
":value" => "assignee.id", ":value" => "assignee.id",
"v-if" => "issue.assignees", "v-if" => "issue.assignees",
"v-for" => "assignee in issue.assignees" } "v-for" => "assignee in issue.assignees",
":data-avatar_url" => "assignee.avatar",
":data-name" => "assignee.name",
":data-username" => "assignee.username" }
.dropdown .dropdown
%button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: "button", ref: "assigneeDropdown", data: { toggle: "dropdown", field_name: "issue[assignee_ids][]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true", multi_select: "true", dropdown: { header: 'Assignee(s)'} }, %button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: "button", ref: "assigneeDropdown", data: { toggle: "dropdown", field_name: "issue[assignee_ids][]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true", multi_select: "true", dropdown: { header: 'Assignee(s)'} },
":data-issuable-id" => "issue.id", ":data-issuable-id" => "issue.id",
......
- failed_builds = @pipeline.statuses.latest.failed - failed_builds = @pipeline.statuses.latest.failed
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('pipelines_graph')
.tabs-holder .tabs-holder
%ul.pipelines-tabs.nav-links.no-top.no-bottom %ul.pipelines-tabs.nav-links.no-top.no-bottom
%li.js-pipeline-tab-link %li.js-pipeline-tab-link
...@@ -21,7 +17,7 @@ ...@@ -21,7 +17,7 @@
.tab-content .tab-content
#js-tab-pipeline.tab-pane #js-tab-pipeline.tab-pane
#js-pipeline-graph-vue{ data: { endpoint: namespace_project_pipeline_path(@project.namespace, @project, @pipeline, format: :json) } } #js-pipeline-graph-vue
#js-tab-builds.tab-pane #js-tab-builds.tab-pane
- if pipeline.yaml_errors.present? - if pipeline.yaml_errors.present?
......
...@@ -7,3 +7,9 @@ ...@@ -7,3 +7,9 @@
= render "projects/pipelines/info" = render "projects/pipelines/info"
= render "projects/pipelines/with_tabs", pipeline: @pipeline = render "projects/pipelines/with_tabs", pipeline: @pipeline
.js-pipeline-details-vue{ data: { endpoint: namespace_project_pipeline_path(@project.namespace, @project, @pipeline, format: :json) } }
- content_for :page_specific_javascripts do
= webpack_bundle_tag('common_vue')
= webpack_bundle_tag('pipelines_details')
...@@ -3,41 +3,48 @@ ...@@ -3,41 +3,48 @@
.fade-right= icon('angle-right') .fade-right= icon('angle-right')
%ul.nav-links.search-filter.scrolling-tabs %ul.nav-links.search-filter.scrolling-tabs
- if @project - if @project
%li{ class: active_when(@scope == 'blobs') } - if project_search_tabs?(:blobs)
= link_to search_filter_path(scope: 'blobs') do %li{ class: active_when(@scope == 'blobs') }
Code = link_to search_filter_path(scope: 'blobs') do
%span.badge Code
= @search_results.blobs_count %span.badge
%li{ class: active_when(@scope == 'issues') } = @search_results.blobs_count
= link_to search_filter_path(scope: 'issues') do - if project_search_tabs?(:issues)
Issues %li{ class: active_when(@scope == 'issues') }
%span.badge = link_to search_filter_path(scope: 'issues') do
= @search_results.issues_count Issues
%li{ class: active_when(@scope == 'merge_requests') } %span.badge
= link_to search_filter_path(scope: 'merge_requests') do = @search_results.issues_count
Merge requests - if project_search_tabs?(:merge_requests)
%span.badge %li{ class: active_when(@scope == 'merge_requests') }
= @search_results.merge_requests_count = link_to search_filter_path(scope: 'merge_requests') do
%li{ class: active_when(@scope == 'milestones') } Merge requests
= link_to search_filter_path(scope: 'milestones') do %span.badge
Milestones = @search_results.merge_requests_count
%span.badge - if project_search_tabs?(:milestones)
= @search_results.milestones_count %li{ class: active_when(@scope == 'milestones') }
%li{ class: active_when(@scope == 'notes') } = link_to search_filter_path(scope: 'milestones') do
= link_to search_filter_path(scope: 'notes') do Milestones
Comments %span.badge
%span.badge = @search_results.milestones_count
= @search_results.notes_count - if project_search_tabs?(:notes)
%li{ class: active_when(@scope == 'wiki_blobs') } %li{ class: active_when(@scope == 'notes') }
= link_to search_filter_path(scope: 'wiki_blobs') do = link_to search_filter_path(scope: 'notes') do
Wiki Comments
%span.badge %span.badge
= @search_results.wiki_blobs_count = @search_results.notes_count
%li{ class: active_when(@scope == 'commits') } - if project_search_tabs?(:wiki)
= link_to search_filter_path(scope: 'commits') do %li{ class: active_when(@scope == 'wiki_blobs') }
Commits = link_to search_filter_path(scope: 'wiki_blobs') do
%span.badge Wiki
= @search_results.commits_count %span.badge
= @search_results.wiki_blobs_count
- if project_search_tabs?(:commits)
%li{ class: active_when(@scope == 'commits') }
= link_to search_filter_path(scope: 'commits') do
Commits
%span.badge
= @search_results.commits_count
- elsif @show_snippets - elsif @show_snippets
%li{ class: active_when(@scope == 'snippet_blobs') } %li{ class: active_when(@scope == 'snippet_blobs') }
......
...@@ -177,7 +177,8 @@ ...@@ -177,7 +177,8 @@
$(document).off('page:restore').on('page:restore', function (event) { $(document).off('page:restore').on('page:restore', function (event) {
if (gl.FilteredSearchManager) { if (gl.FilteredSearchManager) {
new gl.FilteredSearchManager(); const filteredSearchManager = new gl.FilteredSearchManager();
filteredSearchManager.setup();
} }
Issuable.init(); Issuable.init();
new gl.IssuableBulkActions({ new gl.IssuableBulkActions({
......
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
.selectbox.hide-collapsed .selectbox.hide-collapsed
- issuable.assignees.each do |assignee| - issuable.assignees.each do |assignee|
= hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", assignee.id, id: nil = hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", assignee.id, id: nil, data: { avatar_url: assignee.avatar_url, name: assignee.name, username: assignee.username }
- options = { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_ids][]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } } - options = { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_ids][]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
.col-sm-10{ class: ("col-lg-8" if has_due_date) } .col-sm-10{ class: ("col-lg-8" if has_due_date) }
.issuable-form-select-holder.selectbox .issuable-form-select-holder.selectbox
- issuable.assignees.each do |assignee| - issuable.assignees.each do |assignee|
= hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", assignee.id, id: nil, data: { meta: assignee.name } = hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", assignee.id, id: nil, data: { meta: assignee.name, avatar_url: assignee.avatar_url, name: assignee.name, username: assignee.username }
- if issuable.assignees.length === 0 - if issuable.assignees.length === 0
= hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil, data: { meta: '' } = hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil, data: { meta: '' }
......
---
title: Fixed header being over issue boards when in focus mode
merge_request:
author:
---
title: 'Fix: Approvals not reset if changing target branch'
merge_request:
author:
---
title: Add performance deltas between app deployments on Merge Request widget
merge_request: 11730
author:
---
title: 'Fix: Wiki is not searchable with Guest permissions'
merge_request:
author:
---
title: Creates a mediator for pipeline details vue in order to mount several vue apps
with the same data
merge_request:
author:
...@@ -403,9 +403,6 @@ Settings.cron_jobs['geo_repository_sync_worker']['job_class'] ||= 'GeoRepository ...@@ -403,9 +403,6 @@ Settings.cron_jobs['geo_repository_sync_worker']['job_class'] ||= 'GeoRepository
Settings.cron_jobs['geo_file_download_dispatch_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['geo_file_download_dispatch_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['geo_file_download_dispatch_worker']['cron'] ||= '5 * * * *' Settings.cron_jobs['geo_file_download_dispatch_worker']['cron'] ||= '5 * * * *'
Settings.cron_jobs['geo_file_download_dispatch_worker']['job_class'] ||= 'GeoFileDownloadDispatchWorker' Settings.cron_jobs['geo_file_download_dispatch_worker']['job_class'] ||= 'GeoFileDownloadDispatchWorker'
Settings.cron_jobs['gitlab_usage_ping_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['gitlab_usage_ping_worker']['cron'] ||= Settings.send(:cron_random_weekly_time)
Settings.cron_jobs['gitlab_usage_ping_worker']['job_class'] = 'GitlabUsagePingWorker'
Settings.cron_jobs['import_export_project_cleanup_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['import_export_project_cleanup_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['import_export_project_cleanup_worker']['cron'] ||= '0 * * * *' Settings.cron_jobs['import_export_project_cleanup_worker']['cron'] ||= '0 * * * *'
Settings.cron_jobs['import_export_project_cleanup_worker']['job_class'] = 'ImportExportProjectCleanupWorker' Settings.cron_jobs['import_export_project_cleanup_worker']['job_class'] = 'ImportExportProjectCleanupWorker'
......
...@@ -24,6 +24,7 @@ var config = { ...@@ -24,6 +24,7 @@ var config = {
}, },
context: path.join(ROOT_PATH, 'app/assets/javascripts'), context: path.join(ROOT_PATH, 'app/assets/javascripts'),
entry: { entry: {
balsamiq_viewer: './blob/balsamiq_viewer.js',
blob: './blob_edit/blob_bundle.js', blob: './blob_edit/blob_bundle.js',
boards: './boards/boards_bundle.js', boards: './boards/boards_bundle.js',
burndown_chart: './burndown_chart/index.js', burndown_chart: './burndown_chart/index.js',
...@@ -50,8 +51,7 @@ var config = { ...@@ -50,8 +51,7 @@ var config = {
notebook_viewer: './blob/notebook_viewer.js', notebook_viewer: './blob/notebook_viewer.js',
pdf_viewer: './blob/pdf_viewer.js', pdf_viewer: './blob/pdf_viewer.js',
pipelines: './pipelines/index.js', pipelines: './pipelines/index.js',
balsamiq_viewer: './blob/balsamiq_viewer.js', pipelines_details: './pipelines/pipeline_details_bundle.js',
pipelines_graph: './pipelines/graph_bundle.js',
profile: './profile/profile_bundle.js', profile: './profile/profile_bundle.js',
protected_branches: './protected_branches/protected_branches_bundle.js', protected_branches: './protected_branches/protected_branches_bundle.js',
protected_tags: './protected_tags', protected_tags: './protected_tags',
...@@ -163,7 +163,7 @@ var config = { ...@@ -163,7 +163,7 @@ var config = {
'notebook_viewer', 'notebook_viewer',
'pdf_viewer', 'pdf_viewer',
'pipelines', 'pipelines',
'pipelines_graph', 'pipelines_details',
'schedule_form', 'schedule_form',
'schedules_index', 'schedules_index',
'service_desk', 'service_desk',
......
# rubocop:disable Migration/UpdateColumnInBatches
class SetMissingStageOnCiBuilds < ActiveRecord::Migration class SetMissingStageOnCiBuilds < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable Migration/UpdateColumnInBatches
class DropAndReaddHasExternalWikiInProjects < ActiveRecord::Migration class DropAndReaddHasExternalWikiInProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # rubocop:disable Migration/UpdateColumnInBatches
# for more information on how to write migrations for GitLab.
class UpdateMirrorWhenEmptyImportUrlInProjects < ActiveRecord::Migration class UpdateMirrorWhenEmptyImportUrlInProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable Migration/UpdateColumnInBatches
class SetConfidentialIssuesEventsOnWebhooks < ActiveRecord::Migration class SetConfidentialIssuesEventsOnWebhooks < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable Migration/UpdateColumnInBatches
class AddTypeToLabels < ActiveRecord::Migration class AddTypeToLabels < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable Migration/UpdateColumnInBatches
class MakeProjectOwnersMasters < ActiveRecord::Migration class MakeProjectOwnersMasters < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable Migration/UpdateColumnInBatches
class RenameSlackAndMattermostNotificationServices < ActiveRecord::Migration class RenameSlackAndMattermostNotificationServices < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable Migration/UpdateColumnInBatches
class ConvertApplicationSettingsRepositorySizeLimitToBytes < ActiveRecord::Migration class ConvertApplicationSettingsRepositorySizeLimitToBytes < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # rubocop:disable Migration/UpdateColumnInBatches
# for more information on how to write migrations for GitLab.
class ConvertProjectsRepositorySizeLimitToBytes < ActiveRecord::Migration class ConvertProjectsRepositorySizeLimitToBytes < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # rubocop:disable Migration/UpdateColumnInBatches
# for more information on how to write migrations for GitLab.
class ConvertNamespacesRepositorySizeLimitToBytes < ActiveRecord::Migration class ConvertNamespacesRepositorySizeLimitToBytes < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # rubocop:disable Migration/UpdateColumnInBatches
# for more information on how to write migrations for GitLab.
class MigrateAssignees < ActiveRecord::Migration class MigrateAssignees < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # rubocop:disable Migration/UpdateColumnInBatches
# for more information on how to write migrations for GitLab.
class ResetUsersAuthorizedProjectsPopulated < ActiveRecord::Migration class ResetUsersAuthorizedProjectsPopulated < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # rubocop:disable Migration/UpdateColumnInBatches
# for more information on how to write migrations for GitLab.
class ResetRelativePositionForIssue < ActiveRecord::Migration class ResetRelativePositionForIssue < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable Migration/UpdateColumnInBatches
class EnableAutoCancelPendingPipelinesForAll < ActiveRecord::Migration class EnableAutoCancelPendingPipelinesForAll < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
...@@ -217,10 +217,11 @@ as extensions to the Kerberos protocol may result in HTTP authentication headers ...@@ -217,10 +217,11 @@ as extensions to the Kerberos protocol may result in HTTP authentication headers
larger than the default size of 8kB. Configure `large_client_header_buffers` larger than the default size of 8kB. Configure `large_client_header_buffers`
to a larger value in [the NGINX configuration][nginx]. to a larger value in [the NGINX configuration][nginx].
## Helpful links to setup development Kerberos environment. ## Helpful links
- <https://help.ubuntu.com/community/Kerberos> - <https://help.ubuntu.com/community/Kerberos>
- <http://blog.manula.org/2012/04/setting-up-kerberos-server-with-debian.html> - <http://blog.manula.org/2012/04/setting-up-kerberos-server-with-debian.html>
- <http://www.roguelynn.com/words/explain-like-im-5-kerberos/>
[gitlab.yml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/gitlab.yml.example [gitlab.yml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/gitlab.yml.example
[restart gitlab]: ../administration/restart_gitlab.md#installations-from-source [restart gitlab]: ../administration/restart_gitlab.md#installations-from-source
......
...@@ -69,6 +69,8 @@ module Gitlab ...@@ -69,6 +69,8 @@ module Gitlab
end end
def wiki_blobs def wiki_blobs
return Kaminari.paginate_array([]) unless Ability.allowed?(@current_user, :read_wiki, project)
if project.wiki_enabled? && !project.wiki.empty? && query.present? if project.wiki_enabled? && !project.wiki.empty? && query.present?
project.wiki.search( project.wiki.search(
query, query,
......
...@@ -187,19 +187,21 @@ module Gitlab ...@@ -187,19 +187,21 @@ module Gitlab
end end
def wiki_filter def wiki_filter
blob_filter(:wiki_access_level) blob_filter(:wiki_access_level, visible_for_guests: true)
end end
def repository_filter def repository_filter
blob_filter(:repository_access_level) blob_filter(:repository_access_level)
end end
def blob_filter(project_feature_name) def blob_filter(project_feature_name, visible_for_guests: false)
project_ids = visible_for_guests ? limit_project_ids : non_guest_project_ids
conditions = conditions =
if non_guest_project_ids == :any if project_ids == :any
[{ exists: { field: "id" } }] [{ exists: { field: "id" } }]
else else
[{ terms: { id: non_guest_project_ids } }] [{ terms: { id: project_ids } }]
end end
if public_and_internal_projects if public_and_internal_projects
......
require_relative '../../migration_helpers'
module RuboCop
module Cop
module Migration
# Cop that checks if a spec file exists for any migration using
# `update_column_in_batches`.
class UpdateColumnInBatches < RuboCop::Cop::Cop
include MigrationHelpers
MSG = 'Migration running `update_column_in_batches` must have a spec file at' \
' `%s`.'.freeze
def on_send(node)
return unless in_migration?(node)
return unless node.children[1] == :update_column_in_batches
spec_path = spec_filename(node)
unless File.exist?(File.expand_path(spec_path, rails_root))
add_offense(node, :expression, format(MSG, spec_path))
end
end
private
def spec_filename(node)
source_name = node.location.expression.source_buffer.name
path = Pathname.new(source_name).relative_path_from(rails_root)
dirname = File.dirname(path)
.sub(%r{\Adb/(migrate|post_migrate)}, 'spec/migrations')
filename = File.basename(source_name, '.rb').sub(%r{\A\d+_}, '')
File.join(dirname, "#{filename}_spec.rb")
end
def rails_root
Pathname.new(File.expand_path('../../..', __dir__))
end
end
end
end
end
...@@ -3,8 +3,9 @@ module RuboCop ...@@ -3,8 +3,9 @@ module RuboCop
module MigrationHelpers module MigrationHelpers
# Returns true if the given node originated from the db/migrate directory. # Returns true if the given node originated from the db/migrate directory.
def in_migration?(node) def in_migration?(node)
File.dirname(node.location.expression.source_buffer.name). dirname = File.dirname(node.location.expression.source_buffer.name)
end_with?('db/migrate')
dirname.end_with?('db/migrate', 'db/post_migrate')
end end
end end
end end
...@@ -8,3 +8,4 @@ require_relative 'cop/migration/add_index' ...@@ -8,3 +8,4 @@ require_relative 'cop/migration/add_index'
require_relative 'cop/migration/remove_concurrent_index' require_relative 'cop/migration/remove_concurrent_index'
require_relative 'cop/migration/remove_index' require_relative 'cop/migration/remove_index'
require_relative 'cop/migration/reversible_add_column_with_default' require_relative 'cop/migration/reversible_add_column_with_default'
require_relative 'cop/migration/update_column_in_batches'
...@@ -137,6 +137,20 @@ describe AutocompleteController do ...@@ -137,6 +137,20 @@ describe AutocompleteController do
it { expect(body.size).to eq User.count } it { expect(body.size).to eq User.count }
end end
context 'limited users per page' do
let(:per_page) { 2 }
before do
sign_in(user)
get(:users, per_page: per_page)
end
let(:body) { JSON.parse(response.body) }
it { expect(body).to be_kind_of(Array) }
it { expect(body.size).to eq per_page }
end
context 'unauthenticated user' do context 'unauthenticated user' do
let(:public_project) { create(:project, :public) } let(:public_project) { create(:project, :public) }
let(:body) { JSON.parse(response.body) } let(:body) { JSON.parse(response.body) }
......
require 'spec_helper' require 'spec_helper'
describe 'GlobalSearch' do describe 'GlobalSearch' do
let(:features) { %i(issues merge_requests repository builds) } let(:features) { %i(issues merge_requests repository builds wiki) }
let(:admin) { create :user, admin: true } let(:admin) { create :user, admin: true }
let(:auditor) {create :user, auditor: true } let(:auditor) {create :user, auditor: true }
let(:non_member) { create :user } let(:non_member) { create :user }
...@@ -130,11 +130,13 @@ describe 'GlobalSearch' do ...@@ -130,11 +130,13 @@ describe 'GlobalSearch' do
Sidekiq::Testing.inline! do Sidekiq::Testing.inline! do
create :issue, title: 'term', project: project create :issue, title: 'term', project: project
create :merge_request, title: 'term', target_project: project, source_project: project create :merge_request, title: 'term', target_project: project, source_project: project
project.wiki.create_page('index_page', 'term')
project.project_feature.update!(feature_settings) if feature_settings project.project_feature.update!(feature_settings) if feature_settings
project.repository.index_blobs project.repository.index_blobs
project.repository.index_commits project.repository.index_commits
project.wiki.index_blobs
Gitlab::Elastic::Helper.refresh_index Gitlab::Elastic::Helper.refresh_index
end end
...@@ -149,6 +151,7 @@ describe 'GlobalSearch' do ...@@ -149,6 +151,7 @@ describe 'GlobalSearch' do
results = search(user, 'term') results = search(user, 'term')
expect(results.issues_count).to eq(0) expect(results.issues_count).to eq(0)
expect(results.merge_requests_count).to eq(0) expect(results.merge_requests_count).to eq(0)
expect(results.wiki_blobs_count).to eq(0)
expect(search(user, 'def').blobs_count).to eq(0) expect(search(user, 'def').blobs_count).to eq(0)
expect(search(user, 'add').commits_count).to eq(0) expect(search(user, 'add').commits_count).to eq(0)
end end
...@@ -157,6 +160,7 @@ describe 'GlobalSearch' do ...@@ -157,6 +160,7 @@ describe 'GlobalSearch' do
results = search(user, 'term') results = search(user, 'term')
expect(results.issues_count).not_to eq(0) expect(results.issues_count).not_to eq(0)
expect(results.merge_requests_count).not_to eq(0) expect(results.merge_requests_count).not_to eq(0)
expect(results.wiki_blobs_count).not_to eq(0)
expect(search(user, 'def').blobs_count).not_to eq(0) expect(search(user, 'def').blobs_count).not_to eq(0)
expect(search(user, 'add').commits_count).not_to eq(0) expect(search(user, 'add').commits_count).not_to eq(0)
end end
...@@ -164,6 +168,7 @@ describe 'GlobalSearch' do ...@@ -164,6 +168,7 @@ describe 'GlobalSearch' do
def expect_non_code_items_to_be_found(user) def expect_non_code_items_to_be_found(user)
results = search(guest, 'term') results = search(guest, 'term')
expect(results.issues_count).not_to eq(0) expect(results.issues_count).not_to eq(0)
expect(results.wiki_blobs_count).not_to eq(0)
expect(results.merge_requests_count).to eq(0) expect(results.merge_requests_count).to eq(0)
expect(search(guest, 'def').blobs_count).to eq(0) expect(search(guest, 'def').blobs_count).to eq(0)
expect(search(guest, 'add').commits_count).to eq(0) expect(search(guest, 'add').commits_count).to eq(0)
......
...@@ -4,7 +4,9 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -4,7 +4,9 @@ describe 'Issue Boards', feature: true, js: true do
include DragTo include DragTo
let(:project) { create(:empty_project, :public) } let(:project) { create(:empty_project, :public) }
let(:milestone) { create(:milestone, title: "v2.2", project: project) }
let!(:board) { create(:board, project: project) } let!(:board) { create(:board, project: project) }
let!(:board_with_milestone) { create(:board, project: project, milestone: milestone) }
let(:user) { create(:user) } let(:user) { create(:user) }
let!(:user2) { create(:user) } let!(:user2) { create(:user) }
...@@ -509,6 +511,22 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -509,6 +511,22 @@ describe 'Issue Boards', feature: true, js: true do
end end
end end
context 'locked milestone' do
before do
visit namespace_project_board_path(project.namespace, project, board_with_milestone)
wait_for_requests
end
it 'should not have remove button' do
expect(page).to have_selector('.js-visual-token .remove-token', count: 0)
end
it 'should not be able to be backspaced' do
find('.input-token .filtered-search').native.send_key(:backspace)
expect(page).to have_selector('.js-visual-token', count: 1)
end
end
context 'keyboard shortcuts' do context 'keyboard shortcuts' do
before do before do
visit namespace_project_boards_path(project.namespace, project) visit namespace_project_boards_path(project.namespace, project)
......
...@@ -3,6 +3,7 @@ require 'rails_helper' ...@@ -3,6 +3,7 @@ require 'rails_helper'
describe 'New/edit issue', :feature, :js do describe 'New/edit issue', :feature, :js do
include GitlabRoutingHelper include GitlabRoutingHelper
include ActionView::Helpers::JavaScriptHelper include ActionView::Helpers::JavaScriptHelper
include FormHelper
let!(:project) { create(:project) } let!(:project) { create(:project) }
let!(:user) { create(:user)} let!(:user) { create(:user)}
...@@ -23,7 +24,46 @@ describe 'New/edit issue', :feature, :js do ...@@ -23,7 +24,46 @@ describe 'New/edit issue', :feature, :js do
visit new_namespace_project_issue_path(project.namespace, project) visit new_namespace_project_issue_path(project.namespace, project)
end end
describe 'single assignee' do describe 'shorten users API pagination limit' do
before do
# Using `allow_any_instance_of`/`and_wrap_original`, `original` would
# somehow refer to the very block we defined to _wrap_ that method, instead of
# the original method, resulting in infinite recurison when called.
# This is likely a bug with helper modules included into dynamically generated view classes.
# To work around this, we have to hold on to and call to the original implementation manually.
original_issue_dropdown_options = FormHelper.instance_method(:issue_dropdown_options)
allow_any_instance_of(FormHelper).to receive(:issue_dropdown_options).and_wrap_original do |original, *args|
options = original_issue_dropdown_options.bind(original.receiver).call(*args)
options[:data][:per_page] = 2
options
end
visit new_namespace_project_issue_path(project.namespace, project)
click_button 'Unassigned'
wait_for_requests
end
it 'should display selected users even if they are not part of the original API call' do
find('.dropdown-input-field').native.send_keys user2.name
page.within '.dropdown-menu-user' do
expect(page).to have_content user2.name
click_link user2.name
end
find('.js-dropdown-input-clear').click
page.within '.dropdown-menu-user' do
expect(page).to have_content user.name
expect(find('.dropdown-menu-user a.is-active').first(:xpath, '..')['data-user-id']).to eq(user2.id.to_s)
end
end
end
describe 'multiple assignees' do
before do before do
click_button 'Unassigned' click_button 'Unassigned'
......
...@@ -57,6 +57,23 @@ feature 'Issue Sidebar', feature: true do ...@@ -57,6 +57,23 @@ feature 'Issue Sidebar', feature: true do
expect(page.find('.dropdown-menu-user-link.is-active')).to have_content(user.name) expect(page.find('.dropdown-menu-user-link.is-active')).to have_content(user.name)
end end
end end
it 'keeps your filtered term after filtering and dismissing the dropdown' do
find('.dropdown-input-field').native.send_keys user2.name
wait_for_requests
page.within '.dropdown-menu-user' do
expect(page).not_to have_content 'Unassigned'
click_link user2.name
end
find('.js-right-sidebar').click
find('.block.assignee .edit-link').click
expect(page.all('.dropdown-menu-user li').length).to eq(1)
expect(find('.dropdown-input-field').value).to eq(user2.name)
end
end end
context 'as a allowed user' do context 'as a allowed user' do
......
...@@ -57,6 +57,7 @@ describe('Filtered Search Manager', () => { ...@@ -57,6 +57,7 @@ describe('Filtered Search Manager', () => {
input = document.querySelector('.filtered-search'); input = document.querySelector('.filtered-search');
tokensContainer = document.querySelector('.tokens-container'); tokensContainer = document.querySelector('.tokens-container');
manager = new gl.FilteredSearchManager(); manager = new gl.FilteredSearchManager();
manager.setup();
}); });
afterEach(() => { afterEach(() => {
...@@ -72,6 +73,7 @@ describe('Filtered Search Manager', () => { ...@@ -72,6 +73,7 @@ describe('Filtered Search Manager', () => {
spyOn(recentSearchesStoreSrc, 'default'); spyOn(recentSearchesStoreSrc, 'default');
filteredSearchManager = new gl.FilteredSearchManager(); filteredSearchManager = new gl.FilteredSearchManager();
filteredSearchManager.setup();
return filteredSearchManager; return filteredSearchManager;
}); });
...@@ -89,6 +91,7 @@ describe('Filtered Search Manager', () => { ...@@ -89,6 +91,7 @@ describe('Filtered Search Manager', () => {
spyOn(window, 'Flash'); spyOn(window, 'Flash');
filteredSearchManager = new gl.FilteredSearchManager(); filteredSearchManager = new gl.FilteredSearchManager();
filteredSearchManager.setup();
expect(window.Flash).not.toHaveBeenCalled(); expect(window.Flash).not.toHaveBeenCalled();
}); });
......
import { formatRelevantDigits, bytesToKiB } from '~/lib/utils/number_utils'; import { formatRelevantDigits, bytesToKiB, bytesToMiB } from '~/lib/utils/number_utils';
describe('Number Utils', () => { describe('Number Utils', () => {
describe('formatRelevantDigits', () => { describe('formatRelevantDigits', () => {
...@@ -45,4 +45,11 @@ describe('Number Utils', () => { ...@@ -45,4 +45,11 @@ describe('Number Utils', () => {
expect(bytesToKiB(1000)).toEqual(0.9765625); expect(bytesToKiB(1000)).toEqual(0.9765625);
}); });
}); });
describe('bytesToMiB', () => {
it('calculates MiB for the given bytes', () => {
expect(bytesToMiB(1048576)).toEqual(1);
expect(bytesToMiB(1000000)).toEqual(0.95367431640625);
});
});
}); });
...@@ -128,7 +128,6 @@ import '~/notes'; ...@@ -128,7 +128,6 @@ import '~/notes';
beforeEach(() => { beforeEach(() => {
note = { note = {
id: 1, id: 1,
discussion_html: null,
valid: true, valid: true,
note: 'heya', note: 'heya',
html: '<div>heya</div>', html: '<div>heya</div>',
......
...@@ -14,49 +14,42 @@ describe('graph component', () => { ...@@ -14,49 +14,42 @@ describe('graph component', () => {
describe('while is loading', () => { describe('while is loading', () => {
it('should render a loading icon', () => { it('should render a loading icon', () => {
const component = new GraphComponent().$mount('#js-pipeline-graph-vue'); const component = new GraphComponent({
propsData: {
isLoading: true,
pipeline: {},
},
}).$mount('#js-pipeline-graph-vue');
expect(component.$el.querySelector('.loading-icon')).toBeDefined(); expect(component.$el.querySelector('.loading-icon')).toBeDefined();
}); });
}); });
describe('with a successfull response', () => { describe('with data', () => {
const interceptor = (request, next) => { it('should render the graph', () => {
next(request.respondWith(JSON.stringify(graphJSON), { const component = new GraphComponent({
status: 200, propsData: {
})); isLoading: false,
}; pipeline: graphJSON,
},
}).$mount('#js-pipeline-graph-vue');
beforeEach(() => { expect(component.$el.classList.contains('js-pipeline-graph')).toEqual(true);
Vue.http.interceptors.push(interceptor);
});
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
});
it('should render the graph', (done) => {
const component = new GraphComponent().$mount('#js-pipeline-graph-vue');
setTimeout(() => {
expect(component.$el.classList.contains('js-pipeline-graph')).toEqual(true);
expect( expect(
component.$el.querySelector('.stage-column:first-child').classList.contains('no-margin'), component.$el.querySelector('.stage-column:first-child').classList.contains('no-margin'),
).toEqual(true); ).toEqual(true);
expect( expect(
component.$el.querySelector('.stage-column:nth-child(2)').classList.contains('left-margin'), component.$el.querySelector('.stage-column:nth-child(2)').classList.contains('left-margin'),
).toEqual(true); ).toEqual(true);
expect( expect(
component.$el.querySelector('.stage-column:nth-child(2) .build:nth-child(1)').classList.contains('left-connector'), component.$el.querySelector('.stage-column:nth-child(2) .build:nth-child(1)').classList.contains('left-connector'),
).toEqual(true); ).toEqual(true);
expect(component.$el.querySelector('loading-icon')).toBe(null); expect(component.$el.querySelector('loading-icon')).toBe(null);
expect(component.$el.querySelector('.stage-column-list')).toBeDefined(); expect(component.$el.querySelector('.stage-column-list')).toBeDefined();
done();
}, 0);
}); });
}); });
}); });
...@@ -7,6 +7,18 @@ const url = '/root/acets-review-apps/environments/15/deployments/1/metrics'; ...@@ -7,6 +7,18 @@ const url = '/root/acets-review-apps/environments/15/deployments/1/metrics';
const metricsMockData = { const metricsMockData = {
success: true, success: true,
metrics: { metrics: {
memory_before: [
{
metric: {},
value: [1495785220.607, '9572875.906976745'],
},
],
memory_after: [
{
metric: {},
value: [1495787020.607, '4485853.130206379'],
},
],
memory_values: [ memory_values: [
{ {
metric: {}, metric: {},
...@@ -39,7 +51,7 @@ const createComponent = () => { ...@@ -39,7 +51,7 @@ const createComponent = () => {
const messages = { const messages = {
loadingMetrics: 'Loading deployment statistics.', loadingMetrics: 'Loading deployment statistics.',
hasMetrics: 'Deployment memory usage:', hasMetrics: 'Memory usage unchanged from 0MB to 0MB',
loadFailed: 'Failed to load deployment statistics.', loadFailed: 'Failed to load deployment statistics.',
metricsUnavailable: 'Deployment statistics are not available currently.', metricsUnavailable: 'Deployment statistics are not available currently.',
}; };
...@@ -89,17 +101,52 @@ describe('MemoryUsage', () => { ...@@ -89,17 +101,52 @@ describe('MemoryUsage', () => {
}); });
}); });
describe('computed', () => {
describe('memoryChangeType', () => {
it('should return "increased" if memoryFrom value is less than memoryTo value', () => {
vm.memoryFrom = 4.28;
vm.memoryTo = 9.13;
expect(vm.memoryChangeType).toEqual('increased');
});
it('should return "decreased" if memoryFrom value is less than memoryTo value', () => {
vm.memoryFrom = 9.13;
vm.memoryTo = 4.28;
expect(vm.memoryChangeType).toEqual('decreased');
});
it('should return "unchanged" if memoryFrom value equal to memoryTo value', () => {
vm.memoryFrom = 1;
vm.memoryTo = 1;
expect(vm.memoryChangeType).toEqual('unchanged');
});
});
});
describe('methods', () => { describe('methods', () => {
const { metrics, deployment_time } = metricsMockData; const { metrics, deployment_time } = metricsMockData;
describe('getMegabytes', () => {
it('should return Megabytes from provided Bytes value', () => {
const memoryInBytes = '9572875.906976745';
expect(vm.getMegabytes(memoryInBytes)).toEqual('9.13');
});
});
describe('computeGraphData', () => { describe('computeGraphData', () => {
it('should populate sparkline graph', () => { it('should populate sparkline graph', () => {
vm.computeGraphData(metrics, deployment_time); vm.computeGraphData(metrics, deployment_time);
const { hasMetrics, memoryMetrics, deploymentTime } = vm; const { hasMetrics, memoryMetrics, deploymentTime, memoryFrom, memoryTo } = vm;
expect(hasMetrics).toBeTruthy(); expect(hasMetrics).toBeTruthy();
expect(memoryMetrics.length > 0).toBeTruthy(); expect(memoryMetrics.length > 0).toBeTruthy();
expect(deploymentTime).toEqual(deployment_time); expect(deploymentTime).toEqual(deployment_time);
expect(memoryFrom).toEqual('9.13');
expect(memoryTo).toEqual('4.28');
}); });
}); });
......
...@@ -61,11 +61,28 @@ describe Gitlab::Elastic::ProjectSearchResults, lib: true do ...@@ -61,11 +61,28 @@ describe Gitlab::Elastic::ProjectSearchResults, lib: true do
result1 = Gitlab::Elastic::ProjectSearchResults.new(user, 'initial', project.id) result1 = Gitlab::Elastic::ProjectSearchResults.new(user, 'initial', project.id)
expect(result1.commits_count).to eq(1) expect(result1.commits_count).to eq(1)
end end
context 'visibility checks' do
it 'shows wiki for guests' do
project = create :empty_project, :public
guest = create :user
project.add_guest(guest)
# Wiki
project.wiki.create_page('index_page', 'term')
project.wiki.index_blobs
Gitlab::Elastic::Helper.refresh_index
result = Gitlab::Elastic::ProjectSearchResults.new(guest, 'term', project.id)
expect(result.wiki_blobs_count).to eq(1)
end
end
end end
describe "search for commits in non-default branch" do describe "search for commits in non-default branch" do
let(:project) { create(:project, :public, visibility) } let(:project) { create(:project, :public, visibility) }
let(:visibility) { :repository_enabled } let(:visibility) { :repository_enabled }
let(:result) { described_class.new(user, 'initial', project.id, 'test') } let(:result) { described_class.new(user, 'initial', project.id, 'test') }
subject(:commits) { result.objects('commits') } subject(:commits) { result.objects('commits') }
......
...@@ -419,6 +419,14 @@ describe Gitlab::Elastic::SearchResults, lib: true do ...@@ -419,6 +419,14 @@ describe Gitlab::Elastic::SearchResults, lib: true do
expect(results.wiki_blobs_count).to eq 1 expect(results.wiki_blobs_count).to eq 1
end end
it 'finds wiki blobs for guest' do
project_1.add_guest(user)
blobs = results.objects('wiki_blobs')
expect(blobs.first["_source"]["blob"]["content"]).to include("term")
expect(results.wiki_blobs_count).to eq 1
end
it 'finds wiki blobs from public projects only' do it 'finds wiki blobs from public projects only' do
project_2 = create :project, :private project_2 = create :project, :private
project_2.wiki.create_page('index_page', 'term') project_2.wiki.create_page('index_page', 'term')
......
...@@ -123,8 +123,8 @@ describe Gitlab::ProjectSearchResults, lib: true do ...@@ -123,8 +123,8 @@ describe Gitlab::ProjectSearchResults, lib: true do
context 'when wiki is internal' do context 'when wiki is internal' do
let(:project) { create(:project, :public, :wiki_private) } let(:project) { create(:project, :public, :wiki_private) }
it 'finds wiki blobs for members' do it 'finds wiki blobs for guest' do
project.add_reporter(user) project.add_guest(user)
is_expected.not_to be_empty is_expected.not_to be_empty
end end
......
...@@ -8,7 +8,39 @@ describe Namespace, models: true do ...@@ -8,7 +8,39 @@ describe Namespace, models: true do
it { is_expected.to delegate_method(:shared_runners_minutes).to(:namespace_statistics) } it { is_expected.to delegate_method(:shared_runners_minutes).to(:namespace_statistics) }
it { is_expected.to delegate_method(:shared_runners_seconds).to(:namespace_statistics) } it { is_expected.to delegate_method(:shared_runners_seconds).to(:namespace_statistics) }
it { is_expected.to delegate_method(:shared_runners_seconds_last_reset).to(:namespace_statistics) } it { is_expected.to delegate_method(:shared_runners_seconds_last_reset).to(:namespace_statistics) }
it { is_expected.to validate_inclusion_of(:plan).in_array(Namespace::EE_PLANS.keys).allow_nil } it { is_expected.to validate_inclusion_of(:plan).in_array(Namespace::EE_PLANS.keys).allow_blank }
context 'scopes' do
describe '.with_plan' do
let!(:namespace) { create :namespace, plan: namespace_plan }
context 'plan is set' do
let(:namespace_plan) { EE::Namespace::BRONZE_PLAN }
it 'returns namespaces with plan' do
expect(described_class.with_plan).to eq([namespace])
end
end
context 'plan is not set' do
context 'plan is empty string' do
let(:namespace_plan) { '' }
it 'returns no namespace' do
expect(described_class.with_plan).to be_empty
end
end
context 'plan is nil' do
let(:namespace_plan) { nil }
it 'returns no namespace' do
expect(described_class.with_plan).to be_empty
end
end
end
end
end
describe '#feature_available?' do describe '#feature_available?' do
let(:group) { create(:group, plan: plan_license) } let(:group) { create(:group, plan: plan_license) }
......
...@@ -443,6 +443,10 @@ describe MergeRequestPresenter do ...@@ -443,6 +443,10 @@ describe MergeRequestPresenter do
let(:can_push_to_branch) { true } let(:can_push_to_branch) { true }
let(:should_be_rebased) { true } let(:should_be_rebased) { true }
before do
allow(resource).to receive(:source_branch_exists?) { true }
end
it 'returns path' do it 'returns path' do
is_expected is_expected
.to eq("/#{project.full_path}/merge_requests/#{resource.iid}/rebase") .to eq("/#{project.full_path}/merge_requests/#{resource.iid}/rebase")
......
require 'spec_helper'
require 'rubocop'
require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/migration/update_column_in_batches'
describe RuboCop::Cop::Migration::UpdateColumnInBatches do
let(:cop) { described_class.new }
let(:tmp_rails_root) { Rails.root.join('tmp', 'rails_root') }
let(:migration_code) do
<<-END
def up
update_column_in_batches(:projects, :name, "foo") do |table, query|
query.where(table[:name].eq(nil))
end
end
END
end
before do
allow(cop).to receive(:rails_root).and_return(tmp_rails_root)
end
after do
FileUtils.rm_rf(tmp_rails_root)
end
context 'outside of a migration' do
it 'does not register any offenses' do
inspect_source(cop, migration_code)
expect(cop.offenses).to be_empty
end
end
let(:spec_filepath) { tmp_rails_root.join('spec', 'migrations', 'my_super_migration_spec.rb') }
shared_context 'with a migration file' do
before do
FileUtils.mkdir_p(File.dirname(migration_filepath))
@migration_file = File.new(migration_filepath, 'w+')
end
after do
@migration_file.close
end
end
shared_examples 'a migration file with no spec file' do
include_context 'with a migration file'
let(:relative_spec_filepath) { Pathname.new(spec_filepath).relative_path_from(tmp_rails_root) }
it 'registers an offense when using update_column_in_batches' do
inspect_source(cop, migration_code, @migration_file)
aggregate_failures do
expect(cop.offenses.size).to eq(1)
expect(cop.offenses.map(&:line)).to eq([2])
expect(cop.offenses.first.message).
to include("`#{relative_spec_filepath}`")
end
end
end
shared_examples 'a migration file with a spec file' do
include_context 'with a migration file'
before do
FileUtils.mkdir_p(File.dirname(spec_filepath))
@spec_file = File.new(spec_filepath, 'w+')
end
after do
@spec_file.close
end
it 'does not register any offenses' do
inspect_source(cop, migration_code, @migration_file)
expect(cop.offenses).to be_empty
end
end
context 'in a migration' do
let(:migration_filepath) { tmp_rails_root.join('db', 'migrate', '20121220064453_my_super_migration.rb') }
it_behaves_like 'a migration file with no spec file'
it_behaves_like 'a migration file with a spec file'
end
context 'in a post migration' do
let(:migration_filepath) { tmp_rails_root.join('db', 'post_migrate', '20121220064453_my_super_migration.rb') }
it_behaves_like 'a migration file with no spec file'
it_behaves_like 'a migration file with a spec file'
end
end
...@@ -146,4 +146,20 @@ describe MergeRequestEntity do ...@@ -146,4 +146,20 @@ describe MergeRequestEntity do
end end
end end
end end
describe 'when source project is deleted' do
let(:project) { create(:project, :repository) }
let(:fork_project) { create(:project, :repository, forked_from_project: project) }
let(:merge_request) { create(:merge_request, source_project: fork_project, target_project: project) }
it 'returns a blank rebase_path' do
allow(merge_request).to receive(:should_be_rebased?).and_return(true)
fork_project.destroy
merge_request.reload
entity = described_class.new(merge_request, request: request).as_json
expect(entity[:rebase_path]).to be_nil
end
end
end end
...@@ -59,7 +59,7 @@ describe MergeRequests::UpdateService, services: true do ...@@ -59,7 +59,7 @@ describe MergeRequests::UpdateService, services: true do
end end
end end
it 'mathces base expectations' do it 'matches base expectations' do
expect(@merge_request).to be_valid expect(@merge_request).to be_valid
expect(@merge_request.title).to eq('New title') expect(@merge_request.title).to eq('New title')
expect(@merge_request.assignee).to eq(user2) expect(@merge_request.assignee).to eq(user2)
...@@ -531,6 +531,17 @@ describe MergeRequests::UpdateService, services: true do ...@@ -531,6 +531,17 @@ describe MergeRequests::UpdateService, services: true do
end end
end end
context 'updating target_branch' do
it 'resets approvals when target_branch is changed' do
merge_request.target_project.update(reset_approvals_on_push: true, approvals_before_merge: 2)
merge_request.approvals.create(user_id: user2.id)
update_merge_request(target_branch: 'video')
expect(merge_request.reload.approvals).to be_empty
end
end
context 'updating asssignee_id' do context 'updating asssignee_id' do
it 'does not update assignee when assignee_id is invalid' do it 'does not update assignee when assignee_id is invalid' do
merge_request.update(assignee_id: user.id) merge_request.update(assignee_id: user.id)
......
...@@ -26,6 +26,15 @@ describe SearchService, services: true do ...@@ -26,6 +26,15 @@ describe SearchService, services: true do
expect(project).to eq accessible_project expect(project).to eq accessible_project
end end
it 'returns the project for guests' do
search_project = create :empty_project
search_project.add_guest(user)
project = SearchService.new(user, project_id: search_project.id).project
expect(project).to eq search_project
end
end end
context 'when the project is not accessible' do context 'when the project is not accessible' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment