Commit 5fb962e3 authored by Rémy Coutable's avatar Rémy Coutable

Merge remote-tracking branch 'origin/master' into ce-to-ee-2017-12-06

Signed-off-by: default avatarRémy Coutable <remy@rymai.me>
parents 5dbecb8e 44921579
...@@ -181,7 +181,7 @@ gem 're2', '~> 1.1.1' ...@@ -181,7 +181,7 @@ gem 're2', '~> 1.1.1'
gem 'version_sorter', '~> 2.1.0' gem 'version_sorter', '~> 2.1.0'
# Cache # Cache
gem 'redis-rails', '~> 5.0.1' gem 'redis-rails', '~> 5.0.2'
# Redis # Redis
gem 'redis', '~> 3.2' gem 'redis', '~> 3.2'
......
...@@ -728,24 +728,24 @@ GEM ...@@ -728,24 +728,24 @@ GEM
recursive-open-struct (1.0.0) recursive-open-struct (1.0.0)
redcarpet (3.4.0) redcarpet (3.4.0)
redis (3.3.3) redis (3.3.3)
redis-actionpack (5.0.1) redis-actionpack (5.0.2)
actionpack (>= 4.0, < 6) actionpack (>= 4.0, < 6)
redis-rack (>= 1, < 3) redis-rack (>= 1, < 3)
redis-store (>= 1.1.0, < 1.4.0) redis-store (>= 1.1.0, < 2)
redis-activesupport (5.0.1) redis-activesupport (5.0.4)
activesupport (>= 3, < 6) activesupport (>= 3, < 6)
redis-store (~> 1.2.0) redis-store (>= 1.3, < 2)
redis-namespace (1.5.2) redis-namespace (1.5.2)
redis (~> 3.0, >= 3.0.4) redis (~> 3.0, >= 3.0.4)
redis-rack (1.6.0) redis-rack (2.0.3)
rack (~> 1.5) rack (>= 1.5, < 3)
redis-store (~> 1.2.0) redis-store (>= 1.2, < 2)
redis-rails (5.0.1) redis-rails (5.0.2)
redis-actionpack (~> 5.0.0) redis-actionpack (>= 5.0, < 6)
redis-activesupport (~> 5.0.0) redis-activesupport (>= 5.0, < 6)
redis-store (~> 1.2.0) redis-store (>= 1.2, < 2)
redis-store (1.2.0) redis-store (1.4.1)
redis (>= 2.2) redis (>= 2.2, < 5)
representable (3.0.4) representable (3.0.4)
declarative (< 0.1.0) declarative (< 0.1.0)
declarative-option (< 0.2.0) declarative-option (< 0.2.0)
...@@ -1168,7 +1168,7 @@ DEPENDENCIES ...@@ -1168,7 +1168,7 @@ DEPENDENCIES
redcarpet (~> 3.4) redcarpet (~> 3.4)
redis (~> 3.2) redis (~> 3.2)
redis-namespace (~> 1.5.2) redis-namespace (~> 1.5.2)
redis-rails (~> 5.0.1) redis-rails (~> 5.0.2)
request_store (~> 1.3) request_store (~> 1.3)
responders (~> 2.0) responders (~> 2.0)
rouge (~> 2.0) rouge (~> 2.0)
......
...@@ -130,7 +130,8 @@ freeze date (the 7th) should have a corresponding Enterprise Edition merge ...@@ -130,7 +130,8 @@ freeze date (the 7th) should have a corresponding Enterprise Edition merge
request, even if there are no conflicts. This is to reduce the size of the request, even if there are no conflicts. This is to reduce the size of the
subsequent EE merge, as we often merge a lot to CE on the release date. For more subsequent EE merge, as we often merge a lot to CE on the release date. For more
information, see information, see
[limit conflicts with EE when developing on CE][limit_ee_conflicts]. [Automatic CE->EE merge][automatic_ce_ee_merge] and
[Guidelines for implementing Enterprise Edition features][ee_features].
### After the 7th ### After the 7th
...@@ -281,4 +282,5 @@ still an issue I encourage you to open it on the [GitLab.com issue tracker](http ...@@ -281,4 +282,5 @@ still an issue I encourage you to open it on the [GitLab.com issue tracker](http
["Implement design & UI elements" guidelines]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#implement-design-ui-elements ["Implement design & UI elements" guidelines]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#implement-design-ui-elements
[Thoughtbot code review guide]: https://github.com/thoughtbot/guides/tree/master/code-review [Thoughtbot code review guide]: https://github.com/thoughtbot/guides/tree/master/code-review
[done]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#definition-of-done [done]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#definition-of-done
[limit_ee_conflicts]: https://docs.gitlab.com/ce/development/limit_ee_conflicts.html [automatic_ce_ee_merge]: https://docs.gitlab.com/ce/development/automatic_ce_ee_merge.html
[ee_features]: https://docs.gitlab.com/ce/development/ee_features.html
/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */
window.Compare = (function() { export default class Compare {
function Compare(opts) { constructor(opts) {
this.opts = opts; this.opts = opts;
this.source_loading = $(".js-source-loading"); this.source_loading = $(".js-source-loading");
this.target_loading = $(".js-target-loading"); this.target_loading = $(".js-target-loading");
...@@ -34,12 +34,12 @@ window.Compare = (function() { ...@@ -34,12 +34,12 @@ window.Compare = (function() {
this.initialState(); this.initialState();
} }
Compare.prototype.initialState = function() { initialState() {
this.getSourceHtml(); this.getSourceHtml();
return this.getTargetHtml(); this.getTargetHtml();
}; }
Compare.prototype.getTargetProject = function() { getTargetProject() {
return $.ajax({ return $.ajax({
url: this.opts.targetProjectUrl, url: this.opts.targetProjectUrl,
data: { data: {
...@@ -52,22 +52,22 @@ window.Compare = (function() { ...@@ -52,22 +52,22 @@ window.Compare = (function() {
return $('.js-target-branch-dropdown .dropdown-content').html(html); return $('.js-target-branch-dropdown .dropdown-content').html(html);
} }
}); });
}; }
Compare.prototype.getSourceHtml = function() { getSourceHtml() {
return this.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', { return this.constructor.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', {
ref: $("input[name='merge_request[source_branch]']").val() ref: $("input[name='merge_request[source_branch]']").val()
}); });
}; }
Compare.prototype.getTargetHtml = function() { getTargetHtml() {
return this.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', { return this.constructor.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', {
target_project_id: $("input[name='merge_request[target_project_id]']").val(), target_project_id: $("input[name='merge_request[target_project_id]']").val(),
ref: $("input[name='merge_request[target_branch]']").val() ref: $("input[name='merge_request[target_branch]']").val()
}); });
}; }
Compare.prototype.sendAjax = function(url, loading, target, data) { static sendAjax(url, loading, target, data) {
var $target; var $target;
$target = $(target); $target = $(target);
return $.ajax({ return $.ajax({
...@@ -84,7 +84,5 @@ window.Compare = (function() { ...@@ -84,7 +84,5 @@ window.Compare = (function() {
gl.utils.localTimeAgo($('.js-timeago', className)); gl.utils.localTimeAgo($('.js-timeago', className));
} }
}); });
}; }
}
return Compare;
})();
/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, wrap-iife, max-len */ /* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, wrap-iife, max-len */
window.CompareAutocomplete = (function() { export default function initCompareAutocomplete() {
function CompareAutocomplete() { $('.js-compare-dropdown').each(function() {
this.initDropdown(); var $dropdown, selected;
} $dropdown = $(this);
selected = $dropdown.data('selected');
CompareAutocomplete.prototype.initDropdown = function() { const $dropdownContainer = $dropdown.closest('.dropdown');
return $('.js-compare-dropdown').each(function() { const $fieldInput = $(`input[name="${$dropdown.data('field-name')}"]`, $dropdownContainer);
var $dropdown, selected; const $filterInput = $('input[type="search"]', $dropdownContainer);
$dropdown = $(this); $dropdown.glDropdown({
selected = $dropdown.data('selected'); data: function(term, callback) {
const $dropdownContainer = $dropdown.closest('.dropdown'); return $.ajax({
const $fieldInput = $(`input[name="${$dropdown.data('field-name')}"]`, $dropdownContainer); url: $dropdown.data('refs-url'),
const $filterInput = $('input[type="search"]', $dropdownContainer); data: {
$dropdown.glDropdown({ ref: $dropdown.data('ref'),
data: function(term, callback) { search: term,
return $.ajax({
url: $dropdown.data('refs-url'),
data: {
ref: $dropdown.data('ref'),
search: term,
}
}).done(function(refs) {
return callback(refs);
});
},
selectable: true,
filterable: true,
filterRemote: true,
fieldName: $dropdown.data('field-name'),
filterInput: 'input[type="search"]',
renderRow: function(ref) {
var link;
if (ref.header != null) {
return $('<li />').addClass('dropdown-header').text(ref.header);
} else {
link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref));
return $('<li />').append(link);
} }
}, }).done(function(refs) {
id: function(obj, $el) { return callback(refs);
return $el.attr('data-ref'); });
}, },
toggleLabel: function(obj, $el) { selectable: true,
return $el.text().trim(); filterable: true,
} filterRemote: true,
}); fieldName: $dropdown.data('field-name'),
$filterInput.on('keyup', (e) => { filterInput: 'input[type="search"]',
const keyCode = e.keyCode || e.which; renderRow: function(ref) {
if (keyCode !== 13) return; var link;
const text = $filterInput.val(); if (ref.header != null) {
$fieldInput.val(text); return $('<li />').addClass('dropdown-header').text(ref.header);
$('.dropdown-toggle-text', $dropdown).text(text); } else {
$dropdownContainer.removeClass('open'); link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref));
}); return $('<li />').append(link);
$dropdownContainer.on('click', '.dropdown-content a', (e) => {
$dropdown.prop('title', e.target.text.replace(/_+?/g, '-'));
if ($dropdown.hasClass('has-tooltip')) {
$dropdown.tooltip('fixTitle');
} }
}); },
id: function(obj, $el) {
return $el.attr('data-ref');
},
toggleLabel: function(obj, $el) {
return $el.text().trim();
}
});
$filterInput.on('keyup', (e) => {
const keyCode = e.keyCode || e.which;
if (keyCode !== 13) return;
const text = $filterInput.val();
$fieldInput.val(text);
$('.dropdown-toggle-text', $dropdown).text(text);
$dropdownContainer.removeClass('open');
}); });
};
return CompareAutocomplete; $dropdownContainer.on('click', '.dropdown-content a', (e) => {
})(); $dropdown.prop('title', e.target.text.replace(/_+?/g, '-'));
if ($dropdown.hasClass('has-tooltip')) {
$dropdown.tooltip('fixTitle');
}
});
});
}
...@@ -22,8 +22,8 @@ import NewCommitForm from './new_commit_form'; ...@@ -22,8 +22,8 @@ import NewCommitForm from './new_commit_form';
import Project from './project'; import Project from './project';
import projectAvatar from './project_avatar'; import projectAvatar from './project_avatar';
/* global MergeRequest */ /* global MergeRequest */
/* global Compare */ import Compare from './compare';
/* global CompareAutocomplete */ import initCompareAutocomplete from './compare_autocomplete';
/* global PathLocks */ /* global PathLocks */
/* global ProjectFindFile */ /* global ProjectFindFile */
import ProjectNew from './project_new'; import ProjectNew from './project_new';
...@@ -715,7 +715,7 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -715,7 +715,7 @@ import initGroupAnalytics from './init_group_analytics';
projectAvatar(); projectAvatar();
switch (path[1]) { switch (path[1]) {
case 'compare': case 'compare':
new CompareAutocomplete(); initCompareAutocomplete();
break; break;
case 'edit': case 'edit':
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
......
This diff is collapsed.
...@@ -123,18 +123,23 @@ export default { ...@@ -123,18 +123,23 @@ export default {
:title="group.fullName" :title="group.fullName"
class="no-expand" class="no-expand"
data-placement="top" data-placement="top"
> >{{
{{group.name}} // ending bracket must be by closing tag to prevent
</a> // link hover text-decoration from over-extending
group.name
}}</a>
<span <span
v-if="group.permission" v-if="group.permission"
class="access-type" class="user-access-role"
> >
{{s__('GroupsTreeRole|as')}} {{group.permission}} {{group.permission}}
</span> </span>
</div> </div>
<div <div
class="description">{{group.description}}</div> v-if="group.description"
class="description">
{{group.description}}
</div>
</div> </div>
<group-folder <group-folder
v-if="group.isOpen && hasChildren" v-if="group.isOpen && hasChildren"
......
...@@ -248,7 +248,6 @@ export default { ...@@ -248,7 +248,6 @@ export default {
:project-path="projectPath" :project-path="projectPath"
:project-namespace="projectNamespace" :project-namespace="projectNamespace"
:show-delete-button="showDeleteButton" :show-delete-button="showDeleteButton"
:can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete" :enable-autocomplete="enableAutocomplete"
/> />
<div v-else> <div v-else>
......
...@@ -17,11 +17,6 @@ ...@@ -17,11 +17,6 @@
type: String, type: String,
required: true, required: true,
}, },
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
enableAutocomplete: { enableAutocomplete: {
type: Boolean, type: Boolean,
required: false, required: false,
...@@ -47,7 +42,6 @@ ...@@ -47,7 +42,6 @@
<markdown-field <markdown-field
:markdown-preview-path="markdownPreviewPath" :markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath" :markdown-docs-path="markdownDocsPath"
:can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete" :enable-autocomplete="enableAutocomplete"
> >
<textarea <textarea
......
...@@ -41,11 +41,6 @@ ...@@ -41,11 +41,6 @@
required: false, required: false,
default: true, default: true,
}, },
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
enableAutocomplete: { enableAutocomplete: {
type: Boolean, type: Boolean,
required: false, required: false,
...@@ -94,7 +89,6 @@ ...@@ -94,7 +89,6 @@
:form-state="formState" :form-state="formState"
:markdown-preview-path="markdownPreviewPath" :markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath" :markdown-docs-path="markdownDocsPath"
:can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete" :enable-autocomplete="enableAutocomplete"
/> />
<edit-actions <edit-actions
......
...@@ -9,7 +9,7 @@ export default class Job { ...@@ -9,7 +9,7 @@ export default class Job {
this.state = null; this.state = null;
this.options = options || $('.js-build-options').data(); this.options = options || $('.js-build-options').data();
this.pageUrl = this.options.pageUrl; this.pagePath = this.options.pagePath;
this.buildStatus = this.options.buildStatus; this.buildStatus = this.options.buildStatus;
this.state = this.options.logState; this.state = this.options.logState;
this.buildStage = this.options.buildStage; this.buildStage = this.options.buildStage;
...@@ -167,11 +167,11 @@ export default class Job { ...@@ -167,11 +167,11 @@ export default class Job {
getBuildTrace() { getBuildTrace() {
return $.ajax({ return $.ajax({
url: `${this.pageUrl}/trace.json`, url: `${this.pagePath}/trace.json`,
data: { state: this.state }, data: { state: this.state },
}) })
.done((log) => { .done((log) => {
setCiStatusFavicon(`${this.pageUrl}/status.json`); setCiStatusFavicon(`${this.pagePath}/status.json`);
if (log.state) { if (log.state) {
this.state = log.state; this.state = log.state;
...@@ -209,7 +209,7 @@ export default class Job { ...@@ -209,7 +209,7 @@ export default class Job {
} }
if (log.status !== this.buildStatus) { if (log.status !== this.buildStatus) {
gl.utils.visitUrl(this.pageUrl); gl.utils.visitUrl(this.pagePath);
} }
}) })
.fail(() => { .fail(() => {
......
...@@ -40,9 +40,6 @@ import './admin'; ...@@ -40,9 +40,6 @@ import './admin';
import './aside'; import './aside';
import loadAwardsHandler from './awards_handler'; import loadAwardsHandler from './awards_handler';
import bp from './breakpoints'; import bp from './breakpoints';
import './commits';
import './compare';
import './compare_autocomplete';
import './confirm_danger_modal'; import './confirm_danger_modal';
import Flash, { removeFlashClickListener } from './flash'; import Flash, { removeFlashClickListener } from './flash';
import './gl_dropdown'; import './gl_dropdown';
......
...@@ -86,7 +86,7 @@ ...@@ -86,7 +86,7 @@
<div class="note-actions"> <div class="note-actions">
<span <span
v-if="accessLevel" v-if="accessLevel"
class="note-role note-role-access">{{accessLevel}}</span> class="note-role user-access-role">{{accessLevel}}</span>
<div <div
v-if="canAddAwardEmoji" v-if="canAddAwardEmoji"
class="note-actions-item"> class="note-actions-item">
......
...@@ -8,10 +8,18 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ ...@@ -8,10 +8,18 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
}, },
data() { data() {
const notesDataset = document.getElementById('js-vue-notes').dataset; const notesDataset = document.getElementById('js-vue-notes').dataset;
const parsedUserData = JSON.parse(notesDataset.currentUserData);
const currentUserData = parsedUserData ? {
id: parsedUserData.id,
name: parsedUserData.name,
username: parsedUserData.username,
avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url,
path: parsedUserData.path,
} : {};
return { return {
noteableData: JSON.parse(notesDataset.noteableData), noteableData: JSON.parse(notesDataset.noteableData),
currentUserData: JSON.parse(notesDataset.currentUserData), currentUserData,
notesData: { notesData: {
lastFetchedAt: notesDataset.lastFetchedAt, lastFetchedAt: notesDataset.lastFetchedAt,
discussionsPath: notesDataset.discussionsPath, discussionsPath: notesDataset.discussionsPath,
......
import Flash from '../../../flash'; import Flash from '../../../flash';
import AssigneeTitle from './assignee_title'; import AssigneeTitle from './assignee_title';
import Assignees from './assignees'; import Assignees from './assignees';
import Store from '../../stores/sidebar_store'; import Store from '../../stores/sidebar_store';
import Mediator from '../../sidebar_mediator';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
export default { export default {
name: 'SidebarAssignees', name: 'SidebarAssignees',
data() { data() {
return { return {
mediator: new Mediator(),
store: new Store(), store: new Store(),
loading: false, loading: false,
field: '',
}; };
}, },
props: {
mediator: {
type: Object,
required: true,
},
field: {
type: String,
required: true,
},
signedIn: {
type: Boolean,
required: false,
default: false,
},
},
components: { components: {
'assignee-title': AssigneeTitle, 'assignee-title': AssigneeTitle,
assignees: Assignees, assignees: Assignees,
...@@ -61,10 +71,6 @@ export default { ...@@ -61,10 +71,6 @@ export default {
eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees); eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees);
eventHub.$off('sidebar.saveAssignees', this.saveAssignees); eventHub.$off('sidebar.saveAssignees', this.saveAssignees);
}, },
beforeMount() {
this.field = this.$el.dataset.field;
this.signedIn = typeof this.$el.dataset.signedIn !== 'undefined';
},
template: ` template: `
<div> <div>
<assignee-title <assignee-title
......
<script> <script>
import Store from '../../stores/sidebar_store'; import Store from '../../stores/sidebar_store';
import Mediator from '../../sidebar_mediator';
import participants from './participants.vue'; import participants from './participants.vue';
export default { export default {
data() { data() {
return { return {
mediator: new Mediator(),
store: new Store(), store: new Store(),
}; };
}, },
props: {
mediator: {
type: Object,
required: true,
},
},
components: { components: {
participants, participants,
}, },
...@@ -21,6 +25,7 @@ export default { ...@@ -21,6 +25,7 @@ export default {
<participants <participants
:loading="store.isFetching.participants" :loading="store.isFetching.participants"
:participants="store.participants" :participants="store.participants"
:number-of-less-participants="7" /> :number-of-less-participants="7"
/>
</div> </div>
</template> </template>
<script> <script>
import Store from '../../stores/sidebar_store'; import Store from '../../stores/sidebar_store';
import Mediator from '../../sidebar_mediator';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
import Flash from '../../../flash'; import Flash from '../../../flash';
import { __ } from '../../../locale'; import { __ } from '../../../locale';
...@@ -9,11 +8,15 @@ import subscriptions from './subscriptions.vue'; ...@@ -9,11 +8,15 @@ import subscriptions from './subscriptions.vue';
export default { export default {
data() { data() {
return { return {
mediator: new Mediator(),
store: new Store(), store: new Store(),
}; };
}, },
props: {
mediator: {
type: Object,
required: true,
},
},
components: { components: {
subscriptions, subscriptions,
}, },
......
...@@ -10,6 +10,27 @@ import Translate from '../vue_shared/translate'; ...@@ -10,6 +10,27 @@ import Translate from '../vue_shared/translate';
Vue.use(Translate); Vue.use(Translate);
function mountAssigneesComponent(mediator) {
const el = document.getElementById('js-vue-sidebar-assignees');
if (!el) return;
// eslint-disable-next-line no-new
new Vue({
el,
components: {
SidebarAssignees,
},
render: createElement => createElement('sidebar-assignees', {
props: {
mediator,
field: el.dataset.field,
signedIn: el.hasAttribute('data-signed-in'),
},
}),
});
}
function mountConfidentialComponent(mediator) { function mountConfidentialComponent(mediator) {
const el = document.getElementById('js-confidential-entry-point'); const el = document.getElementById('js-confidential-entry-point');
...@@ -49,9 +70,10 @@ function mountLockComponent(mediator) { ...@@ -49,9 +70,10 @@ function mountLockComponent(mediator) {
}).$mount(el); }).$mount(el);
} }
function mountParticipantsComponent() { function mountParticipantsComponent(mediator) {
const el = document.querySelector('.js-sidebar-participants-entry-point'); const el = document.querySelector('.js-sidebar-participants-entry-point');
// eslint-disable-next-line no-new
if (!el) return; if (!el) return;
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
...@@ -60,11 +82,15 @@ function mountParticipantsComponent() { ...@@ -60,11 +82,15 @@ function mountParticipantsComponent() {
components: { components: {
sidebarParticipants, sidebarParticipants,
}, },
render: createElement => createElement('sidebar-participants', {}), render: createElement => createElement('sidebar-participants', {
props: {
mediator,
},
}),
}); });
} }
function mountSubscriptionsComponent() { function mountSubscriptionsComponent(mediator) {
const el = document.querySelector('.js-sidebar-subscriptions-entry-point'); const el = document.querySelector('.js-sidebar-subscriptions-entry-point');
if (!el) return; if (!el) return;
...@@ -75,22 +101,35 @@ function mountSubscriptionsComponent() { ...@@ -75,22 +101,35 @@ function mountSubscriptionsComponent() {
components: { components: {
sidebarSubscriptions, sidebarSubscriptions,
}, },
render: createElement => createElement('sidebar-subscriptions', {}), render: createElement => createElement('sidebar-subscriptions', {
props: {
mediator,
},
}),
}); });
} }
function mount(mediator) { function mountTimeTrackingComponent() {
const sidebarAssigneesEl = document.getElementById('js-vue-sidebar-assignees'); const el = document.getElementById('issuable-time-tracker');
// Only create the sidebarAssignees vue app if it is found in the DOM
// We currently do not use sidebarAssignees for the MR page
if (sidebarAssigneesEl) {
new Vue(SidebarAssignees).$mount(sidebarAssigneesEl);
}
if (!el) return;
// eslint-disable-next-line no-new
new Vue({
el,
components: {
SidebarTimeTracking,
},
render: createElement => createElement('sidebar-time-tracking', {}),
});
}
export function mountSidebar(mediator) {
mountAssigneesComponent(mediator);
mountConfidentialComponent(mediator); mountConfidentialComponent(mediator);
mountLockComponent(mediator); mountLockComponent(mediator);
mountParticipantsComponent(); mountParticipantsComponent(mediator);
mountSubscriptionsComponent(); mountSubscriptionsComponent(mediator);
new SidebarMoveIssue( new SidebarMoveIssue(
mediator, mediator,
...@@ -98,7 +137,9 @@ function mount(mediator) { ...@@ -98,7 +137,9 @@ function mount(mediator) {
$('.js-move-issue-confirmation-button'), $('.js-move-issue-confirmation-button'),
).init(); ).init();
new Vue(SidebarTimeTracking).$mount('#issuable-time-tracker'); mountTimeTrackingComponent();
} }
export default mount; export function getSidebarOptions() {
return JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
}
import mountSidebarEE from 'ee/sidebar/mount_sidebar'; import Mediator from './sidebar_mediator';
import Mediator from 'ee/sidebar/sidebar_mediator'; import { mountSidebar, getSidebarOptions } from './mount_sidebar';
import mountSidebar from './mount_sidebar';
function domContentLoaded() { function domContentLoaded() {
const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML); const mediator = new Mediator(getSidebarOptions());
const mediator = new Mediator(sidebarOptions);
mediator.fetch(); mediator.fetch();
mountSidebar(mediator); mountSidebar(mediator);
mountSidebarEE(mediator);
} }
document.addEventListener('DOMContentLoaded', domContentLoaded); document.addEventListener('DOMContentLoaded', domContentLoaded);
......
...@@ -7,7 +7,6 @@ export default class SidebarMediator { ...@@ -7,7 +7,6 @@ export default class SidebarMediator {
if (!SidebarMediator.singleton) { if (!SidebarMediator.singleton) {
this.initSingleton(options); this.initSingleton(options);
} }
return SidebarMediator.singleton; return SidebarMediator.singleton;
} }
......
export default class SidebarStore { export default class SidebarStore {
constructor(store) { constructor(options) {
if (!SidebarStore.singleton) { if (!SidebarStore.singleton) {
const { currentUser, rootPath, editable } = store; this.initSingleton(options);
this.currentUser = currentUser;
this.rootPath = rootPath;
this.editable = editable;
this.timeEstimate = 0;
this.totalTimeSpent = 0;
this.humanTimeEstimate = '';
this.humanTimeSpent = '';
this.assignees = [];
this.isFetching = {
assignees: true,
participants: true,
subscriptions: true,
};
this.isLoading = {};
this.autocompleteProjects = [];
this.moveToProjectId = 0;
this.isLockDialogOpen = false;
this.participants = [];
this.subscribed = null;
SidebarStore.singleton = this;
} }
return SidebarStore.singleton; return SidebarStore.singleton;
} }
initSingleton(options) {
const { currentUser, rootPath, editable } = options;
this.currentUser = currentUser;
this.rootPath = rootPath;
this.editable = editable;
this.timeEstimate = 0;
this.totalTimeSpent = 0;
this.humanTimeEstimate = '';
this.humanTimeSpent = '';
this.assignees = [];
this.isFetching = {
assignees: true,
participants: true,
subscriptions: true,
};
this.isLoading = {};
this.autocompleteProjects = [];
this.moveToProjectId = 0;
this.isLockDialogOpen = false;
this.participants = [];
this.subscribed = null;
SidebarStore.singleton = this;
}
setAssigneeData(data) { setAssigneeData(data) {
this.isFetching.assignees = false; this.isFetching.assignees = false;
if (data.assignees) { if (data.assignees) {
......
...@@ -25,11 +25,6 @@ ...@@ -25,11 +25,6 @@
type: String, type: String,
required: false, required: false,
}, },
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
enableAutocomplete: { enableAutocomplete: {
type: Boolean, type: Boolean,
required: false, required: false,
...@@ -139,7 +134,6 @@ ...@@ -139,7 +134,6 @@
<markdown-toolbar <markdown-toolbar
:markdown-docs-path="markdownDocsPath" :markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath" :quick-actions-docs-path="quickActionsDocsPath"
:can-attach-file="canAttachFile"
/> />
</div> </div>
</div> </div>
......
...@@ -9,11 +9,6 @@ ...@@ -9,11 +9,6 @@
type: String, type: String,
required: false, required: false,
}, },
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
}, },
}; };
</script> </script>
...@@ -46,10 +41,7 @@ ...@@ -46,10 +41,7 @@
are supported are supported
</template> </template>
</div> </div>
<span <span class="uploading-container">
v-if="canAttachFile"
class="uploading-container"
>
<span class="uploading-progress-container hide"> <span class="uploading-progress-container hide">
<i <i
class="fa fa-file-image-o toolbar-button-icon" class="fa fa-file-image-o toolbar-button-icon"
......
...@@ -58,3 +58,4 @@ ...@@ -58,3 +58,4 @@
@import "framework/snippets"; @import "framework/snippets";
@import "framework/memory_graph"; @import "framework/memory_graph";
@import "framework/responsive_tables"; @import "framework/responsive_tables";
@import "framework/stacked-progress-bar";
...@@ -463,13 +463,11 @@ ul.indent-list { ...@@ -463,13 +463,11 @@ ul.indent-list {
ul.group-list-tree { ul.group-list-tree {
li.group-row { li.group-row {
&.has-description { &.has-description .title {
.title { line-height: inherit;
line-height: inherit;
}
} }
.title { &:not(.has-description) .title {
line-height: $list-text-height; line-height: $list-text-height;
} }
} }
......
...@@ -50,6 +50,11 @@ ...@@ -50,6 +50,11 @@
&:not(.disabled) { &:not(.disabled) {
cursor: pointer; cursor: pointer;
} }
svg {
width: $gl-padding;
height: $gl-padding;
}
} }
} }
......
.stacked-progress-bar {
display: flex;
height: 20px;
width: 350px;
border-radius: 10px;
overflow: hidden;
background-color: $theme-gray-100;
.status-unavailable,
.status-green,
.status-neutral,
.status-red, {
height: 100%;
font-weight: normal;
color: $white-light;
line-height: 20px;
&.has-value {
padding: 0 10px;
}
&:hover {
cursor: pointer;
}
}
.status-unavailable {
padding: 0 10px;
color: $theme-gray-700;
}
.status-green {
background-color: $green-500;
&:hover {
background-color: $green-600;
}
}
.status-neutral {
background-color: $theme-gray-200;
color: $gl-gray-dark;
&:hover {
background-color: $theme-gray-300;
}
}
.status-red {
background-color: $red-500;
&:hover {
background-color: $red-600;
}
}
}
...@@ -415,7 +415,6 @@ $location-icon-color: #e7e9ed; ...@@ -415,7 +415,6 @@ $location-icon-color: #e7e9ed;
* Notes * Notes
*/ */
$notes-light-color: $gl-text-color-secondary; $notes-light-color: $gl-text-color-secondary;
$notes-role-color: $gl-text-color-secondary;
$note-disabled-comment-color: #b2b2b2; $note-disabled-comment-color: #b2b2b2;
$note-targe3-outside: #fffff0; $note-targe3-outside: #fffff0;
$note-targe3-inside: #ffffd3; $note-targe3-inside: #ffffd3;
...@@ -564,6 +563,7 @@ $jq-ui-default-color: #777; ...@@ -564,6 +563,7 @@ $jq-ui-default-color: #777;
/* /*
* Label * Label
*/ */
$label-padding: 7px;
$label-gray-bg: #f8fafc; $label-gray-bg: #f8fafc;
$label-inverse-bg: #333; $label-inverse-bg: #333;
$label-remove-border: rgba(0, 0, 0, .1); $label-remove-border: rgba(0, 0, 0, .1);
......
.geo-admin-container {
.page-title,
.page-title-separator {
margin-top: 10px;
}
.title-text {
line-height: 34px;
}
.page-subtitle {
margin-bottom: 24px;
}
}
.geo-node-status {
td {
vertical-align: top;
}
.help-block {
width: 135px;
text-align: right;
}
.node-info {
font-weight: $gl-font-weight-bold;
}
.event-timestamp {
font-weight: normal;
color: $theme-gray-800;
}
.sync-status {
font-weight: normal;
svg {
vertical-align: middle;
}
.sync-status-icon svg,
.sync-status-timestamp {
fill: $theme-gray-700;
color: $theme-gray-700;
}
&.sync-status-failure {
.sync-status-icon svg,
.sync-status-timestamp {
fill: $red-700;
color: $red-700;
}
}
}
}
.advanced-geo-node-status-toggler {
display: block;
.show-advance-chevron {
margin-top: 2px;
}
}
.geo-node-healthy { .geo-node-healthy {
color: $gl-success; color: $gl-success;
} }
...@@ -52,6 +117,20 @@ ...@@ -52,6 +117,20 @@
white-space: pre-wrap; white-space: pre-wrap;
} }
.geo-nodes {
.health-message {
padding: 1px 8px;
background-color: $red-100;
color: $red-500;
border-radius: $border-radius-default;
font-weight: 500;
}
}
.geo-node-version-mismatch {
color: $gl-danger;
}
.node-badge { .node-badge {
color: $white-light; color: $white-light;
display: inline-block; display: inline-block;
......
...@@ -262,6 +262,18 @@ table.pipeline-project-metrics tr td { ...@@ -262,6 +262,18 @@ table.pipeline-project-metrics tr td {
} }
} }
.user-access-role {
display: inline-block;
color: $gl-text-color-secondary;
font-size: 12px;
line-height: 20px;
margin: -5px 3px;
padding: 0 $label-padding;
border: 1px solid $border-color;
border-radius: $label-border-radius;
font-weight: $gl-font-weight-normal;
}
.plan-badge { .plan-badge {
margin-right: 15px; margin-right: 15px;
......
...@@ -471,7 +471,8 @@ ...@@ -471,7 +471,8 @@
} }
} }
.milestone-title span { .milestone-title span,
.collapse-truncated-title {
@include str-truncated(100%); @include str-truncated(100%);
display: block; display: block;
margin: 0 4px; margin: 0 4px;
......
...@@ -104,7 +104,7 @@ ...@@ -104,7 +104,7 @@
} }
.color-label { .color-label {
padding: 3px 7px; padding: 3px $label-padding;
border-radius: $label-border-radius; border-radius: $label-border-radius;
} }
......
...@@ -811,6 +811,10 @@ ...@@ -811,6 +811,10 @@
.failed { .failed {
color: $red-500; color: $red-500;
} }
.neutral {
color: $gl-gray-light;
}
} }
} }
} }
......
...@@ -619,26 +619,17 @@ ul.notes { ...@@ -619,26 +619,17 @@ ul.notes {
} }
.note-role { .note-role {
margin: 0 3px;
}
.note-role-special {
position: relative; position: relative;
display: inline-block; display: inline-block;
color: $notes-role-color; color: $gl-text-color-secondary;
font-size: 12px; font-size: 12px;
line-height: 20px; text-shadow: 0 0 15px $gl-text-color-inverted;
margin: 0 3px;
&.note-role-access {
padding: 0 7px;
border: 1px solid $border-color;
border-radius: $label-border-radius;
}
&.note-role-special {
text-shadow: 0 0 15px $gl-text-color-inverted;
}
} }
/** /**
* Line note button on the side of diffs * Line note button on the side of diffs
*/ */
......
module RendersMemberAccess
def prepare_groups_for_rendering(groups)
preload_max_member_access_for_collection(Group, groups)
groups
end
def prepare_projects_for_rendering(projects)
preload_max_member_access_for_collection(Project, projects)
projects
end
private
def preload_max_member_access_for_collection(klass, collection)
return if !current_user || collection.blank?
method_name = "max_member_access_for_#{klass.name.underscore}_ids"
current_user.public_send(method_name, collection.ids) # rubocop:disable GitlabSecurity/PublicSend
end
end
module UploadsActions module UploadsActions
include Gitlab::Utils::StrongMemoize
def create def create
link_to_file = UploadService.new(model, params[:file], uploader_class).execute link_to_file = UploadService.new(model, params[:file], uploader_class).execute
...@@ -24,4 +26,25 @@ module UploadsActions ...@@ -24,4 +26,25 @@ module UploadsActions
send_file uploader.file.path, disposition: disposition send_file uploader.file.path, disposition: disposition
end end
private
def uploader
strong_memoize(:uploader) do
return if show_model.nil?
file_uploader = FileUploader.new(show_model, params[:secret])
file_uploader.retrieve_from_store!(params[:filename])
file_uploader
end
end
def image_or_video?
uploader && uploader.exists? && uploader.image_or_video?
end
def uploader_class
FileUploader
end
end end
class Dashboard::ProjectsController < Dashboard::ApplicationController class Dashboard::ProjectsController < Dashboard::ApplicationController
include ParamsBackwardCompatibility include ParamsBackwardCompatibility
include RendersMemberAccess
before_action :set_non_archived_param before_action :set_non_archived_param
before_action :default_sorting before_action :default_sorting
...@@ -45,10 +46,12 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -45,10 +46,12 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
end end
def load_projects(finder_params) def load_projects(finder_params)
ProjectsFinder projects = ProjectsFinder
.new(params: finder_params, current_user: current_user) .new(params: finder_params, current_user: current_user)
.execute .execute
.includes(:route, :creator, namespace: [:route, :owner]) .includes(:route, :creator, namespace: [:route, :owner])
prepare_projects_for_rendering(projects)
end end
def load_events def load_events
......
class Explore::ProjectsController < Explore::ApplicationController class Explore::ProjectsController < Explore::ApplicationController
include ParamsBackwardCompatibility include ParamsBackwardCompatibility
include RendersMemberAccess
before_action :set_non_archived_param before_action :set_non_archived_param
...@@ -49,10 +50,12 @@ class Explore::ProjectsController < Explore::ApplicationController ...@@ -49,10 +50,12 @@ class Explore::ProjectsController < Explore::ApplicationController
private private
def load_projects def load_projects
ProjectsFinder.new(current_user: current_user, params: params) projects = ProjectsFinder.new(current_user: current_user, params: params)
.execute .execute
.includes(:route, namespace: :route) .includes(:route, namespace: :route)
.page(params[:page]) .page(params[:page])
.without_count .without_count
prepare_projects_for_rendering(projects)
end end
end end
class Groups::UploadsController < Groups::ApplicationController
include UploadsActions
skip_before_action :group, if: -> { action_name == 'show' && image_or_video? }
before_action :authorize_upload_file!, only: [:create]
private
def show_model
strong_memoize(:show_model) do
group_id = params[:group_id]
Group.find_by_full_path(group_id)
end
end
def authorize_upload_file!
render_404 unless can?(current_user, :upload_file, group)
end
def uploader
strong_memoize(:uploader) do
file_uploader = uploader_class.new(show_model, params[:secret])
file_uploader.retrieve_from_store!(params[:filename])
file_uploader
end
end
def uploader_class
NamespaceFileUploader
end
alias_method :model, :group
end
...@@ -8,31 +8,13 @@ class Projects::UploadsController < Projects::ApplicationController ...@@ -8,31 +8,13 @@ class Projects::UploadsController < Projects::ApplicationController
private private
def uploader def show_model
return @uploader if defined?(@uploader) strong_memoize(:show_model) do
namespace = params[:namespace_id]
id = params[:project_id]
namespace = params[:namespace_id] Project.find_by_full_path("#{namespace}/#{id}")
id = params[:project_id]
file_project = Project.find_by_full_path("#{namespace}/#{id}")
if file_project.nil?
@uploader = nil
return
end end
@uploader = FileUploader.new(file_project, params[:secret])
@uploader.retrieve_from_store!(params[:filename])
@uploader
end
def image_or_video?
uploader && uploader.exists? && uploader.image_or_video?
end
def uploader_class
FileUploader
end end
alias_method :model, :project alias_method :model, :project
......
class UsersController < ApplicationController class UsersController < ApplicationController
include RoutableActions include RoutableActions
include RendersMemberAccess
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
before_action :user, except: [:exists] before_action :user, except: [:exists]
...@@ -116,14 +117,20 @@ class UsersController < ApplicationController ...@@ -116,14 +117,20 @@ class UsersController < ApplicationController
@projects = @projects =
PersonalProjectsFinder.new(user).execute(current_user) PersonalProjectsFinder.new(user).execute(current_user)
.page(params[:page]) .page(params[:page])
prepare_projects_for_rendering(@projects)
end end
def load_contributed_projects def load_contributed_projects
@contributed_projects = contributed_projects.joined(user) @contributed_projects = contributed_projects.joined(user)
prepare_projects_for_rendering(@contributed_projects)
end end
def load_groups def load_groups
@groups = JoinedGroupsFinder.new(user).execute(current_user) @groups = JoinedGroupsFinder.new(user).execute(current_user)
prepare_groups_for_rendering(@groups)
end end
def load_snippets def load_snippets
......
...@@ -20,8 +20,7 @@ module BuildsHelper ...@@ -20,8 +20,7 @@ module BuildsHelper
def javascript_build_options def javascript_build_options
{ {
page_url: project_job_url(@project, @build), page_path: project_job_path(@project, @build),
build_url: project_job_url(@project, @build, :json),
build_status: @build.status, build_status: @build.status,
build_stage: @build.stage, build_stage: @build.stage,
log_state: '' log_state: ''
......
...@@ -86,6 +86,8 @@ module MarkupHelper ...@@ -86,6 +86,8 @@ module MarkupHelper
return '' unless text.present? return '' unless text.present?
context[:project] ||= @project context[:project] ||= @project
context[:group] ||= @group
html = markdown_unsafe(text, context) html = markdown_unsafe(text, context)
prepare_for_rendering(html, context) prepare_for_rendering(html, context)
end end
......
...@@ -253,7 +253,7 @@ module Ci ...@@ -253,7 +253,7 @@ module Ci
variables += pipeline.predefined_variables variables += pipeline.predefined_variables
variables += runner.predefined_variables if runner variables += runner.predefined_variables if runner
variables += project.container_registry_variables variables += project.container_registry_variables
variables += project.deployment_variables if has_environment? variables += project.deployment_variables(environment: environment) if has_environment?
variables += project.auto_devops_variables variables += project.auto_devops_variables
variables += yaml_variables variables += yaml_variables
variables += user_variables variables += user_variables
......
module Ci module Ci
class JobArtifact < ActiveRecord::Base class JobArtifact < ActiveRecord::Base
include AfterCommitQueue
extend Gitlab::Ci::Model extend Gitlab::Ci::Model
belongs_to :project belongs_to :project
...@@ -9,6 +10,12 @@ module Ci ...@@ -9,6 +10,12 @@ module Ci
mount_uploader :file, JobArtifactUploader mount_uploader :file, JobArtifactUploader
after_save if: :file_changed?, on: [:create, :update] do
run_after_commit do
file.schedule_migration_to_object_storage
end
end
enum file_type: { enum file_type: {
archive: 1, archive: 1,
metadata: 2 metadata: 2
......
...@@ -3,7 +3,7 @@ module Ci ...@@ -3,7 +3,7 @@ module Ci
extend Gitlab::Ci::Model extend Gitlab::Ci::Model
include HasVariable include HasVariable
include Presentable include Presentable
prepend EE::Ci::Variable prepend HasEnvironmentScope
belongs_to :project belongs_to :project
......
module Clusters module Clusters
class Cluster < ActiveRecord::Base class Cluster < ActiveRecord::Base
include Presentable include Presentable
prepend HasEnvironmentScope
self.table_name = 'clusters' self.table_name = 'clusters'
...@@ -26,6 +27,7 @@ module Clusters ...@@ -26,6 +27,7 @@ module Clusters
accepts_nested_attributes_for :platform_kubernetes, update_only: true accepts_nested_attributes_for :platform_kubernetes, update_only: true
validates :name, cluster_name: true validates :name, cluster_name: true
validate :unique_environment_scope, if: :has_project?
validate :restrict_modification, on: :update validate :restrict_modification, on: :update
delegate :status, to: :provider, allow_nil: true delegate :status, to: :provider, allow_nil: true
...@@ -91,6 +93,15 @@ module Clusters ...@@ -91,6 +93,15 @@ module Clusters
private private
def unique_environment_scope
if project.clusters.where(environment_scope: environment_scope).where.not(id: self.id).exists?
errors.add(:base, "cannot add duplicated environment scope")
return false
end
true
end
def restrict_modification def restrict_modification
if provider&.on_creation? if provider&.on_creation?
errors.add(:base, "cannot modify during creation") errors.add(:base, "cannot modify during creation")
...@@ -99,5 +110,9 @@ module Clusters ...@@ -99,5 +110,9 @@ module Clusters
true true
end end
def has_project?
projects.exists?
end
end end
end end
# Returns and caches in thread max member access for a resource
#
module BulkMemberAccessLoad
extend ActiveSupport::Concern
included do
# Determine the maximum access level for a group of resources in bulk.
#
# Returns a Hash mapping resource ID -> maximum access level.
def max_member_access_for_resource_ids(resource_klass, resource_ids, memoization_index = self.id, &block)
raise 'Block is mandatory' unless block_given?
resource_ids = resource_ids.uniq
key = max_member_access_for_resource_key(resource_klass, memoization_index)
access = {}
if RequestStore.active?
RequestStore.store[key] ||= {}
access = RequestStore.store[key]
end
# Look up only the IDs we need
resource_ids = resource_ids - access.keys
return access if resource_ids.empty?
resource_access = yield(resource_ids)
access.merge!(resource_access)
missing_resource_ids = resource_ids - resource_access.keys
missing_resource_ids.each do |resource_id|
access[resource_id] = Gitlab::Access::NO_ACCESS
end
access
end
private
def max_member_access_for_resource_key(klass, memoization_index)
"max_member_access_for_#{klass.name.underscore.pluralize}:#{memoization_index}"
end
end
end
...@@ -61,7 +61,7 @@ module Mentionable ...@@ -61,7 +61,7 @@ module Mentionable
cache_key: [self, attr], cache_key: [self, attr],
author: author, author: author,
skip_project_check: skip_project_check? skip_project_check: skip_project_check?
) ).merge(mentionable_params)
extractor.analyze(text, options) extractor.analyze(text, options)
end end
...@@ -82,7 +82,7 @@ module Mentionable ...@@ -82,7 +82,7 @@ module Mentionable
return [] unless matches_cross_reference_regex? return [] unless matches_cross_reference_regex?
refs = all_references(current_user) refs = all_references(current_user)
refs = (refs.issues + refs.merge_requests + refs.commits) refs = (refs.issues + refs.merge_requests + refs.commits + refs.epics)
# We're using this method instead of Array diffing because that requires # We're using this method instead of Array diffing because that requires
# both of the object's `hash` values to be the same, which may not be the # both of the object's `hash` values to be the same, which may not be the
...@@ -157,4 +157,8 @@ module Mentionable ...@@ -157,4 +157,8 @@ module Mentionable
def skip_project_check? def skip_project_check?
false false
end end
def mentionable_params
{}
end
end end
...@@ -3,6 +3,7 @@ module Mentionable ...@@ -3,6 +3,7 @@ module Mentionable
def self.reference_pattern(link_patterns, issue_pattern) def self.reference_pattern(link_patterns, issue_pattern)
Regexp.union(link_patterns, Regexp.union(link_patterns,
issue_pattern, issue_pattern,
Epic.reference_pattern,
Commit.reference_pattern, Commit.reference_pattern,
MergeRequest.reference_pattern) MergeRequest.reference_pattern)
end end
......
...@@ -137,16 +137,20 @@ class Environment < ActiveRecord::Base ...@@ -137,16 +137,20 @@ class Environment < ActiveRecord::Base
end end
end end
def deployment_platform
project.deployment_platform(environment: self)
end
def has_terminals? def has_terminals?
project.deployment_platform.present? && available? && last_deployment.present? deployment_platform.present? && available? && last_deployment.present?
end end
def terminals def terminals
project.deployment_platform.terminals(self) if has_terminals? deployment_platform.terminals(self) if has_terminals?
end end
def rollout_status def rollout_status
project.deployment_platform.rollout_status(self) if has_terminals? deployment_platform.rollout_status(self) if has_terminals?
end end
def has_metrics? def has_metrics?
......
# Placeholder class for model that is implemented in EE # Placeholder class for model that is implemented in EE
# It will reserve (ee#3853) '&' as a reference prefix, but the table does not exists in CE # It reserves '&' as a reference prefix, but the table does not exists in CE
class Epic < ActiveRecord::Base class Epic < ActiveRecord::Base
prepend EE::Epic prepend EE::Epic
# TODO: this will be implemented as part of #3853 def self.reference_prefix
def to_reference '&'
end
def self.reference_prefix_escaped
'&amp;'
end end
end end
...@@ -120,104 +120,22 @@ class GeoNode < ActiveRecord::Base ...@@ -120,104 +120,22 @@ class GeoNode < ActiveRecord::Base
end end
end end
def projects_include?(project_id)
return true if restricted_project_ids.nil?
restricted_project_ids.include?(project_id)
end
def restricted_project_ids
return unless namespaces.presence
relations = namespaces.map { |namespace| namespace.all_projects.select(:id) }
Project.unscoped
.from("(#{Gitlab::SQL::Union.new(relations).to_sql}) #{Project.table_name}")
.pluck(:id)
end
def lfs_objects
relation =
if restricted_project_ids
LfsObject.joins(:projects).where(projects: { id: restricted_project_ids })
else
LfsObject.all
end
relation.with_files_stored_locally
end
def projects def projects
if restricted_project_ids if selective_sync?
Project.where(id: restricted_project_ids) Project.where(namespace_id: Gitlab::GroupHierarchy.new(namespaces).base_and_descendants.select(:id))
else else
Project.all Project.all
end end
end end
def project_registries def projects_include?(project_id)
if restricted_project_ids return true unless selective_sync?
Geo::ProjectRegistry.where(project_id: restricted_project_ids)
else
Geo::ProjectRegistry.all
end
end
def filtered_project_registries(type = nil)
case type
when 'repository'
project_registries.failed_repos
when 'wiki'
project_registries.failed_wikis
else
project_registries.failed
end
end
def uploads
if restricted_project_ids
uploads_table = Upload.arel_table
group_uploads = uploads_table[:model_type].eq('Namespace').and(uploads_table[:model_id].in(Gitlab::Geo.current_node.namespace_ids))
project_uploads = uploads_table[:model_type].eq('Project').and(uploads_table[:model_id].in(restricted_project_ids))
other_uploads = uploads_table[:model_type].not_in(%w[Namespace Project])
Upload.where(group_uploads.or(project_uploads).or(other_uploads))
else
Upload.all
end
end
def lfs_objects_synced_count
return unless secondary?
relation = Geo::FileRegistry.lfs_objects.synced
if restricted_project_ids
relation = relation.where(file_id: lfs_objects.pluck(:id))
end
relation.count
end
def lfs_objects_failed_count
return unless secondary?
Geo::FileRegistry.lfs_objects.failed.count
end
def attachments_synced_count
return unless secondary?
upload_ids = uploads.pluck(:id)
synced_ids = Geo::FileRegistry.attachments.synced.pluck(:file_id)
(synced_ids & upload_ids).length projects.where(id: project_id).exists?
end end
def attachments_failed_count def selective_sync?
return unless secondary? namespaces.exists?
Geo::FileRegistry.attachments.failed.count
end end
def find_or_build_status def find_or_build_status
......
...@@ -2,7 +2,7 @@ class GeoNodeStatus < ActiveRecord::Base ...@@ -2,7 +2,7 @@ class GeoNodeStatus < ActiveRecord::Base
belongs_to :geo_node belongs_to :geo_node
# Whether we were successful in reaching this node # Whether we were successful in reaching this node
attr_accessor :success attr_accessor :success, :health_status, :version, :revision
# Be sure to keep this consistent with Prometheus naming conventions # Be sure to keep this consistent with Prometheus naming conventions
PROMETHEUS_METRICS = { PROMETHEUS_METRICS = {
...@@ -26,7 +26,6 @@ class GeoNodeStatus < ActiveRecord::Base ...@@ -26,7 +26,6 @@ class GeoNodeStatus < ActiveRecord::Base
def self.current_node_status def self.current_node_status
current_node = Gitlab::Geo.current_node current_node = Gitlab::Geo.current_node
return unless current_node return unless current_node
status = current_node.find_or_build_status status = current_node.find_or_build_status
...@@ -47,8 +46,8 @@ class GeoNodeStatus < ActiveRecord::Base ...@@ -47,8 +46,8 @@ class GeoNodeStatus < ActiveRecord::Base
end end
def self.allowed_params def self.allowed_params
excluded_params = %w(id last_successful_status_check_at created_at updated_at).freeze excluded_params = %w(id created_at updated_at).freeze
extra_params = %w(success health last_event_timestamp cursor_last_event_timestamp).freeze extra_params = %w(success health health_status last_event_timestamp cursor_last_event_timestamp version revision).freeze
self.column_names - excluded_params + extra_params self.column_names - excluded_params + extra_params
end end
...@@ -63,21 +62,21 @@ class GeoNodeStatus < ActiveRecord::Base ...@@ -63,21 +62,21 @@ class GeoNodeStatus < ActiveRecord::Base
latest_event = Geo::EventLog.latest_event latest_event = Geo::EventLog.latest_event
self.last_event_id = latest_event&.id self.last_event_id = latest_event&.id
self.last_event_date = latest_event&.created_at self.last_event_date = latest_event&.created_at
self.repositories_count = geo_node.projects.count self.repositories_count = projects_finder.count_projects
self.lfs_objects_count = geo_node.lfs_objects.count self.lfs_objects_count = lfs_objects_finder.count_lfs_objects
self.attachments_count = geo_node.uploads.count self.attachments_count = attachments_finder.count_attachments
self.last_successful_status_check_at = Time.now self.last_successful_status_check_at = Time.now
if Gitlab::Geo.secondary? if Gitlab::Geo.secondary?
self.db_replication_lag_seconds = Gitlab::Geo::HealthCheck.db_replication_lag_seconds self.db_replication_lag_seconds = Gitlab::Geo::HealthCheck.db_replication_lag_seconds
self.cursor_last_event_id = Geo::EventLogState.last_processed&.event_id self.cursor_last_event_id = Geo::EventLogState.last_processed&.event_id
self.cursor_last_event_date = Geo::EventLog.find_by(id: self.cursor_last_event_id)&.created_at self.cursor_last_event_date = Geo::EventLog.find_by(id: self.cursor_last_event_id)&.created_at
self.repositories_synced_count = geo_node.project_registries.synced.count self.repositories_synced_count = projects_finder.count_synced_project_registries
self.repositories_failed_count = geo_node.project_registries.failed.count self.repositories_failed_count = projects_finder.count_failed_project_registries
self.lfs_objects_synced_count = geo_node.lfs_objects_synced_count self.lfs_objects_synced_count = lfs_objects_finder.count_synced_lfs_objects
self.lfs_objects_failed_count = geo_node.lfs_objects_failed_count self.lfs_objects_failed_count = lfs_objects_finder.count_failed_lfs_objects
self.attachments_synced_count = geo_node.attachments_synced_count self.attachments_synced_count = attachments_finder.count_synced_attachments
self.attachments_failed_count = geo_node.attachments_failed_count self.attachments_failed_count = attachments_finder.count_failed_attachments
end end
self self
...@@ -89,6 +88,10 @@ class GeoNodeStatus < ActiveRecord::Base ...@@ -89,6 +88,10 @@ class GeoNodeStatus < ActiveRecord::Base
status_message.blank? || status_message == 'Healthy'.freeze status_message.blank? || status_message == 'Healthy'.freeze
end end
def health_status
@health_status || (healthy? ? 'Healthy' : 'Unhealthy')
end
def last_successful_status_check_timestamp def last_successful_status_check_timestamp
self.last_successful_status_check_at.to_i self.last_successful_status_check_at.to_i
end end
...@@ -131,6 +134,18 @@ class GeoNodeStatus < ActiveRecord::Base ...@@ -131,6 +134,18 @@ class GeoNodeStatus < ActiveRecord::Base
private private
def attachments_finder
@attachments_finder ||= Geo::AttachmentRegistryFinder.new(current_node: geo_node)
end
def lfs_objects_finder
@lfs_objects_finder ||= Geo::LfsObjectRegistryFinder.new(current_node: geo_node)
end
def projects_finder
@projects_finder ||= Geo::ProjectRegistryFinder.new(current_node: geo_node)
end
def sync_percentage(total, synced) def sync_percentage(total, synced)
return 0 if !total.present? || total.zero? return 0 if !total.present? || total.zero?
......
...@@ -344,6 +344,10 @@ class Group < Namespace ...@@ -344,6 +344,10 @@ class Group < Namespace
end end
end end
def hashed_storage?(_feature)
false
end
private private
def update_two_factor_requirement def update_two_factor_requirement
......
class LfsObject < ActiveRecord::Base class LfsObject < ActiveRecord::Base
prepend EE::LfsObject prepend EE::LfsObject
include AfterCommitQueue
has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, through: :lfs_objects_projects has_many :projects, through: :lfs_objects_projects
...@@ -10,6 +11,12 @@ class LfsObject < ActiveRecord::Base ...@@ -10,6 +11,12 @@ class LfsObject < ActiveRecord::Base
mount_uploader :file, LfsObjectUploader mount_uploader :file, LfsObjectUploader
after_save if: :file_changed?, on: [:create, :update] do
run_after_commit do
file.schedule_migration_to_object_storage
end
end
def project_allowed_access?(project) def project_allowed_access?(project)
projects.exists?(project.lfs_storage_project.id) projects.exists?(project.lfs_storage_project.id)
end end
......
...@@ -41,6 +41,7 @@ class Namespace < ActiveRecord::Base ...@@ -41,6 +41,7 @@ class Namespace < ActiveRecord::Base
namespace_path: true namespace_path: true
validate :nesting_level_allowed validate :nesting_level_allowed
validate :allowed_path_by_redirects
delegate :name, to: :owner, allow_nil: true, prefix: true delegate :name, to: :owner, allow_nil: true, prefix: true
...@@ -270,4 +271,14 @@ class Namespace < ActiveRecord::Base ...@@ -270,4 +271,14 @@ class Namespace < ActiveRecord::Base
Namespace.where(id: descendants.select(:id)) Namespace.where(id: descendants.select(:id))
.update_all(share_with_group_lock: true) .update_all(share_with_group_lock: true)
end end
def allowed_path_by_redirects
return if path.nil?
errors.add(:path, "#{path} has been taken before. Please use another one") if namespace_previously_created_with_same_path?
end
def namespace_previously_created_with_same_path?
RedirectRoute.permanent.exists?(path: path)
end
end end
...@@ -232,7 +232,6 @@ class Project < ActiveRecord::Base ...@@ -232,7 +232,6 @@ class Project < ActiveRecord::Base
delegate :members, to: :team, prefix: true delegate :members, to: :team, prefix: true
delegate :add_user, :add_users, to: :team delegate :add_user, :add_users, to: :team
delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team
delegate :empty_repo?, to: :repository
# Validations # Validations
validates :creator, presence: true, on: :create validates :creator, presence: true, on: :create
...@@ -507,6 +506,10 @@ class Project < ActiveRecord::Base ...@@ -507,6 +506,10 @@ class Project < ActiveRecord::Base
auto_devops&.enabled.nil? && !current_application_settings.auto_devops_enabled? auto_devops&.enabled.nil? && !current_application_settings.auto_devops_enabled?
end end
def empty_repo?
repository.empty?
end
def repository_storage_path def repository_storage_path
Gitlab.config.repositories.storages[repository_storage].try(:[], 'path') Gitlab.config.repositories.storages[repository_storage].try(:[], 'path')
end end
...@@ -906,8 +909,7 @@ class Project < ActiveRecord::Base ...@@ -906,8 +909,7 @@ class Project < ActiveRecord::Base
@ci_service ||= ci_services.reorder(nil).find_by(active: true) @ci_service ||= ci_services.reorder(nil).find_by(active: true)
end end
# TODO: This will be extended for multiple enviroment clusters def deployment_platform(environment: nil)
def deployment_platform
@deployment_platform ||= clusters.find_by(enabled: true)&.platform_kubernetes @deployment_platform ||= clusters.find_by(enabled: true)&.platform_kubernetes
@deployment_platform ||= services.where(category: :deployment).reorder(nil).find_by(active: true) @deployment_platform ||= services.where(category: :deployment).reorder(nil).find_by(active: true)
end end
...@@ -1558,10 +1560,8 @@ class Project < ActiveRecord::Base ...@@ -1558,10 +1560,8 @@ class Project < ActiveRecord::Base
ProtectedTag.protected?(self, ref) ProtectedTag.protected?(self, ref)
end end
def deployment_variables def deployment_variables(environment: nil)
return [] unless deployment_platform deployment_platform(environment: environment)&.predefined_variables || []
deployment_platform.predefined_variables
end end
def auto_devops_variables def auto_devops_variables
......
class ProjectTeam class ProjectTeam
include BulkMemberAccessLoad
attr_accessor :project attr_accessor :project
def initialize(project) def initialize(project)
...@@ -159,39 +161,16 @@ class ProjectTeam ...@@ -159,39 +161,16 @@ class ProjectTeam
# #
# Returns a Hash mapping user ID -> maximum access level. # Returns a Hash mapping user ID -> maximum access level.
def max_member_access_for_user_ids(user_ids) def max_member_access_for_user_ids(user_ids)
user_ids = user_ids.uniq max_member_access_for_resource_ids(User, user_ids, project.id) do |user_ids|
key = "max_member_access:#{project.id}" project.project_authorizations
.where(user: user_ids)
access = {} .group(:user_id)
.maximum(:access_level)
if RequestStore.active?
RequestStore.store[key] ||= {}
access = RequestStore.store[key]
end end
# Look up only the IDs we need
user_ids = user_ids - access.keys
return access if user_ids.empty?
users_access = project.project_authorizations
.where(user: user_ids)
.group(:user_id)
.maximum(:access_level)
access.merge!(users_access)
missing_user_ids = user_ids - users_access.keys
missing_user_ids.each do |user_id|
access[user_id] = Gitlab::Access::NO_ACCESS
end
access
end end
def max_member_access(user_id) def max_member_access(user_id)
max_member_access_for_user_ids([user_id])[user_id] || Gitlab::Access::NO_ACCESS max_member_access_for_user_ids([user_id])[user_id]
end end
private private
......
...@@ -17,4 +17,32 @@ class RedirectRoute < ActiveRecord::Base ...@@ -17,4 +17,32 @@ class RedirectRoute < ActiveRecord::Base
where(wheres, path, "#{sanitize_sql_like(path)}/%") where(wheres, path, "#{sanitize_sql_like(path)}/%")
end end
scope :permanent, -> do
if column_permanent_exists?
where(permanent: true)
else
none
end
end
scope :temporary, -> do
if column_permanent_exists?
where(permanent: [false, nil])
else
all
end
end
default_value_for :permanent, false
def permanent=(value)
if self.class.column_permanent_exists?
super
end
end
def self.column_permanent_exists?
ActiveRecord::Base.connection.column_exists?(:redirect_routes, :permanent)
end
end end
...@@ -44,7 +44,7 @@ class Repository ...@@ -44,7 +44,7 @@ class Repository
issue_template_names merge_request_template_names).freeze issue_template_names merge_request_template_names).freeze
# Methods that use cache_method but only memoize the value # Methods that use cache_method but only memoize the value
MEMOIZED_CACHED_METHODS = %i(license empty_repo?).freeze MEMOIZED_CACHED_METHODS = %i(license).freeze
# Certain method caches should be refreshed when certain types of files are # Certain method caches should be refreshed when certain types of files are
# changed. This Hash maps file types (as returned by Gitlab::FileDetector) to # changed. This Hash maps file types (as returned by Gitlab::FileDetector) to
...@@ -504,7 +504,11 @@ class Repository ...@@ -504,7 +504,11 @@ class Repository
end end
cache_method :exists? cache_method :exists?
delegate :empty?, to: :raw_repository def empty?
return true unless exists?
!has_visible_content?
end
cache_method :empty? cache_method :empty?
# The size of this repository in megabytes. # The size of this repository in megabytes.
...@@ -993,13 +997,8 @@ class Repository ...@@ -993,13 +997,8 @@ class Repository
end end
end end
def empty_repo?
!exists? || !has_visible_content?
end
cache_method :empty_repo?, memoize_only: true
def search_files_by_content(query, ref) def search_files_by_content(query, ref)
return [] if empty_repo? || query.blank? return [] if empty? || query.blank?
offset = 2 offset = 2
args = %W(grep -i -I -n --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref}) args = %W(grep -i -I -n --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref})
...@@ -1008,7 +1007,7 @@ class Repository ...@@ -1008,7 +1007,7 @@ class Repository
end end
def search_files_by_name(query, ref) def search_files_by_name(query, ref)
return [] if empty_repo? || query.blank? return [] if empty? || query.blank?
args = %W(ls-tree --full-tree -r #{ref || root_ref} --name-status | #{Regexp.escape(query)}) args = %W(ls-tree --full-tree -r #{ref || root_ref} --name-status | #{Regexp.escape(query)})
......
...@@ -8,6 +8,8 @@ class Route < ActiveRecord::Base ...@@ -8,6 +8,8 @@ class Route < ActiveRecord::Base
presence: true, presence: true,
uniqueness: { case_sensitive: false } uniqueness: { case_sensitive: false }
validate :ensure_permanent_paths
after_create :delete_conflicting_redirects after_create :delete_conflicting_redirects
after_update :delete_conflicting_redirects, if: :path_changed? after_update :delete_conflicting_redirects, if: :path_changed?
after_update :create_redirect_for_old_path after_update :create_redirect_for_old_path
...@@ -40,7 +42,7 @@ class Route < ActiveRecord::Base ...@@ -40,7 +42,7 @@ class Route < ActiveRecord::Base
# We are not calling route.delete_conflicting_redirects here, in hopes # We are not calling route.delete_conflicting_redirects here, in hopes
# of avoiding deadlocks. The parent (self, in this method) already # of avoiding deadlocks. The parent (self, in this method) already
# called it, which deletes conflicts for all descendants. # called it, which deletes conflicts for all descendants.
route.create_redirect(old_path) if attributes[:path] route.create_redirect(old_path, permanent: permanent_redirect?) if attributes[:path]
end end
end end
end end
...@@ -50,16 +52,30 @@ class Route < ActiveRecord::Base ...@@ -50,16 +52,30 @@ class Route < ActiveRecord::Base
end end
def conflicting_redirects def conflicting_redirects
RedirectRoute.matching_path_and_descendants(path) RedirectRoute.temporary.matching_path_and_descendants(path)
end end
def create_redirect(path) def create_redirect(path, permanent: false)
RedirectRoute.create(source: source, path: path) RedirectRoute.create(source: source, path: path, permanent: permanent)
end end
private private
def create_redirect_for_old_path def create_redirect_for_old_path
create_redirect(path_was) if path_changed? create_redirect(path_was, permanent: permanent_redirect?) if path_changed?
end
def permanent_redirect?
source_type != "Project"
end
def ensure_permanent_paths
return if path.nil?
errors.add(:path, "#{path} has been taken before. Please use another one") if conflicting_redirect_exists?
end
def conflicting_redirect_exists?
RedirectRoute.permanent.matching_path_and_descendants(path).exists?
end end
end end
...@@ -16,6 +16,7 @@ class SystemNoteMetadata < ActiveRecord::Base ...@@ -16,6 +16,7 @@ class SystemNoteMetadata < ActiveRecord::Base
opened closed merged duplicate locked unlocked opened closed merged duplicate locked unlocked
outdated outdated
approved unapproved relate unrelate approved unapproved relate unrelate
epic_issue_added issue_added_to_epic epic_issue_removed issue_removed_from_epic
].freeze ].freeze
validates :note, presence: true validates :note, presence: true
......
...@@ -17,6 +17,7 @@ class User < ActiveRecord::Base ...@@ -17,6 +17,7 @@ class User < ActiveRecord::Base
include FeatureGate include FeatureGate
include CreatedAtFilterable include CreatedAtFilterable
include IgnorableColumn include IgnorableColumn
include BulkMemberAccessLoad
prepend EE::GeoAwareAvatar prepend EE::GeoAwareAvatar
prepend EE::User prepend EE::User
...@@ -1167,6 +1168,34 @@ class User < ActiveRecord::Base ...@@ -1167,6 +1168,34 @@ class User < ActiveRecord::Base
super super
end end
# Determine the maximum access level for a group of projects in bulk.
#
# Returns a Hash mapping project ID -> maximum access level.
def max_member_access_for_project_ids(project_ids)
max_member_access_for_resource_ids(Project, project_ids) do |project_ids|
project_authorizations.where(project: project_ids)
.group(:project_id)
.maximum(:access_level)
end
end
def max_member_access_for_project(project_id)
max_member_access_for_project_ids([project_id])[project_id]
end
# Determine the maximum access level for a group of groups in bulk.
#
# Returns a Hash mapping project ID -> maximum access level.
def max_member_access_for_group_ids(group_ids)
max_member_access_for_resource_ids(Group, group_ids) do |group_ids|
group_members.where(source: group_ids).group(:source_id).maximum(:access_level)
end
end
def max_member_access_for_group(group_id)
max_member_access_for_group_ids([group_id])[group_id]
end
protected protected
# override, from Devise::Validatable # override, from Devise::Validatable
......
...@@ -40,6 +40,7 @@ class GroupPolicy < BasePolicy ...@@ -40,6 +40,7 @@ class GroupPolicy < BasePolicy
rule { guest }.policy do rule { guest }.policy do
enable :read_group enable :read_group
enable :read_list enable :read_list
enable :upload_file
end end
rule { admin } .enable :read_group rule { admin } .enable :read_group
......
...@@ -7,6 +7,7 @@ class GeoNodeStatusEntity < Grape::Entity ...@@ -7,6 +7,7 @@ class GeoNodeStatusEntity < Grape::Entity
expose :health do |node| expose :health do |node|
node.healthy? ? 'Healthy' : node.health node.healthy? ? 'Healthy' : node.health
end end
expose :health_status
expose :attachments_count expose :attachments_count
expose :attachments_synced_count expose :attachments_synced_count
...@@ -37,4 +38,23 @@ class GeoNodeStatusEntity < Grape::Entity ...@@ -37,4 +38,23 @@ class GeoNodeStatusEntity < Grape::Entity
expose :cursor_last_event_timestamp expose :cursor_last_event_timestamp
expose :last_successful_status_check_timestamp expose :last_successful_status_check_timestamp
expose :version
expose :revision
expose :namespaces, using: NamespaceEntity
private
def namespaces
object.geo_node.namespaces
end
def version
Gitlab::VERSION
end
def revision
Gitlab::REVISION
end
end end
class IssueSidebarEntity < IssuableSidebarEntity class IssueSidebarEntity < IssuableSidebarEntity
prepend ::EE::IssueSidebarEntity
expose :assignees, using: API::Entities::UserBasic expose :assignees, using: API::Entities::UserBasic
end end
class NamespaceEntity < Grape::Entity
expose :id, :name, :path, :kind, :full_path, :parent_id
end
...@@ -14,19 +14,23 @@ module Ci ...@@ -14,19 +14,23 @@ module Ci
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, mirror_update: false, &block) def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, mirror_update: false, &block)
@pipeline = Ci::Pipeline.new @pipeline = Ci::Pipeline.new
command = OpenStruct.new(source: source, command = Gitlab::Ci::Pipeline::Chain::Command.new(
origin_ref: params[:ref], source: source,
checkout_sha: params[:checkout_sha], origin_ref: params[:ref],
after_sha: params[:after], checkout_sha: params[:checkout_sha],
before_sha: params[:before], after_sha: params[:after],
trigger_request: trigger_request, before_sha: params[:before],
schedule: schedule, trigger_request: trigger_request,
ignore_skip_ci: ignore_skip_ci, schedule: schedule,
save_incompleted: save_on_errors, ignore_skip_ci: ignore_skip_ci,
allow_mirror_update: mirror_update, save_incompleted: save_on_errors,
seeds_block: block, seeds_block: block,
project: project, project: project,
current_user: current_user) current_user: current_user,
# EE specific
allow_mirror_update: mirror_update
)
sequence = Gitlab::Ci::Pipeline::Chain::Sequence sequence = Gitlab::Ci::Pipeline::Chain::Sequence
.new(pipeline, command, SEQUENCE) .new(pipeline, command, SEQUENCE)
......
...@@ -29,7 +29,13 @@ module Clusters ...@@ -29,7 +29,13 @@ module Clusters
end end
def can_create_cluster? def can_create_cluster?
<<<<<<< HEAD
project.clusters.empty? project.clusters.empty?
=======
return true if project.clusters.empty?
project.feature_available?(:multiple_clusters)
>>>>>>> origin/master
end end
end end
end end
...@@ -2,7 +2,7 @@ module Geo ...@@ -2,7 +2,7 @@ module Geo
class FileService class FileService
attr_reader :object_type, :object_db_id attr_reader :object_type, :object_db_id
DEFAULT_OBJECT_TYPES = %w[attachment avatar file personal_file].freeze DEFAULT_OBJECT_TYPES = %w[attachment avatar file namespace_file personal_file].freeze
DEFAULT_SERVICE_TYPE = 'file'.freeze DEFAULT_SERVICE_TYPE = 'file'.freeze
def initialize(object_type, object_db_id) def initialize(object_type, object_db_id)
......
...@@ -6,7 +6,8 @@ module Geo ...@@ -6,7 +6,8 @@ module Geo
def call(geo_node) def call(geo_node)
return GeoNodeStatus.current_node_status if geo_node.current? return GeoNodeStatus.current_node_status if geo_node.current?
data = { success: false } data = GeoNodeStatus.find_or_initialize_by(geo_node: geo_node).attributes
data = data.merge(success: false, health_status: 'Offline')
begin begin
response = self.class.get(geo_node.status_url, headers: headers, timeout: timeout) response = self.class.get(geo_node.status_url, headers: headers, timeout: timeout)
...@@ -29,8 +30,10 @@ module Geo ...@@ -29,8 +30,10 @@ module Geo
end end
rescue Gitlab::Geo::GeoNodeNotFoundError rescue Gitlab::Geo::GeoNodeNotFoundError
data[:health] = 'This GitLab instance does not appear to be configured properly as a Geo node. Make sure the URLs are using the correct fully-qualified domain names.' data[:health] = 'This GitLab instance does not appear to be configured properly as a Geo node. Make sure the URLs are using the correct fully-qualified domain names.'
data[:health_status] = 'Unhealthy'
rescue OpenSSL::Cipher::CipherError rescue OpenSSL::Cipher::CipherError
data[:health] = 'Error decrypting the Geo secret from the database. Check that the primary uses the correct db_key_base.' data[:health] = 'Error decrypting the Geo secret from the database. Check that the primary uses the correct db_key_base.'
data[:health_status] = 'Unhealthy'
rescue HTTParty::Error, Timeout::Error, SocketError, SystemCallError, OpenSSL::SSL::SSLError => e rescue HTTParty::Error, Timeout::Error, SocketError, SystemCallError, OpenSSL::SSL::SSLError => e
data[:health] = e.message data[:health] = e.message
end end
......
module Projects module Projects
class ForkService < BaseService class ForkService < BaseService
def execute def execute(fork_to_project = nil)
if fork_to_project
link_existing_project(fork_to_project)
else
fork_new_project
end
end
private
def link_existing_project(fork_to_project)
return if fork_to_project.forked?
link_fork_network(fork_to_project)
fork_to_project
end
def fork_new_project
new_params = { new_params = {
forked_from_project_id: @project.id, forked_from_project_id: @project.id,
visibility_level: allowed_visibility_level, visibility_level: allowed_visibility_level,
...@@ -21,15 +39,11 @@ module Projects ...@@ -21,15 +39,11 @@ module Projects
builds_access_level = @project.project_feature.builds_access_level builds_access_level = @project.project_feature.builds_access_level
new_project.project_feature.update_attributes(builds_access_level: builds_access_level) new_project.project_feature.update_attributes(builds_access_level: builds_access_level)
refresh_forks_count
link_fork_network(new_project) link_fork_network(new_project)
new_project new_project
end end
private
def fork_network def fork_network
if @project.fork_network if @project.fork_network
@project.fork_network @project.fork_network
...@@ -43,9 +57,17 @@ module Projects ...@@ -43,9 +57,17 @@ module Projects
end end
end end
def link_fork_network(new_project) def link_fork_network(fork_to_project)
fork_network.fork_network_members.create(project: new_project, fork_network.fork_network_members.create(project: fork_to_project,
forked_from_project: @project) forked_from_project: @project)
# TODO: remove this when ForkedProjectLink model is removed
unless fork_to_project.forked_project_link
fork_to_project.create_forked_project_link(forked_to_project: fork_to_project,
forked_from_project: @project)
end
refresh_forks_count
end end
def refresh_forks_count def refresh_forks_count
......
module ProtectedBranches
class AccessLevelParams
prepend EE::ProtectedBranches::AccessLevelParams
attr_reader :type, :params
def initialize(type, params)
@type = type
@params = params_with_default(params)
end
def access_levels
ce_style_access_level
end
private
def params_with_default(params)
params[:"#{type}_access_level"] ||= Gitlab::Access::MASTER if use_default_access_level?(params)
params
end
def use_default_access_level?(params)
true
end
def ce_style_access_level
access_level = params[:"#{type}_access_level"]
return [] unless access_level
[{ access_level: access_level }]
end
end
end
module ProtectedBranches
class ApiService < BaseService
prepend EE::ProtectedBranches::ApiService
def create
@push_params = AccessLevelParams.new(:push, params)
@merge_params = AccessLevelParams.new(:merge, params)
verify_params!
protected_branch_params = {
name: params[:name],
push_access_levels_attributes: @push_params.access_levels,
merge_access_levels_attributes: @merge_params.access_levels
}
::ProtectedBranches::CreateService.new(@project, @current_user, protected_branch_params).execute
end
private
def verify_params!
# EE-only
end
end
end
...@@ -566,6 +566,36 @@ module SystemNoteService ...@@ -566,6 +566,36 @@ module SystemNoteService
create_note(NoteSummary.new(noteable, noteable.project, user, body, action: 'unrelate')) create_note(NoteSummary.new(noteable, noteable.project, user, body, action: 'unrelate'))
end end
def epic_issue(epic, issue, user, type)
return unless validate_epic_issue_action_type(type)
action = type == :added ? 'epic_issue_added' : 'epic_issue_removed'
body = "#{type} issue #{issue.to_reference(epic.group)}"
create_note(NoteSummary.new(epic, nil, user, body, action: action))
end
def issue_on_epic(issue, epic, user, type)
return unless validate_epic_issue_action_type(type)
if type == :added
direction = 'to'
action = 'issue_added_to_epic'
else
direction = 'from'
action = 'issue_removed_from_epic'
end
body = "#{type} #{direction} epic #{epic.to_reference(issue.project)}"
create_note(NoteSummary.new(issue, issue.project, user, body, action: action))
end
def validate_epic_issue_action_type(type)
[:added, :removed].include?(type)
end
# Called when the merge request is approved by user # Called when the merge request is approved by user
# #
# noteable - Noteable object # noteable - Noteable object
......
...@@ -29,11 +29,11 @@ class FileUploader < GitlabUploader ...@@ -29,11 +29,11 @@ class FileUploader < GitlabUploader
# model - Object that responds to `full_path` and `disk_path` # model - Object that responds to `full_path` and `disk_path`
# #
# Returns a String without a trailing slash # Returns a String without a trailing slash
def self.dynamic_path_segment(project) def self.dynamic_path_segment(model)
if project.hashed_storage?(:attachments) if model.hashed_storage?(:attachments)
dynamic_path_builder(project.disk_path) dynamic_path_builder(model.disk_path)
else else
dynamic_path_builder(project.full_path) dynamic_path_builder(model.full_path)
end end
end end
......
class LfsObjectUploader < ObjectStoreUploader class LfsObjectUploader < ObjectStoreUploader
storage_options Gitlab.config.lfs storage_options Gitlab.config.lfs
after :store, :schedule_migration_to_object_storage
def self.local_store_path def self.local_store_path
Gitlab.config.lfs.storage_path Gitlab.config.lfs.storage_path
......
class NamespaceFileUploader < FileUploader
def self.base_dir
File.join(root_dir, '-', 'system', 'namespace')
end
def self.dynamic_path_segment(model)
dynamic_path_builder(model.id.to_s)
end
private
def secure_url
File.join('/uploads', @secret, file.filename)
end
end
...@@ -128,7 +128,6 @@ ...@@ -128,7 +128,6 @@
- if License.feature_available?(:repository_mirrors) - if License.feature_available?(:repository_mirrors)
= render partial: 'repository_mirrors_form', locals: { f: f } = render partial: 'repository_mirrors_form', locals: { f: f }
= render partial: 'repository_remote_mirrors_form', locals: { f: f }
%fieldset %fieldset
%legend Sign-up Restrictions %legend Sign-up Restrictions
......
- page_title 'Geo nodes' - page_title 'Geo nodes'
%h3.page-title - @content_class = "geo-admin-container"
Geo Nodes %h2.page-title.clearfix
%span.title-text.pull-left
Geo Nodes
= link_to "New node", new_admin_geo_node_path, class: 'btn btn-create pull-right'
%hr.page-title-separator
%p.light %p.page-subtitle.light
With #{link_to 'GitLab Geo', help_page_path('gitlab-geo/README'), class: 'vlink'} you can install a special With #{link_to 'GitLab Geo', help_page_path('gitlab-geo/README'), class: 'vlink'} you can install a special
read-only and replicated instance anywhere. read-only and replicated instance anywhere.
Before you add nodes, follow the Before you add nodes, follow the
...@@ -11,16 +15,6 @@ ...@@ -11,16 +15,6 @@
%strong exact order %strong exact order
they appear. they appear.
%hr
- if Gitlab::Geo.license_allows?
= form_for [:admin, @node], as: :geo_node, url: admin_geo_nodes_path, html: { class: 'form-horizontal js-geo-node-form' } do |f|
= render partial: 'form', locals: { form: f, geo_node: @node }
.form-actions
= f.submit 'Add Node', class: 'btn btn-create'
%hr
- if @nodes.any? - if @nodes.any?
.panel.panel-default .panel.panel-default
.panel-heading .panel-heading
...@@ -35,62 +29,100 @@ ...@@ -35,62 +29,100 @@
.node-badge.current-node Current node .node-badge.current-node Current node
- if node.primary? - if node.primary?
.node-badge.primary-node Primary .node-badge.primary-node Primary
%span.help-block Primary node %p
%span.help-block Primary node
%p
%span.help-block
GitLab version:
%span.js-primary-version= Gitlab::VERSION
%small.js-primary-revision
= "(#{Gitlab::REVISION})"
- else - else
= status_loading_icon = status_loading_icon
- if node.restricted_project_ids %table.geo-node-status.js-geo-node-status.hidden
%p %tr
%span.help-block %td
Namespaces to replicate: .help-block
%strong.node-info GitLab version:
= node_selected_namespaces_to_replicate(node) %td
.js-geo-node-status{ style: 'display: none' } .node-info.prepend-top-5.prepend-left-5.js-secondary-version
- if node.enabled? - if node.enabled?
%p %tr
%span.help-block %td
Health Status: .help-block.prepend-top-10
%span.js-health-status Health Status:
%p %td
%span.help-block .health-status.prepend-top-10.prepend-left-5.js-health-status
Repositories synced: %tr
%strong.node-info.js-repositories-synced %td
%p .help-block.prepend-top-10
%span.help-block Repositories:
Repositories failed: %td
%strong.node-info.js-repositories-failed .node-info.prepend-top-10.prepend-left-5.stacked-progress-bar.js-repositories
%p %span.status-unavailable.js-stats-unavailable
%span.help-block Not available
LFS objects synced: %span.status-green.js-synced
%strong.node-info.js-lfs-objects-synced %span.status-neutral.js-waiting
%p %span.status-red.js-failed
%span.help-block %tr
LFS objects failed: %td
%strong.node-info.js-lfs-objects-failed .help-block.prepend-top-10
%p LFS objects:
%span.help-block %td
Attachments synced: .node-info.prepend-top-10.prepend-left-5.stacked-progress-bar.js-lfs-objects
%strong.node-info.js-attachments-synced %span.status-unavailable.js-stats-unavailable
%p Not available
%span.help-block %span.status-green.js-synced
Attachments failed: %span.status-neutral.js-waiting
%strong.node-info.js-attachments-failed %span.status-red.js-failed
%p %tr
.advanced-geo-node-status-container %td
.advanced-status.hidden .help-block.prepend-top-10
%span.help-block Attachments:
%td
.node-info.prepend-top-10.prepend-left-5.stacked-progress-bar.js-attachments
%span.status-unavailable.js-stats-unavailable
Not available
%span.status-green.js-synced
%span.status-neutral.js-waiting
%span.status-red.js-failed
%tr
%td
.help-block.prepend-top-10
Sync settings:
%td
.node-info.prepend-top-10.prepend-left-5.js-sync-settings
%span.js-sync-type
%span.has-tooltip.sync-status.js-sync-status
%i.sync-status-icon.js-sync-status-icon
%span.sync-status-timestamp.js-sync-status-timestamp
%tr.js-advanced-status.hidden
%td
.help-block.prepend-top-10
Database replication lag: Database replication lag:
%strong.node-info.js-db-replication-lag %td
%span.help-block .node-info.prepend-top-10.prepend-left-5.js-db-replication-lag
%tr.js-advanced-status.hidden
%td
.help-block.prepend-top-10
Last event ID seen from primary: Last event ID seen from primary:
%strong.node-info.js-last-event-seen %td
%span.help-block .node-info.prepend-top-10.prepend-left-5.js-last-event-seen
%span.js-event-id
%span.event-timestamp.js-event-timestamp.has-tooltip
%tr.js-advanced-status.hidden
%td
.help-block.prepend-top-10
Last event ID processed by cursor: Last event ID processed by cursor:
%strong.node-info.js-last-cursor-event %td
%button.btn-link.js-advanced-geo-node-status-toggler .node-info.prepend-top-10.prepend-left-5.js-last-cursor-event
%span> Advanced %span.js-event-id
= icon('angle-down') %span.event-timestamp.js-event-timestamp.has-tooltip
%p %button.btn-link.advanced-geo-node-status-toggler.js-advanced-geo-node-status-toggler
.js-health %span> Advanced
%span.js-advance-toggle.show-advance-chevron.pull-right.inline.prepend-left-5
= sprite_icon('angle-down', css_class: 's16')
%p.health-message.hidden.js-health-message
- if Gitlab::Database.read_write? - if Gitlab::Database.read_write?
.node-actions .node-actions
......
- page_title 'New node'
- @content_class = "geo-admin-container"
%h2.page-title
%span.title-text
New node
%hr.page-title-separator
- if Gitlab::Geo.license_allows?
= form_for [:admin, @node], as: :geo_node, url: admin_geo_nodes_path, html: { class: 'form-horizontal js-geo-node-form' } do |f|
= render partial: 'form', locals: { form: f, geo_node: @node }
.form-actions
= f.submit 'Add Node', class: 'btn btn-create'
= render 'shared/projects/list', projects: @projects, ci: true = render 'shared/projects/list', projects: @projects, ci: true, user: current_user
= render 'shared/projects/list', projects: projects = render 'shared/projects/list', projects: projects, user: current_user
...@@ -13,7 +13,15 @@ ...@@ -13,7 +13,15 @@
.location-badge= label .location-badge= label
.search-input-wrap .search-input-wrap
.dropdown{ data: { url: search_autocomplete_path } } .dropdown{ data: { url: search_autocomplete_path } }
= search_field_tag 'search', nil, placeholder: 'Search', class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options', spellcheck: false, tabindex: '1', autocomplete: 'off', data: { toggle: 'dropdown', issues_path: issues_dashboard_url, mr_path: merge_requests_dashboard_url }, aria: { label: 'Search' } = search_field_tag 'search', nil, placeholder: 'Search',
class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options',
spellcheck: false,
tabindex: '1',
autocomplete: 'off',
data: { toggle: 'dropdown',
issues_path: issues_dashboard_path,
mr_path: merge_requests_dashboard_path },
aria: { label: 'Search' }
.dropdown-menu.dropdown-select .dropdown-menu.dropdown-select
= dropdown_content do = dropdown_content do
%ul %ul
......
...@@ -4,4 +4,10 @@ ...@@ -4,4 +4,10 @@
- nav "group" - nav "group"
- @left_sidebar = true - @left_sidebar = true
- content_for :page_specific_javascripts do
- if current_user
-# haml-lint:disable InlineJavaScript
:javascript
window.uploads_path = "#{group_uploads_path(@group)}";
= render template: "layouts/application" = render template: "layouts/application"
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
%td.branch-commit %td.branch-commit
- if can?(current_user, :read_build, job) - if can?(current_user, :read_build, job)
= link_to project_job_url(job.project, job) do = link_to project_job_path(job.project, job) do
%span.build-link ##{job.id} %span.build-link ##{job.id}
- else - else
%span.build-link ##{job.id} %span.build-link ##{job.id}
......
...@@ -14,3 +14,11 @@ ...@@ -14,3 +14,11 @@
= link_to project_clusters_path(@project), class: "js-all-tab" do = link_to project_clusters_path(@project), class: "js-all-tab" do
= s_("ClusterIntegration|All") = s_("ClusterIntegration|All")
%span.badge= @all_count %span.badge= @all_count
<<<<<<< HEAD
=======
.nav-controls
- if @project.feature_available?(:multiple_clusters)
= link_to s_("ClusterIntegration|Add cluster"), new_project_cluster_path(@project), class: "btn btn-success btn-add-cluster has-tooltip js-add-cluster"
- else
= link_to s_("ClusterIntegration|Add cluster"), new_project_cluster_path(@project), class: "btn btn-success btn-add-cluster disabled has-tooltip js-add-cluster", title: s_("ClusterIntegration|Multiple clusters are available in GitLab Entreprise Edition Premium and Ultimate")
>>>>>>> origin/master
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
- page_title _("Commits"), @ref - page_title _("Commits"), @ref
= content_for :meta_tags do = content_for :meta_tags do
= auto_discovery_link_tag(:atom, project_commits_url(@project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits") = auto_discovery_link_tag(:atom, project_commits_path(@project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits")
.js-project-commits-show{ 'data-commits-limit' => @limit } .js-project-commits-show{ 'data-commits-limit' => @limit }
%div{ class: container_class } %div{ class: container_class }
......
...@@ -14,4 +14,4 @@ ...@@ -14,4 +14,4 @@
notes_path: notes_url, notes_path: notes_url,
last_fetched_at: Time.now.to_i, last_fetched_at: Time.now.to_i,
noteable_data: serialize_issuable(@issue), noteable_data: serialize_issuable(@issue),
current_user_data: UserSerializer.new.represent(current_user).to_json } } current_user_data: UserSerializer.new.represent(current_user, only_path: true).to_json } }
...@@ -87,10 +87,11 @@ ...@@ -87,10 +87,11 @@
%h3.panel-title %h3.panel-title
Related issues Related issues
#merge-requests{ data: { url: referenced_merge_requests_project_issue_url(@project, @issue) } }
#merge-requests{ data: { url: referenced_merge_requests_project_issue_path(@project, @issue) } }
// This element is filled in using JavaScript. // This element is filled in using JavaScript.
#related-branches{ data: { url: related_branches_project_issue_url(@project, @issue) } } #related-branches{ data: { url: related_branches_project_issue_path(@project, @issue) } }
// This element is filled in using JavaScript. // This element is filled in using JavaScript.
.content-block.emoji-block .content-block.emoji-block
......
...@@ -85,7 +85,7 @@ ...@@ -85,7 +85,7 @@
= icon('bug', text: 'Fogbugz') = icon('bug', text: 'Fogbugz')
%div %div
- if gitea_import_enabled? - if gitea_import_enabled?
= link_to new_import_gitea_url, class: 'btn import_gitea' do = link_to new_import_gitea_path, class: 'btn import_gitea' do
= custom_icon('go_logo') = custom_icon('go_logo')
Gitea Gitea
%div %div
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
%span.note-role.note-role-special.has-tooltip{ title: _("This is the author's first Merge Request to this project.") } %span.note-role.note-role-special.has-tooltip{ title: _("This is the author's first Merge Request to this project.") }
= issuable_first_contribution_icon = issuable_first_contribution_icon
- if access.nonzero? - if access.nonzero?
%span.note-role.note-role-access= Gitlab::Access.human_access(access) %span.note-role.user-access-role= Gitlab::Access.human_access(access)
- if note.resolvable? - if note.resolvable?
- can_resolve = can?(current_user, :resolve_note, note) - can_resolve = can?(current_user, :resolve_note, note)
......
...@@ -3,5 +3,5 @@ ...@@ -3,5 +3,5 @@
GitLab may not work properly because you are using an outdated web browser. GitLab may not work properly because you are using an outdated web browser.
%br %br
Please install a Please install a
= link_to 'supported web browser', help_page_url('install/requirements', anchor: 'supported-web-browsers') = link_to 'supported web browser', help_page_path('install/requirements', anchor: 'supported-web-browsers')
for a better experience. for a better experience.
- group_member = local_assigns[:group_member] - user = local_assigns.fetch(:user, current_user)
- full_name = true unless local_assigns[:full_name] == false - access = user&.max_member_access_for_group(group.id)
- group_name = full_name ? group.full_name : group.name
- css_class = '' unless local_assigns[:css_class]
- css_class += " no-description" if group.description.blank?
%li.group-row{ class: css_class }
- if group_member
.controls.hidden-xs
- if can?(current_user, :admin_group, group)
= link_to edit_group_path(group), class: "btn" do
= sprite_icon('settings')
= link_to leave_group_group_members_path(group), data: { confirm: leave_confirmation_message(group) }, method: :delete, class: "btn", title: s_("GroupsTree|Leave this group") do
= icon('sign-out')
%li.group-row{ class: ('no-description' if group.description.blank?) }
.stats .stats
%span %span
= icon('bookmark') = icon('bookmark')
...@@ -30,11 +18,10 @@ ...@@ -30,11 +18,10 @@
= link_to group do = link_to group do
= group_icon(group, class: "avatar s40 hidden-xs") = group_icon(group, class: "avatar s40 hidden-xs")
.title .title
= link_to group_name, group, class: 'group-name' = link_to group.full_name, group, class: 'group-name'
- if group_member - if access&.nonzero?
as %span.user-access-role= Gitlab::Access.human_access(access)
%span= group_member.human_access
- if group.description.present? - if group.description.present?
.description .description
......
- if groups.any? - if groups.any?
- user = local_assigns[:user]
%ul.content-list %ul.content-list
- groups.each_with_index do |group, i| - groups.each_with_index do |group, i|
= render "shared/groups/group", group: group = render "shared/groups/group", group: group, user: user
- else - else
.nothing-here-block= s_("GroupsEmptyState|No groups found") .nothing-here-block= s_("GroupsEmptyState|No groups found")
- todo = issuable_todo(issuable) - todo = issuable_todo(issuable)
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('sidebar') = page_specific_javascript_bundle_tag('ee_sidebar')
%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: current_user.present? } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } %aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: current_user.present? } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
.issuable-sidebar{ data: { endpoint: "#{issuable_json_path(issuable)}" } } .issuable-sidebar{ data: { endpoint: "#{issuable_json_path(issuable)}" } }
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
= render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable, is_collapsed: true = render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable, is_collapsed: true
.block.assignee .block.assignee
= render "shared/issuable/sidebar_assignees", issuable: issuable, can_edit_issuable: can_edit_issuable, signed_in: current_user.present? = render "shared/issuable/sidebar_assignees", issuable: issuable, can_edit_issuable: can_edit_issuable, signed_in: current_user.present?
= render "shared/issuable/sidebar_item_epic", issuable: issuable
.block.milestone .block.milestone
.sidebar-collapsed-icon .sidebar-collapsed-icon
= icon('clock-o', 'aria-hidden': 'true') = icon('clock-o', 'aria-hidden': 'true')
......
...@@ -5,18 +5,20 @@ ...@@ -5,18 +5,20 @@
- forks = false unless local_assigns[:forks] == true - forks = false unless local_assigns[:forks] == true
- ci = false unless local_assigns[:ci] == true - ci = false unless local_assigns[:ci] == true
- skip_namespace = false unless local_assigns[:skip_namespace] == true - skip_namespace = false unless local_assigns[:skip_namespace] == true
- user = local_assigns[:user]
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true - show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true
- remote = false unless local_assigns[:remote] == true - remote = false unless local_assigns[:remote] == true
- load_pipeline_status(projects)
.js-projects-list-holder .js-projects-list-holder
- if any_projects?(projects) - if any_projects?(projects)
- load_pipeline_status(projects)
%ul.projects-list %ul.projects-list
- projects.each_with_index do |project, i| - projects.each_with_index do |project, i|
- css_class = (i >= projects_limit) || project.pending_delete? ? 'hide' : nil - css_class = (i >= projects_limit) || project.pending_delete? ? 'hide' : nil
= render "shared/projects/project", project: project, skip_namespace: skip_namespace, = render "shared/projects/project", project: project, skip_namespace: skip_namespace,
avatar: avatar, stars: stars, css_class: css_class, ci: ci, use_creator_avatar: use_creator_avatar, avatar: avatar, stars: stars, css_class: css_class, ci: ci, use_creator_avatar: use_creator_avatar,
forks: forks, show_last_commit_as_description: show_last_commit_as_description forks: forks, show_last_commit_as_description: show_last_commit_as_description, user: user
- if @private_forks_count && @private_forks_count > 0 - if @private_forks_count && @private_forks_count > 0
%li.project-row.private-forks-notice %li.project-row.private-forks-notice
......
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.
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