Commit afc1fac0 authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'upstream/master' into 24196-protected-variables

* upstream/master: (89 commits)
  Revert "Merge branch 'grpc-1.3.4' into 'master'"
  Return nil when looking up config for unknown LDAP provider
  Avoid crash when trying to parse string with invalid UTF-8 sequence
  Enable Gitaly by default in GitLab 9.3
  Don’t create comment on JIRA if link already exists
  Disable sub_group_issuables_spec.rb for mysql
  Fix math rendering on blob pages
  Add changelog
  Don't allow to pass a user to ProjectWiki#http_url_to_repo
  Revert "Merge branch '1937-https-clone-url-username' into 'master'"
  Fix bottom padding for build page
  Fix /unsubscribe slash command creating extra todos
  Fix omniauth-google-oauth2 dependencies in Gemfile.lock
  Update looks job log
  'New issue'/'New merge request' dropdowns should show only projects with issues/merge requests feature enabled
  Fix spec for Members::AuthorizedDestroyService
  31616-add-uptime-of-gitlab-instance-in-admin-area
  Set head pipeline when creating merge requests
  Create a separate helper to check if we show particular tab on a search page
  Add performance deltas between app deployments on Merge Request widget
  ...
parents b5e8de5c 228926da
---
engines:
brakeman:
enabled: true
bundler-audit:
enabled: true
duplication:
enabled: true
config:
languages:
- ruby
- javascript
eslint:
enabled: true
fixme:
enabled: true
rubocop:
enabled: true
ratings:
paths:
- Gemfile.lock
- "**.erb"
- "**.haml"
- "**.rb"
- "**.rhtml"
- "**.slim"
- "**.inc"
- "**.js"
- "**.jsx"
- "**.module"
exclude_paths:
- config/
- db/
- features/
- node_modules/
- spec/
- vendor/
- lib/api/v3/
...@@ -57,7 +57,7 @@ linters: ...@@ -57,7 +57,7 @@ linters:
# Reports when you define the same property twice in a single rule set. # Reports when you define the same property twice in a single rule set.
DuplicateProperty: DuplicateProperty:
enabled: false enabled: true
# Separate rule, function, and mixin declarations with empty lines. # Separate rule, function, and mixin declarations with empty lines.
EmptyLineBetweenBlocks: EmptyLineBetweenBlocks:
......
...@@ -341,7 +341,7 @@ GEM ...@@ -341,7 +341,7 @@ GEM
grape-entity (0.6.0) grape-entity (0.6.0)
activesupport activesupport
multi_json (>= 1.3.2) multi_json (>= 1.3.2)
grpc (1.3.4) grpc (1.2.5)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
googleauth (~> 0.5.1) googleauth (~> 0.5.1)
haml (4.0.7) haml (4.0.7)
...@@ -499,11 +499,10 @@ GEM ...@@ -499,11 +499,10 @@ GEM
omniauth (~> 1.0) omniauth (~> 1.0)
omniauth-oauth2 (~> 1.0) omniauth-oauth2 (~> 1.0)
omniauth-google-oauth2 (0.4.1) omniauth-google-oauth2 (0.4.1)
addressable (~> 2.3) jwt (~> 1.5.2)
jwt (~> 1.0)
multi_json (~> 1.3) multi_json (~> 1.3)
omniauth (>= 1.1.1) omniauth (>= 1.1.1)
omniauth-oauth2 (~> 1.3.1) omniauth-oauth2 (>= 1.3.1)
omniauth-kerberos (0.3.0) omniauth-kerberos (0.3.0)
omniauth-multipassword omniauth-multipassword
timfel-krb5-auth (~> 0.8) timfel-krb5-auth (~> 0.8)
...@@ -1060,4 +1059,4 @@ DEPENDENCIES ...@@ -1060,4 +1059,4 @@ DEPENDENCIES
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
BUNDLED WITH BUNDLED WITH
1.14.6 1.15.0
...@@ -111,7 +111,7 @@ export default class BlobViewer { ...@@ -111,7 +111,7 @@ export default class BlobViewer {
BlobViewer.loadViewer(newViewer) BlobViewer.loadViewer(newViewer)
.then((viewer) => { .then((viewer) => {
$(viewer).syntaxHighlight(); $(viewer).renderGFM();
this.$fileHolder.trigger('highlight:line'); this.$fileHolder.trigger('highlight:line');
gl.utils.handleLocationHash(); gl.utils.handleLocationHash();
......
...@@ -70,6 +70,7 @@ $(() => { ...@@ -70,6 +70,7 @@ $(() => {
gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId); gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId);
this.filterManager = new FilteredSearchBoards(Store.filter, true); this.filterManager = new FilteredSearchBoards(Store.filter, true);
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();
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import FilteredSearchContainer from '../filtered_search/container'; import FilteredSearchContainer from '../filtered_search/container';
export default class FilteredSearchBoards extends gl.FilteredSearchManager { export default class FilteredSearchBoards extends gl.FilteredSearchManager {
constructor(store, updateUrl = false) { constructor(store, updateUrl = false, cantEdit = []) {
super('boards'); super('boards');
this.store = store; this.store = store;
...@@ -11,6 +11,8 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { ...@@ -11,6 +11,8 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
// Issue boards is slightly different, we handle all the requests async // Issue boards is slightly different, we handle all the requests async
// instead or reloading the page, we just re-fire the list ajax requests // instead or reloading the page, we just re-fire the list ajax requests
this.isHandledAsync = true; this.isHandledAsync = true;
this.cantEdit = cantEdit;
} }
updateObject(path) { updateObject(path) {
...@@ -40,4 +42,8 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { ...@@ -40,4 +42,8 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
// Get the placeholder back if search is empty // Get the placeholder back if search is empty
this.filteredSearchInput.dispatchEvent(new Event('input')); this.filteredSearchInput.dispatchEvent(new Event('input'));
} }
canEdit(tokenName) {
return this.cantEdit.indexOf(tokenName) === -1;
}
} }
This diff is collapsed.
...@@ -124,7 +124,8 @@ import ShortcutsBlob from './shortcuts_blob'; ...@@ -124,7 +124,8 @@ import ShortcutsBlob from './shortcuts_blob';
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({
......
...@@ -2,9 +2,9 @@ import './dropdown_hint'; ...@@ -2,9 +2,9 @@ import './dropdown_hint';
import './dropdown_non_user'; import './dropdown_non_user';
import './dropdown_user'; import './dropdown_user';
import './dropdown_utils'; import './dropdown_utils';
import './filtered_search_token_keys';
import './filtered_search_dropdown_manager'; import './filtered_search_dropdown_manager';
import './filtered_search_dropdown'; import './filtered_search_dropdown';
import './filtered_search_manager'; import './filtered_search_manager';
import './filtered_search_token_keys';
import './filtered_search_tokenizer'; import './filtered_search_tokenizer';
import './filtered_search_visual_tokens'; import './filtered_search_visual_tokens';
...@@ -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;
...@@ -17,16 +18,18 @@ class FilteredSearchManager { ...@@ -17,16 +18,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) => {
...@@ -47,12 +50,12 @@ class FilteredSearchManager { ...@@ -47,12 +50,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();
...@@ -141,7 +144,9 @@ class FilteredSearchManager { ...@@ -141,7 +144,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();
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();
} }
...@@ -240,8 +245,10 @@ class FilteredSearchManager { ...@@ -240,8 +245,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 (token) { if (token && canEdit) {
gl.FilteredSearchVisualTokens.editToken(token); gl.FilteredSearchVisualTokens.editToken(token);
this.tokenChange(); this.tokenChange();
} }
...@@ -391,7 +398,12 @@ class FilteredSearchManager { ...@@ -391,7 +398,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 +
...@@ -410,18 +422,27 @@ class FilteredSearchManager { ...@@ -410,18 +422,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;
...@@ -516,6 +537,11 @@ class FilteredSearchManager { ...@@ -516,6 +537,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));
} }
...@@ -740,6 +740,12 @@ GitLabDropdown = (function() { ...@@ -740,6 +740,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]);
} }
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, consistent-return, prefer-arrow-callback, no-return-assign, object-shorthand, comma-dangle, no-param-reassign, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, consistent-return, prefer-arrow-callback, no-return-assign, object-shorthand, comma-dangle, no-param-reassign, max-len */
(function() { function notificationGranted(message, opts, onclick) {
(function(w) { var notification;
var notificationGranted, notifyMe, notifyPermissions; notification = new Notification(message, opts);
notificationGranted = function(message, opts, onclick) { setTimeout(function() {
var notification; // Hide the notification after X amount of seconds
notification = new Notification(message, opts); return notification.close();
setTimeout(function() { }, 8000);
return notification.close();
// Hide the notification after X amount of seconds return notification.onclick = onclick || notification.close;
}, 8000); }
if (onclick) {
return notification.onclick = onclick;
}
};
notifyPermissions = function() {
if ('Notification' in window) {
return Notification.requestPermission();
}
};
notifyMe = function(message, body, icon, onclick) {
var opts;
opts = {
body: body,
icon: icon
};
// Let's check if the browser supports notifications
if (!('Notification' in window)) {
// do nothing function notifyPermissions() {
} else if (Notification.permission === 'granted') { if ('Notification' in window) {
// If it's okay let's create a notification return Notification.requestPermission();
}
}
function notifyMe(message, body, icon, onclick) {
var opts;
opts = {
body: body,
icon: icon
};
// Let's check if the browser supports notifications
if (!('Notification' in window)) {
// do nothing
} else if (Notification.permission === 'granted') {
// If it's okay let's create a notification
return notificationGranted(message, opts, onclick);
} else if (Notification.permission !== 'denied') {
return Notification.requestPermission(function(permission) {
// If the user accepts, let's create a notification
if (permission === 'granted') {
return notificationGranted(message, opts, onclick); return notificationGranted(message, opts, onclick);
} else if (Notification.permission !== 'denied') {
return Notification.requestPermission(function(permission) {
// If the user accepts, let's create a notification
if (permission === 'granted') {
return notificationGranted(message, opts, onclick);
}
});
} }
}; });
w.notify = notifyMe; }
return w.notifyPermissions = notifyPermissions; }
})(window);
}).call(window); const notify = {
notificationGranted,
notifyPermissions,
notifyMe,
};
export default notify;
...@@ -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);
}
...@@ -56,7 +56,6 @@ import './lib/utils/animate'; ...@@ -56,7 +56,6 @@ import './lib/utils/animate';
import './lib/utils/bootstrap_linked_tabs'; import './lib/utils/bootstrap_linked_tabs';
import './lib/utils/common_utils'; import './lib/utils/common_utils';
import './lib/utils/datetime_utility'; import './lib/utils/datetime_utility';
import './lib/utils/notify';
import './lib/utils/pretty_time'; import './lib/utils/pretty_time';
import './lib/utils/text_utility'; import './lib/utils/text_utility';
import './lib/utils/url_utility'; import './lib/utils/url_utility';
......
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 userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
export default {
props: [
'pipeline',
],
computed: {
user() {
return !!this.pipeline.user;
},
},
components: {
userAvatarLink,
},
template: `
<td>
<a
:href="pipeline.path"
class="js-pipeline-url-link">
<span class="pipeline-id">#{{pipeline.id}}</span>
</a>
<span>by</span>
<user-avatar-link
v-if="user"
class="js-pipeline-url-user"
:link-href="pipeline.user.web_url"
:img-src="pipeline.user.avatar_url"
:tooltip-text="pipeline.user.name"
/>
<span
v-if="!user"
class="js-pipeline-url-api api">
API
</span>
<span
v-if="pipeline.flags.latest"
class="js-pipeline-url-lastest label label-success has-tooltip"
title="Latest pipeline for this branch"
data-original-title="Latest pipeline for this branch">
latest
</span>
<span
v-if="pipeline.flags.yaml_errors"
class="js-pipeline-url-yaml label label-danger has-tooltip"
:title="pipeline.yaml_errors"
:data-original-title="pipeline.yaml_errors">
yaml invalid
</span>
<span
v-if="pipeline.flags.stuck"
class="js-pipeline-url-stuck label label-warning">
stuck
</span>
</td>
`,
};
<script>
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import tooltipMixin from '../../vue_shared/mixins/tooltip';
export default {
props: {
pipeline: {
type: Object,
required: true,
},
},
components: {
userAvatarLink,
},
mixins: [
tooltipMixin,
],
computed: {
user() {
return this.pipeline.user;
},
},
};
</script>
<template>
<td>
<a
:href="pipeline.path"
class="js-pipeline-url-link">
<span class="pipeline-id">#{{pipeline.id}}</span>
</a>
<span>by</span>
<user-avatar-link
v-if="user"
class="js-pipeline-url-user"
:link-href="pipeline.user.web_url"
:img-src="pipeline.user.avatar_url"
:tooltip-text="pipeline.user.name"
/>
<span
v-if="!user"
class="js-pipeline-url-api api">
API
</span>
<span
v-if="pipeline.flags.latest"
class="js-pipeline-url-lastest label label-success"
title="Latest pipeline for this branch"
ref="tooltip">
latest
</span>
<span
v-if="pipeline.flags.yaml_errors"
class="js-pipeline-url-yaml label label-danger"
:title="pipeline.yaml_errors"
ref="tooltip">
yaml invalid
</span>
<span
v-if="pipeline.flags.stuck"
class="js-pipeline-url-stuck label label-warning">
stuck
</span>
</td>
</template>
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;
} }
} }
...@@ -51,6 +51,9 @@ import Api from './api'; ...@@ -51,6 +51,9 @@ import Api from './api';
this.groupId = $(select).data('group-id'); this.groupId = $(select).data('group-id');
this.includeGroups = $(select).data('include-groups'); this.includeGroups = $(select).data('include-groups');
this.orderBy = $(select).data('order-by') || 'id'; this.orderBy = $(select).data('order-by') || 'id';
this.withIssuesEnabled = $(select).data('with-issues-enabled');
this.withMergeRequestsEnabled = $(select).data('with-merge-requests-enabled');
placeholder = "Search for project"; placeholder = "Search for project";
if (this.includeGroups) { if (this.includeGroups) {
placeholder += " or group"; placeholder += " or group";
...@@ -84,7 +87,11 @@ import Api from './api'; ...@@ -84,7 +87,11 @@ import Api from './api';
if (_this.groupId) { if (_this.groupId) {
return Api.groupProjects(_this.groupId, query.term, projectsCallback); return Api.groupProjects(_this.groupId, query.term, projectsCallback);
} else { } else {
return Api.projects(query.term, { order_by: _this.orderBy }, projectsCallback); return Api.projects(query.term, {
order_by: _this.orderBy,
with_issues_enabled: _this.withIssuesEnabled,
with_merge_requests_enabled: _this.withMergeRequestsEnabled
}, projectsCallback);
} }
}; };
})(this), })(this),
......
...@@ -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"
......
...@@ -13,7 +13,7 @@ export default { ...@@ -13,7 +13,7 @@ export default {
}, },
data() { data() {
return { return {
removeSourceBranch: true, removeSourceBranch: this.mr.shouldRemoveSourceBranch,
mergeWhenBuildSucceeds: false, mergeWhenBuildSucceeds: false,
useCommitMessageWithDescription: false, useCommitMessageWithDescription: false,
setToMergeWhenPipelineSucceeds: false, setToMergeWhenPipelineSucceeds: false,
...@@ -69,6 +69,9 @@ export default { ...@@ -69,6 +69,9 @@ export default {
|| this.isMakingRequest || this.isMakingRequest
|| this.mr.preventMerge); || this.mr.preventMerge);
}, },
isRemoveSourceBranchButtonDisabled() {
return this.isMergeButtonDisabled || !this.mr.canRemoveSourceBranch;
},
shouldShowSquashBeforeMerge() { shouldShowSquashBeforeMerge() {
const { commitsCount, enableSquashBeforeMerge } = this.mr; const { commitsCount, enableSquashBeforeMerge } = this.mr;
return enableSquashBeforeMerge && commitsCount > 1; return enableSquashBeforeMerge && commitsCount > 1;
...@@ -252,8 +255,9 @@ export default { ...@@ -252,8 +255,9 @@ export default {
<template v-if="isMergeAllowed()"> <template v-if="isMergeAllowed()">
<label class="spacing"> <label class="spacing">
<input <input
id="remove-source-branch-input"
v-model="removeSourceBranch" v-model="removeSourceBranch"
:disabled="isMergeButtonDisabled" :disabled="isRemoveSourceBranchButtonDisabled"
type="checkbox"/> Remove source branch type="checkbox"/> Remove source branch
</label> </label>
......
...@@ -41,3 +41,4 @@ export { default as getStateKey } from './stores/get_state_key'; ...@@ -41,3 +41,4 @@ export { default as getStateKey } from './stores/get_state_key';
export { default as mrWidgetOptions } from './mr_widget_options'; export { default as mrWidgetOptions } from './mr_widget_options';
export { default as stateMaps } from './stores/state_maps'; export { default as stateMaps } from './stores/state_maps';
export { default as SquashBeforeMerge } from './components/states/mr_widget_squash_before_merge'; export { default as SquashBeforeMerge } from './components/states/mr_widget_squash_before_merge';
export { default as notify } from '../lib/utils/notify';
...@@ -4,6 +4,8 @@ import { ...@@ -4,6 +4,8 @@ import {
} from './dependencies'; } from './dependencies';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
gl.mrWidgetData.gitlabLogo = gon.gitlab_logo;
const vm = new Vue(mrWidgetOptions); const vm = new Vue(mrWidgetOptions);
window.gl.mrWidget = { window.gl.mrWidget = {
......
...@@ -29,6 +29,7 @@ import { ...@@ -29,6 +29,7 @@ import {
eventHub, eventHub,
stateMaps, stateMaps,
SquashBeforeMerge, SquashBeforeMerge,
notify,
} from './dependencies'; } from './dependencies';
export default { export default {
...@@ -77,8 +78,10 @@ export default { ...@@ -77,8 +78,10 @@ export default {
this.service.checkStatus() this.service.checkStatus()
.then(res => res.json()) .then(res => res.json())
.then((res) => { .then((res) => {
this.handleNotification(res);
this.mr.setData(res); this.mr.setData(res);
this.setFavicon(); this.setFavicon();
if (cb) { if (cb) {
cb.call(null, res); cb.call(null, res);
} }
...@@ -136,6 +139,15 @@ export default { ...@@ -136,6 +139,15 @@ export default {
new Flash('Something went wrong. Please try again.'); // eslint-disable-line new Flash('Something went wrong. Please try again.'); // eslint-disable-line
}); });
}, },
handleNotification(data) {
if (data.ci_status === this.mr.ciStatus) return;
const label = data.pipeline.details.status.label;
const title = `Pipeline ${label}`;
const message = `Pipeline ${label} for "${data.title}"`;
notify.notifyMe(title, message, this.mr.gitlabLogo);
},
resumePolling() { resumePolling() {
this.pollingInterval.resume(); this.pollingInterval.resume();
}, },
......
...@@ -5,6 +5,8 @@ export default class MergeRequestStore { ...@@ -5,6 +5,8 @@ export default class MergeRequestStore {
constructor(data) { constructor(data) {
this.sha = data.diff_head_sha; this.sha = data.diff_head_sha;
this.gitlabLogo = data.gitlabLogo;
this.setData(data); this.setData(data);
} }
...@@ -50,7 +52,7 @@ export default class MergeRequestStore { ...@@ -50,7 +52,7 @@ export default class MergeRequestStore {
this.cancelAutoMergePath = data.cancel_merge_when_pipeline_succeeds_path; this.cancelAutoMergePath = data.cancel_merge_when_pipeline_succeeds_path;
this.removeWIPPath = data.remove_wip_path; this.removeWIPPath = data.remove_wip_path;
this.sourceBranchRemoved = !data.source_branch_exists; this.sourceBranchRemoved = !data.source_branch_exists;
this.shouldRemoveSourceBranch = (data.merge_params || {}).should_remove_source_branch || false; this.shouldRemoveSourceBranch = data.remove_source_branch || false;
this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false; this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false;
this.mergeWhenPipelineSucceeds = data.merge_when_pipeline_succeeds || false; this.mergeWhenPipelineSucceeds = data.merge_when_pipeline_succeeds || false;
this.mergePath = data.merge_path; this.mergePath = data.merge_path;
......
...@@ -4,7 +4,7 @@ import PipelinesActionsComponent from '../../pipelines/components/pipelines_acti ...@@ -4,7 +4,7 @@ import PipelinesActionsComponent from '../../pipelines/components/pipelines_acti
import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts'; import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts';
import ciBadge from './ci_badge_link.vue'; import ciBadge from './ci_badge_link.vue';
import PipelinesStageComponent from '../../pipelines/components/stage.vue'; import PipelinesStageComponent from '../../pipelines/components/stage.vue';
import PipelinesUrlComponent from '../../pipelines/components/pipeline_url'; import PipelinesUrlComponent from '../../pipelines/components/pipeline_url.vue';
import PipelinesTimeagoComponent from '../../pipelines/components/time_ago'; import PipelinesTimeagoComponent from '../../pipelines/components/time_ago';
import CommitComponent from './commit'; import CommitComponent from './commit';
......
...@@ -34,6 +34,7 @@ ...@@ -34,6 +34,7 @@
@import "framework/selects.scss"; @import "framework/selects.scss";
@import "framework/sidebar.scss"; @import "framework/sidebar.scss";
@import "framework/tables.scss"; @import "framework/tables.scss";
@import "framework/notes.scss";
@import "framework/timeline.scss"; @import "framework/timeline.scss";
@import "framework/typography.scss"; @import "framework/typography.scss";
@import "framework/zen.scss"; @import "framework/zen.scss";
......
...@@ -23,7 +23,6 @@ ...@@ -23,7 +23,6 @@
.row-content-block { .row-content-block {
margin-top: 0; margin-top: 0;
margin-bottom: -$gl-padding;
background-color: $gray-light; background-color: $gray-light;
padding: $gl-padding; padding: $gl-padding;
margin-bottom: 0; margin-bottom: 0;
......
gl-emoji { gl-emoji {
display: inline-block;
display: inline-flex; display: inline-flex;
vertical-align: middle; vertical-align: middle;
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
......
...@@ -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;
......
...@@ -11,7 +11,6 @@ ...@@ -11,7 +11,6 @@
> li { > li {
padding: 10px 15px; padding: 10px 15px;
min-height: 20px; min-height: 20px;
border-bottom: 1px solid $list-border-light;
border-bottom: 1px solid $list-border; border-bottom: 1px solid $list-border;
&::after { &::after {
......
@mixin notes-media($condition, $breakpoint-width) {
@media (#{$condition}-width: ($breakpoint-width)) {
@content;
}
// Diff is side by side
.notes_content.parallel & {
// We hide at double what we normally hide at because
// there are two columns of notes
@media (#{$condition}-width: (2 * $breakpoint-width)) {
@content;
}
}
}
...@@ -96,7 +96,6 @@ ...@@ -96,7 +96,6 @@
.select2-search-field input { .select2-search-field input {
padding: 5px $gl-padding / 2; padding: 5px $gl-padding / 2;
font-size: 13px;
height: auto; height: auto;
font-family: inherit; font-family: inherit;
font-size: inherit; font-size: inherit;
......
...@@ -3,6 +3,12 @@ ...@@ -3,6 +3,12 @@
margin: 0; margin: 0;
padding: 0; padding: 0;
&::before {
@include notes-media('max', $screen-xs-max) {
background: none;
}
}
.system-note { .system-note {
.note-text { .note-text {
color: $gl-text-color !important; color: $gl-text-color !important;
...@@ -23,6 +29,16 @@ ...@@ -23,6 +29,16 @@
.timeline-entry-inner { .timeline-entry-inner {
position: relative; position: relative;
@include notes-media('max', $screen-xs-max) {
.timeline-icon {
display: none;
}
.timeline-content {
margin-left: 0;
}
}
} }
&:target, &:target,
...@@ -40,24 +56,6 @@ ...@@ -40,24 +56,6 @@
} }
} }
@media (max-width: $screen-xs-max) {
.timeline {
&::before {
background: none;
}
}
.timeline-entry .timeline-entry-inner {
.timeline-icon {
display: none;
}
.timeline-content {
margin-left: 0;
}
}
}
.discussion .timeline-entry { .discussion .timeline-entry {
margin: 0; margin: 0;
border-right: none; border-right: none;
......
...@@ -72,7 +72,9 @@ ...@@ -72,7 +72,9 @@
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
height: 475px; // Needed for PhantomJS height: 475px; // Needed for PhantomJS
// scss-lint:disable DuplicateProperty
height: calc(100vh - 222px); height: calc(100vh - 222px);
// scss-lint:enable DuplicateProperty
min-height: 475px; min-height: 475px;
transition: width .2s; transition: width .2s;
......
...@@ -29,129 +29,140 @@ ...@@ -29,129 +29,140 @@
} }
} }
.build-page { @keyframes blinking-scroll-button {
pre.trace { 0% { opacity: 0.2; }
background: $builds-trace-bg; 25% { opacity: 0.5; }
color: $white-light; 50% { opacity: 0.7; }
font-family: $monospace_font; 100% { opacity: 1; }
white-space: pre-wrap; }
overflow: auto;
overflow-y: hidden;
font-size: 12px;
.fa-spinner {
font-size: 24px;
margin-left: 20px;
}
}
.environment-information {
background-color: $gray-light;
border: 1px solid $border-color;
padding: 12px $gl-padding;
border-radius: $border-radius-default;
svg { .build-page {
position: relative; .sticky {
top: 1px; position: absolute;
margin-right: 5px; left: 0;
} right: 0;
} }
.truncated-info { .build-trace-container {
text-align: center; position: absolute;
border-bottom: 1px solid; top: 225px;
background-color: $black; left: 15px;
height: 45px; bottom: 10px;
padding: 15px; background: $black;
color: $gray-darkest;
font-family: $monospace_font;
font-size: 12px;
&.affix { &.sidebar-expanded {
top: 0; right: 305px;
} }
// with sidebar &.sidebar-collapsed {
&.affix.sidebar-expanded { right: 16px;
right: 312px;
left: 22px;
} }
// without sidebar code {
&.affix.sidebar-collapsed { background: $black;
right: 20px; color: $gray-darkest;
left: 20px;
} }
&.affix-top { .top-bar {
position: absolute;
top: 0; top: 0;
margin: 0 auto; height: 35px;
right: 5px; display: flex;
left: 5px; justify-content: flex-end;
} border-bottom: 1px outset $white-light;
.truncated-info-size { .truncated-info {
margin: 0 5px; margin: 0 auto;
} align-self: center;
.raw-link { .truncated-info-size {
color: inherit; margin: 0 5px;
margin-left: 5px; }
text-decoration: underline;
.raw-link {
color: inherit;
margin-left: 5px;
text-decoration: underline;
}
}
} }
}
}
.scroll-controls { .controllers {
height: 100%; display: flex;
align-self: center;
font-size: 15px;
.scroll-step { svg {
width: 31px; height: 15px;
margin: 0 0 0 auto; display: block;
} fill: $white-light;
}
.scroll-link, a,
.autoscroll-container { .btn-scroll {
right: 25px; margin: 0 8px;
z-index: 1; color: $white-light;
} }
.scroll-link { .btn-scroll.animate {
position: fixed; .first-triangle {
display: block; animation: blinking-scroll-button 1s ease infinite;
margin-bottom: 10px; animation-delay: .3s;
}
&.scroll-top .gitlab-icon-scroll-up-hover, .second-triangle {
&.scroll-top:hover .gitlab-icon-scroll-up, animation: blinking-scroll-button 1s ease infinite;
&.scroll-bottom .gitlab-icon-scroll-down-hover, animation-delay: .2s;
&.scroll-bottom:hover .gitlab-icon-scroll-down { }
display: none;
}
&.scroll-top:hover .gitlab-icon-scroll-up-hover, .third-triangle {
&.scroll-bottom:hover .gitlab-icon-scroll-down-hover { animation: blinking-scroll-button 1s ease infinite;
display: inline-block; }
}
&.scroll-top { &:disabled {
top: 10px; opacity: 1;
} }
}
&.scroll-bottom { .btn-scroll:disabled {
bottom: -2px; opacity: 0.35;
cursor: not-allowed;
}
} }
} }
.autoscroll-container { .bash {
position: absolute; top: 35px;
left: 10px;
bottom: 0;
overflow-y: hidden;
padding-bottom: 20px;
padding-right: 20px;
} }
&.sidebar-expanded { .environment-information {
background-color: $gray-light;
border: 1px solid $border-color;
padding: 12px $gl-padding;
border-radius: $border-radius-default;
.scroll-link, svg {
.autoscroll-container { position: relative;
right: ($gutter_width + ($gl-padding * 2)); top: 1px;
margin-right: 5px;
} }
} }
.build-loader-animation {
position: relative;
width: 6px;
height: 6px;
margin: auto auto 12px 2px;
border-radius: 50%;
animation: blinking-dots 1s linear infinite;
}
} }
.status-message { .status-message {
...@@ -223,32 +234,6 @@ ...@@ -223,32 +234,6 @@
} }
} }
.build-trace {
background: $black;
color: $gray-darkest;
white-space: pre;
overflow-x: auto;
font-size: 12px;
position: relative;
.fa-spinner {
font-size: 24px;
}
.bash {
display: block;
}
.build-loader-animation {
position: relative;
width: 6px;
height: 6px;
margin: auto auto 12px 2px;
border-radius: 50%;
animation: blinking-dots 1s linear infinite;
}
}
.right-sidebar.build-sidebar { .right-sidebar.build-sidebar {
padding: $gl-padding 0; padding: $gl-padding 0;
...@@ -390,6 +375,10 @@ ...@@ -390,6 +375,10 @@
.container-fluid.container-limited { .container-fluid.container-limited {
max-width: 100%; max-width: 100%;
} }
.content-wrapper {
padding-bottom: 6px;
}
} }
.build-detail-row { .build-detail-row {
......
...@@ -36,7 +36,6 @@ ...@@ -36,7 +36,6 @@
pre.commit-message { pre.commit-message {
background: none; background: none;
padding: 0; padding: 0;
margin: 0;
border: none; border: none;
margin: 20px 0; margin: 20px 0;
border-radius: 0; border-radius: 0;
......
...@@ -94,7 +94,6 @@ ...@@ -94,7 +94,6 @@
.old_line, .old_line,
.new_line { .new_line {
margin: 0; margin: 0;
padding: 0;
border: none; border: none;
padding: 0 5px; padding: 0 5px;
border-right: 1px solid; border-right: 1px solid;
......
...@@ -64,6 +64,10 @@ ...@@ -64,6 +64,10 @@
} }
} }
.btn .text-center {
display: inline;
}
.commit-title { .commit-title {
margin: 0; margin: 0;
} }
......
...@@ -204,7 +204,6 @@ ul.related-merge-requests > li { ...@@ -204,7 +204,6 @@ ul.related-merge-requests > li {
.dropdown-toggle { .dropdown-toggle {
.fa-caret-down { .fa-caret-down {
pointer-events: none; pointer-events: none;
margin-left: 0;
color: inherit; color: inherit;
margin-left: 0; margin-left: 0;
} }
......
...@@ -520,17 +520,13 @@ ...@@ -520,17 +520,13 @@
position: absolute; position: absolute;
border-top: 2px solid $border-color; border-top: 2px solid $border-color;
height: 1px; height: 1px;
top: 8px; top: 9px;
width: 8px; width: 8px;
left: 0; left: 0;
} }
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
&::before {
top: 14px;
}
} }
} }
...@@ -539,7 +535,7 @@ ...@@ -539,7 +535,7 @@
width: 2px; width: 2px;
background: $border-color; background: $border-color;
position: absolute; position: absolute;
top: -5px; top: -9px;
} }
} }
......
...@@ -18,7 +18,7 @@ ul.notes { ...@@ -18,7 +18,7 @@ ul.notes {
margin-left: 55px; margin-left: 55px;
&.timeline-content-form { &.timeline-content-form {
@media (max-width: $screen-sm-max) { @include notes-media('max', $screen-sm-max) {
margin-left: 0; margin-left: 0;
} }
} }
...@@ -120,7 +120,7 @@ ul.notes { ...@@ -120,7 +120,7 @@ ul.notes {
.note-header { .note-header {
@media (max-width: $screen-xs-min) { @include notes-media('max', $screen-xs-min) {
.inline { .inline {
display: block; display: block;
} }
...@@ -152,7 +152,7 @@ ul.notes { ...@@ -152,7 +152,7 @@ ul.notes {
padding-left: 0; padding-left: 0;
clear: both; clear: both;
@media (min-width: $screen-sm-min) { @include notes-media('min', $screen-sm-min) {
margin-left: 65px; margin-left: 65px;
} }
...@@ -200,7 +200,7 @@ ul.notes { ...@@ -200,7 +200,7 @@ ul.notes {
} }
.timeline-content { .timeline-content {
@media (min-width: $screen-sm-min) { @include notes-media('min', $screen-sm-min) {
margin-left: 30px; margin-left: 30px;
} }
} }
...@@ -370,7 +370,7 @@ ul.notes { ...@@ -370,7 +370,7 @@ ul.notes {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@media (max-width: $screen-xs-max) { @include notes-media('max', $screen-xs-max) {
flex-flow: row wrap; flex-flow: row wrap;
} }
} }
...@@ -385,7 +385,7 @@ ul.notes { ...@@ -385,7 +385,7 @@ ul.notes {
} }
.note-header-author-name { .note-header-author-name {
@media (max-width: $screen-xs-max) { @include notes-media('max', $screen-xs-max) {
display: none; display: none;
} }
} }
...@@ -393,7 +393,7 @@ ul.notes { ...@@ -393,7 +393,7 @@ ul.notes {
.note-headline-light { .note-headline-light {
display: inline; display: inline;
@media (max-width: $screen-xs-min) { @include notes-media('max', $screen-xs-min) {
display: block; display: block;
} }
} }
...@@ -435,7 +435,7 @@ ul.notes { ...@@ -435,7 +435,7 @@ ul.notes {
margin-left: 10px; margin-left: 10px;
color: $gray-darkest; color: $gray-darkest;
@media (max-width: $screen-xs-max) { @include notes-media('max', $screen-xs-max) {
float: none; float: none;
margin-left: 0; margin-left: 0;
} }
...@@ -446,7 +446,7 @@ ul.notes { ...@@ -446,7 +446,7 @@ ul.notes {
} }
.discussion-actions { .discussion-actions {
@media (max-width: $screen-md-max) { @include notes-media('max', $screen-md-max) {
float: none; float: none;
margin-left: 0; margin-left: 0;
...@@ -460,7 +460,7 @@ ul.notes { ...@@ -460,7 +460,7 @@ ul.notes {
display: inline; display: inline;
line-height: 20px; line-height: 20px;
@media (min-width: $screen-sm-min) { @include notes-media('min', $screen-sm-min) {
margin-left: 10px; margin-left: 10px;
line-height: 24px; line-height: 24px;
} }
...@@ -629,7 +629,7 @@ ul.notes { ...@@ -629,7 +629,7 @@ ul.notes {
} }
.line-resolve-all-container { .line-resolve-all-container {
@media (min-width: $screen-sm-min) { @include notes-media('min', $screen-sm-min) {
margin-right: 0; margin-right: 0;
padding-left: $gl-padding; padding-left: $gl-padding;
} }
...@@ -744,10 +744,6 @@ ul.notes { ...@@ -744,10 +744,6 @@ ul.notes {
// Merge request notes in diffs // Merge request notes in diffs
.diff-file { .diff-file {
// Diff is side by side
.notes_content.parallel .note-header .note-header-author-name {
display: block;
}
// Diff is inline // Diff is inline
.notes_content .note-header .note-headline-light { .notes_content .note-header .note-headline-light {
display: inline-block; display: inline-block;
......
...@@ -88,6 +88,10 @@ ...@@ -88,6 +88,10 @@
} }
} }
.btn .text-center {
display: inline;
}
.tooltip { .tooltip {
white-space: nowrap; white-space: nowrap;
} }
......
...@@ -247,7 +247,6 @@ ...@@ -247,7 +247,6 @@
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
line-height: 13px; line-height: 13px;
padding: $gl-vert-padding $gl-padding;
letter-spacing: .4px; letter-spacing: .4px;
padding: 6px 14px; padding: 6px 14px;
text-align: center; text-align: center;
......
...@@ -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 = @users.page(params[:page]) @users = @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])
......
...@@ -64,6 +64,8 @@ class GroupsController < Groups::ApplicationController ...@@ -64,6 +64,8 @@ class GroupsController < Groups::ApplicationController
end end
def subgroups def subgroups
return not_found unless Group.supports_nested_groups?
@nested_groups = GroupsFinder.new(current_user, parent: group).execute @nested_groups = GroupsFinder.new(current_user, parent: group).execute
@nested_groups = @nested_groups.search(params[:filter_groups]) if params[:filter_groups].present? @nested_groups = @nested_groups.search(params[:filter_groups]) if params[:filter_groups].present?
end end
......
...@@ -56,7 +56,7 @@ module ButtonHelper ...@@ -56,7 +56,7 @@ module ButtonHelper
content_tag (append_link ? :a : :span), protocol, content_tag (append_link ? :a : :span), protocol,
class: klass, class: klass,
href: (project.http_url_to_repo(current_user) if append_link), href: (project.http_url_to_repo if append_link),
data: { data: {
html: true, html: true,
placement: placement, placement: placement,
......
...@@ -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)
...@@ -259,7 +276,7 @@ module ProjectsHelper ...@@ -259,7 +276,7 @@ module ProjectsHelper
when 'ssh' when 'ssh'
project.ssh_url_to_repo project.ssh_url_to_repo
else else
project.http_url_to_repo(current_user) project.http_url_to_repo
end end
end end
......
...@@ -45,6 +45,14 @@ module SelectsHelper ...@@ -45,6 +45,14 @@ module SelectsHelper
end end
end end
with_feature_enabled_data_attribute =
case opts.delete(:with_feature_enabled)
when 'issues' then 'data-with-issues-enabled'
when 'merge_requests' then 'data-with-merge-requests-enabled'
end
opts[with_feature_enabled_data_attribute] = true
hidden_field_tag(id, opts[:selected], opts) hidden_field_tag(id, opts[:selected], opts)
end end
......
...@@ -13,6 +13,7 @@ module SubmoduleHelper ...@@ -13,6 +13,7 @@ module SubmoduleHelper
if url =~ /([^\/:]+)\/([^\/]+(?:\.git)?)\Z/ if url =~ /([^\/:]+)\/([^\/]+(?:\.git)?)\Z/
namespace, project = $1, $2 namespace, project = $1, $2
project.rstrip!
project.sub!(/\.git\z/, '') project.sub!(/\.git\z/, '')
if self_url?(url, namespace, project) if self_url?(url, namespace, project)
......
...@@ -10,9 +10,9 @@ module Ci ...@@ -10,9 +10,9 @@ module Ci
has_one :last_pipeline, -> { order(id: :desc) }, class_name: 'Ci::Pipeline' has_one :last_pipeline, -> { order(id: :desc) }, class_name: 'Ci::Pipeline'
has_many :pipelines has_many :pipelines
validates :cron, unless: :importing_or_inactive?, cron: true, presence: { unless: :importing_or_inactive? } validates :cron, unless: :importing?, cron: true, presence: { unless: :importing? }
validates :cron_timezone, cron_timezone: true, presence: { unless: :importing_or_inactive? } validates :cron_timezone, cron_timezone: true, presence: { unless: :importing? }
validates :ref, presence: { unless: :importing_or_inactive? } validates :ref, presence: { unless: :importing? }
validates :description, presence: true validates :description, presence: true
before_save :set_next_run_at before_save :set_next_run_at
...@@ -32,10 +32,6 @@ module Ci ...@@ -32,10 +32,6 @@ module Ci
update_attribute(:active, false) update_attribute(:active, false)
end end
def importing_or_inactive?
importing? || inactive?
end
def runnable_by_owner? def runnable_by_owner?
Ability.allowed?(owner, :create_pipeline, project) Ability.allowed?(owner, :create_pipeline, project)
end end
......
...@@ -84,89 +84,6 @@ module Routable ...@@ -84,89 +84,6 @@ module Routable
joins(:route).where(wheres.join(' OR ')) joins(:route).where(wheres.join(' OR '))
end end
end end
# Builds a relation to find multiple objects that are nested under user membership
#
# Usage:
#
# Klass.member_descendants(1)
#
# Returns an ActiveRecord::Relation.
def member_descendants(user_id)
joins(:route).
joins("INNER JOIN routes r2 ON routes.path LIKE CONCAT(r2.path, '/%')
INNER JOIN members ON members.source_id = r2.source_id
AND members.source_type = r2.source_type").
where('members.user_id = ?', user_id)
end
# Builds a relation to find multiple objects that are nested under user
# membership. Includes the parent, as opposed to `#member_descendants`
# which only includes the descendants.
#
# Usage:
#
# Klass.member_self_and_descendants(1)
#
# Returns an ActiveRecord::Relation.
def member_self_and_descendants(user_id)
joins(:route).
joins("INNER JOIN routes r2 ON routes.path LIKE CONCAT(r2.path, '/%')
OR routes.path = r2.path
INNER JOIN members ON members.source_id = r2.source_id
AND members.source_type = r2.source_type").
where('members.user_id = ?', user_id)
end
# Returns all objects in a hierarchy, where any node in the hierarchy is
# under the user membership.
#
# Usage:
#
# Klass.member_hierarchy(1)
#
# Examples:
#
# Given the following group tree...
#
# _______group_1_______
# | |
# | |
# nested_group_1 nested_group_2
# | |
# | |
# nested_group_1_1 nested_group_2_1
#
#
# ... the following results are returned:
#
# * the user is a member of group 1
# => 'group_1',
# 'nested_group_1', nested_group_1_1',
# 'nested_group_2', 'nested_group_2_1'
#
# * the user is a member of nested_group_2
# => 'group1',
# 'nested_group_2', 'nested_group_2_1'
#
# * the user is a member of nested_group_2_1
# => 'group1',
# 'nested_group_2', 'nested_group_2_1'
#
# Returns an ActiveRecord::Relation.
def member_hierarchy(user_id)
paths = member_self_and_descendants(user_id).pluck('routes.path')
return none if paths.empty?
wheres = paths.map do |path|
"#{connection.quote(path)} = routes.path
OR
#{connection.quote(path)} LIKE CONCAT(routes.path, '/%')"
end
joins(:route).where(wheres.join(' OR '))
end
end end
def full_name def full_name
......
...@@ -3,7 +3,11 @@ module SelectForProjectAuthorization ...@@ -3,7 +3,11 @@ module SelectForProjectAuthorization
module ClassMethods module ClassMethods
def select_for_project_authorization def select_for_project_authorization
select("members.user_id, projects.id AS project_id, members.access_level") select("projects.id AS project_id, members.access_level")
end
def select_as_master_for_project_authorization
select(["projects.id AS project_id", "#{Gitlab::Access::MASTER} AS access_level"])
end end
end end
end end
...@@ -38,6 +38,10 @@ class Group < Namespace ...@@ -38,6 +38,10 @@ class Group < Namespace
after_save :update_two_factor_requirement after_save :update_two_factor_requirement
class << self class << self
def supports_nested_groups?
Gitlab::Database.postgresql?
end
# Searches for groups matching the given query. # Searches for groups matching the given query.
# #
# This method uses ILIKE on PostgreSQL and LIKE on MySQL. # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
...@@ -78,7 +82,7 @@ class Group < Namespace ...@@ -78,7 +82,7 @@ class Group < Namespace
if current_scope.joins_values.include?(:shared_projects) if current_scope.joins_values.include?(:shared_projects)
joins('INNER JOIN namespaces project_namespace ON project_namespace.id = projects.namespace_id') joins('INNER JOIN namespaces project_namespace ON project_namespace.id = projects.namespace_id')
.where('project_namespace.share_with_group_lock = ?', false) .where('project_namespace.share_with_group_lock = ?', false)
.select("members.user_id, projects.id AS project_id, LEAST(project_group_links.group_access, members.access_level) AS access_level") .select("projects.id AS project_id, LEAST(project_group_links.group_access, members.access_level) AS access_level")
else else
super super
end end
......
...@@ -107,7 +107,7 @@ class Milestone < ActiveRecord::Base ...@@ -107,7 +107,7 @@ class Milestone < ActiveRecord::Base
end end
def participants def participants
User.joins(assigned_issues: :milestone).where("milestones.id = ?", id) User.joins(assigned_issues: :milestone).where("milestones.id = ?", id).uniq
end end
def self.sort(method) def self.sort(method)
......
...@@ -176,26 +176,20 @@ class Namespace < ActiveRecord::Base ...@@ -176,26 +176,20 @@ class Namespace < ActiveRecord::Base
projects.with_shared_runners.any? projects.with_shared_runners.any?
end end
# Scopes the model on ancestors of the record # Returns all the ancestors of the current namespaces.
def ancestors def ancestors
if parent_id return self.class.none unless parent_id
path = route ? route.path : full_path
paths = []
until path.blank? Gitlab::GroupHierarchy.
path = path.rpartition('/').first new(self.class.where(id: parent_id)).
paths << path base_and_ancestors
end
self.class.joins(:route).where('routes.path IN (?)', paths).reorder('routes.path ASC')
else
self.class.none
end
end end
# Scopes the model on direct and indirect children of the record # Returns all the descendants of the current namespace.
def descendants def descendants
self.class.joins(:route).merge(Route.inside_path(route.path)).reorder('routes.path ASC') Gitlab::GroupHierarchy.
new(self.class.where(parent_id: id)).
base_and_descendants
end end
def user_ids_for_project_authorizations def user_ids_for_project_authorizations
......
...@@ -271,6 +271,7 @@ class Project < ActiveRecord::Base ...@@ -271,6 +271,7 @@ class Project < ActiveRecord::Base
scope :with_builds_enabled, -> { with_feature_enabled(:builds) } scope :with_builds_enabled, -> { with_feature_enabled(:builds) }
scope :with_issues_enabled, -> { with_feature_enabled(:issues) } scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) }
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 } enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
...@@ -873,10 +874,8 @@ class Project < ActiveRecord::Base ...@@ -873,10 +874,8 @@ class Project < ActiveRecord::Base
url_to_repo url_to_repo
end end
def http_url_to_repo(user = nil) def http_url_to_repo
credentials = Gitlab::UrlSanitizer.http_credentials_for_user(user) "#{web_url}.git"
Gitlab::UrlSanitizer.new("#{web_url}.git", credentials: credentials).full_url
end end
def user_can_push_to_empty_repo?(user) def user_can_push_to_empty_repo?(user)
......
...@@ -6,6 +6,12 @@ class ProjectAuthorization < ActiveRecord::Base ...@@ -6,6 +6,12 @@ class ProjectAuthorization < ActiveRecord::Base
validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
validates :user, uniqueness: { scope: [:project, :access_level] }, presence: true validates :user, uniqueness: { scope: [:project, :access_level] }, presence: true
def self.select_from_union(union)
select(['project_id', 'MAX(access_level) AS access_level']).
from("(#{union.to_sql}) #{ProjectAuthorization.table_name}").
group(:project_id)
end
def self.insert_authorizations(rows, per_batch = 1000) def self.insert_authorizations(rows, per_batch = 1000)
rows.each_slice(per_batch) do |slice| rows.each_slice(per_batch) do |slice|
tuples = slice.map do |tuple| tuples = slice.map do |tuple|
......
...@@ -239,17 +239,26 @@ class JiraService < IssueTrackerService ...@@ -239,17 +239,26 @@ class JiraService < IssueTrackerService
return unless client_url.present? return unless client_url.present?
jira_request do jira_request do
if issue.comments.build.save!(body: message) remote_link = find_remote_link(issue, remote_link_props[:object][:url])
remote_link = issue.remotelink.build if remote_link
remote_link.save!(remote_link_props) remote_link.save!(remote_link_props)
result_message = "#{self.class.name} SUCCESS: Successfully posted to #{client_url}." elsif issue.comments.build.save!(body: message)
new_remote_link = issue.remotelink.build
new_remote_link.save!(remote_link_props)
end end
result_message = "#{self.class.name} SUCCESS: Successfully posted to #{client_url}."
Rails.logger.info(result_message) Rails.logger.info(result_message)
result_message result_message
end end
end end
def find_remote_link(issue, url)
links = jira_request { issue.remotelink.all }
links.find { |link| link.object["url"] == url }
end
def build_remote_link_props(url:, title:, resolved: false) def build_remote_link_props(url:, title:, resolved: false)
status = { status = {
resolved: resolved resolved: resolved
......
...@@ -42,11 +42,8 @@ class ProjectWiki ...@@ -42,11 +42,8 @@ class ProjectWiki
url_to_repo url_to_repo
end end
def http_url_to_repo(user = nil) def http_url_to_repo
url = "#{Gitlab.config.gitlab.url}/#{path_with_namespace}.git" "#{Gitlab.config.gitlab.url}/#{path_with_namespace}.git"
credentials = Gitlab::UrlSanitizer.http_credentials_for_user(user)
Gitlab::UrlSanitizer.new(url, credentials: credentials).full_url
end end
def wiki_base_path def wiki_base_path
......
...@@ -10,9 +10,12 @@ class User < ActiveRecord::Base ...@@ -10,9 +10,12 @@ class User < ActiveRecord::Base
include Sortable include Sortable
include CaseSensitivity include CaseSensitivity
include TokenAuthenticatable include TokenAuthenticatable
include IgnorableColumn
DEFAULT_NOTIFICATION_LEVEL = :participating DEFAULT_NOTIFICATION_LEVEL = :participating
ignore_column :authorized_projects_populated
add_authentication_token_field :authentication_token add_authentication_token_field :authentication_token
add_authentication_token_field :incoming_email_token add_authentication_token_field :incoming_email_token
add_authentication_token_field :rss_token add_authentication_token_field :rss_token
...@@ -218,7 +221,6 @@ class User < ActiveRecord::Base ...@@ -218,7 +221,6 @@ class User < ActiveRecord::Base
scope :blocked, -> { with_states(:blocked, :ldap_blocked) } scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
scope :external, -> { where(external: true) } scope :external, -> { where(external: true) }
scope :active, -> { with_state(:active).non_internal } scope :active, -> { with_state(:active).non_internal }
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') } scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) } scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('last_sign_in_at', 'DESC')) } scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('last_sign_in_at', 'DESC')) }
...@@ -510,23 +512,16 @@ class User < ActiveRecord::Base ...@@ -510,23 +512,16 @@ class User < ActiveRecord::Base
Group.where("namespaces.id IN (#{union.to_sql})") Group.where("namespaces.id IN (#{union.to_sql})")
end end
def nested_groups # Returns a relation of groups the user has access to, including their parent
Group.member_descendants(id) # and child groups (recursively).
end
def all_expanded_groups def all_expanded_groups
Group.member_hierarchy(id) Gitlab::GroupHierarchy.new(groups).all_groups
end end
def expanded_groups_requiring_two_factor_authentication def expanded_groups_requiring_two_factor_authentication
all_expanded_groups.where(require_two_factor_authentication: true) all_expanded_groups.where(require_two_factor_authentication: true)
end end
def nested_groups_projects
Project.joins(:namespace).where('namespaces.parent_id IS NOT NULL').
member_descendants(id)
end
def refresh_authorized_projects def refresh_authorized_projects
Users::RefreshAuthorizedProjectsService.new(self).execute Users::RefreshAuthorizedProjectsService.new(self).execute
end end
...@@ -535,18 +530,15 @@ class User < ActiveRecord::Base ...@@ -535,18 +530,15 @@ class User < ActiveRecord::Base
project_authorizations.where(project_id: project_ids).delete_all project_authorizations.where(project_id: project_ids).delete_all
end end
def set_authorized_projects_column
unless authorized_projects_populated
update_column(:authorized_projects_populated, true)
end
end
def authorized_projects(min_access_level = nil) def authorized_projects(min_access_level = nil)
refresh_authorized_projects unless authorized_projects_populated # We're overriding an association, so explicitly call super with no
# arguments or it would be passed as `force_reload` to the association
# We're overriding an association, so explicitly call super with no arguments or it would be passed as `force_reload` to the association
projects = super() projects = super()
projects = projects.where('project_authorizations.access_level >= ?', min_access_level) if min_access_level
if min_access_level
projects = projects.
where('project_authorizations.access_level >= ?', min_access_level)
end
projects projects
end end
......
...@@ -39,6 +39,7 @@ class MergeRequestEntity < IssuableEntity ...@@ -39,6 +39,7 @@ class MergeRequestEntity < IssuableEntity
expose :commits_count expose :commits_count
expose :cannot_be_merged?, as: :has_conflicts expose :cannot_be_merged?, as: :has_conflicts
expose :can_be_merged?, as: :can_be_merged expose :can_be_merged?, as: :can_be_merged
expose :remove_source_branch?, as: :remove_source_branch
expose :project_archived do |merge_request| expose :project_archived do |merge_request|
merge_request.project.archived? merge_request.project.archived?
......
...@@ -61,6 +61,16 @@ module Ci ...@@ -61,6 +61,16 @@ module Ci
private private
def update_merge_requests_head_pipeline
merge_requests = MergeRequest.where(source_branch: @pipeline.ref, source_project: @pipeline.project)
merge_requests = merge_requests.select do |mr|
mr.diff_head_sha == @pipeline.sha
end
MergeRequest.where(id: merge_requests).update_all(head_pipeline_id: @pipeline.id)
end
def skip_ci? def skip_ci?
return false unless pipeline.git_commit_message return false unless pipeline.git_commit_message
pipeline.git_commit_message =~ /\[(ci[ _-]skip|skip[ _-]ci)\]/i pipeline.git_commit_message =~ /\[(ci[ _-]skip|skip[ _-]ci)\]/i
...@@ -118,11 +128,6 @@ module Ci ...@@ -118,11 +128,6 @@ module Ci
origin_sha && origin_sha != Gitlab::Git::BLANK_SHA origin_sha && origin_sha != Gitlab::Git::BLANK_SHA
end end
def update_merge_requests_head_pipeline
MergeRequest.where(source_branch: @pipeline.ref, source_project: @pipeline.project).
update_all(head_pipeline_id: @pipeline.id)
end
def error(message, save: false) def error(message, save: false)
pipeline.errors.add(:base, message) pipeline.errors.add(:base, message)
pipeline.drop if save pipeline.drop if save
......
...@@ -11,7 +11,9 @@ module MergeRequests ...@@ -11,7 +11,9 @@ module MergeRequests
merge_request = MergeRequest.new merge_request = MergeRequest.new
merge_request.source_project = source_project merge_request.source_project = source_project
merge_request.source_branch = params[:source_branch]
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)
merge_request.head_pipeline = head_pipeline_for(merge_request)
create(merge_request) create(merge_request)
end end
...@@ -22,5 +24,21 @@ module MergeRequests ...@@ -22,5 +24,21 @@ module MergeRequests
todo_service.new_merge_request(issuable, current_user) todo_service.new_merge_request(issuable, current_user)
issuable.cache_merge_request_closes_issues!(current_user) issuable.cache_merge_request_closes_issues!(current_user)
end end
private
def head_pipeline_for(merge_request)
return unless merge_request.source_project
sha = merge_request.source_branch_head&.id
return unless sha
pipelines =
Ci::Pipeline.where(ref: merge_request.source_branch, project_id: merge_request.source_project.id, sha: sha).
order(id: :desc)
pipelines.first
end
end end
end end
...@@ -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
......
...@@ -73,12 +73,11 @@ module Users ...@@ -73,12 +73,11 @@ module Users
# remove - The IDs of the authorization rows to remove. # remove - The IDs of the authorization rows to remove.
# add - Rows to insert in the form `[user id, project id, access level]` # add - Rows to insert in the form `[user id, project id, access level]`
def update_authorizations(remove = [], add = []) def update_authorizations(remove = [], add = [])
return if remove.empty? && add.empty? && user.authorized_projects_populated return if remove.empty? && add.empty?
User.transaction do User.transaction do
user.remove_project_authorizations(remove) unless remove.empty? user.remove_project_authorizations(remove) unless remove.empty?
ProjectAuthorization.insert_authorizations(add) unless add.empty? ProjectAuthorization.insert_authorizations(add) unless add.empty?
user.set_authorized_projects_column
end end
# Since we batch insert authorization rows, Rails' associations may get # Since we batch insert authorization rows, Rails' associations may get
...@@ -101,38 +100,13 @@ module Users ...@@ -101,38 +100,13 @@ module Users
end end
def fresh_authorizations def fresh_authorizations
ProjectAuthorization. klass = if Group.supports_nested_groups?
unscoped. Gitlab::ProjectAuthorizations::WithNestedGroups
select('project_id, MAX(access_level) AS access_level'). else
from("(#{project_authorizations_union.to_sql}) #{ProjectAuthorization.table_name}"). Gitlab::ProjectAuthorizations::WithoutNestedGroups
group(:project_id) end
end
private
# Returns a union query of projects that the user is authorized to access
def project_authorizations_union
relations = [
# Personal projects
user.personal_projects.select("#{user.id} AS user_id, projects.id AS project_id, #{Gitlab::Access::MASTER} AS access_level"),
# Projects the user is a member of
user.projects.select_for_project_authorization,
# Projects of groups the user is a member of
user.groups_projects.select_for_project_authorization,
# Projects of subgroups of groups the user is a member of
user.nested_groups_projects.select_for_project_authorization,
# Projects shared with groups the user is a member of
user.groups.joins(:shared_projects).select_for_project_authorization,
# Projects shared with subgroups of groups the user is a member of
user.nested_groups.joins(:shared_projects).select_for_project_authorization
]
Gitlab::SQL::Union.new(relations) klass.new(user).calculate
end end
end end
end end
...@@ -6,16 +6,21 @@ ...@@ -6,16 +6,21 @@
# Values are checked for formatting and exclusion from a list of illegal path # Values are checked for formatting and exclusion from a list of illegal path
# names. # names.
class DynamicPathValidator < ActiveModel::EachValidator class DynamicPathValidator < ActiveModel::EachValidator
extend Gitlab::Git::EncodingHelper
class << self class << self
def valid_user_path?(path) def valid_user_path?(path)
encode!(path)
"#{path}/" =~ Gitlab::PathRegex.root_namespace_path_regex "#{path}/" =~ Gitlab::PathRegex.root_namespace_path_regex
end end
def valid_group_path?(path) def valid_group_path?(path)
encode!(path)
"#{path}/" =~ Gitlab::PathRegex.full_namespace_path_regex "#{path}/" =~ Gitlab::PathRegex.full_namespace_path_regex
end end
def valid_project_path?(path) def valid_project_path?(path)
encode!(path)
"#{path}/" =~ Gitlab::PathRegex.full_project_path_regex "#{path}/" =~ Gitlab::PathRegex.full_project_path_regex
end end
end end
......
...@@ -31,3 +31,8 @@ ...@@ -31,3 +31,8 @@
%h1 #{number_to_human_size(disk[:bytes_used])} / #{number_to_human_size(disk[:bytes_total])} %h1 #{number_to_human_size(disk[:bytes_used])} / #{number_to_human_size(disk[:bytes_total])}
%p= disk[:disk_name] %p= disk[:disk_name]
%p= disk[:mount_path] %p= disk[:mount_path]
.col-sm-4
.light-well
%h4 Uptime
.data
%h1= time_ago_with_tooltip(Rails.application.config.booted_at)
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
.nav-controls .nav-controls
= link_to params.merge(rss_url_options), class: 'btn has-tooltip', title: 'Subscribe' do = link_to params.merge(rss_url_options), class: 'btn has-tooltip', title: 'Subscribe' do
= icon('rss') = icon('rss')
= render 'shared/new_project_item_select', path: 'issues/new', label: "New issue" = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues'
= render 'shared/issuable/filter', type: :issues = render 'shared/issuable/filter', type: :issues
= render 'shared/issues' = render 'shared/issues'
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
.top-area .top-area
= render 'shared/issuable/nav', type: :merge_requests = render 'shared/issuable/nav', type: :merge_requests
.nav-controls .nav-controls
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request" = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", with_feature_enabled: 'merge_requests'
= render 'shared/issuable/filter', type: :merge_requests = render 'shared/issuable/filter', type: :merge_requests
= render 'shared/merge_requests' = render 'shared/merge_requests'
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
= nav_link(page: group_path(@group)) do = nav_link(page: group_path(@group)) do
= link_to group_path(@group) do = link_to group_path(@group) do
Projects Projects
= nav_link(page: subgroups_group_path(@group)) do - if Group.supports_nested_groups?
= link_to subgroups_group_path(@group) do = nav_link(page: subgroups_group_path(@group)) do
Subgroups = link_to subgroups_group_path(@group) do
Subgroups
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
.fade-right .fade-right
= icon('angle-right') = icon('angle-right')
%ul.nav-links.scrolling-tabs %ul.nav-links.scrolling-tabs
= nav_link(controller: %w(dashboard admin projects users groups builds runners), html_options: {class: 'home'}) do = nav_link(controller: %w(dashboard admin projects users groups builds runners cohorts), html_options: {class: 'home'}) do
= link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do
%span %span
Overview Overview
......
...@@ -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", 'max-select' => 1, dropdown: { header: 'Assignee' } }, %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", 'max-select' => 1, dropdown: { header: 'Assignee' } },
":data-issuable-id" => "issue.id", ":data-issuable-id" => "issue.id",
......
...@@ -68,15 +68,8 @@ ...@@ -68,15 +68,8 @@
- elsif @build.runner - elsif @build.runner
\##{@build.runner.id} \##{@build.runner.id}
.btn-group.btn-group-justified{ role: :group } .btn-group.btn-group-justified{ role: :group }
- if @build.has_trace?
= link_to 'Raw', raw_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default'
- if @build.active? - if @build.active?
= link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post = link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post
- if can?(current_user, :update_build, @project) && @build.erasable?
= link_to erase_namespace_project_build_path(@project.namespace, @project, @build),
class: "btn btn-sm btn-default", method: :post,
data: { confirm: "Are you sure you want to erase this build?" } do
Erase
- if @build.trigger_request - if @build.trigger_request
.build-widget .build-widget
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
- if @build.stuck? - if @build.stuck?
- unless @build.any_runners_online? - unless @build.any_runners_online?
.bs-callout.bs-callout-warning .bs-callout.bs-callout-warning.js-build-stuck
%p %p
- if no_runners_for_project?(@build.project) - if no_runners_for_project?(@build.project)
This job is stuck, because the project doesn't have any runners online assigned to it. This job is stuck, because the project doesn't have any runners online assigned to it.
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
Runners page Runners page
- if @build.starts_environment? - if @build.starts_environment?
.prepend-top-default .prepend-top-default.js-environment-container
.environment-information .environment-information
- if @build.outdated_deployment? - if @build.outdated_deployment?
= ci_icon_for_status('success_with_warnings') = ci_icon_for_status('success_with_warnings')
...@@ -47,39 +47,51 @@ ...@@ -47,39 +47,51 @@
- if environment.try(:last_deployment) - if environment.try(:last_deployment)
and will overwrite the #{deployment_link(environment.last_deployment, text: 'latest deployment')} and will overwrite the #{deployment_link(environment.last_deployment, text: 'latest deployment')}
.prepend-top-default .prepend-top-default.js-build-erased
- if @build.erased? - if @build.erased?
.erased.alert.alert-warning .erased.alert.alert-warning
- if @build.erased_by_user? - if @build.erased_by_user?
Job has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)} Job has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)}
- else - else
Job has been erased #{time_ago_with_tooltip(@build.erased_at)} Job has been erased #{time_ago_with_tooltip(@build.erased_at)}
- else
#js-build-scroll.scroll-controls .prepend-top-default
.scroll-step .build-trace-container#build-trace
%a.scroll-link.scroll-top{ href: '#up-build-trace', id: 'scroll-top', title: 'Scroll to top' } .top-bar.sticky
= custom_icon('scroll_up')
= custom_icon('scroll_up_hover_active')
%a.scroll-link.scroll-bottom{ href: '#down-build-trace', id: 'scroll-bottom', title: 'Scroll to bottom' }
= custom_icon('scroll_down')
= custom_icon('scroll_down_hover_active')
- if @build.active?
.autoscroll-container
%span.status-message#autoscroll-status{ data: { state: 'disabled' } }
%span.status-text Autoscroll active
%i.status-icon
= custom_icon('scroll_down_hover_active')
#up-build-trace
%pre.build-trace#build-trace
.js-truncated-info.truncated-info.hidden< .js-truncated-info.truncated-info.hidden<
Showing last Showing last
%span.js-truncated-info-size.truncated-info-size>< %span.js-truncated-info-size.truncated-info-size><
KiB of log - KiB of log -
%a.js-raw-link.raw-link{ :href => raw_namespace_project_build_path(@project.namespace, @project, @build) }>< Complete Raw %a.js-raw-link.raw-link{ href: raw_namespace_project_build_path(@project.namespace, @project, @build) }>< Complete Raw
%code.bash.js-build-output .controllers
.build-loader-animation.js-build-refresh - if @build.has_trace?
= link_to raw_namespace_project_build_path(@project.namespace, @project, @build),
title: 'Open raw trace',
data: { placement: 'top', container: 'body' },
class: 'js-raw-link-controller has-tooltip' do
= icon('download')
- if can?(current_user, :update_build, @project) && @build.erasable?
= link_to erase_namespace_project_build_path(@project.namespace, @project, @build),
method: :post,
data: { confirm: 'Are you sure you want to erase this build?', placement: 'top', container: 'body' },
title: 'Erase Build',
class: 'has-tooltip js-erase-link' do
= icon('trash')
#down-build-trace %button.js-scroll-up.btn-scroll.btn-transparent.btn-blank.has-tooltip{ type: 'button',
disabled: true,
title: 'Scroll Up',
data: { placement: 'top', container: 'body'} }
= custom_icon('scroll_up')
%button.js-scroll-down.btn-scroll.btn-transparent.btn-blank.has-tooltip{ type: 'button',
disabled: true,
title: 'Scroll Down',
data: { placement: 'top', container: 'body'} }
= custom_icon('scroll_down')
.bash.sticky.js-scroll-container
%code.js-build-output
.build-loader-animation.js-build-refresh
= render "sidebar" = render "sidebar"
......
...@@ -4,7 +4,8 @@ ...@@ -4,7 +4,8 @@
= pipeline_schedule.description = pipeline_schedule.description
%td.branch-name-cell %td.branch-name-cell
= icon('code-fork') = icon('code-fork')
= link_to pipeline_schedule.ref, project_ref_path(@project, pipeline_schedule.ref), class: "ref-name" - if pipeline_schedule.ref
= link_to pipeline_schedule.ref, project_ref_path(@project, pipeline_schedule.ref), class: "ref-name"
%td %td
- if pipeline_schedule.last_pipeline - if pipeline_schedule.last_pipeline
.status-icon-container{ class: "ci-status-icon-#{pipeline_schedule.last_pipeline.status}" } .status-icon-container{ class: "ci-status-icon-#{pipeline_schedule.last_pipeline.status}" }
......
- 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?
......
...@@ -2,13 +2,13 @@ ...@@ -2,13 +2,13 @@
%ul %ul
%li %li
Total: Total:
%strong= pluralize @project.builds.count(:all), 'build' %strong= pluralize @project.builds.count(:all), 'job'
%li %li
Successful: Successful:
%strong= pluralize @project.builds.success.count(:all), 'build' %strong= pluralize @project.builds.success.count(:all), 'job'
%li %li
Failed: Failed:
%strong= pluralize @project.builds.failed.count(:all), 'build' %strong= pluralize @project.builds.failed.count(:all), 'job'
%li %li
Success ratio: Success ratio:
%strong %strong
......
...@@ -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') }
......
- if @projects.any? - if @projects.any?
.project-item-select-holder .project-item-select-holder
= project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at' } = project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at' }, with_feature_enabled: local_assigns[:with_feature_enabled]
%a.btn.btn-new.new-project-item-select-button %a.btn.btn-new.new-project-item-select-button
= local_assigns[:label] = local_assigns[:label]
= icon('caret-down') = icon('caret-down')
......
<svg width="16" height="33" class="gitlab-icon-scroll-down" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg"> <svg width="12" height="16" viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg">
<path fill="#ffffff" d="M1.385 5.534v12.47a4.145 4.145 0 0 0 4.144 4.15h4.942a4.151 4.151 0 0 0 4.144-4.15V5.535a4.145 4.145 0 0 0-4.144-4.15H5.53a4.151 4.151 0 0 0-4.144 4.15zM8.88 30.27v-4.351a.688.688 0 0 0-.69-.688.687.687 0 0 0-.69.688v4.334l-1.345-1.346a.69.69 0 0 0-.976.976l2.526 2.526a.685.685 0 0 0 .494.2.685.685 0 0 0 .493-.2l2.526-2.526a.69.69 0 1 0-.976-.976L8.88 30.27zM0 5.534A5.536 5.536 0 0 1 5.529 0h4.942A5.53 5.53 0 0 1 16 5.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 18.005V5.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0V6.544z" fill-rule="evenodd"/> <path class="first-triangle" d="M1.048 14.155a.508.508 0 0 0-.32.105c-.091.07-.136.154-.136.25v.71c0 .095.045.178.135.249.09.07.197.105.321.105h10.043c.124 0 .23-.035.321-.105.09-.07.136-.154.136-.25v-.71c0-.095-.045-.178-.136-.249a.508.508 0 0 0-.32-.105"/>
<path class="second-triangle" d="M.687 8.027c-.09-.087-.122-.16-.093-.22.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 12.91a.458.458 0 0 1-.136.089h-.37a.626.626 0 0 1-.136-.09"/>
<path class="third-triangle" d="M.687 1.027C.597.94.565.867.594.807c.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 5.91A.458.458 0 0 1 6.257 6h-.37a.626.626 0 0 1-.136-.09"/>
</svg> </svg>
<svg width="16" height="33" class="gitlab-icon-scroll-down-hover" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg">
<path fill="#ffffff" d="M8.88 30.27v-4.351a.688.688 0 0 0-.69-.688.687.687 0 0 0-.69.688v4.334l-1.345-1.346a.69.69 0 0 0-.976.976l2.526 2.526a.685.685 0 0 0 .494.2.685.685 0 0 0 .493-.2l2.526-2.526a.69.69 0 1 0-.976-.976L8.88 30.27zM0 5.534A5.536 5.536 0 0 1 5.529 0h4.942A5.53 5.53 0 0 1 16 5.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 18.005V5.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0V6.544z" fill-rule="evenodd"/>
</svg>
<svg width="16" height="33" class="gitlab-icon-scroll-up" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg"> <svg width="12" height="16" viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg"><path d="M1.048 1.845a.508.508 0 0 1-.32-.105c-.091-.07-.136-.154-.136-.25V.78c0-.095.045-.178.135-.249a.508.508 0 0 1 .321-.105h10.043c.124 0 .23.035.321.105.09.07.136.154.136.25v.71c0 .095-.045.178-.136.249a.508.508 0 0 1-.32.105"/><path d="M.687 7.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 3.09A.458.458 0 0 0 6.257 3h-.37a.626.626 0 0 0-.136.09"/><path d="M.687 14.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 10.09A.458.458 0 0 0 6.257 10h-.37a.626.626 0 0 0-.136.09"/></svg>
<path fill="#ffffff" d="M1.385 14.534v12.47a4.145 4.145 0 0 0 4.144 4.15h4.942a4.151 4.151 0 0 0 4.144-4.15v-12.47a4.145 4.145 0 0 0-4.144-4.15H5.53a4.151 4.151 0 0 0-4.144 4.15zM8.88 2.609V6.96a.688.688 0 0 1-.69.688.687.687 0 0 1-.69-.688V2.627L6.155 3.972a.69.69 0 0 1-.976-.976L7.705.47a.685.685 0 0 1 .494-.2.685.685 0 0 1 .493.2l2.526 2.526a.69.69 0 1 1-.976.976L8.88 2.609zM0 14.534A5.536 5.536 0 0 1 5.529 9h4.942A5.53 5.53 0 0 1 16 14.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 27.005V14.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0v-2.143z" fill-rule="evenodd"/>
</svg>
<svg width="16" height="33" class="gitlab-icon-scroll-up-hover" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg">
<path fill="#ffffff" d="M8.88 2.646l1.362 1.362a.69.69 0 0 0 .976-.976L8.692.507A.685.685 0 0 0 8.2.306a.685.685 0 0 0-.494.2L5.179 3.033a.69.69 0 1 0 .976.976L7.5 2.663v4.179c0 .38.306.688.69.688.381 0 .69-.306.69-.688V2.646zM0 14.534A5.536 5.536 0 0 1 5.529 9h4.942A5.53 5.53 0 0 1 16 14.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 27.005V14.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0v-2.143z" fill-rule="evenodd"/>
</svg>
...@@ -157,7 +157,8 @@ ...@@ -157,7 +157,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 } }
......
...@@ -5,3 +5,13 @@ ...@@ -5,3 +5,13 @@
-# This check is duplicated below, to avoid conflicts with EE. -# This check is duplicated below, to avoid conflicts with EE.
- return unless issuable.can_remove_source_branch?(current_user) - return unless issuable.can_remove_source_branch?(current_user)
.form-group
.col-sm-10.col-sm-offset-2
- if issuable.can_remove_source_branch?(current_user)
.checkbox
- initial_checkbox_value = issuable.merge_params.key?('force_remove_source_branch') ? issuable.force_remove_source_branch? : true
= label_tag 'merge_request[force_remove_source_branch]' do
= hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil
= check_box_tag 'merge_request[force_remove_source_branch]', '1', initial_checkbox_value
Remove source branch when merge request is accepted.
...@@ -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: 'New issue'/'New merge request' dropdowns should show only projects with issues/merge requests feature enabled
merge_request: 19107
author: blackst0ne
---
title: Replace 'starred_projects.feature' spinach test with an rspec analog
merge_request: 11752
author: blackst0ne
---
title: Don’t create comment on JIRA if it already exists for the entity
merge_request:
author:
---
title: Add performance deltas between app deployments on Merge Request widget
merge_request: 11730
author:
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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