Commit 6c72d8a2 authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'upstream/master' into rename-builds-controller

* upstream/master: (116 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 b7c3c55d 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/
......@@ -52,7 +52,7 @@ stages:
.use-pg: &use-pg
services:
- postgres:9.1
- postgres:9.2
- redis:alpine
.use-mysql: &use-mysql
......@@ -487,25 +487,6 @@ lint:javascript:report:
paths:
- eslint-report.html
# Trigger docs build
# https://gitlab.com/gitlab-com/doc-gitlab-com/blob/master/README.md#deployment-process
trigger_docs:
stage: post-test
image: "alpine"
<<: *dedicated-runner
before_script:
- apk update && apk add curl
variables:
GIT_STRATEGY: "none"
cache: {}
artifacts: {}
script:
- "HTTP_STATUS=$(curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master -F variables[PROJECT]=${CI_PROJECT_NAME} --silent --output curl.log --write-out '%{http_code}' https://gitlab.com/api/v3/projects/1794617/trigger/builds)"
- if [ "${HTTP_STATUS}" -ne "201" ]; then echo "Error ${HTTP_STATUS}"; cat curl.log; echo; exit 1; fi
only:
- master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
pages:
before_script: []
stage: pages
......
......@@ -11,11 +11,11 @@ linters:
# !global, !important, and !optional flags.
BangFormat:
enabled: false
# Whether or not to prefer `border: 0` over `border: none`.
BorderZero:
enabled: false
# Reports when you define a rule set using a selector with chained classes
# (a.k.a. adjoining classes).
ChainedClasses:
......@@ -25,13 +25,13 @@ linters:
# (e.g. `color: green` is a color keyword)
ColorKeyword:
enabled: false
# Prefer color literals (keywords or hexadecimal codes) to be used only in
# variable declarations. They should be referred to via variables everywhere
# else.
ColorVariable:
enabled: true
# Which form of comments to prefer in CSS.
Comment:
enabled: false
......@@ -39,7 +39,7 @@ linters:
# Reports @debug statements (which you probably left behind accidentally).
DebugStatement:
enabled: false
# Rule sets should be ordered as follows:
# - @extend declarations
# - @include declarations without inner @content
......@@ -54,19 +54,19 @@ linters:
# more information.
DisableLinterReason:
enabled: true
# Reports when you define the same property twice in a single rule set.
DuplicateProperty:
enabled: false
enabled: true
# Separate rule, function, and mixin declarations with empty lines.
EmptyLineBetweenBlocks:
enabled: true
# Reports when you have an empty rule set.
EmptyRule:
enabled: true
# Reports when you have an @extend directive.
ExtendDirective:
enabled: false
......@@ -75,49 +75,49 @@ linters:
# when adding lines to the file, since SCM systems such as git won't
# think that you touched the last line.
FinalNewline:
enabled: false
enabled: true
# HEX colors should use three-character values where possible.
HexLength:
enabled: false
# HEX color values should use lower-case colors to differentiate between
# letters and numbers, e.g. `#E3E3E3` vs. `#e3e3e3`.
HexNotation:
enabled: true
# Avoid using ID selectors.
IdSelector:
enabled: false
# The basenames of @imported SCSS partials should not begin with an
# underscore and should not include the filename extension.
ImportPath:
enabled: false
# Avoid using !important in properties. It is usually indicative of a
# misunderstanding of CSS specificity and can lead to brittle code.
ImportantRule:
enabled: false
# Indentation should always be done in increments of 2 spaces.
Indentation:
enabled: true
width: 2
# Don't write leading zeros for numeric values with a decimal point.
LeadingZero:
enabled: false
# Reports when you define the same selector twice in a single sheet.
MergeableSelector:
enabled: false
# Functions, mixins, variables, and placeholders should be declared
# with all lowercase letters and hyphens instead of underscores.
NameFormat:
enabled: false
# Avoid nesting selectors too deeply.
NestingDepth:
enabled: false
......@@ -129,12 +129,12 @@ linters:
# Sort properties in a strict order.
PropertySortOrder:
enabled: false
# Reports when you use an unknown or disabled CSS property
# (ignoring vendor-prefixed properties).
PropertySpelling:
enabled: false
# Configure which units are allowed for property values.
PropertyUnits:
enabled: false
......@@ -144,25 +144,25 @@ linters:
# be declared with one colon.
PseudoElement:
enabled: true
# Avoid qualifying elements in selectors (also known as "tag-qualifying").
QualifyingElement:
enabled: false
# Don't write selectors with a depth of applicability greater than 3.
SelectorDepth:
enabled: false
# Selectors should always use hyphenated-lowercase, rather than camelCase or
# snake_case.
SelectorFormat:
enabled: false
convention: hyphenated_lowercase
# Prefer the shortest shorthand form possible for properties that support it.
Shorthand:
enabled: true
# Each property should have its own line, except in the special case of
# single line rulesets.
SingleLinePerProperty:
......@@ -173,11 +173,11 @@ linters:
# individual selector occupy a single line.
SingleLinePerSelector:
enabled: true
# Commas in lists should be followed by a space.
SpaceAfterComma:
enabled: false
# Properties should be formatted with a single space separating the colon
# from the property's value.
SpaceAfterPropertyColon:
......@@ -197,12 +197,12 @@ linters:
# colon.
SpaceAfterVariableName:
enabled: false
# Operators should be formatted with a single space on both sides of an
# infix operator.
SpaceAroundOperator:
enabled: true
# Opening braces should be preceded by a single space.
SpaceBeforeBrace:
enabled: true
......@@ -210,7 +210,7 @@ linters:
# Parentheses should not be padded with spaces.
SpaceBetweenParens:
enabled: false
# Enforces that string literals should be written with a consistent form
# of quotes (single or double).
StringQuotes:
......@@ -241,7 +241,7 @@ linters:
# be unnecessary.
UnnecessaryParentReference:
enabled: false
# URLs should be valid and not contain protocols or domain names.
UrlFormat:
enabled: true
......
......@@ -341,7 +341,7 @@ GEM
grape-entity (0.6.0)
activesupport
multi_json (>= 1.3.2)
grpc (1.3.4)
grpc (1.2.5)
google-protobuf (~> 3.1)
googleauth (~> 0.5.1)
haml (4.0.7)
......@@ -499,11 +499,10 @@ GEM
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.0)
omniauth-google-oauth2 (0.4.1)
addressable (~> 2.3)
jwt (~> 1.0)
jwt (~> 1.5.2)
multi_json (~> 1.3)
omniauth (>= 1.1.1)
omniauth-oauth2 (~> 1.3.1)
omniauth-oauth2 (>= 1.3.1)
omniauth-kerberos (0.3.0)
omniauth-multipassword
timfel-krb5-auth (~> 0.8)
......@@ -1060,4 +1059,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
1.14.6
1.15.0
......@@ -111,7 +111,7 @@ export default class BlobViewer {
BlobViewer.loadViewer(newViewer)
.then((viewer) => {
$(viewer).syntaxHighlight();
$(viewer).renderGFM();
this.$fileHolder.trigger('highlight:line');
gl.utils.handleLocationHash();
......
......@@ -70,6 +70,7 @@ $(() => {
gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId);
this.filterManager = new FilteredSearchBoards(Store.filter, true);
this.filterManager.setup();
// Listen for updateTokens event
eventHub.$on('updateTokens', this.updateTokens);
......
......@@ -13,6 +13,7 @@ export default {
FilteredSearchContainer.container = this.$el;
this.filteredSearch = new FilteredSearchBoards(this.store);
this.filteredSearch.setup();
this.filteredSearch.removeTokens();
this.filteredSearch.handleInputPlaceholder();
this.filteredSearch.toggleClearSearchButton();
......
......@@ -2,7 +2,7 @@
import FilteredSearchContainer from '../filtered_search/container';
export default class FilteredSearchBoards extends gl.FilteredSearchManager {
constructor(store, updateUrl = false) {
constructor(store, updateUrl = false, cantEdit = []) {
super('boards');
this.store = store;
......@@ -11,6 +11,8 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
// Issue boards is slightly different, we handle all the requests async
// instead or reloading the page, we just re-fire the list ajax requests
this.isHandledAsync = true;
this.cantEdit = cantEdit;
}
updateObject(path) {
......@@ -40,4 +42,8 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
// Get the placeholder back if search is empty
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';
case 'projects:merge_requests:index':
case 'projects:issues:index':
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();
new gl.IssuableBulkActions({
......
......@@ -2,9 +2,9 @@ import './dropdown_hint';
import './dropdown_non_user';
import './dropdown_user';
import './dropdown_utils';
import './filtered_search_token_keys';
import './filtered_search_dropdown_manager';
import './filtered_search_dropdown';
import './filtered_search_manager';
import './filtered_search_token_keys';
import './filtered_search_tokenizer';
import './filtered_search_visual_tokens';
......@@ -6,6 +6,7 @@ import eventHub from './event_hub';
class FilteredSearchManager {
constructor(page) {
this.page = page;
this.container = FilteredSearchContainer.container;
this.filteredSearchInput = this.container.querySelector('.filtered-search');
this.filteredSearchInputForm = this.filteredSearchInput.form;
......@@ -17,16 +18,18 @@ class FilteredSearchManager {
isLocalStorageAvailable: RecentSearchesService.isAvailable(),
allowedKeys: this.filteredSearchTokenKeys.getKeys(),
});
const searchHistoryDropdownElement = document.querySelector('.js-filtered-search-history-dropdown');
const projectPath = searchHistoryDropdownElement ?
searchHistoryDropdownElement.dataset.projectFullPath : 'project';
this.searchHistoryDropdownElement = document.querySelector('.js-filtered-search-history-dropdown');
const projectPath = this.searchHistoryDropdownElement ?
this.searchHistoryDropdownElement.dataset.projectFullPath : 'project';
let recentSearchesPagePrefix = 'issue-recent-searches';
if (page === 'merge_requests') {
if (this.page === 'merge_requests') {
recentSearchesPagePrefix = 'merge-request-recent-searches';
}
const recentSearchesKey = `${projectPath}-${recentSearchesPagePrefix}`;
this.recentSearchesService = new RecentSearchesService(recentSearchesKey);
}
setup() {
// Fetch recent searches from localStorage
this.fetchingRecentSearchesPromise = this.recentSearchesService.fetch()
.catch((error) => {
......@@ -47,12 +50,12 @@ class FilteredSearchManager {
if (this.filteredSearchInput) {
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.recentSearchesStore,
this.recentSearchesService,
searchHistoryDropdownElement,
this.searchHistoryDropdownElement,
);
this.recentSearchesRoot.init();
......@@ -141,7 +144,9 @@ class FilteredSearchManager {
if (e.keyCode === 8 || e.keyCode === 46) {
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();
gl.FilteredSearchVisualTokens.removeLastTokenPartial();
}
......@@ -240,8 +245,10 @@ class FilteredSearchManager {
editToken(e) {
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);
this.tokenChange();
}
......@@ -391,7 +398,12 @@ class FilteredSearchManager {
if (condition) {
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 {
// Sanitize value since URL converts spaces into +
// Replace before decode so that we know what was originally + versus the encoded +
......@@ -410,18 +422,27 @@ class FilteredSearchManager {
}
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') {
const id = parseInt(value, 10);
if (usernameParams[id]) {
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') {
const id = parseInt(value, 10);
if (usernameParams[id]) {
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') {
hasFilteredSearch = true;
......@@ -516,6 +537,11 @@ class FilteredSearchManager {
this.filteredSearchInput.dispatchEvent(new CustomEvent('input'));
this.search();
}
// eslint-disable-next-line class-methods-use-this
canEdit() {
return true;
}
}
window.gl = window.gl || {};
......
......@@ -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 `
<div class="selectable" role="button">
<div class="name"></div>
<div class="value-container">
<div class="value"></div>
<div class="remove-token" role="button">
<i class="fa fa-close"></i>
</div>
${removeTokenMarkup}
</div>
</div>
`;
......@@ -84,13 +91,13 @@ class FilteredSearchVisualTokens {
}
}
static addVisualTokenElement(name, value, isSearchTerm) {
static addVisualTokenElement(name, value, isSearchTerm, canEdit) {
const li = document.createElement('li');
li.classList.add('js-visual-token');
li.classList.add(isSearchTerm ? 'filtered-search-term' : 'filtered-search-token');
if (value) {
li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML();
li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML(canEdit);
FilteredSearchVisualTokens.renderVisualTokenValue(li, name, value);
} else {
li.innerHTML = '<div class="name"></div>';
......@@ -114,20 +121,20 @@ class FilteredSearchVisualTokens {
}
}
static addFilterVisualToken(tokenName, tokenValue) {
static addFilterVisualToken(tokenName, tokenValue, canEdit) {
const { lastVisualToken, isLastVisualTokenValid }
= FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
const addVisualTokenElement = FilteredSearchVisualTokens.addVisualTokenElement;
if (isLastVisualTokenValid) {
addVisualTokenElement(tokenName, tokenValue, false);
addVisualTokenElement(tokenName, tokenValue, false, canEdit);
} else {
const previousTokenName = lastVisualToken.querySelector('.name').innerText;
const tokensContainer = FilteredSearchContainer.container.querySelector('.tokens-container');
tokensContainer.removeChild(lastVisualToken);
const value = tokenValue || tokenName;
addVisualTokenElement(previousTokenName, value, false);
addVisualTokenElement(previousTokenName, value, false, canEdit);
}
}
......
......@@ -468,8 +468,8 @@ GitLabDropdown = (function() {
// Process the data to make sure rendered data
// 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));
}
......@@ -740,6 +740,12 @@ GitLabDropdown = (function() {
$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) {
$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 */
(function() {
(function(w) {
var notificationGranted, notifyMe, notifyPermissions;
notificationGranted = function(message, opts, onclick) {
var notification;
notification = new Notification(message, opts);
setTimeout(function() {
return notification.close();
// Hide the notification after X amount of seconds
}, 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)) {
function notificationGranted(message, opts, onclick) {
var notification;
notification = new Notification(message, opts);
setTimeout(function() {
// Hide the notification after X amount of seconds
return notification.close();
}, 8000);
return notification.onclick = onclick || notification.close;
}
// do nothing
} else if (Notification.permission === 'granted') {
// If it's okay let's create a notification
function notifyPermissions() {
if ('Notification' in window) {
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);
} 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) {
export function bytesToKiB(number) {
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';
import './lib/utils/bootstrap_linked_tabs';
import './lib/utils/common_utils';
import './lib/utils/datetime_utility';
import './lib/utils/notify';
import './lib/utils/pretty_time';
import './lib/utils/text_utility';
import './lib/utils/url_utility';
......
......@@ -285,7 +285,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
// Similar to `toggler_behavior` in the discussion tab
const hash = window.gl.utils.getLocationHash();
const anchor = hash && $container.find(`[id="${hash}"]`);
if (anchor) {
if (anchor && anchor.length > 0) {
const notesContent = anchor.closest('.notes_content');
const lineType = notesContent.hasClass('new') ? 'new' : 'old';
notes.toggleDiffNote({
......
This diff is collapsed.
<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 loadingIcon from '../../../vue_shared/components/loading_icon.vue';
import '../../../flash';
export default {
props: {
isLoading: {
type: Boolean,
required: true,
},
pipeline: {
type: Object,
required: true,
},
},
components: {
stageColumnComponent,
loadingIcon,
},
data() {
const DOMdata = document.getElementById('js-pipeline-graph-vue').dataset;
const store = new PipelineStore();
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();
}
});
computed: {
graph() {
return this.pipeline.details && this.pipeline.details.stages;
},
},
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) {
return name.charAt(0).toUpperCase() + name.slice(1);
},
......@@ -101,7 +65,7 @@
v-if="!isLoading"
class="stage-column-list">
<stage-column-component
v-for="(stage, index) in state.graph"
v-for="(stage, index) in graph"
:title="capitalizeStageName(stage.name)"
:jobs="stage.groups"
: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 {
constructor() {
this.state = {};
this.state.graph = [];
this.state.pipeline = {};
}
storeGraph(graph = []) {
this.state.graph = graph;
storePipeline(pipeline = {}) {
this.state.pipeline = pipeline;
}
}
......@@ -51,6 +51,9 @@ import Api from './api';
this.groupId = $(select).data('group-id');
this.includeGroups = $(select).data('include-groups');
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";
if (this.includeGroups) {
placeholder += " or group";
......@@ -84,7 +87,11 @@ import Api from './api';
if (_this.groupId) {
return Api.groupProjects(_this.groupId, query.term, projectsCallback);
} 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),
......
......@@ -35,6 +35,7 @@ function UsersSelect(currentUser, els) {
options.showCurrentUser = $dropdown.data('current-user');
options.todoFilter = $dropdown.data('todo-filter');
options.todoStateFilter = $dropdown.data('todo-state-filter');
options.perPage = $dropdown.data('per-page');
showNullUser = $dropdown.data('null-user');
defaultNullUser = $dropdown.data('null-user-default');
showMenuAbove = $dropdown.data('showMenuAbove');
......@@ -214,7 +215,36 @@ function UsersSelect(currentUser, els) {
glDropdown.options.processData(term, users, callback);
}.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 index;
let j;
......@@ -645,7 +675,7 @@ UsersSelect.prototype.users = function(query, options, callback) {
url: url,
data: {
search: query,
per_page: 20,
per_page: options.perPage || 20,
active: true,
project_id: options.projectId || null,
group_id: options.groupId || null,
......
import statusCodes from '~/lib/utils/http_status';
import { bytesToMiB } from '~/lib/utils/number_utils';
import MemoryGraph from '../../vue_shared/components/memory_graph';
import MRWidgetService from '../services/mr_widget_service';
......@@ -9,8 +11,8 @@ export default {
},
data() {
return {
// memoryFrom: 0,
// memoryTo: 0,
memoryFrom: 0,
memoryTo: 0,
memoryMetrics: [],
deploymentTime: 0,
hasMetrics: false,
......@@ -35,18 +37,38 @@ export default {
shouldShowMetricsUnavailable() {
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: {
getMegabytes(bytesString) {
const valueInBytes = Number(bytesString).toFixed(2);
return (bytesToMiB(valueInBytes)).toFixed(2);
},
computeGraphData(metrics, deploymentTime) {
this.loadingMetrics = false;
const { memory_values } = metrics;
// if (memory_previous.length > 0) {
// this.memoryFrom = Number(memory_previous[0].value[1]).toFixed(2);
// }
//
// if (memory_current.length > 0) {
// this.memoryTo = Number(memory_current[0].value[1]).toFixed(2);
// }
const { memory_before, memory_after, memory_values } = metrics;
// 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_before.length > 0) {
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) {
this.hasMetrics = true;
......@@ -102,7 +124,7 @@ export default {
<p
v-if="shouldShowMemoryGraph"
class="usage-info js-usage-info">
Deployment memory usage:
Memory usage <b>{{memoryChangeType}}</b> from {{memoryFrom}}MB to {{memoryTo}}MB
</p>
<p
v-if="shouldShowLoadFailure"
......
......@@ -13,7 +13,7 @@ export default {
},
data() {
return {
removeSourceBranch: true,
removeSourceBranch: this.mr.shouldRemoveSourceBranch,
mergeWhenBuildSucceeds: false,
useCommitMessageWithDescription: false,
setToMergeWhenPipelineSucceeds: false,
......@@ -69,6 +69,9 @@ export default {
|| this.isMakingRequest
|| this.mr.preventMerge);
},
isRemoveSourceBranchButtonDisabled() {
return this.isMergeButtonDisabled || !this.mr.canRemoveSourceBranch;
},
shouldShowSquashBeforeMerge() {
const { commitsCount, enableSquashBeforeMerge } = this.mr;
return enableSquashBeforeMerge && commitsCount > 1;
......@@ -252,8 +255,9 @@ export default {
<template v-if="isMergeAllowed()">
<label class="spacing">
<input
id="remove-source-branch-input"
v-model="removeSourceBranch"
:disabled="isMergeButtonDisabled"
:disabled="isRemoveSourceBranchButtonDisabled"
type="checkbox"/> Remove source branch
</label>
......
......@@ -41,3 +41,4 @@ export { default as getStateKey } from './stores/get_state_key';
export { default as mrWidgetOptions } from './mr_widget_options';
export { default as stateMaps } from './stores/state_maps';
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 {
} from './dependencies';
document.addEventListener('DOMContentLoaded', () => {
gl.mrWidgetData.gitlabLogo = gon.gitlab_logo;
const vm = new Vue(mrWidgetOptions);
window.gl.mrWidget = {
......
......@@ -29,6 +29,7 @@ import {
eventHub,
stateMaps,
SquashBeforeMerge,
notify,
} from './dependencies';
export default {
......@@ -77,8 +78,10 @@ export default {
this.service.checkStatus()
.then(res => res.json())
.then((res) => {
this.handleNotification(res);
this.mr.setData(res);
this.setFavicon();
if (cb) {
cb.call(null, res);
}
......@@ -136,6 +139,15 @@ export default {
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() {
this.pollingInterval.resume();
},
......
......@@ -5,6 +5,8 @@ export default class MergeRequestStore {
constructor(data) {
this.sha = data.diff_head_sha;
this.gitlabLogo = data.gitlabLogo;
this.setData(data);
}
......@@ -50,7 +52,7 @@ export default class MergeRequestStore {
this.cancelAutoMergePath = data.cancel_merge_when_pipeline_succeeds_path;
this.removeWIPPath = data.remove_wip_path;
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.mergeWhenPipelineSucceeds = data.merge_when_pipeline_succeeds || false;
this.mergePath = data.merge_path;
......
<script>
import ciIconBadge from './ci_badge_link.vue';
import timeagoTooltip from './time_ago_tooltip.vue';
import tooltipMixin from '../mixins/tooltip';
import userAvatarLink from './user_avatar/user_avatar_link.vue';
/**
* Renders header component for job and pipeline page based on UI mockups
*
* Used in:
* - job show page
* - pipeline show page
*/
export default {
props: {
status: {
type: Object,
required: true,
},
itemName: {
type: String,
required: true,
},
itemId: {
type: Number,
required: true,
},
time: {
type: String,
required: true,
},
user: {
type: Object,
required: true,
},
actions: {
type: Array,
required: false,
default: () => [],
},
},
mixins: [
tooltipMixin,
],
components: {
ciIconBadge,
timeagoTooltip,
userAvatarLink,
},
computed: {
userAvatarAltText() {
return `${this.user.name}'s avatar`;
},
},
methods: {
onClickAction(action) {
this.$emit('postAction', action);
},
},
};
</script>
<template>
<header class="page-content-header top-area">
<section class="header-main-content">
<ci-icon-badge :status="status" />
<strong>
{{itemName}} #{{itemId}}
</strong>
triggered
<timeago-tooltip :time="time" />
by
<user-avatar-link
:link-href="user.web_url"
:img-src="user.avatar_url"
:img-alt="userAvatarAltText"
:tooltip-text="user.name"
:img-size="24"
/>
<a
:href="user.web_url"
:title="user.email"
class="js-user-link commit-committer-link"
ref="tooltip">
{{user.name}}
</a>
</section>
<section
class="header-action-button nav-controls"
v-if="actions.length">
<template
v-for="action in actions">
<a
v-if="action.type === 'link'"
:href="action.path"
:class="action.cssClass">
{{action.label}}
</a>
<button
v-else="action.type === 'button'"
@click="onClickAction(action)"
:class="action.cssClass"
type="button">
{{action.label}}
</button>
</template>
</section>
</header>
</template>
......@@ -4,7 +4,7 @@ import PipelinesActionsComponent from '../../pipelines/components/pipelines_acti
import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts';
import ciBadge from './ci_badge_link.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 CommitComponent from './commit';
......
<script>
import tooltipMixin from '../mixins/tooltip';
import timeagoMixin from '../mixins/timeago';
import '../../lib/utils/datetime_utility';
/**
* Port of ruby helper time_ago_with_tooltip
*/
export default {
props: {
time: {
type: String,
required: true,
},
tooltipPlacement: {
type: String,
required: false,
default: 'top',
},
shortFormat: {
type: Boolean,
required: false,
default: false,
},
cssClass: {
type: String,
required: false,
default: '',
},
},
mixins: [
tooltipMixin,
timeagoMixin,
],
computed: {
timeagoCssClass() {
return this.shortFormat ? 'js-short-timeago' : 'js-timeago';
},
},
};
</script>
<template>
<time
:class="[timeagoCssClass, cssClass]"
class="js-timeago js-timeago-render"
:title="tooltipTitle(time)"
:data-placement="tooltipPlacement"
data-container="body"
ref="tooltip">
{{timeFormated(time)}}
</time>
</template>
import '../../lib/utils/datetime_utility';
/**
* Mixin with time ago methods used in some vue components
*/
export default {
methods: {
timeFormated(time) {
const timeago = gl.utils.getTimeago();
return timeago.format(time);
},
tooltipTitle(time) {
return gl.utils.formatDate(time);
},
},
};
......@@ -34,6 +34,7 @@
@import "framework/selects.scss";
@import "framework/sidebar.scss";
@import "framework/tables.scss";
@import "framework/notes.scss";
@import "framework/timeline.scss";
@import "framework/typography.scss";
@import "framework/zen.scss";
......
......@@ -10,7 +10,7 @@
top: 0;
margin-top: 3px;
padding: $gl-padding;
z-index: 9;
z-index: 300;
width: 300px;
font-size: 14px;
background-color: $white-light;
......
......@@ -23,7 +23,6 @@
.row-content-block {
margin-top: 0;
margin-bottom: -$gl-padding;
background-color: $gray-light;
padding: $gl-padding;
margin-bottom: 0;
......
gl-emoji {
display: inline-block;
display: inline-flex;
vertical-align: middle;
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
......
......@@ -104,6 +104,22 @@
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 {
padding-right: 0;
}
......@@ -111,7 +127,7 @@
.remove-token {
display: inline-block;
padding-left: 4px;
padding-right: 8px;
padding-right: 0;
.fa-close {
color: $gl-text-color-secondary;
......@@ -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 {
.name {
background-color: $filter-name-selected-color;
......
......@@ -36,6 +36,10 @@
border-radius: 0;
}
}
&:empty {
margin: 0;
}
}
@media (max-width: $screen-sm-max) {
......
......@@ -11,7 +11,6 @@
> li {
padding: 10px 15px;
min-height: 20px;
border-bottom: 1px solid $list-border-light;
border-bottom: 1px solid $list-border;
&::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 @@
.select2-search-field input {
padding: 5px $gl-padding / 2;
font-size: 13px;
height: auto;
font-family: inherit;
font-size: inherit;
......
......@@ -3,6 +3,12 @@
margin: 0;
padding: 0;
&::before {
@include notes-media('max', $screen-xs-max) {
background: none;
}
}
.system-note {
.note-text {
color: $gl-text-color !important;
......@@ -23,6 +29,16 @@
.timeline-entry-inner {
position: relative;
@include notes-media('max', $screen-xs-max) {
.timeline-icon {
display: none;
}
.timeline-content {
margin-left: 0;
}
}
}
&:target,
......@@ -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 {
margin: 0;
border-right: none;
......
......@@ -21,6 +21,10 @@
margin-top: 0;
}
> :last-child {
margin-bottom: 0;
}
// Single code lines should wrap
code {
font-family: $monospace_font;
......@@ -157,7 +161,7 @@
ul,
ol {
padding: 0;
margin: 0 0 16px !important;
margin: 0 0 16px;
}
ul:dir(rtl),
......
......@@ -72,7 +72,9 @@
@media (min-width: $screen-sm-min) {
height: 475px; // Needed for PhantomJS
// scss-lint:disable DuplicateProperty
height: calc(100vh - 222px);
// scss-lint:enable DuplicateProperty
min-height: 475px;
transition: width .2s;
......
......@@ -29,129 +29,140 @@
}
}
.build-page {
pre.trace {
background: $builds-trace-bg;
color: $white-light;
font-family: $monospace_font;
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;
@keyframes blinking-scroll-button {
0% { opacity: 0.2; }
25% { opacity: 0.5; }
50% { opacity: 0.7; }
100% { opacity: 1; }
}
svg {
position: relative;
top: 1px;
margin-right: 5px;
}
.build-page {
.sticky {
position: absolute;
left: 0;
right: 0;
}
.truncated-info {
text-align: center;
border-bottom: 1px solid;
background-color: $black;
height: 45px;
padding: 15px;
.build-trace-container {
position: absolute;
top: 225px;
left: 15px;
bottom: 10px;
background: $black;
color: $gray-darkest;
font-family: $monospace_font;
font-size: 12px;
&.affix {
top: 0;
&.sidebar-expanded {
right: 305px;
}
// with sidebar
&.affix.sidebar-expanded {
right: 312px;
left: 22px;
&.sidebar-collapsed {
right: 16px;
}
// without sidebar
&.affix.sidebar-collapsed {
right: 20px;
left: 20px;
code {
background: $black;
color: $gray-darkest;
}
&.affix-top {
position: absolute;
.top-bar {
top: 0;
margin: 0 auto;
right: 5px;
left: 5px;
}
height: 35px;
display: flex;
justify-content: flex-end;
border-bottom: 1px outset $white-light;
.truncated-info-size {
margin: 0 5px;
}
.truncated-info {
margin: 0 auto;
align-self: center;
.raw-link {
color: inherit;
margin-left: 5px;
text-decoration: underline;
.truncated-info-size {
margin: 0 5px;
}
.raw-link {
color: inherit;
margin-left: 5px;
text-decoration: underline;
}
}
}
}
}
.scroll-controls {
height: 100%;
.controllers {
display: flex;
align-self: center;
font-size: 15px;
.scroll-step {
width: 31px;
margin: 0 0 0 auto;
}
svg {
height: 15px;
display: block;
fill: $white-light;
}
.scroll-link,
.autoscroll-container {
right: 25px;
z-index: 1;
}
a,
.btn-scroll {
margin: 0 8px;
color: $white-light;
}
.scroll-link {
position: fixed;
display: block;
margin-bottom: 10px;
.btn-scroll.animate {
.first-triangle {
animation: blinking-scroll-button 1s ease infinite;
animation-delay: .3s;
}
&.scroll-top .gitlab-icon-scroll-up-hover,
&.scroll-top:hover .gitlab-icon-scroll-up,
&.scroll-bottom .gitlab-icon-scroll-down-hover,
&.scroll-bottom:hover .gitlab-icon-scroll-down {
display: none;
}
.second-triangle {
animation: blinking-scroll-button 1s ease infinite;
animation-delay: .2s;
}
&.scroll-top:hover .gitlab-icon-scroll-up-hover,
&.scroll-bottom:hover .gitlab-icon-scroll-down-hover {
display: inline-block;
}
.third-triangle {
animation: blinking-scroll-button 1s ease infinite;
}
&.scroll-top {
top: 10px;
}
&:disabled {
opacity: 1;
}
}
&.scroll-bottom {
bottom: -2px;
.btn-scroll:disabled {
opacity: 0.35;
cursor: not-allowed;
}
}
}
.autoscroll-container {
position: absolute;
.bash {
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,
.autoscroll-container {
right: ($gutter_width + ($gl-padding * 2));
svg {
position: relative;
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 {
......@@ -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 {
padding: $gl-padding 0;
......@@ -390,6 +375,10 @@
.container-fluid.container-limited {
max-width: 100%;
}
.content-wrapper {
padding-bottom: 6px;
}
}
.build-detail-row {
......
......@@ -36,7 +36,6 @@
pre.commit-message {
background: none;
padding: 0;
margin: 0;
border: none;
margin: 20px 0;
border-radius: 0;
......
......@@ -94,7 +94,6 @@
.old_line,
.new_line {
margin: 0;
padding: 0;
border: none;
padding: 0 5px;
border-right: 1px solid;
......
......@@ -64,6 +64,10 @@
}
}
.btn .text-center {
display: inline;
}
.commit-title {
margin: 0;
}
......
......@@ -431,7 +431,7 @@
}
.detail-page-description {
padding: 16px 0 0;
padding: 16px 0;
small {
color: $gray-darkest;
......@@ -441,7 +441,7 @@
.edited-text {
color: $gray-darkest;
display: block;
margin: 0 0 16px;
margin: 16px 0 0;
.author_link {
color: $gray-darkest;
......
......@@ -204,7 +204,6 @@ ul.related-merge-requests > li {
.dropdown-toggle {
.fa-caret-down {
pointer-events: none;
margin-left: 0;
color: inherit;
margin-left: 0;
}
......
......@@ -184,4 +184,4 @@
}
}
}
}
\ No newline at end of file
}
......@@ -520,17 +520,13 @@
position: absolute;
border-top: 2px solid $border-color;
height: 1px;
top: 8px;
top: 9px;
width: 8px;
left: 0;
}
&:last-child {
margin-bottom: 0;
&::before {
top: 14px;
}
}
}
......@@ -539,7 +535,7 @@
width: 2px;
background: $border-color;
position: absolute;
top: -5px;
top: -9px;
}
}
......
......@@ -28,7 +28,7 @@
.note-edit-form {
.note-form-actions {
position: relative;
margin: $gl-padding 0;
margin: $gl-padding 0 0;
}
.note-preview-holder {
......@@ -124,10 +124,18 @@
}
.discussion-form {
padding: $gl-padding-top $gl-padding;
padding: $gl-padding-top $gl-padding $gl-padding;
background-color: $white-light;
}
.discussion-notes .disabled-comment {
padding: 6px 0;
}
.notes-form > li {
border: 0;
}
.note-edit-form {
display: none;
font-size: 14px;
......
......@@ -14,24 +14,11 @@ ul.notes {
margin: 0;
padding: 0;
.timeline-icon {
float: left;
svg {
width: 16px;
height: 16px;
fill: $gray-darkest;
position: absolute;
left: 0;
top: 16px;
}
}
.timeline-content {
margin-left: 55px;
&.timeline-content-form {
@media (max-width: $screen-sm-max) {
@include notes-media('max', $screen-sm-max) {
margin-left: 0;
}
}
......@@ -56,21 +43,22 @@ ul.notes {
position: relative;
}
.note {
padding: $gl-padding $gl-btn-padding 0;
> li {
padding: $gl-padding $gl-btn-padding;
display: block;
position: relative;
border-bottom: 1px solid $white-normal;
&:last-child {
// Override `.timeline > li:last-child { border-bottom: none; }`
border-bottom: 1px solid $white-normal;
}
&.being-posted {
pointer-events: none;
opacity: 0.5;
.dummy-avatar {
display: inline-block;
height: 40px;
width: 40px;
border-radius: 50%;
background-color: $kdb-border;
border: 1px solid darken($kdb-border, 25%);
}
......@@ -126,13 +114,13 @@ ul.notes {
.note-awards {
.js-awards-block {
margin-bottom: 16px;
margin-top: 16px;
}
}
.note-header {
@media (max-width: $screen-xs-min) {
@include notes-media('max', $screen-xs-min) {
.inline {
display: block;
}
......@@ -161,10 +149,10 @@ ul.notes {
.system-note {
font-size: 14px;
padding: 0;
padding-left: 0;
clear: both;
@media (min-width: $screen-sm-min) {
@include notes-media('min', $screen-sm-min) {
margin-left: 65px;
}
......@@ -198,11 +186,22 @@ ul.notes {
}
}
.timeline-content {
padding: 14px 10px;
.timeline-icon {
float: left;
@media (min-width: $screen-sm-min) {
margin-left: 20px;
svg {
width: 16px;
height: 16px;
fill: $gray-darkest;
position: absolute;
left: 0;
top: 2px;
}
}
.timeline-content {
@include notes-media('min', $screen-sm-min) {
margin-left: 30px;
}
}
......@@ -371,7 +370,7 @@ ul.notes {
display: flex;
justify-content: space-between;
@media (max-width: $screen-xs-max) {
@include notes-media('max', $screen-xs-max) {
flex-flow: row wrap;
}
}
......@@ -386,7 +385,7 @@ ul.notes {
}
.note-header-author-name {
@media (max-width: $screen-xs-max) {
@include notes-media('max', $screen-xs-max) {
display: none;
}
}
......@@ -394,7 +393,7 @@ ul.notes {
.note-headline-light {
display: inline;
@media (max-width: $screen-xs-min) {
@include notes-media('max', $screen-xs-min) {
display: block;
}
}
......@@ -436,7 +435,7 @@ ul.notes {
margin-left: 10px;
color: $gray-darkest;
@media (max-width: $screen-xs-max) {
@include notes-media('max', $screen-xs-max) {
float: none;
margin-left: 0;
}
......@@ -447,7 +446,7 @@ ul.notes {
}
.discussion-actions {
@media (max-width: $screen-md-max) {
@include notes-media('max', $screen-md-max) {
float: none;
margin-left: 0;
......@@ -461,7 +460,7 @@ ul.notes {
display: inline;
line-height: 20px;
@media (min-width: $screen-sm-min) {
@include notes-media('min', $screen-sm-min) {
margin-left: 10px;
line-height: 24px;
}
......@@ -596,10 +595,15 @@ ul.notes {
.discussion-body,
.diff-file {
.notes .note {
padding: 10px 15px;
padding-left: $gl-padding;
padding-right: $gl-padding;
&.system-note {
padding: 0;
padding-left: 0;
@media (min-width: $screen-sm-min) {
margin-left: 70px;
}
}
}
}
......@@ -613,17 +617,11 @@ ul.notes {
}
.disabled-comment {
margin-left: -$gl-padding-top;
margin-right: -$gl-padding-top;
background-color: $gray-light;
border-radius: $border-radius-base;
border: 1px solid $border-gray-normal;
color: $note-disabled-comment-color;
line-height: 200px;
.disabled-comment-text {
line-height: normal;
}
padding: 90px 0;
a {
color: $gl-link-color;
......@@ -631,7 +629,7 @@ ul.notes {
}
.line-resolve-all-container {
@media (min-width: $screen-sm-min) {
@include notes-media('min', $screen-sm-min) {
margin-right: 0;
padding-left: $gl-padding;
}
......@@ -673,7 +671,7 @@ ul.notes {
.line-resolve-all {
vertical-align: middle;
display: inline-block;
padding: 6px 10px;
padding: 5px 10px 6px;
background-color: $gray-light;
border: 1px solid $border-color;
border-radius: $border-radius-default;
......@@ -686,6 +684,10 @@ ul.notes {
.line-resolve-btn {
margin-right: 5px;
svg {
vertical-align: middle;
}
}
}
......@@ -722,6 +724,10 @@ ul.notes {
}
}
.line-resolve-text {
vertical-align: middle;
}
.discussion-next-btn {
svg {
margin: 0;
......@@ -738,10 +744,6 @@ ul.notes {
// Merge request notes in diffs
.diff-file {
// Diff is side by side
.notes_content.parallel .note-header .note-header-author-name {
display: block;
}
// Diff is inline
.notes_content .note-header .note-headline-light {
display: inline-block;
......
......@@ -88,6 +88,10 @@
}
}
.btn .text-center {
display: inline;
}
.tooltip {
white-space: nowrap;
}
......
......@@ -247,7 +247,6 @@
font-size: 13px;
font-weight: 600;
line-height: 13px;
padding: $gl-vert-padding $gl-padding;
letter-spacing: .4px;
padding: 6px 14px;
text-align: center;
......@@ -384,10 +383,6 @@ a.deploy-project-label {
}
}
.last-push-widget {
margin-top: -1px;
}
.fork-namespaces {
.row {
-webkit-flex-wrap: wrap;
......
......@@ -9,7 +9,7 @@ class AutocompleteController < ApplicationController
@users = @users.where.not(id: params[:skip_users]) if params[:skip_users].present?
@users = @users.active
@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
@users = @users.todo_authors(current_user.id, params[:todo_state_filter])
......
......@@ -8,7 +8,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@projects = load_projects(params.merge(non_public: true)).page(params[:page])
respond_to do |format|
format.html { @last_push = current_user.recent_push }
format.html
format.atom do
load_events
render layout: false
......@@ -25,7 +25,6 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@projects = load_projects(params.merge(starred: true)).
includes(:forked_from_project, :tags).page(params[:page])
@last_push = current_user.recent_push
@groups = []
respond_to do |format|
......
......@@ -9,8 +9,6 @@ class DashboardController < Dashboard::ApplicationController
respond_to :html
def activity
@last_push = current_user.recent_push
respond_to do |format|
format.html
......
......@@ -64,6 +64,8 @@ class GroupsController < Groups::ApplicationController
end
def subgroups
return not_found unless Group.supports_nested_groups?
@nested_groups = GroupsFinder.new(current_user, parent: group).execute
@nested_groups = @nested_groups.search(params[:filter_groups]) if params[:filter_groups].present?
end
......@@ -165,7 +167,6 @@ class GroupsController < Groups::ApplicationController
def user_actions
if current_user
@last_push = current_user.recent_push
@notification_setting = current_user.notification_settings_for(group)
end
end
......
......@@ -56,7 +56,7 @@ module ButtonHelper
content_tag (append_link ? :a : :span), protocol,
class: klass,
href: (project.http_url_to_repo(current_user) if append_link),
href: (project.http_url_to_repo if append_link),
data: {
html: true,
placement: placement,
......
......@@ -85,6 +85,12 @@ module ProjectsHelper
@nav_tabs ||= get_project_nav_tabs(@project, current_user)
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)
project_nav_tabs.include? name
end
......@@ -116,6 +122,7 @@ module ProjectsHelper
def last_push_event
return unless current_user
return current_user.recent_push unless @project
project_ids = [@project.id]
if fork = current_user.fork_of(@project)
......@@ -203,7 +210,17 @@ module ProjectsHelper
nav_tabs << :container_registry
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,
milestones: :read_milestone,
pipelines: :read_pipeline,
......@@ -215,14 +232,15 @@ module ProjectsHelper
team: :read_project_member,
wiki: :read_wiki
}
end
tab_ability_map.each do |tab, ability|
if can?(current_user, ability, project)
nav_tabs << tab
end
end
nav_tabs.flatten
def search_tab_ability_map
@search_tab_ability_map ||= tab_ability_map.merge(
blobs: :download_code,
commits: :download_code,
merge_requests: :read_merge_request,
notes: [:read_merge_request, :download_code, :read_issue, :read_project_snippet]
)
end
def project_lfs_status(project)
......@@ -258,7 +276,7 @@ module ProjectsHelper
when 'ssh'
project.ssh_url_to_repo
else
project.http_url_to_repo(current_user)
project.http_url_to_repo
end
end
......
......@@ -45,6 +45,14 @@ module SelectsHelper
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)
end
......
......@@ -13,6 +13,7 @@ module SubmoduleHelper
if url =~ /([^\/:]+)\/([^\/]+(?:\.git)?)\Z/
namespace, project = $1, $2
project.rstrip!
project.sub!(/\.git\z/, '')
if self_url?(url, namespace, project)
......
......@@ -10,9 +10,9 @@ module Ci
has_one :last_pipeline, -> { order(id: :desc) }, class_name: 'Ci::Pipeline'
has_many :pipelines
validates :cron, unless: :importing_or_inactive?, cron: true, presence: { unless: :importing_or_inactive? }
validates :cron_timezone, cron_timezone: true, presence: { unless: :importing_or_inactive? }
validates :ref, presence: { unless: :importing_or_inactive? }
validates :cron, unless: :importing?, cron: true, presence: { unless: :importing? }
validates :cron_timezone, cron_timezone: true, presence: { unless: :importing? }
validates :ref, presence: { unless: :importing? }
validates :description, presence: true
before_save :set_next_run_at
......@@ -32,10 +32,6 @@ module Ci
update_attribute(:active, false)
end
def importing_or_inactive?
importing? || inactive?
end
def runnable_by_owner?
Ability.allowed?(owner, :create_pipeline, project)
end
......
......@@ -84,89 +84,6 @@ module Routable
joins(:route).where(wheres.join(' OR '))
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
def full_name
......
......@@ -3,7 +3,11 @@ module SelectForProjectAuthorization
module ClassMethods
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
......@@ -38,6 +38,10 @@ class Group < Namespace
after_save :update_two_factor_requirement
class << self
def supports_nested_groups?
Gitlab::Database.postgresql?
end
# Searches for groups matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
......@@ -78,7 +82,7 @@ class Group < Namespace
if current_scope.joins_values.include?(:shared_projects)
joins('INNER JOIN namespaces project_namespace ON project_namespace.id = projects.namespace_id')
.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
super
end
......
......@@ -107,7 +107,7 @@ class Milestone < ActiveRecord::Base
end
def participants
User.joins(assigned_issues: :milestone).where("milestones.id = ?", id)
User.joins(assigned_issues: :milestone).where("milestones.id = ?", id).uniq
end
def self.sort(method)
......
......@@ -176,26 +176,20 @@ class Namespace < ActiveRecord::Base
projects.with_shared_runners.any?
end
# Scopes the model on ancestors of the record
# Returns all the ancestors of the current namespaces.
def ancestors
if parent_id
path = route ? route.path : full_path
paths = []
return self.class.none unless parent_id
until path.blank?
path = path.rpartition('/').first
paths << path
end
self.class.joins(:route).where('routes.path IN (?)', paths).reorder('routes.path ASC')
else
self.class.none
end
Gitlab::GroupHierarchy.
new(self.class.where(id: parent_id)).
base_and_ancestors
end
# Scopes the model on direct and indirect children of the record
# Returns all the descendants of the current namespace.
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
def user_ids_for_project_authorizations
......
......@@ -271,6 +271,7 @@ class Project < ActiveRecord::Base
scope :with_builds_enabled, -> { with_feature_enabled(:builds) }
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 }
......@@ -873,10 +874,8 @@ class Project < ActiveRecord::Base
url_to_repo
end
def http_url_to_repo(user = nil)
credentials = Gitlab::UrlSanitizer.http_credentials_for_user(user)
Gitlab::UrlSanitizer.new("#{web_url}.git", credentials: credentials).full_url
def http_url_to_repo
"#{web_url}.git"
end
def user_can_push_to_empty_repo?(user)
......
......@@ -6,6 +6,12 @@ class ProjectAuthorization < ActiveRecord::Base
validates :access_level, inclusion: { in: Gitlab::Access.all_values }, 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)
rows.each_slice(per_batch) do |slice|
tuples = slice.map do |tuple|
......
......@@ -2,9 +2,10 @@ class JiraService < IssueTrackerService
include Gitlab::Routing.url_helpers
validates :url, url: true, presence: true, if: :activated?
validates :api_url, url: true, allow_blank: true
validates :project_key, presence: true, if: :activated?
prop_accessor :username, :password, :url, :project_key,
prop_accessor :username, :password, :url, :api_url, :project_key,
:jira_issue_transition_id, :title, :description
before_update :reset_password
......@@ -25,20 +26,18 @@ class JiraService < IssueTrackerService
super do
self.properties = {
title: issues_tracker['title'],
url: issues_tracker['url']
url: issues_tracker['url'],
api_url: issues_tracker['api_url']
}
end
end
def reset_password
# don't reset the password if a new one is provided
if url_changed? && !password_touched?
self.password = nil
end
self.password = nil if reset_password?
end
def options
url = URI.parse(self.url)
url = URI.parse(client_url)
{
username: self.username,
......@@ -87,7 +86,8 @@ class JiraService < IssueTrackerService
def fields
[
{ type: 'text', name: 'url', title: 'URL', placeholder: 'https://jira.example.com' },
{ type: 'text', name: 'url', title: 'Web URL', placeholder: 'https://jira.example.com' },
{ type: 'text', name: 'api_url', title: 'JIRA API URL', placeholder: 'If different from Web URL' },
{ type: 'text', name: 'project_key', placeholder: 'Project Key' },
{ type: 'text', name: 'username', placeholder: '' },
{ type: 'password', name: 'password', placeholder: '' },
......@@ -186,7 +186,7 @@ class JiraService < IssueTrackerService
end
def test_settings
return unless url.present?
return unless client_url.present?
# Test settings by getting the project
jira_request { jira_project.present? }
end
......@@ -236,20 +236,29 @@ class JiraService < IssueTrackerService
end
def send_message(issue, message, remote_link_props)
return unless url.present?
return unless client_url.present?
jira_request do
if issue.comments.build.save!(body: message)
remote_link = issue.remotelink.build
remote_link = find_remote_link(issue, remote_link_props[:object][:url])
if remote_link
remote_link.save!(remote_link_props)
result_message = "#{self.class.name} SUCCESS: Successfully posted to #{url}."
elsif issue.comments.build.save!(body: message)
new_remote_link = issue.remotelink.build
new_remote_link.save!(remote_link_props)
end
result_message = "#{self.class.name} SUCCESS: Successfully posted to #{client_url}."
Rails.logger.info(result_message)
result_message
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)
status = {
resolved: resolved
......@@ -295,7 +304,20 @@ class JiraService < IssueTrackerService
yield
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError, OpenSSL::SSL::SSLError => e
Rails.logger.info "#{self.class.name} Send message ERROR: #{url} - #{e.message}"
Rails.logger.info "#{self.class.name} Send message ERROR: #{client_url} - #{e.message}"
nil
end
def client_url
api_url.present? ? api_url : url
end
def reset_password?
# don't reset the password if a new one is provided
return false if password_touched?
return true if api_url_changed?
return false if api_url.present?
url_changed?
end
end
......@@ -42,11 +42,8 @@ class ProjectWiki
url_to_repo
end
def http_url_to_repo(user = nil)
url = "#{Gitlab.config.gitlab.url}/#{path_with_namespace}.git"
credentials = Gitlab::UrlSanitizer.http_credentials_for_user(user)
Gitlab::UrlSanitizer.new(url, credentials: credentials).full_url
def http_url_to_repo
"#{Gitlab.config.gitlab.url}/#{path_with_namespace}.git"
end
def wiki_base_path
......
......@@ -10,9 +10,12 @@ class User < ActiveRecord::Base
include Sortable
include CaseSensitivity
include TokenAuthenticatable
include IgnorableColumn
DEFAULT_NOTIFICATION_LEVEL = :participating
ignore_column :authorized_projects_populated
add_authentication_token_field :authentication_token
add_authentication_token_field :incoming_email_token
add_authentication_token_field :rss_token
......@@ -218,7 +221,6 @@ class User < ActiveRecord::Base
scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
scope :external, -> { where(external: true) }
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 :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')) }
......@@ -510,23 +512,16 @@ class User < ActiveRecord::Base
Group.where("namespaces.id IN (#{union.to_sql})")
end
def nested_groups
Group.member_descendants(id)
end
# Returns a relation of groups the user has access to, including their parent
# and child groups (recursively).
def all_expanded_groups
Group.member_hierarchy(id)
Gitlab::GroupHierarchy.new(groups).all_groups
end
def expanded_groups_requiring_two_factor_authentication
all_expanded_groups.where(require_two_factor_authentication: true)
end
def nested_groups_projects
Project.joins(:namespace).where('namespaces.parent_id IS NOT NULL').
member_descendants(id)
end
def refresh_authorized_projects
Users::RefreshAuthorizedProjectsService.new(self).execute
end
......@@ -535,18 +530,15 @@ class User < ActiveRecord::Base
project_authorizations.where(project_id: project_ids).delete_all
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)
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 = 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
end
......@@ -919,13 +911,13 @@ class User < ActiveRecord::Base
end
def assigned_open_merge_requests_count(force: false)
Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force) do
Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force, expires_in: 20.minutes) do
MergeRequestsFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
end
end
def assigned_open_issues_count(force: false)
Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force) do
Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force, expires_in: 20.minutes) do
IssuesFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
end
end
......
......@@ -39,6 +39,7 @@ class MergeRequestEntity < IssuableEntity
expose :commits_count
expose :cannot_be_merged?, as: :has_conflicts
expose :can_be_merged?, as: :can_be_merged
expose :remove_source_branch?, as: :remove_source_branch
expose :project_archived do |merge_request|
merge_request.project.archived?
......
......@@ -61,6 +61,16 @@ module Ci
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?
return false unless pipeline.git_commit_message
pipeline.git_commit_message =~ /\[(ci[ _-]skip|skip[ _-]ci)\]/i
......@@ -118,11 +128,6 @@ module Ci
origin_sha && origin_sha != Gitlab::Git::BLANK_SHA
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)
pipeline.errors.add(:base, message)
pipeline.drop if save
......
......@@ -28,6 +28,7 @@ module Issues
notification_service.close_issue(issue, current_user) if notifications
todo_service.close_issue(issue, current_user)
execute_hooks(issue, 'close')
invalidate_cache_counts(issue.assignees, issue)
end
issue
......
......@@ -8,6 +8,7 @@ module Issues
create_note(issue)
notification_service.reopen_issue(issue, current_user)
execute_hooks(issue, 'reopen')
invalidate_cache_counts(issue.assignees, issue)
end
issue
......
......@@ -13,6 +13,7 @@ module MergeRequests
notification_service.close_mr(merge_request, current_user)
todo_service.close_merge_request(merge_request, current_user)
execute_hooks(merge_request, 'close')
invalidate_cache_counts(merge_request.assignees, merge_request)
end
merge_request
......
......@@ -11,7 +11,9 @@ module MergeRequests
merge_request = MergeRequest.new
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.head_pipeline = head_pipeline_for(merge_request)
create(merge_request)
end
......@@ -22,5 +24,21 @@ module MergeRequests
todo_service.new_merge_request(issuable, current_user)
issuable.cache_merge_request_closes_issues!(current_user)
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
......@@ -13,6 +13,7 @@ module MergeRequests
create_note(merge_request)
notification_service.merge_mr(merge_request, current_user)
execute_hooks(merge_request, 'merge')
invalidate_cache_counts(merge_request.assignees, merge_request)
end
private
......
......@@ -10,6 +10,7 @@ module MergeRequests
execute_hooks(merge_request, 'reopen')
merge_request.reload_diff(current_user)
merge_request.mark_as_unchecked
invalidate_cache_counts(merge_request.assignees, merge_request)
end
merge_request
......
......@@ -12,7 +12,7 @@ class SearchService
@project =
if params[:project_id].present?
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
nil
end
......
......@@ -73,12 +73,11 @@ module Users
# remove - The IDs of the authorization rows to remove.
# add - Rows to insert in the form `[user id, project id, access level]`
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.remove_project_authorizations(remove) unless remove.empty?
ProjectAuthorization.insert_authorizations(add) unless add.empty?
user.set_authorized_projects_column
end
# Since we batch insert authorization rows, Rails' associations may get
......@@ -101,38 +100,13 @@ module Users
end
def fresh_authorizations
ProjectAuthorization.
unscoped.
select('project_id, MAX(access_level) AS access_level').
from("(#{project_authorizations_union.to_sql}) #{ProjectAuthorization.table_name}").
group(:project_id)
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
]
klass = if Group.supports_nested_groups?
Gitlab::ProjectAuthorizations::WithNestedGroups
else
Gitlab::ProjectAuthorizations::WithoutNestedGroups
end
Gitlab::SQL::Union.new(relations)
klass.new(user).calculate
end
end
end
......@@ -6,16 +6,21 @@
# Values are checked for formatting and exclusion from a list of illegal path
# names.
class DynamicPathValidator < ActiveModel::EachValidator
extend Gitlab::Git::EncodingHelper
class << self
def valid_user_path?(path)
encode!(path)
"#{path}/" =~ Gitlab::PathRegex.root_namespace_path_regex
end
def valid_group_path?(path)
encode!(path)
"#{path}/" =~ Gitlab::PathRegex.full_namespace_path_regex
end
def valid_project_path?(path)
encode!(path)
"#{path}/" =~ Gitlab::PathRegex.full_project_path_regex
end
end
......
......@@ -31,3 +31,8 @@
%h1 #{number_to_human_size(disk[:bytes_used])} / #{number_to_human_size(disk[:bytes_total])}
%p= disk[:disk_name]
%p= disk[:mount_path]
.col-sm-4
.light-well
%h4 Uptime
.data
%h1= time_ago_with_tooltip(Rails.application.config.booted_at)
.hidden-xs
= render "events/event_last_push", event: @last_push
.nav-block.activities
.controls
= link_to dashboard_projects_path(rss_url_options), class: 'btn rss-btn has-tooltip', title: 'Subscribe' do
......
- @no_container = true
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity")
- page_title "Activity"
- header_title "Activity", activity_dashboard_path
= render 'dashboard/activity_head'
.hidden-xs
= render "projects/last_push"
%div{ class: container_class }
= render 'dashboard/activity_head'
%section.activities
= render 'activities'
%section.activities
= render 'activities'
......@@ -8,7 +8,7 @@
.nav-controls
= link_to params.merge(rss_url_options), class: 'btn has-tooltip', title: 'Subscribe' do
= 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/issues'
......@@ -4,7 +4,7 @@
.top-area
= render 'shared/issuable/nav', type: :merge_requests
.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/merge_requests'
- @no_container = true
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity")
- page_title "Projects"
- header_title "Projects", dashboard_projects_path
- unless show_user_callout?
= render 'shared/user_callout'
= render "projects/last_push"
- if @projects.any? || params[:name]
= render 'dashboard/projects_head'
%div{ class: container_class }
- unless show_user_callout?
= render 'shared/user_callout'
- if @last_push
= render "events/event_last_push", event: @last_push
- if @projects.any? || params[:name]
= render 'dashboard/projects_head'
- if @projects.any? || params[:name]
= render 'projects'
- else
= render "zero_authorized_projects"
- if @projects.any? || params[:name]
= render 'projects'
- else
= render "zero_authorized_projects"
- @no_container = true
- page_title "Starred Projects"
- header_title "Projects", dashboard_projects_path
= render 'dashboard/projects_head'
= render "projects/last_push"
- if @last_push
= render "events/event_last_push", event: @last_push
%div{ class: container_class }
= render 'dashboard/projects_head'
- if @projects.any? || params[:filter_projects]
= render 'projects'
- else
%h3 You don't have starred projects yet
%p.slead Visit project page and press on star icon and it will appear on this page.
- if @projects.any? || params[:filter_projects]
= render 'projects'
- else
%h3 You don't have starred projects yet
%p.slead Visit project page and press on star icon and it will appear on this page.
.discussion-notes
%ul.notes{ data: { discussion_id: discussion.id } }
= render partial: "shared/notes/note", collection: discussion.notes, as: :note
.flash-container
- if current_user
.discussion-reply-holder
.flash-container
.discussion-reply-holder
- if can_create_note?
- if discussion.potentially_resolvable?
- line_type = local_assigns.fetch(:line_type, nil)
......@@ -19,3 +20,10 @@
= render "discussions/jump_to_next", discussion: discussion
- else
= link_to_reply_discussion(discussion)
- elsif !current_user
.disabled-comment.text-center
Please
= link_to "register", new_session_path(:user, redirect_to_referer: 'yes')
or
= link_to "sign in", new_session_path(:user, redirect_to_referer: 'yes')
to reply
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.
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