Commit cee2ecf5 authored by Valery Sizov's avatar Valery Sizov

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ee into move_ff_to_ce

parents 7d21d203 6b532527
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
## 10.0.2 (2017-09-27)
- [FIXED] Send valid project path as name for Jira dev panel.
- [FIXED] Fix delta size check to handle commit or nil objects.
## 10.0.1 (2017-09-23) ## 10.0.1 (2017-09-23)
- No changes. - No changes.
......
...@@ -2,6 +2,13 @@ ...@@ -2,6 +2,13 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 10.0.2 (2017-09-27)
- [FIXED] Notes will not show an empty bubble when the author isn't a member. !14450
- [FIXED] Some checks in `rake gitlab:check` were failling with 'undefined method `run_command`'. !14469
- [FIXED] Make locked setting of Runner to not affect jobs scheduling. !14483
- [FIXED] Re-allow `name` attribute on user-provided anchor HTML.
## 10.0.1 (2017-09-23) ## 10.0.1 (2017-09-23)
- [FIXED] Fix duplicate key errors in PostDeployMigrateUserExternalMailData migration. - [FIXED] Fix duplicate key errors in PostDeployMigrateUserExternalMailData migration.
...@@ -78,6 +85,8 @@ entry. ...@@ -78,6 +85,8 @@ entry.
- [FIXED] Fixed merge request changes bar jumping. - [FIXED] Fixed merge request changes bar jumping.
- [FIXED] Improve migrations using triggers. - [FIXED] Improve migrations using triggers.
- [FIXED] Fix ConvDev Index nav item and Monitoring submenu regression. - [FIXED] Fix ConvDev Index nav item and Monitoring submenu regression.
- [FIXED] disabling notifications globally now properly turns off group/project added
emails !13325
- [DEPRECATED] Deprecate custom SSH client configuration for the git user. !13930 - [DEPRECATED] Deprecate custom SSH client configuration for the git user. !13930
- [CHANGED] allow all users to delete their account. !13636 (Jacopo Beschi @jacopo-beschi) - [CHANGED] allow all users to delete their account. !13636 (Jacopo Beschi @jacopo-beschi)
- [CHANGED] Use full path of project's avatar in webhooks. !13649 (Vitaliy @blackst0ne Klachkov) - [CHANGED] Use full path of project's avatar in webhooks. !13649 (Vitaliy @blackst0ne Klachkov)
......
...@@ -26,7 +26,7 @@ gem 'doorkeeper', '~> 4.2.0' ...@@ -26,7 +26,7 @@ gem 'doorkeeper', '~> 4.2.0'
gem 'doorkeeper-openid_connect', '~> 1.1.0' gem 'doorkeeper-openid_connect', '~> 1.1.0'
gem 'omniauth', '~> 1.4.2' gem 'omniauth', '~> 1.4.2'
gem 'omniauth-auth0', '~> 1.4.1' gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-azure-oauth2', '~> 0.0.6' gem 'omniauth-azure-oauth2', '~> 0.0.9'
gem 'omniauth-cas3', '~> 1.1.4' gem 'omniauth-cas3', '~> 1.1.4'
gem 'omniauth-facebook', '~> 4.0.0' gem 'omniauth-facebook', '~> 4.0.0'
gem 'omniauth-github', '~> 1.1.1' gem 'omniauth-github', '~> 1.1.1'
......
...@@ -215,7 +215,7 @@ GEM ...@@ -215,7 +215,7 @@ GEM
factory_girl_rails (4.7.0) factory_girl_rails (4.7.0)
factory_girl (~> 4.7.0) factory_girl (~> 4.7.0)
railties (>= 3.0.0) railties (>= 3.0.0)
faraday (0.12.1) faraday (0.12.2)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
faraday_middleware (0.11.0.1) faraday_middleware (0.11.0.1)
faraday (>= 0.7.4, < 1.0) faraday (>= 0.7.4, < 1.0)
...@@ -545,10 +545,10 @@ GEM ...@@ -545,10 +545,10 @@ GEM
omniauth-oauth2 (~> 1.1) omniauth-oauth2 (~> 1.1)
omniauth-authentiq (0.3.1) omniauth-authentiq (0.3.1)
omniauth-oauth2 (~> 1.3, >= 1.3.1) omniauth-oauth2 (~> 1.3, >= 1.3.1)
omniauth-azure-oauth2 (0.0.6) omniauth-azure-oauth2 (0.0.9)
jwt (~> 1.0) jwt (~> 1.0)
omniauth (~> 1.0) omniauth (~> 1.0)
omniauth-oauth2 (~> 1.1) omniauth-oauth2 (~> 1.4)
omniauth-cas3 (1.1.4) omniauth-cas3 (1.1.4)
addressable (~> 2.3) addressable (~> 2.3)
nokogiri (~> 1.7, >= 1.7.1) nokogiri (~> 1.7, >= 1.7.1)
...@@ -574,7 +574,7 @@ GEM ...@@ -574,7 +574,7 @@ GEM
omniauth-oauth (1.1.0) omniauth-oauth (1.1.0)
oauth oauth
omniauth (~> 1.0) omniauth (~> 1.0)
omniauth-oauth2 (1.3.1) omniauth-oauth2 (1.4.0)
oauth2 (~> 1.0) oauth2 (~> 1.0)
omniauth (~> 1.2) omniauth (~> 1.2)
omniauth-oauth2-generic (0.2.2) omniauth-oauth2-generic (0.2.2)
...@@ -1113,7 +1113,7 @@ DEPENDENCIES ...@@ -1113,7 +1113,7 @@ DEPENDENCIES
omniauth (~> 1.4.2) omniauth (~> 1.4.2)
omniauth-auth0 (~> 1.4.1) omniauth-auth0 (~> 1.4.1)
omniauth-authentiq (~> 0.3.1) omniauth-authentiq (~> 0.3.1)
omniauth-azure-oauth2 (~> 0.0.6) omniauth-azure-oauth2 (~> 0.0.9)
omniauth-cas3 (~> 1.1.4) omniauth-cas3 (~> 1.1.4)
omniauth-facebook (~> 4.0.0) omniauth-facebook (~> 4.0.0)
omniauth-github (~> 1.1.1) omniauth-github (~> 1.1.1)
......
...@@ -95,9 +95,6 @@ $(() => { ...@@ -95,9 +95,6 @@ $(() => {
}); });
Store.rootPath = this.boardsEndpoint; Store.rootPath = this.boardsEndpoint;
this.filterManager = new FilteredSearchBoards(Store.filter, true, [(this.milestoneTitle ? 'milestone' : null)]);
this.filterManager.setup();
// Listen for updateTokens event // Listen for updateTokens event
eventHub.$on('updateTokens', this.updateTokens); eventHub.$on('updateTokens', this.updateTokens);
}, },
...@@ -105,6 +102,9 @@ $(() => { ...@@ -105,6 +102,9 @@ $(() => {
eventHub.$off('updateTokens', this.updateTokens); eventHub.$off('updateTokens', this.updateTokens);
}, },
mounted () { mounted () {
this.filterManager = new FilteredSearchBoards(Store.filter, true, [(this.milestoneTitle ? 'milestone' : null)]);
this.filterManager.setup();
Store.disabled = this.disabled; Store.disabled = this.disabled;
gl.boardService.all() gl.boardService.all()
.then(response => response.json()) .then(response => response.json())
......
export default {
props: {
count: {
type: Number,
required: true,
},
},
template: `
<span v-if="count === 50" class="events-info pull-right">
<i class="fa fa-warning has-tooltip"
aria-hidden="true"
:title="n__('Limited to showing %d event at most', 'Limited to showing %d events at most', 50)"
data-placement="top"></i>
{{ n__('Showing %d event', 'Showing %d events', 50) }}
</span>
`,
};
<script>
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
count: {
type: Number,
required: true,
},
},
directives: {
tooltip,
},
};
</script>
<template>
<span v-if="count === 50" class="events-info pull-right">
<i
class="fa fa-warning"
v-tooltip
aria-hidden="true"
:title="n__('Limited to showing %d event at most', 'Limited to showing %d events at most', 50)"
data-placement="top"></i>
{{ n__('Showing %d event', 'Showing %d events', 50) }}
</span>
</template>
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageCodeComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item">
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="mergeRequest.author.avatarUrl"/>
<h5 class="item-title merge-merquest-title">
<a :href="mergeRequest.url">
{{ mergeRequest.title }}
</a>
</h5>
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
&middot;
<span>
{{ s__('OpenedNDaysAgo|Opened') }}
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
</span>
<span>
{{ s__('ByAuthor|by') }}
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
</span>
</div>
<div class="item-time">
<total-time :time="mergeRequest.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
<script>
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
export default {
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
};
</script>
<template>
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item">
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="mergeRequest.author.avatarUrl"/>
<h5 class="item-title merge-merquest-title">
<a :href="mergeRequest.url">
{{ mergeRequest.title }}
</a>
</h5>
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
&middot;
<span>
{{ s__('OpenedNDaysAgo|Opened') }}
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
</span>
<span>
{{ s__('ByAuthor|by') }}
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
</span>
</div>
<div class="item-time">
<total-time :time="mergeRequest.totalTime"></total-time>
</div>
</li>
</ul>
</div>
</template>
<script>
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
export default {
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
};
</script>
<template>
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li
v-for="(issue, i) in items"
:key="i"
class="stage-event-item">
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="issue.author.avatarUrl"/>
<h5 class="item-title issue-title">
<a class="issue-title" :href="issue.url">
{{ issue.title }}
</a>
</h5>
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
&middot;
<span>
{{ s__('OpenedNDaysAgo|Opened') }}
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
</span>
<span>
{{ s__('ByAuthor|by') }}
<a :href="issue.author.webUrl" class="issue-author-link">
{{ issue.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="issue.totalTime"/>
</div>
</li>
</ul>
</div>
</template>
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageIssueComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item">
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="issue.author.avatarUrl"/>
<h5 class="item-title issue-title">
<a class="issue-title" :href="issue.url">
{{ issue.title }}
</a>
</h5>
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
&middot;
<span>
{{ s__('OpenedNDaysAgo|Opened') }}
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
</span>
<span>
{{ s__('ByAuthor|by') }}
<a :href="issue.author.webUrl" class="issue-author-link">
{{ issue.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="issue.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import iconCommit from '../svg/icon_commit.svg';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StagePlanComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
data() {
return { iconCommit };
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="commit in items" class="stage-event-item">
<div class="item-details item-conmmit-component">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="commit.author.avatarUrl"/>
<h5 class="item-title commit-title">
<a :href="commit.commitUrl">
{{ commit.title }}
</a>
</h5>
<span>
{{ s__('FirstPushedBy|First') }}
<span class="commit-icon">${iconCommit}</span>
<a :href="commit.commitUrl" class="commit-hash-link commit-sha">{{ commit.shortSha }}</a>
{{ s__('FirstPushedBy|pushed by') }}
<a :href="commit.author.webUrl" class="commit-author-link">
{{ commit.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="commit.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
<script>
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import iconCommit from '../svg/icon_commit.svg';
export default {
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
computed: {
iconCommit() {
return iconCommit;
},
},
};
</script>
<template>
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li
v-for="(commit, i) in items"
:key="i"
class="stage-event-item">
<div class="item-details item-conmmit-component">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="commit.author.avatarUrl"/>
<h5 class="item-title commit-title">
<a :href="commit.commitUrl">
{{ commit.title }}
</a>
</h5>
<span>
{{ s__('FirstPushedBy|First') }}
<span class="commit-icon" v-html="iconCommit"></span>
<a :href="commit.commitUrl" class="commit-hash-link commit-sha">{{ commit.shortSha }}</a>
{{ s__('FirstPushedBy|pushed by') }}
<a :href="commit.author.webUrl" class="commit-author-link">
{{ commit.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="commit.totalTime" />
</div>
</li>
</ul>
</div>
</template>
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageProductionComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item">
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="issue.author.avatarUrl"/>
<h5 class="item-title issue-title">
<a class="issue-title" :href="issue.url">
{{ issue.title }}
</a>
</h5>
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
&middot;
<span>
{{ s__('OpenedNDaysAgo|Opened') }}
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
</span>
<span>
{{ s__('ByAuthor|by') }}
<a :href="issue.author.webUrl" class="issue-author-link">
{{ issue.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="issue.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageReviewComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item">
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="mergeRequest.author.avatarUrl"/>
<h5 class="item-title merge-merquest-title">
<a :href="mergeRequest.url">
{{ mergeRequest.title }}
</a>
</h5>
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
&middot;
<span>
{{ s__('OpenedNDaysAgo|Opened') }}
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
</span>
<span>
{{ s__('ByAuthor|by') }}
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
</span>
<template v-if="mergeRequest.state === 'closed'">
<span class="merge-request-state">
<i class="fa fa-ban"></i>
{{ mergeRequest.state.toUpperCase() }}
</span>
</template>
<template v-else>
<span class="merge-request-branch" v-if="mergeRequest.branch">
<i class= "fa fa-code-fork"></i>
<a :href="mergeRequest.branch.url">{{ mergeRequest.branch.name }}</a>
</span>
</template>
</div>
<div class="item-time">
<total-time :time="mergeRequest.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
<script>
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
export default {
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
};
</script>
<template>
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li
v-for="(mergeRequest, i) in items"
:key="i"
class="stage-event-item">
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="mergeRequest.author.avatarUrl"/>
<h5 class="item-title merge-merquest-title">
<a :href="mergeRequest.url">
{{ mergeRequest.title }}
</a>
</h5>
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
&middot;
<span>
{{ s__('OpenedNDaysAgo|Opened') }}
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
</span>
<span>
{{ s__('ByAuthor|by') }}
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
</span>
<template v-if="mergeRequest.state === 'closed'">
<span class="merge-request-state">
<i class="fa fa-ban"></i>
{{ mergeRequest.state.toUpperCase() }}
</span>
</template>
<template v-else>
<span class="merge-request-branch" v-if="mergeRequest.branch">
<i class= "fa fa-code-fork"></i>
<a :href="mergeRequest.branch.url">{{ mergeRequest.branch.name }}</a>
</span>
</template>
</div>
<div class="item-time">
<total-time :time="mergeRequest.totalTime"/>
</div>
</li>
</ul>
</div>
</template>
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import iconBranch from '../svg/icon_branch.svg';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageStagingComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
data() {
return { iconBranch };
},
components: {
userAvatarImage,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="build in items" class="stage-event-item item-build-component">
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="build.author.avatarUrl"/>
<h5 class="item-title">
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
<i class="fa fa-code-fork"></i>
<a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a>
<span class="icon-branch">${iconBranch}</span>
<a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a>
</h5>
<span>
<a :href="build.url" class="build-date">{{ build.date }}</a>
{{ s__('ByAuthor|by') }}
<a :href="build.author.webUrl" class="issue-author-link">
{{ build.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="build.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
<script>
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import iconBranch from '../svg/icon_branch.svg';
export default {
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
computed: {
iconBranch() {
return iconBranch;
},
},
};
</script>
<template>
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li
v-for="(build, i) in items"
class="stage-event-item item-build-component"
:key="i">
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="build.author.avatarUrl"/>
<h5 class="item-title">
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
<i class="fa fa-code-fork"></i>
<a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a>
<span class="icon-branch" v-html="iconBranch"></span>
<a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a>
</h5>
<span>
<a :href="build.url" class="build-date">{{ build.date }}</a>
{{ s__('ByAuthor|by') }}
<a :href="build.author.webUrl" class="issue-author-link">
{{ build.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="build.totalTime"/>
</div>
</li>
</ul>
</div>
</template>
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import iconBuildStatus from '../svg/icon_build_status.svg';
import iconBranch from '../svg/icon_branch.svg';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageTestComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
data() {
return { iconBuildStatus, iconBranch };
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="build in items" class="stage-event-item item-build-component">
<div class="item-details">
<h5 class="item-title">
<span class="icon-build-status">${iconBuildStatus}</span>
<a :href="build.url" class="item-build-name">{{ build.name }}</a>
&middot;
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
<i class="fa fa-code-fork"></i>
<a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a>
<span class="icon-branch">${iconBranch}</span>
<a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a>
</h5>
<span>
<a :href="build.url" class="issue-date">
{{ build.date }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="build.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
<script>
import iconBuildStatus from '../svg/icon_build_status.svg';
import iconBranch from '../svg/icon_branch.svg';
export default {
props: {
items: Array,
stage: Object,
},
computed: {
iconBuildStatus() {
return iconBuildStatus;
},
iconBranch() {
return iconBranch;
},
},
};
</script>
<template>
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li
v-for="(build, i) in items"
:key="i"
class="stage-event-item item-build-component">
<div class="item-details">
<h5 class="item-title">
<span class="icon-build-status" v-html="iconBuildStatus"></span>
<a :href="build.url" class="item-build-name">{{ build.name }}</a>
&middot;
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
<i class="fa fa-code-fork"></i>
<a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a>
<span class="icon-branch" v-html="iconBranch"></span>
<a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a>
</h5>
<span>
<a :href="build.url" class="issue-date">
{{ build.date }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="build.totalTime"/>
</div>
</li>
</ul>
</div>
</template>
/* eslint-disable no-param-reassign */
import Vue from 'vue';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.TotalTimeComponent = Vue.extend({
props: {
time: Object,
},
template: `
<span class="total-time">
<template v-if="Object.keys(time).length">
<template v-if="time.days">{{ time.days }} <span>{{ n__('day', 'days', time.days) }}</span></template>
<template v-if="time.hours">{{ time.hours }} <span>{{ n__('Time|hr', 'Time|hrs', time.hours) }}</span></template>
<template v-if="time.mins && !time.days">{{ time.mins }} <span>{{ n__('Time|min', 'Time|mins', time.mins) }}</span></template>
<template v-if="time.seconds && Object.keys(time).length === 1 || time.seconds === 0">{{ time.seconds }} <span>{{ s__('Time|s') }}</span></template>
</template>
<template v-else>
--
</template>
</span>
`,
});
<script>
export default {
props: {
time: {
type: Object,
required: false,
default: () => ({}),
},
},
computed: {
hasData() {
return Object.keys(this.time).length;
},
},
};
</script>
<template>
<span class="total-time">
<template v-if="hasData">
<template v-if="time.days">{{ time.days }} <span>{{ n__('day', 'days', time.days) }}</span></template>
<template v-if="time.hours">{{ time.hours }} <span>{{ n__('Time|hr', 'Time|hrs', time.hours) }}</span></template>
<template v-if="time.mins && !time.days">{{ time.mins }} <span>{{ n__('Time|min', 'Time|mins', time.mins) }}</span></template>
<template v-if="time.seconds && hasDa === 1 || time.seconds === 0">{{ time.seconds }} <span>{{ s__('Time|s') }}</span></template>
</template>
<template v-else>
--
</template>
</span>
</template>
...@@ -3,60 +3,63 @@ ...@@ -3,60 +3,63 @@
import Vue from 'vue'; import Vue from 'vue';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import Translate from '../vue_shared/translate'; import Translate from '../vue_shared/translate';
import LimitWarningComponent from './components/limit_warning_component'; import limitWarningComponent from './components/limit_warning_component.vue';
import './components/stage_code_component'; import stageCodeComponent from './components/stage_code_component.vue';
import './components/stage_issue_component'; import stagePlanComponent from './components/stage_plan_component.vue';
import './components/stage_plan_component'; import stageComponent from './components/stage_component.vue';
import './components/stage_production_component'; import stageReviewComponent from './components/stage_review_component.vue';
import './components/stage_review_component'; import stageStagingComponent from './components/stage_staging_component.vue';
import './components/stage_staging_component'; import stageTestComponent from './components/stage_test_component.vue';
import './components/stage_test_component'; import totalTime from './components/total_time_component.vue';
import './components/total_time_component'; import CycleAnalyticsService from './cycle_analytics_service';
import './cycle_analytics_service'; import CycleAnalyticsStore from './cycle_analytics_store';
import './cycle_analytics_store';
Vue.use(Translate); Vue.use(Translate);
$(() => { $(() => {
const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed'; const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
const cycleAnalyticsEl = document.querySelector('#cycle-analytics');
const cycleAnalyticsStore = gl.cycleAnalytics.CycleAnalyticsStore;
const cycleAnalyticsService = new gl.cycleAnalytics.CycleAnalyticsService({
requestPath: cycleAnalyticsEl.dataset.requestPath,
});
gl.cycleAnalyticsApp = new Vue({ gl.cycleAnalyticsApp = new Vue({
el: '#cycle-analytics', el: '#cycle-analytics',
name: 'CycleAnalytics', name: 'CycleAnalytics',
data: { data() {
state: cycleAnalyticsStore.state, const cycleAnalyticsEl = document.querySelector('#cycle-analytics');
const cycleAnalyticsService = new CycleAnalyticsService({
requestPath: cycleAnalyticsEl.dataset.requestPath,
});
return {
store: CycleAnalyticsStore,
state: CycleAnalyticsStore.state,
isLoading: false, isLoading: false,
isLoadingStage: false, isLoadingStage: false,
isEmptyStage: false, isEmptyStage: false,
hasError: false, hasError: false,
startDate: 30, startDate: 30,
isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE), isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE),
service: cycleAnalyticsService,
};
}, },
computed: { computed: {
currentStage() { currentStage() {
return cycleAnalyticsStore.currentActiveStage(); return this.store.currentActiveStage();
}, },
}, },
components: { components: {
'stage-issue-component': gl.cycleAnalytics.StageIssueComponent, 'stage-issue-component': stageComponent,
'stage-plan-component': gl.cycleAnalytics.StagePlanComponent, 'stage-plan-component': stagePlanComponent,
'stage-code-component': gl.cycleAnalytics.StageCodeComponent, 'stage-code-component': stageCodeComponent,
'stage-test-component': gl.cycleAnalytics.StageTestComponent, 'stage-test-component': stageTestComponent,
'stage-review-component': gl.cycleAnalytics.StageReviewComponent, 'stage-review-component': stageReviewComponent,
'stage-staging-component': gl.cycleAnalytics.StageStagingComponent, 'stage-staging-component': stageStagingComponent,
'stage-production-component': gl.cycleAnalytics.StageProductionComponent, 'stage-production-component': stageComponent,
}, },
created() { created() {
this.fetchCycleAnalyticsData(); this.fetchCycleAnalyticsData();
}, },
methods: { methods: {
handleError() { handleError() {
cycleAnalyticsStore.setErrorState(true); this.store.setErrorState(true);
return new Flash('There was an error while fetching cycle analytics data.'); return new Flash('There was an error while fetching cycle analytics data.');
}, },
initDropdown() { initDropdown() {
...@@ -77,17 +80,17 @@ $(() => { ...@@ -77,17 +80,17 @@ $(() => {
this.isLoading = true; this.isLoading = true;
cycleAnalyticsService this.service
.fetchCycleAnalyticsData(fetchOptions) .fetchCycleAnalyticsData(fetchOptions)
.done((response) => { .then(resp => resp.json())
cycleAnalyticsStore.setCycleAnalyticsData(response); .then((response) => {
this.store.setCycleAnalyticsData(response);
this.selectDefaultStage(); this.selectDefaultStage();
this.initDropdown(); this.initDropdown();
this.isLoading = false;
}) })
.error(() => { .catch(() => {
this.handleError(); this.handleError();
})
.always(() => {
this.isLoading = false; this.isLoading = false;
}); });
}, },
...@@ -100,27 +103,27 @@ $(() => { ...@@ -100,27 +103,27 @@ $(() => {
if (this.currentStage === stage) return; if (this.currentStage === stage) return;
if (!stage.isUserAllowed) { if (!stage.isUserAllowed) {
cycleAnalyticsStore.setActiveStage(stage); this.store.setActiveStage(stage);
return; return;
} }
this.isLoadingStage = true; this.isLoadingStage = true;
cycleAnalyticsStore.setStageEvents([], stage); this.store.setStageEvents([], stage);
cycleAnalyticsStore.setActiveStage(stage); this.store.setActiveStage(stage);
cycleAnalyticsService this.service
.fetchStageData({ .fetchStageData({
stage, stage,
startDate: this.startDate, startDate: this.startDate,
}) })
.done((response) => { .then(resp => resp.json())
.then((response) => {
this.isEmptyStage = !response.events.length; this.isEmptyStage = !response.events.length;
cycleAnalyticsStore.setStageEvents(response.events, stage); this.store.setStageEvents(response.events, stage);
this.isLoadingStage = false;
}) })
.error(() => { .catch(() => {
this.isEmptyStage = true; this.isEmptyStage = true;
})
.always(() => {
this.isLoadingStage = false; this.isLoadingStage = false;
}); });
}, },
...@@ -132,6 +135,6 @@ $(() => { ...@@ -132,6 +135,6 @@ $(() => {
}); });
// Register global components // Register global components
Vue.component('limit-warning', LimitWarningComponent); Vue.component('limit-warning', limitWarningComponent);
Vue.component('total-time', gl.cycleAnalytics.TotalTimeComponent); Vue.component('total-time', totalTime);
}); });
/* eslint-disable no-param-reassign */ import Vue from 'vue';
import VueResource from 'vue-resource';
const global = window.gl || (window.gl = {}); Vue.use(VueResource);
global.cycleAnalytics = global.cycleAnalytics || {};
class CycleAnalyticsService { export default class CycleAnalyticsService {
constructor(options) { constructor(options) {
this.requestPath = options.requestPath; this.requestPath = options.requestPath;
this.cycleAnalytics = Vue.resource(this.requestPath);
} }
fetchCycleAnalyticsData(options) { fetchCycleAnalyticsData(options = { startDate: 30 }) {
options = options || { startDate: 30 }; return this.cycleAnalytics.get({ cycle_analytics: { start_date: options.startDate } });
return $.ajax({
url: this.requestPath,
method: 'GET',
dataType: 'json',
contentType: 'application/json',
data: {
cycle_analytics: {
start_date: options.startDate,
},
},
});
} }
fetchStageData(options) { fetchStageData(options) {
...@@ -30,12 +19,12 @@ class CycleAnalyticsService { ...@@ -30,12 +19,12 @@ class CycleAnalyticsService {
startDate, startDate,
} = options; } = options;
return $.get(`${this.requestPath}/events/${stage.name}.json`, { return Vue.http.get(`${this.requestPath}/events/${stage.name}.json`, {
params: {
cycle_analytics: { cycle_analytics: {
start_date: startDate, start_date: startDate,
}, },
},
}); });
} }
} }
global.cycleAnalytics.CycleAnalyticsService = CycleAnalyticsService;
...@@ -4,9 +4,6 @@ import { __ } from '../locale'; ...@@ -4,9 +4,6 @@ import { __ } from '../locale';
import '../lib/utils/text_utility'; import '../lib/utils/text_utility';
import DEFAULT_EVENT_OBJECTS from './default_event_objects'; import DEFAULT_EVENT_OBJECTS from './default_event_objects';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
const EMPTY_STAGE_TEXTS = { const EMPTY_STAGE_TEXTS = {
issue: __('The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.'), issue: __('The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.'),
plan: __('The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.'), plan: __('The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.'),
...@@ -17,7 +14,7 @@ const EMPTY_STAGE_TEXTS = { ...@@ -17,7 +14,7 @@ const EMPTY_STAGE_TEXTS = {
production: __('The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.'), production: __('The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.'),
}; };
global.cycleAnalytics.CycleAnalyticsStore = { export default {
state: { state: {
summary: '', summary: '',
stats: '', stats: '',
......
...@@ -602,6 +602,7 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -602,6 +602,7 @@ import initGroupAnalytics from './init_group_analytics';
case 'groups:analytics:show': case 'groups:analytics:show':
initGroupAnalytics(); initGroupAnalytics();
break; break;
} }
switch (path[0]) { switch (path[0]) {
case 'sessions': case 'sessions':
......
...@@ -34,7 +34,7 @@ export const canShowActiveSubItems = (el) => { ...@@ -34,7 +34,7 @@ export const canShowActiveSubItems = (el) => {
export const canShowSubItems = () => bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md' || bp.getBreakpointSize() === 'lg'; export const canShowSubItems = () => bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md' || bp.getBreakpointSize() === 'lg';
export const getHideSubItemsInterval = () => { export const getHideSubItemsInterval = () => {
if (!currentOpenMenu) return 0; if (!currentOpenMenu || !mousePos.length) return 0;
const currentMousePos = mousePos[mousePos.length - 1]; const currentMousePos = mousePos[mousePos.length - 1];
const prevMousePos = mousePos[0]; const prevMousePos = mousePos[0];
......
document.addEventListener('DOMContentLoaded', () => {
const showGroupLink = () => {
const $cnLink = $('.cn-link');
const $filterLink = $('.filter-link');
const $checkedSync = $('input[name="sync_method"]:checked').val() === 'group';
$cnLink.toggle($checkedSync);
$filterLink.toggle(!$checkedSync);
};
$('input[name="sync_method"]').on('change', showGroupLink);
showGroupLink();
});
export const isSticky = (el, scrollY, stickyTop) => { export const createPlaceholder = () => {
const placeholder = document.createElement('div');
placeholder.classList.add('sticky-placeholder');
return placeholder;
};
export const isSticky = (el, scrollY, stickyTop, insertPlaceholder) => {
const top = Math.floor(el.offsetTop - scrollY); const top = Math.floor(el.offsetTop - scrollY);
if (top <= stickyTop) { if (top <= stickyTop && !el.classList.contains('is-stuck')) {
const placeholder = insertPlaceholder ? createPlaceholder() : null;
const heightBefore = el.offsetHeight;
el.classList.add('is-stuck'); el.classList.add('is-stuck');
} else {
if (insertPlaceholder) {
el.parentNode.insertBefore(placeholder, el.nextElementSibling);
placeholder.style.height = `${heightBefore - el.offsetHeight}px`;
}
} else if (top > stickyTop && el.classList.contains('is-stuck')) {
el.classList.remove('is-stuck'); el.classList.remove('is-stuck');
if (insertPlaceholder && el.nextElementSibling && el.nextElementSibling.classList.contains('sticky-placeholder')) {
el.nextElementSibling.remove();
}
} }
}; };
export default (el) => { export default (el, insertPlaceholder = true) => {
if (!el) return; if (!el) return;
const computedStyle = window.getComputedStyle(el); const computedStyle = window.getComputedStyle(el);
...@@ -17,7 +37,7 @@ export default (el) => { ...@@ -17,7 +37,7 @@ export default (el) => {
const stickyTop = parseInt(computedStyle.top, 10); const stickyTop = parseInt(computedStyle.top, 10);
document.addEventListener('scroll', () => isSticky(el, window.scrollY, stickyTop), { document.addEventListener('scroll', () => isSticky(el, window.scrollY, stickyTop, insertPlaceholder), {
passive: true, passive: true,
}); });
}; };
...@@ -28,52 +28,56 @@ ...@@ -28,52 +28,56 @@
// </div> // </div>
// </div> // </div>
// //
(function() {
this.LineHighlighter = (function() { const LineHighlighter = function(options = {}) {
// CSS class applied to highlighted lines options.highlightLineClass = options.highlightLineClass || 'hll';
LineHighlighter.prototype.highlightClass = 'hll'; options.fileHolderSelector = options.fileHolderSelector || '.file-holder';
options.scrollFileHolder = options.scrollFileHolder || false;
// Internal copy of location.hash so we're not dependent on `location` in tests options.hash = options.hash || location.hash;
LineHighlighter.prototype._hash = '';
this.options = options;
function LineHighlighter(hash) { this._hash = options.hash;
if (hash == null) { this.highlightLineClass = options.highlightLineClass;
// Initialize a LineHighlighter object
//
// hash - String URL hash for dependency injection in tests
hash = location.hash;
}
this.setHash = this.setHash.bind(this); this.setHash = this.setHash.bind(this);
this.highlightLine = this.highlightLine.bind(this); this.highlightLine = this.highlightLine.bind(this);
this.clickHandler = this.clickHandler.bind(this); this.clickHandler = this.clickHandler.bind(this);
this.highlightHash = this.highlightHash.bind(this); this.highlightHash = this.highlightHash.bind(this);
this._hash = hash;
this.bindEvents(); this.bindEvents();
this.highlightHash(); this.highlightHash();
} };
LineHighlighter.prototype.bindEvents = function() {
const $fileHolder = $(this.options.fileHolderSelector);
LineHighlighter.prototype.bindEvents = function() {
const $fileHolder = $('.file-holder');
$fileHolder.on('click', 'a[data-line-number]', this.clickHandler); $fileHolder.on('click', 'a[data-line-number]', this.clickHandler);
$fileHolder.on('highlight:line', this.highlightHash); $fileHolder.on('highlight:line', this.highlightHash);
}; };
LineHighlighter.prototype.highlightHash = function() { LineHighlighter.prototype.highlightHash = function() {
var range; var range;
if (this._hash !== '') { if (this._hash !== '') {
range = this.hashToRange(this._hash); range = this.hashToRange(this._hash);
if (range[0]) { if (range[0]) {
this.highlightRange(range); this.highlightRange(range);
$.scrollTo("#L" + range[0], { const lineSelector = `#L${range[0]}`;
const scrollOptions = {
// Scroll to the first highlighted line on initial load // Scroll to the first highlighted line on initial load
// Offset -50 for the sticky top bar, and another -100 for some context // Offset -50 for the sticky top bar, and another -100 for some context
offset: -150 offset: -150
}); };
if (this.options.scrollFileHolder) {
$(this.options.fileHolderSelector).scrollTo(lineSelector, scrollOptions);
} else {
$.scrollTo(lineSelector, scrollOptions);
} }
} }
}; }
};
LineHighlighter.prototype.clickHandler = function(event) { LineHighlighter.prototype.clickHandler = function(event) {
var current, lineNumber, range; var current, lineNumber, range;
event.preventDefault(); event.preventDefault();
this.clearHighlight(); this.clearHighlight();
...@@ -93,25 +97,24 @@ ...@@ -93,25 +97,24 @@
this.setHash(range[0], range[1]); this.setHash(range[0], range[1]);
return this.highlightRange(range); return this.highlightRange(range);
} }
}; };
LineHighlighter.prototype.clearHighlight = function() { LineHighlighter.prototype.clearHighlight = function() {
return $("." + this.highlightClass).removeClass(this.highlightClass); return $("." + this.highlightLineClass).removeClass(this.highlightLineClass);
// Unhighlight previously highlighted lines };
};
// Convert a URL hash String into line numbers // Convert a URL hash String into line numbers
// //
// hash - Hash String // hash - Hash String
// //
// Examples: // Examples:
// //
// hashToRange('#L5') # => [5, null] // hashToRange('#L5') # => [5, null]
// hashToRange('#L5-15') # => [5, 15] // hashToRange('#L5-15') # => [5, 15]
// hashToRange('#foo') # => [null, null] // hashToRange('#foo') # => [null, null]
// //
// Returns an Array // Returns an Array
LineHighlighter.prototype.hashToRange = function(hash) { LineHighlighter.prototype.hashToRange = function(hash) {
var first, last, matches; var first, last, matches;
// ?L(\d+)(?:-(\d+))?$/) // ?L(\d+)(?:-(\d+))?$/)
matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/); matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/);
...@@ -122,19 +125,19 @@ ...@@ -122,19 +125,19 @@
} else { } else {
return [null, null]; return [null, null];
} }
}; };
// Highlight a single line // Highlight a single line
// //
// lineNumber - Line number to highlight // lineNumber - Line number to highlight
LineHighlighter.prototype.highlightLine = function(lineNumber) { LineHighlighter.prototype.highlightLine = function(lineNumber) {
return $("#LC" + lineNumber).addClass(this.highlightClass); return $("#LC" + lineNumber).addClass(this.highlightLineClass);
}; };
// Highlight all lines within a range // Highlight all lines within a range
// //
// range - Array containing the starting and ending line numbers // range - Array containing the starting and ending line numbers
LineHighlighter.prototype.highlightRange = function(range) { LineHighlighter.prototype.highlightRange = function(range) {
var i, lineNumber, ref, ref1, results; var i, lineNumber, ref, ref1, results;
if (range[1]) { if (range[1]) {
results = []; results = [];
...@@ -145,10 +148,10 @@ ...@@ -145,10 +148,10 @@
} else { } else {
return this.highlightLine(range[0]); return this.highlightLine(range[0]);
} }
}; };
// Set the URL hash string // Set the URL hash string
LineHighlighter.prototype.setHash = function(firstLineNumber, lastLineNumber) { LineHighlighter.prototype.setHash = function(firstLineNumber, lastLineNumber) {
var hash; var hash;
if (lastLineNumber) { if (lastLineNumber) {
hash = "#L" + firstLineNumber + "-" + lastLineNumber; hash = "#L" + firstLineNumber + "-" + lastLineNumber;
...@@ -157,19 +160,17 @@ ...@@ -157,19 +160,17 @@
} }
this._hash = hash; this._hash = hash;
return this.__setLocationHash__(hash); return this.__setLocationHash__(hash);
}; };
// Make the actual hash change in the browser // Make the actual hash change in the browser
// //
// This method is stubbed in tests. // This method is stubbed in tests.
LineHighlighter.prototype.__setLocationHash__ = function(value) { LineHighlighter.prototype.__setLocationHash__ = function(value) {
return history.pushState({ return history.pushState({
url: value url: value
// We're using pushState instead of assigning location.hash directly to // We're using pushState instead of assigning location.hash directly to
// prevent the page from scrolling on the hashchange event // prevent the page from scrolling on the hashchange event
}, document.title, value); }, document.title, value);
}; };
return LineHighlighter; window.LineHighlighter = LineHighlighter;
})();
}).call(window);
...@@ -352,7 +352,7 @@ import { ...@@ -352,7 +352,7 @@ import {
} }
expandViewContainer() { expandViewContainer() {
const $wrapper = $('.content-wrapper .container-fluid'); const $wrapper = $('.content-wrapper .container-fluid').not('.breadcrumbs');
if (this.fixedLayoutPref === null) { if (this.fixedLayoutPref === null) {
this.fixedLayoutPref = $wrapper.hasClass('container-limited'); this.fixedLayoutPref = $wrapper.hasClass('container-limited');
} }
......
<script> <script>
/* global LineHighlighter */
import Store from '../stores/repo_store'; import Store from '../stores/repo_store';
export default { export default {
data: () => Store, data: () => Store,
mounted() {
this.highlightFile();
},
computed: { computed: {
html() { html() {
return this.activeFile.html; return this.activeFile.html;
}, },
}, },
methods: { methods: {
highlightFile() { highlightFile() {
$(this.$el).find('.file-content').syntaxHighlight(); $(this.$el).find('.file-content').syntaxHighlight();
}, },
}, },
mounted() {
this.highlightFile();
this.lineHighlighter = new LineHighlighter({
fileHolderSelector: '.blob-viewer-container',
scrollFileHolder: true,
});
},
watch: { watch: {
html() { html() {
this.$nextTick(() => { this.$nextTick(() => {
......
...@@ -178,8 +178,8 @@ const RepoHelper = { ...@@ -178,8 +178,8 @@ const RepoHelper = {
setFile(data, file) { setFile(data, file) {
const newFile = data; const newFile = data;
newFile.url = file.url || Service.url; // Grab the URL from service, happens on page refresh.
newFile.url = file.url;
if (newFile.render_error === 'too_large' || newFile.render_error === 'collapsed') { if (newFile.render_error === 'too_large' || newFile.render_error === 'collapsed') {
newFile.tooLarge = true; newFile.tooLarge = true;
} }
......
...@@ -260,7 +260,7 @@ ...@@ -260,7 +260,7 @@
position: relative; position: relative;
border: 1px solid $blue-300; border: 1px solid $blue-300;
border-radius: $border-radius-default; border-radius: $border-radius-default;
background-color: $blue-25; background-color: $blue-50;
justify-content: center; justify-content: center;
.dismiss-button { .dismiss-button {
......
...@@ -779,6 +779,14 @@ ...@@ -779,6 +779,14 @@
white-space: normal; white-space: normal;
width: 100%; width: 100%;
&.dropdown-menu-user-link {
white-space: nowrap;
.dropdown-menu-user-username {
display: block;
}
}
// make sure the text color is not overriden // make sure the text color is not overriden
&.text-danger { &.text-danger {
color: $brand-danger; color: $brand-danger;
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
// Header // Header
header.navbar-gitlab-new { header.navbar-gitlab-new {
background: linear-gradient(to right, $color-900, $color-800); background-color: $color-900;
.navbar-collapse { .navbar-collapse {
color: $color-200; color: $color-200;
...@@ -201,7 +201,7 @@ body { ...@@ -201,7 +201,7 @@ body {
@include gitlab-theme($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-700, $theme-gray-700, $theme-gray-100, $theme-gray-700); @include gitlab-theme($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-700, $theme-gray-700, $theme-gray-100, $theme-gray-700);
header.navbar-gitlab-new { header.navbar-gitlab-new {
background: $theme-gray-100; background-color: $theme-gray-100;
box-shadow: 0 2px 0 0 $border-color; box-shadow: 0 2px 0 0 $border-color;
.logo-text svg { .logo-text svg {
...@@ -242,10 +242,10 @@ body { ...@@ -242,10 +242,10 @@ body {
&:hover { &:hover {
background-color: $white-light; background-color: $white-light;
box-shadow: inset 0 0 0 1px $blue-100; box-shadow: inset 0 0 0 1px $blue-200;
.location-badge { .location-badge {
box-shadow: inset 0 0 0 1px $blue-100; box-shadow: inset 0 0 0 1px $blue-200;
} }
} }
} }
......
...@@ -142,7 +142,43 @@ ...@@ -142,7 +142,43 @@
} }
@mixin green-status-color { @mixin green-status-color {
@include status-color($green-50, $green-500, $green-700); @include status-color($green-100, $green-500, $green-700);
}
@mixin fade($gradient-direction, $gradient-color) {
visibility: hidden;
opacity: 0;
z-index: 2;
position: absolute;
bottom: 12px;
width: 43px;
height: 30px;
transition-duration: .3s;
-webkit-transform: translateZ(0);
background: linear-gradient(to $gradient-direction, $gradient-color 45%, rgba($gradient-color, 0.4));
&.scrolling {
visibility: visible;
opacity: 1;
transition-duration: .3s;
}
.fa {
position: relative;
top: 5px;
font-size: 18px;
}
}
@mixin scrolling-links() {
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
display: flex;
&::-webkit-scrollbar {
display: none;
}
} }
@mixin fade($gradient-direction, $gradient-color) { @mixin fade($gradient-direction, $gradient-color) {
......
...@@ -306,6 +306,8 @@ header.navbar-gitlab-new { ...@@ -306,6 +306,8 @@ header.navbar-gitlab-new {
display: flex; display: flex;
width: 100%; width: 100%;
position: relative; position: relative;
padding-top: $gl-padding / 2;
padding-bottom: $gl-padding / 2;
align-items: center; align-items: center;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
} }
...@@ -317,11 +319,6 @@ header.navbar-gitlab-new { ...@@ -317,11 +319,6 @@ header.navbar-gitlab-new {
align-self: center; align-self: center;
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
@media (max-width: $screen-xs-max) {
padding-left: 17px;
border-left: 1px solid $gl-text-color-quaternary;
}
.avatar-tile { .avatar-tile {
margin-right: 4px; margin-right: 4px;
border: 1px solid $border-color; border: 1px solid $border-color;
...@@ -351,6 +348,7 @@ header.navbar-gitlab-new { ...@@ -351,6 +348,7 @@ header.navbar-gitlab-new {
display: flex; display: flex;
align-items: center; align-items: center;
position: relative; position: relative;
padding: 2px 0;
&:not(:last-child) { &:not(:last-child) {
margin-right: 20px; margin-right: 20px;
...@@ -386,7 +384,7 @@ header.navbar-gitlab-new { ...@@ -386,7 +384,7 @@ header.navbar-gitlab-new {
margin: 0; margin: 0;
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
line-height: 1; line-height: 16px;
a { a {
color: $gl-text-color; color: $gl-text-color;
......
...@@ -461,6 +461,13 @@ $new-sidebar-collapsed-width: 50px; ...@@ -461,6 +461,13 @@ $new-sidebar-collapsed-width: 50px;
font-size: 18px; font-size: 18px;
} }
} }
@media (max-width: $screen-xs-max) {
+ .breadcrumbs-links {
padding-left: 17px;
border-left: 1px solid $gl-text-color-quaternary;
}
}
} }
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
......
...@@ -137,7 +137,7 @@ $well-border: #eee; ...@@ -137,7 +137,7 @@ $well-border: #eee;
//## //##
$code-color: $red-600; $code-color: $red-600;
$code-bg: lighten($red-50, 2%); $code-bg: lighten($red-100, 2%);
$kbd-color: $white-light; $kbd-color: $white-light;
$kbd-bg: #333; $kbd-bg: #333;
......
...@@ -7,6 +7,7 @@ $gutter_inner_width: 250px; ...@@ -7,6 +7,7 @@ $gutter_inner_width: 250px;
$sidebar-transition-duration: .15s; $sidebar-transition-duration: .15s;
$sidebar-breakpoint: 1024px; $sidebar-breakpoint: 1024px;
$default-transition-duration: .15s; $default-transition-duration: .15s;
$right-sidebar-transition-duration: .3s;
/* /*
* Color schema * Color schema
...@@ -29,46 +30,45 @@ $gray-dark: darken($gray-light, $darken-dark-factor); ...@@ -29,46 +30,45 @@ $gray-dark: darken($gray-light, $darken-dark-factor);
$gray-darker: #eee; $gray-darker: #eee;
$gray-darkest: #c4c4c4; $gray-darkest: #c4c4c4;
$green-25: #f6fcf8; $green-50: #f1fdf6;
$green-50: #e4f5eb; $green-100: #dcf5e7;
$green-100: #bae6cc; $green-200: #b3e6c8;
$green-200: #8dd5aa; $green-300: #75d09b;
$green-300: #5fc488; $green-400: #37b96d;
$green-400: #3cb76f;
$green-500: #1aaa55; $green-500: #1aaa55;
$green-600: #168f48; $green-600: #168f48;
$green-700: #12753a; $green-700: #12753a;
$green-800: #0e5a2d; $green-800: #0e5a2d;
$green-900: #0a4020; $green-900: #0a4020;
$green-950: #072b15;
$blue-25: #f6fafd; $blue-50: #f6fafe;
$blue-50: #e4eff9; $blue-100: #e4f0fb;
$blue-100: #bcd7f1; $blue-200: #b8d6f4;
$blue-200: #8fbce8; $blue-300: #73afea;
$blue-300: #62a1df; $blue-400: #2e87e0;
$blue-400: #418cd8;
$blue-500: #1f78d1; $blue-500: #1f78d1;
$blue-600: #1b69b6; $blue-600: #1b69b6;
$blue-700: #17599c; $blue-700: #17599c;
$blue-800: #134a81; $blue-800: #134a81;
$blue-900: #0f3b66; $blue-900: #0f3b66;
$blue-950: #0a2744;
$orange-25: #fffcf8; $orange-50: #fffaf4;
$orange-50: #fff2e1; $orange-100: #fff1de;
$orange-100: #fedfb3; $orange-200: #fed69f;
$orange-200: #feca81; $orange-300: #fdbc60;
$orange-300: #fdb44f; $orange-400: #fca121;
$orange-400: #fca429;
$orange-500: #fc9403; $orange-500: #fc9403;
$orange-600: #de7e00; $orange-600: #de7e00;
$orange-700: #c26700; $orange-700: #c26700;
$orange-800: #a35100; $orange-800: #a35200;
$orange-900: #853b00; $orange-900: #853c00;
$orange-950: #592800;
$red-25: #fef7f6; $red-50: #fef6f5;
$red-50: #fbe7e4; $red-100: #fbe5e1;
$red-100: #f4c4bc; $red-200: #f2b4a9;
$red-200: #ed9d90;
$red-300: #e67664; $red-300: #e67664;
$red-400: #e05842; $red-400: #e05842;
$red-500: #db3b21; $red-500: #db3b21;
...@@ -76,6 +76,7 @@ $red-600: #c0341d; ...@@ -76,6 +76,7 @@ $red-600: #c0341d;
$red-700: #a62d19; $red-700: #a62d19;
$red-800: #8b2615; $red-800: #8b2615;
$red-900: #711e11; $red-900: #711e11;
$red-950: #4b140b;
// GitLab themes // GitLab themes
...@@ -186,8 +187,8 @@ $list-text-disabled-color: $gl-text-color-tertiary; ...@@ -186,8 +187,8 @@ $list-text-disabled-color: $gl-text-color-tertiary;
$list-border-light: #eee; $list-border-light: #eee;
$list-border: rgba(0, 0, 0, 0.05); $list-border: rgba(0, 0, 0, 0.05);
$list-text-height: 42px; $list-text-height: 42px;
$list-warning-row-bg: $orange-50; $list-warning-row-bg: $orange-100;
$list-warning-row-border: $orange-100; $list-warning-row-border: $orange-200;
$list-warning-row-color: $orange-700; $list-warning-row-color: $orange-700;
/* /*
...@@ -216,8 +217,8 @@ $gl-sidebar-padding: 22px; ...@@ -216,8 +217,8 @@ $gl-sidebar-padding: 22px;
/* /*
* Misc * Misc
*/ */
$row-hover: $blue-25; $row-hover: $blue-50;
$row-hover-border: $blue-100; $row-hover-border: $blue-200;
$progress-color: #c0392b; $progress-color: #c0392b;
$header-height: 50px; $header-height: 50px;
$new-navbar-height: 40px; $new-navbar-height: 40px;
...@@ -272,8 +273,8 @@ $time-color: #999; ...@@ -272,8 +273,8 @@ $time-color: #999;
$project-member-show-color: #aaa; $project-member-show-color: #aaa;
$gl-promo-color: #aaa; $gl-promo-color: #aaa;
$error-bg: $red-400; $error-bg: $red-400;
$warning-message-bg: $orange-50; $warning-message-bg: $orange-100;
$warning-message-border: $orange-100; $warning-message-border: $orange-200;
$warning-message-color: $orange-700; $warning-message-color: $orange-700;
$control-group-descr-color: #666; $control-group-descr-color: #666;
$table-permission-x-bg: #d9edf7; $table-permission-x-bg: #d9edf7;
...@@ -459,17 +460,17 @@ $builds-trace-bg: #111; ...@@ -459,17 +460,17 @@ $builds-trace-bg: #111;
/* /*
* Callout * Callout
*/ */
$callout-danger-bg: $red-50; $callout-danger-bg: $red-100;
$callout-danger-border: $red-100; $callout-danger-border: $red-200;
$callout-danger-color: $red-700; $callout-danger-color: $red-700;
$callout-warning-bg: $orange-50; $callout-warning-bg: $orange-100;
$callout-warning-border: $orange-100; $callout-warning-border: $orange-200;
$callout-warning-color: $orange-700; $callout-warning-color: $orange-700;
$callout-info-bg: $blue-50; $callout-info-bg: $blue-100;
$callout-info-border: $blue-100; $callout-info-border: $blue-200;
$callout-info-color: $blue-700; $callout-info-color: $blue-700;
$callout-success-bg: $green-50; $callout-success-bg: $green-100;
$callout-success-border: $green-100; $callout-success-border: $green-200;
$callout-success-color: $green-700; $callout-success-color: $green-700;
/* /*
......
...@@ -80,11 +80,25 @@ ...@@ -80,11 +80,25 @@
.boards-list { .boards-list {
height: calc(100vh - 50px); height: calc(100vh - 50px);
} }
.issue-boards-sidebar {
height: 100%;
top: 0;
}
} }
} }
.boards-app { .boards-app {
position: relative; position: relative;
@media (min-width: $screen-sm-min) {
transition: width $right-sidebar-transition-duration;
width: 100%;
&.is-compact {
width: calc(100% - #{$gutter_width});
}
}
} }
.boards-app-loading { .boards-app-loading {
...@@ -108,11 +122,6 @@ ...@@ -108,11 +122,6 @@
height: calc(100vh - 222px); height: calc(100vh - 222px);
// scss-lint:enable DuplicateProperty // scss-lint:enable DuplicateProperty
min-height: 475px; min-height: 475px;
transition: width .2s;
&.is-compact {
width: calc(100% - 290px);
}
} }
} }
...@@ -469,14 +478,6 @@ ...@@ -469,14 +478,6 @@
.page-with-layout-nav.page-with-sub-nav .issue-boards-sidebar, .page-with-layout-nav.page-with-sub-nav .issue-boards-sidebar,
.page-with-new-sidebar.page-with-sidebar .issue-boards-sidebar { .page-with-new-sidebar.page-with-sidebar .issue-boards-sidebar {
position: absolute;
&.right-sidebar {
top: 0;
bottom: 0;
height: 100%;
}
.issuable-sidebar-header { .issuable-sidebar-header {
position: relative; position: relative;
} }
...@@ -514,8 +515,8 @@ ...@@ -514,8 +515,8 @@
.right-sidebar.right-sidebar-expanded { .right-sidebar.right-sidebar-expanded {
&.boards-sidebar-slide-enter-active, &.boards-sidebar-slide-enter-active,
&.boards-sidebar-slide-leave-active { &.boards-sidebar-slide-leave-active {
transition: width .2s, transition: width $right-sidebar-transition-duration,
padding .2s; padding $right-sidebar-transition-duration;
} }
&.boards-sidebar-slide-enter, &.boards-sidebar-slide-enter,
......
...@@ -83,7 +83,7 @@ $space-between-cards: 8px; ...@@ -83,7 +83,7 @@ $space-between-cards: 8px;
border-top-color: $color-low-score; border-top-color: $color-low-score;
.card-score-big { .card-score-big {
background-color: $red-25; background-color: $red-50;
} }
} }
...@@ -91,7 +91,7 @@ $space-between-cards: 8px; ...@@ -91,7 +91,7 @@ $space-between-cards: 8px;
border-top-color: $color-average-score; border-top-color: $color-average-score;
.card-score-big { .card-score-big {
background-color: $orange-25; background-color: $orange-50;
} }
} }
...@@ -99,7 +99,7 @@ $space-between-cards: 8px; ...@@ -99,7 +99,7 @@ $space-between-cards: 8px;
border-top-color: $color-high-score; border-top-color: $color-high-score;
.card-score-big { .card-score-big {
background-color: $green-25; background-color: $green-50;
} }
} }
......
...@@ -451,7 +451,7 @@ ...@@ -451,7 +451,7 @@
} }
.files { .files {
margin-top: -1px; margin-top: 1px;
.diff-file:last-child { .diff-file:last-child {
margin-bottom: 0; margin-bottom: 0;
...@@ -586,11 +586,6 @@ ...@@ -586,11 +586,6 @@
top: 76px; top: 76px;
} }
+ .files,
+ .alert {
margin-top: 1px;
}
&:not(.is-stuck) .diff-stats-additions-deletions-collapsed { &:not(.is-stuck) .diff-stats-additions-deletions-collapsed {
display: none; display: none;
} }
...@@ -605,11 +600,6 @@ ...@@ -605,11 +600,6 @@
.inline-parallel-buttons { .inline-parallel-buttons {
display: none; display: none;
} }
+ .files,
+ .alert {
margin-top: 32px;
}
} }
} }
} }
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
.is-confidential { .is-confidential {
color: $orange-600; color: $orange-600;
background-color: $orange-50; background-color: $orange-100;
border-radius: $border-radius-default; border-radius: $border-radius-default;
padding: 5px; padding: 5px;
margin: 0 3px 0 -4px; margin: 0 3px 0 -4px;
...@@ -223,14 +223,14 @@ ...@@ -223,14 +223,14 @@
top: $new-navbar-height; top: $new-navbar-height;
bottom: 0; bottom: 0;
right: 0; right: 0;
transition: width .3s; transition: width $right-sidebar-transition-duration;
background: $gray-light; background: $gray-light;
z-index: 200; z-index: 200;
overflow: hidden; overflow: hidden;
.issuable-sidebar { .issuable-sidebar {
width: calc(100% + 100px); width: calc(100% + 100px);
height: calc(100% - #{$new-navbar-height}); height: 100%;
overflow-y: scroll; overflow-y: scroll;
overflow-x: hidden; overflow-x: hidden;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
......
...@@ -255,7 +255,7 @@ $colors: ( ...@@ -255,7 +255,7 @@ $colors: (
&.saved { &.saved {
.editor { .editor {
border-top: solid 2px $green-200; border-top: solid 2px $green-300;
} }
} }
......
...@@ -103,7 +103,7 @@ ...@@ -103,7 +103,7 @@
.confidential-issue-warning { .confidential-issue-warning {
color: $orange-600; color: $orange-600;
background-color: $orange-50; background-color: $orange-100;
border-radius: $border-radius-default $border-radius-default 0 0; border-radius: $border-radius-default $border-radius-default 0 0;
border: 1px solid $border-gray-normal; border: 1px solid $border-gray-normal;
border-bottom: none; border-bottom: none;
......
...@@ -674,20 +674,20 @@ a.linked-pipeline-mini-item { ...@@ -674,20 +674,20 @@ a.linked-pipeline-mini-item {
// Dropdown button animation in mini pipeline graph // Dropdown button animation in mini pipeline graph
&.ci-status-icon-success { &.ci-status-icon-success {
@include mini-pipeline-graph-color($green-50, $green-500, $green-600); @include mini-pipeline-graph-color($green-100, $green-500, $green-600);
} }
&.ci-status-icon-failed { &.ci-status-icon-failed {
@include mini-pipeline-graph-color($red-50, $red-500, $red-600); @include mini-pipeline-graph-color($red-100, $red-500, $red-600);
} }
&.ci-status-icon-pending, &.ci-status-icon-pending,
&.ci-status-icon-success_with_warnings { &.ci-status-icon-success_with_warnings {
@include mini-pipeline-graph-color($orange-50, $orange-500, $orange-600); @include mini-pipeline-graph-color($orange-100, $orange-500, $orange-600);
} }
&.ci-status-icon-running { &.ci-status-icon-running {
@include mini-pipeline-graph-color($blue-50, $blue-400, $blue-600); @include mini-pipeline-graph-color($blue-100, $blue-400, $blue-600);
} }
&.ci-status-icon-canceled, &.ci-status-icon-canceled,
......
...@@ -292,7 +292,7 @@ table.u2f-registrations { ...@@ -292,7 +292,7 @@ table.u2f-registrations {
padding: 32px; padding: 32px;
border: 1px solid $blue-300; border: 1px solid $blue-300;
border-radius: $border-radius-default; border-radius: $border-radius-default;
background-color: $blue-25; background-color: $blue-50;
position: relative; position: relative;
display: flex; display: flex;
justify-content: center; justify-content: center;
...@@ -376,7 +376,7 @@ table.u2f-registrations { ...@@ -376,7 +376,7 @@ table.u2f-registrations {
.nav-wip { .nav-wip {
border: 1px solid $blue-500; border: 1px solid $blue-500;
background: $blue-25; background: $blue-50;
padding: $gl-padding; padding: $gl-padding;
margin-bottom: $gl-padding; margin-bottom: $gl-padding;
......
...@@ -54,6 +54,10 @@ ...@@ -54,6 +54,10 @@
border-radius: $border-radius-default; border-radius: $border-radius-default;
color: $almost-black; color: $almost-black;
.code.white pre .hll {
background-color: $well-light-border !important;
}
.tree-content-holder { .tree-content-holder {
display: flex; display: flex;
min-height: 300px; min-height: 300px;
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
} }
&.ci-failed { &.ci-failed {
@include status-color($red-50, $red-500, $red-600); @include status-color($red-100, $red-500, $red-600);
} }
&.ci-success { &.ci-success {
...@@ -39,12 +39,12 @@ ...@@ -39,12 +39,12 @@
&.ci-pending, &.ci-pending,
&.ci-failed_with_warnings, &.ci-failed_with_warnings,
&.ci-success_with_warnings { &.ci-success_with_warnings {
@include status-color($orange-50, $orange-500, $orange-700); @include status-color($orange-100, $orange-500, $orange-700);
} }
&.ci-info, &.ci-info,
&.ci-running { &.ci-running {
@include status-color($blue-50, $blue-500, $blue-600); @include status-color($blue-100, $blue-500, $blue-600);
} }
&.ci-created, &.ci-created,
......
...@@ -15,3 +15,9 @@ ...@@ -15,3 +15,9 @@
-ms-animation: none !important; -ms-animation: none !important;
animation: none !important; animation: none !important;
} }
// Disable sticky changes bar for tests
.diff-files-changed {
position: relative !important;
top: 0 !important;
}
class Admin::ApplicationsController < Admin::ApplicationController class Admin::ApplicationsController < Admin::ApplicationController
include OauthApplications include OauthApplications
prepend EE::Admin::ApplicationsController
before_action :set_application, only: [:show, :edit, :update, :destroy] before_action :set_application, only: [:show, :edit, :update, :destroy]
before_action :load_scopes, only: [:new, :create, :edit, :update] before_action :load_scopes, only: [:new, :create, :edit, :update]
...@@ -22,8 +23,7 @@ class Admin::ApplicationsController < Admin::ApplicationController ...@@ -22,8 +23,7 @@ class Admin::ApplicationsController < Admin::ApplicationController
@application = Doorkeeper::Application.new(application_params) @application = Doorkeeper::Application.new(application_params)
if @application.save if @application.save
flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create]) redirect_to_admin_page
redirect_to admin_application_url(@application)
else else
render :new render :new
end end
...@@ -42,6 +42,13 @@ class Admin::ApplicationsController < Admin::ApplicationController ...@@ -42,6 +42,13 @@ class Admin::ApplicationsController < Admin::ApplicationController
redirect_to admin_applications_url, status: 302, notice: 'Application was successfully destroyed.' redirect_to admin_applications_url, status: 302, notice: 'Application was successfully destroyed.'
end end
protected
def redirect_to_admin_page
flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
redirect_to admin_application_url(@application)
end
private private
def set_application def set_application
......
...@@ -128,7 +128,7 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -128,7 +128,7 @@ class Admin::UsersController < Admin::ApplicationController
end end
respond_to do |format| respond_to do |format|
result = Users::UpdateService.new(user, user_params_with_pass).execute do |user| result = Users::UpdateService.new(current_user, user_params_with_pass.merge(user: user)).execute do |user|
user.skip_reconfirmation! user.skip_reconfirmation!
end end
...@@ -155,7 +155,7 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -155,7 +155,7 @@ class Admin::UsersController < Admin::ApplicationController
def remove_email def remove_email
email = user.emails.find(params[:email_id]) email = user.emails.find(params[:email_id])
success = Emails::DestroyService.new(user, email: email.email).execute success = Emails::DestroyService.new(current_user, user: user, email: email.email).execute
respond_to do |format| respond_to do |format|
if success if success
...@@ -226,7 +226,7 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -226,7 +226,7 @@ class Admin::UsersController < Admin::ApplicationController
end end
def update_user(&block) def update_user(&block)
result = Users::UpdateService.new(user).execute(&block) result = Users::UpdateService.new(current_user, user: user).execute(&block)
result[:status] == :success result[:status] == :success
end end
......
module OauthApplications module OauthApplications
extend ActiveSupport::Concern extend ActiveSupport::Concern
prepend ::EE::Concerns::OauthApplications
included do included do
before_action :prepare_scopes, only: [:create, :update] before_action :prepare_scopes, only: [:create, :update]
......
class ConfirmationsController < Devise::ConfirmationsController class ConfirmationsController < Devise::ConfirmationsController
prepend ::EE::ConfirmationsController
def almost_there def almost_there
flash[:notice] = nil flash[:notice] = nil
render layout: "devise_empty" render layout: "devise_empty"
...@@ -12,10 +14,14 @@ class ConfirmationsController < Devise::ConfirmationsController ...@@ -12,10 +14,14 @@ class ConfirmationsController < Devise::ConfirmationsController
def after_confirmation_path_for(resource_name, resource) def after_confirmation_path_for(resource_name, resource)
if signed_in?(resource_name) if signed_in?(resource_name)
after_sign_in_path_for(resource) after_sign_in(resource)
else else
flash[:notice] += " Please sign in." flash[:notice] += " Please sign in."
new_session_path(resource_name) new_session_path(resource_name)
end end
end end
def after_sign_in(resource)
after_sign_in_path_for(resource)
end
end end
...@@ -43,6 +43,6 @@ class Groups::LdapGroupLinksController < Groups::ApplicationController ...@@ -43,6 +43,6 @@ class Groups::LdapGroupLinksController < Groups::ApplicationController
end end
def ldap_group_link_params def ldap_group_link_params
params.require(:ldap_group_link).permit(:cn, :group_access, :provider) params.require(:ldap_group_link).permit(:cn, :filter, :group_access, :provider)
end end
end end
...@@ -3,6 +3,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController ...@@ -3,6 +3,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
include Gitlab::GonHelper include Gitlab::GonHelper
include PageLayoutHelper include PageLayoutHelper
include OauthApplications include OauthApplications
prepend ::EE::Oauth::ApplicationsController
before_action :verify_user_oauth_applications_enabled before_action :verify_user_oauth_applications_enabled
before_action :authenticate_user! before_action :authenticate_user!
...@@ -21,14 +22,20 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController ...@@ -21,14 +22,20 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
@application.owner = current_user @application.owner = current_user
if @application.save if @application.save
flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create]) redirect_to_oauth_application_page
redirect_to oauth_application_url(@application)
else else
set_index_vars set_index_vars
render :index render :index
end end
end end
protected
def redirect_to_oauth_application_page
flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
redirect_to oauth_application_url(@application)
end
private private
def verify_user_oauth_applications_enabled def verify_user_oauth_applications_enabled
......
...@@ -3,6 +3,8 @@ class PasswordsController < Devise::PasswordsController ...@@ -3,6 +3,8 @@ class PasswordsController < Devise::PasswordsController
before_action :prevent_ldap_reset, only: [:create] before_action :prevent_ldap_reset, only: [:create]
before_action :throttle_reset, only: [:create] before_action :throttle_reset, only: [:create]
prepend EE::PasswordsController
def edit def edit
super super
reset_password_token = Devise.token_generator.digest( reset_password_token = Devise.token_generator.digest(
......
...@@ -2,7 +2,7 @@ class Profiles::AvatarsController < Profiles::ApplicationController ...@@ -2,7 +2,7 @@ class Profiles::AvatarsController < Profiles::ApplicationController
def destroy def destroy
@user = current_user @user = current_user
Users::UpdateService.new(@user).execute { |user| user.remove_avatar! } Users::UpdateService.new(current_user, user: @user).execute { |user| user.remove_avatar! }
redirect_to profile_path, status: 302 redirect_to profile_path, status: 302
end end
......
...@@ -5,7 +5,7 @@ class Profiles::EmailsController < Profiles::ApplicationController ...@@ -5,7 +5,7 @@ class Profiles::EmailsController < Profiles::ApplicationController
end end
def create def create
@email = Emails::CreateService.new(current_user, email_params).execute @email = Emails::CreateService.new(current_user, email_params.merge(user: current_user)).execute
if @email.errors.blank? if @email.errors.blank?
NotificationService.new.new_email(@email) NotificationService.new.new_email(@email)
...@@ -19,7 +19,7 @@ class Profiles::EmailsController < Profiles::ApplicationController ...@@ -19,7 +19,7 @@ class Profiles::EmailsController < Profiles::ApplicationController
def destroy def destroy
@email = current_user.emails.find(params[:id]) @email = current_user.emails.find(params[:id])
Emails::DestroyService.new(current_user, email: @email.email).execute Emails::DestroyService.new(current_user, user: current_user, email: @email.email).execute
respond_to do |format| respond_to do |format|
format.html { redirect_to profile_emails_url, status: 302 } format.html { redirect_to profile_emails_url, status: 302 }
......
class Profiles::KeysController < Profiles::ApplicationController class Profiles::KeysController < Profiles::ApplicationController
prepend ::EE::Profiles::KeysController
skip_before_action :authenticate_user!, only: [:get_keys] skip_before_action :authenticate_user!, only: [:get_keys]
def index def index
...@@ -14,7 +16,7 @@ class Profiles::KeysController < Profiles::ApplicationController ...@@ -14,7 +16,7 @@ class Profiles::KeysController < Profiles::ApplicationController
@key = Keys::CreateService.new(current_user, key_params).execute @key = Keys::CreateService.new(current_user, key_params).execute
if @key.persisted? if @key.persisted?
redirect_to profile_key_path(@key) redirect_to_profile_key_path
else else
@keys = current_user.keys.select(&:persisted?) @keys = current_user.keys.select(&:persisted?)
render :index render :index
...@@ -50,6 +52,12 @@ class Profiles::KeysController < Profiles::ApplicationController ...@@ -50,6 +52,12 @@ class Profiles::KeysController < Profiles::ApplicationController
end end
end end
protected
def redirect_to_profile_key_path
redirect_to profile_key_path(@key)
end
private private
def key_params def key_params
......
...@@ -7,7 +7,7 @@ class Profiles::NotificationsController < Profiles::ApplicationController ...@@ -7,7 +7,7 @@ class Profiles::NotificationsController < Profiles::ApplicationController
end end
def update def update
result = Users::UpdateService.new(current_user, user_params).execute result = Users::UpdateService.new(current_user, user_params.merge(user: current_user)).execute
if result[:status] == :success if result[:status] == :success
flash[:notice] = "Notification settings saved" flash[:notice] = "Notification settings saved"
......
...@@ -21,10 +21,10 @@ class Profiles::PasswordsController < Profiles::ApplicationController ...@@ -21,10 +21,10 @@ class Profiles::PasswordsController < Profiles::ApplicationController
password_automatically_set: false password_automatically_set: false
} }
result = Users::UpdateService.new(@user, password_attributes).execute result = Users::UpdateService.new(current_user, password_attributes.merge(user: @user)).execute
if result[:status] == :success if result[:status] == :success
Users::UpdateService.new(@user, password_expires_at: nil).execute Users::UpdateService.new(current_user, user: @user, password_expires_at: nil).execute
redirect_to root_path, notice: 'Password successfully changed' redirect_to root_path, notice: 'Password successfully changed'
else else
...@@ -46,7 +46,7 @@ class Profiles::PasswordsController < Profiles::ApplicationController ...@@ -46,7 +46,7 @@ class Profiles::PasswordsController < Profiles::ApplicationController
return return
end end
result = Users::UpdateService.new(@user, password_attributes).execute result = Users::UpdateService.new(current_user, password_attributes.merge(user: @user)).execute
if result[:status] == :success if result[:status] == :success
flash[:notice] = "Password was successfully updated. Please login with it" flash[:notice] = "Password was successfully updated. Please login with it"
......
...@@ -6,7 +6,7 @@ class Profiles::PreferencesController < Profiles::ApplicationController ...@@ -6,7 +6,7 @@ class Profiles::PreferencesController < Profiles::ApplicationController
def update def update
begin begin
result = Users::UpdateService.new(user, preferences_params).execute result = Users::UpdateService.new(current_user, preferences_params.merge(user: user)).execute
if result[:status] == :success if result[:status] == :success
flash[:notice] = 'Preferences saved.' flash[:notice] = 'Preferences saved.'
......
...@@ -10,7 +10,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController ...@@ -10,7 +10,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
current_user.otp_grace_period_started_at = Time.current current_user.otp_grace_period_started_at = Time.current
end end
Users::UpdateService.new(current_user).execute! Users::UpdateService.new(current_user, user: current_user).execute!
if two_factor_authentication_required? && !current_user.two_factor_enabled? if two_factor_authentication_required? && !current_user.two_factor_enabled?
two_factor_authentication_reason( two_factor_authentication_reason(
...@@ -41,7 +41,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController ...@@ -41,7 +41,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
def create def create
if current_user.validate_and_consume_otp!(params[:pin_code]) if current_user.validate_and_consume_otp!(params[:pin_code])
Users::UpdateService.new(current_user, otp_required_for_login: true).execute! do |user| Users::UpdateService.new(current_user, user: current_user, otp_required_for_login: true).execute! do |user|
@codes = user.generate_otp_backup_codes! @codes = user.generate_otp_backup_codes!
end end
...@@ -70,7 +70,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController ...@@ -70,7 +70,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
end end
def codes def codes
Users::UpdateService.new(current_user).execute! do |user| Users::UpdateService.new(current_user, user: current_user).execute! do |user|
@codes = user.generate_otp_backup_codes! @codes = user.generate_otp_backup_codes!
end end
end end
......
...@@ -10,7 +10,7 @@ class ProfilesController < Profiles::ApplicationController ...@@ -10,7 +10,7 @@ class ProfilesController < Profiles::ApplicationController
def update def update
respond_to do |format| respond_to do |format|
result = Users::UpdateService.new(@user, user_params).execute result = Users::UpdateService.new(current_user, user_params.merge(user: @user)).execute
if result[:status] == :success if result[:status] == :success
message = "Profile was successfully updated" message = "Profile was successfully updated"
...@@ -25,7 +25,7 @@ class ProfilesController < Profiles::ApplicationController ...@@ -25,7 +25,7 @@ class ProfilesController < Profiles::ApplicationController
end end
def reset_private_token def reset_private_token
Users::UpdateService.new(@user).execute! do |user| Users::UpdateService.new(current_user, user: @user).execute! do |user|
user.reset_authentication_token! user.reset_authentication_token!
end end
...@@ -35,7 +35,7 @@ class ProfilesController < Profiles::ApplicationController ...@@ -35,7 +35,7 @@ class ProfilesController < Profiles::ApplicationController
end end
def reset_incoming_email_token def reset_incoming_email_token
Users::UpdateService.new(@user).execute! do |user| Users::UpdateService.new(current_user, user: @user).execute! do |user|
user.reset_incoming_email_token! user.reset_incoming_email_token!
end end
...@@ -45,7 +45,7 @@ class ProfilesController < Profiles::ApplicationController ...@@ -45,7 +45,7 @@ class ProfilesController < Profiles::ApplicationController
end end
def reset_rss_token def reset_rss_token
Users::UpdateService.new(@user).execute! do |user| Users::UpdateService.new(current_user, user: @user).execute! do |user|
user.reset_rss_token! user.reset_rss_token!
end end
...@@ -61,7 +61,7 @@ class ProfilesController < Profiles::ApplicationController ...@@ -61,7 +61,7 @@ class ProfilesController < Profiles::ApplicationController
end end
def update_username def update_username
result = Users::UpdateService.new(@user, username: user_params[:username]).execute result = Users::UpdateService.new(current_user, user: @user, username: user_params[:username]).execute
options = if result[:status] == :success options = if result[:status] == :success
{ notice: "Username successfully changed" } { notice: "Username successfully changed" }
......
...@@ -57,7 +57,7 @@ class SessionsController < Devise::SessionsController ...@@ -57,7 +57,7 @@ class SessionsController < Devise::SessionsController
return unless user && user.require_password_creation? return unless user && user.require_password_creation?
Users::UpdateService.new(user).execute do |user| Users::UpdateService.new(current_user, user: user).execute do |user|
@token = user.generate_reset_token @token = user.generate_reset_token
end end
......
...@@ -13,22 +13,29 @@ module AvatarsHelper ...@@ -13,22 +13,29 @@ module AvatarsHelper
user_name = options[:user].try(:name) || options[:user_name] user_name = options[:user].try(:name) || options[:user_name]
avatar_url = options[:url] || avatar_icon(options[:user] || options[:user_email], avatar_size) avatar_url = options[:url] || avatar_icon(options[:user] || options[:user_email], avatar_size)
has_tooltip = options[:has_tooltip].nil? ? true : options[:has_tooltip] has_tooltip = options[:has_tooltip].nil? ? true : options[:has_tooltip]
data_attributes = {} data_attributes = options[:data] || {}
css_class = %W[avatar s#{avatar_size}].push(*options[:css_class]) css_class = %W[avatar s#{avatar_size}].push(*options[:css_class])
if has_tooltip if has_tooltip
css_class.push('has-tooltip') css_class.push('has-tooltip')
data_attributes = { container: 'body' } data_attributes[:container] = 'body'
end end
image_tag( if options[:lazy]
avatar_url, css_class << 'lazy'
class: css_class, data_attributes[:src] = avatar_url
avatar_url = LazyImageTagHelper.placeholder_image
end
image_options = {
alt: "#{user_name}'s avatar", alt: "#{user_name}'s avatar",
title: user_name, src: avatar_url,
data: data_attributes, data: data_attributes,
lazy: true class: css_class,
) title: user_name
}
tag(:img, image_options)
end end
def user_avatar(options = {}) def user_avatar(options = {})
......
...@@ -81,6 +81,6 @@ module BoardsHelper ...@@ -81,6 +81,6 @@ module BoardsHelper
end end
def boards_link_text def boards_link_text
_("Board") s_("IssueBoards|Board")
end end
end end
...@@ -239,8 +239,8 @@ module ProjectsHelper ...@@ -239,8 +239,8 @@ module ProjectsHelper
end end
end end
def has_projects_or_name?(projects, params) def show_projects?(projects, params)
!!(params[:name] || any_projects?(projects)) !!(params[:personal] || params[:name] || any_projects?(projects))
end end
private private
......
...@@ -110,6 +110,10 @@ module SortingHelper ...@@ -110,6 +110,10 @@ module SortingHelper
s_('SortOptions|Due date') s_('SortOptions|Due date')
end end
def sort_title_less_weight
s_('SortOptions|Less weight')
end
def sort_title_due_date_later def sort_title_due_date_later
s_('SortOptions|Due later') s_('SortOptions|Due later')
end end
...@@ -126,6 +130,10 @@ module SortingHelper ...@@ -126,6 +130,10 @@ module SortingHelper
s_('SortOptions|Largest group') s_('SortOptions|Largest group')
end end
def sort_title_more_weight
s_('SortOptions|More weight')
end
def sort_title_largest_repo def sort_title_largest_repo
s_('SortOptions|Largest repository') s_('SortOptions|Largest repository')
end end
...@@ -138,10 +146,6 @@ module SortingHelper ...@@ -138,10 +146,6 @@ module SortingHelper
s_('SortOptions|Last updated') s_('SortOptions|Last updated')
end end
def sort_title_less_weight
s_('SortOptions|Less weight')
end
def sort_title_milestone def sort_title_milestone
s_('SortOptions|Milestone') s_('SortOptions|Milestone')
end end
...@@ -154,10 +158,6 @@ module SortingHelper ...@@ -154,10 +158,6 @@ module SortingHelper
s_('SortOptions|Milestone due soon') s_('SortOptions|Milestone due soon')
end end
def sort_title_more_weight
s_('SortOptions|More weight')
end
def sort_title_name def sort_title_name
s_('SortOptions|Name') s_('SortOptions|Name')
end end
...@@ -178,6 +178,10 @@ module SortingHelper ...@@ -178,6 +178,10 @@ module SortingHelper
s_('SortOptions|Oldest created') s_('SortOptions|Oldest created')
end end
def sort_title_weight
s_('SortOptions|Weight')
end
def sort_title_oldest_joined def sort_title_oldest_joined
s_('SortOptions|Oldest joined') s_('SortOptions|Oldest joined')
end end
...@@ -222,10 +226,6 @@ module SortingHelper ...@@ -222,10 +226,6 @@ module SortingHelper
s_('SortOptions|Most popular') s_('SortOptions|Most popular')
end end
def sort_title_weight
s_('SortOptions|Weight')
end
# Values. # Values.
def sort_value_access_level_asc def sort_value_access_level_asc
'access_level_asc' 'access_level_asc'
......
...@@ -447,7 +447,7 @@ module Ci ...@@ -447,7 +447,7 @@ module Ci
def update_duration def update_duration
return unless started_at return unless started_at
self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self) self.duration = Gitlab::Ci::Pipeline::Duration.from_pipeline(self)
end end
def execute_hooks def execute_hooks
......
...@@ -175,7 +175,7 @@ module Ci ...@@ -175,7 +175,7 @@ module Ci
end end
def assignable_for?(project) def assignable_for?(project)
!locked? || projects.exists?(id: project.id) is_shared? || projects.exists?(id: project.id)
end end
def accepting_tags?(build) def accepting_tags?(build)
......
...@@ -25,8 +25,8 @@ class Commit ...@@ -25,8 +25,8 @@ class Commit
DIFF_HARD_LIMIT_FILES = 1000 DIFF_HARD_LIMIT_FILES = 1000
DIFF_HARD_LIMIT_LINES = 50000 DIFF_HARD_LIMIT_LINES = 50000
# The SHA can be between 7 and 40 hex characters. MIN_SHA_LENGTH = 7
COMMIT_SHA_PATTERN = '\h{7,40}'.freeze COMMIT_SHA_PATTERN = /\h{#{MIN_SHA_LENGTH},40}/.freeze
def banzai_render_context(field) def banzai_render_context(field)
context = { pipeline: :single_line, project: self.project } context = { pipeline: :single_line, project: self.project }
...@@ -53,7 +53,7 @@ class Commit ...@@ -53,7 +53,7 @@ class Commit
# Truncate sha to 8 characters # Truncate sha to 8 characters
def truncate_sha(sha) def truncate_sha(sha)
sha[0..7] sha[0..MIN_SHA_LENGTH]
end end
def max_diff_options def max_diff_options
...@@ -100,7 +100,7 @@ class Commit ...@@ -100,7 +100,7 @@ class Commit
def self.reference_pattern def self.reference_pattern
@reference_pattern ||= %r{ @reference_pattern ||= %r{
(?:#{Project.reference_pattern}#{reference_prefix})? (?:#{Project.reference_pattern}#{reference_prefix})?
(?<commit>\h{7,40}) (?<commit>#{COMMIT_SHA_PATTERN})
}x }x
end end
...@@ -216,9 +216,8 @@ class Commit ...@@ -216,9 +216,8 @@ class Commit
@raw.respond_to?(method, include_private) || super @raw.respond_to?(method, include_private) || super
end end
# Truncate sha to 8 characters
def short_id def short_id
@raw.short_id(7) @raw.short_id(MIN_SHA_LENGTH)
end end
def diff_refs def diff_refs
......
...@@ -230,12 +230,20 @@ class Group < Namespace ...@@ -230,12 +230,20 @@ class Group < Namespace
ldap_group_links.first.try(:cn) ldap_group_links.first.try(:cn)
end end
def ldap_filter
ldap_group_links.first.try(:filter)
end
def ldap_access def ldap_access
ldap_group_links.first.try(:group_access) ldap_group_links.first.try(:group_access)
end end
def ldap_cn_or_filter_present?
ldap_cn.present? || ldap_filter.present?
end
def ldap_synced? def ldap_synced?
Gitlab.config.ldap.enabled && ldap_cn.present? Gitlab.config.ldap.enabled && ldap_cn_or_filter_present?
end end
def post_create_hook def post_create_hook
......
...@@ -2,13 +2,28 @@ class LdapGroupLink < ActiveRecord::Base ...@@ -2,13 +2,28 @@ class LdapGroupLink < ActiveRecord::Base
include Gitlab::Access include Gitlab::Access
belongs_to :group belongs_to :group
validates :cn, :group_access, :group_id, presence: true BLANK_ATTRIBUTES = %w[cn filter].freeze
validates :cn, uniqueness: { scope: [:group_id, :provider] }
with_options if: :cn do |link|
link.validates :cn, uniqueness: { scope: [:group_id, :provider] }
link.validates :cn, presence: true
link.validates :filter, absence: true
end
with_options if: :filter do |link|
link.validates :filter, uniqueness: { scope: [:group_id, :provider] }
link.validates :filter, ldap_filter: true, presence: true
link.validates :cn, absence: true
end
validates :group_access, :group_id, presence: true
validates :group_access, inclusion: { in: Gitlab::Access.all_values } validates :group_access, inclusion: { in: Gitlab::Access.all_values }
validates :provider, presence: true validates :provider, presence: true
scope :with_provider, ->(provider) { where(provider: provider) } scope :with_provider, ->(provider) { where(provider: provider) }
before_validation :nullify_blank_attributes
def access_field def access_field
group_access group_access
end end
...@@ -27,4 +42,10 @@ class LdapGroupLink < ActiveRecord::Base ...@@ -27,4 +42,10 @@ class LdapGroupLink < ActiveRecord::Base
def provider_label def provider_label
config.label config.label
end end
private
def nullify_blank_attributes
BLANK_ATTRIBUTES.each { |attr| self[attr] = nil if self[attr].blank? }
end
end end
...@@ -37,6 +37,7 @@ class License < ActiveRecord::Base ...@@ -37,6 +37,7 @@ class License < ActiveRecord::Base
cross_project_pipelines cross_project_pipelines
db_load_balancing db_load_balancing
deploy_board deploy_board
extended_audit_events
file_locks file_locks
geo geo
group_issue_boards group_issue_boards
...@@ -105,6 +106,7 @@ class License < ActiveRecord::Base ...@@ -105,6 +106,7 @@ class License < ActiveRecord::Base
auditor_user auditor_user
db_load_balancing db_load_balancing
elastic_search elastic_search
extended_audit_events
geo geo
ldap_extras ldap_extras
object_storage object_storage
......
...@@ -249,6 +249,9 @@ class Project < ActiveRecord::Base ...@@ -249,6 +249,9 @@ class Project < ActiveRecord::Base
scope :pending_delete, -> { where(pending_delete: true) } scope :pending_delete, -> { where(pending_delete: true) }
scope :without_deleted, -> { where(pending_delete: false) } scope :without_deleted, -> { where(pending_delete: false) }
scope :with_hashed_storage, -> { where('storage_version >= 1') }
scope :with_legacy_storage, -> { where(storage_version: [nil, 0]) }
scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) } scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) }
scope :sorted_by_stars, -> { reorder('projects.star_count DESC') } scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }
...@@ -1550,13 +1553,37 @@ class Project < ActiveRecord::Base ...@@ -1550,13 +1553,37 @@ class Project < ActiveRecord::Base
end end
def legacy_storage? def legacy_storage?
self.storage_version.nil? [nil, 0].include?(self.storage_version)
end
def hashed_storage?
self.storage_version && self.storage_version >= 1
end end
def renamed? def renamed?
persisted? && path_changed? persisted? && path_changed?
end end
def migrate_to_hashed_storage!
return if hashed_storage?
update!(repository_read_only: true)
if repo_reference_count > 0 || wiki_reference_count > 0
ProjectMigrateHashedStorageWorker.perform_in(Gitlab::ReferenceCounter::REFERENCE_EXPIRE_TIME, id)
else
ProjectMigrateHashedStorageWorker.perform_async(id)
end
end
def storage_version=(value)
super
@storage = nil if storage_version_changed?
end
def gl_repository(is_wiki:)
Gitlab::GlRepository.gl_repository(self, is_wiki)
def merge_method def merge_method
if self.merge_requests_ff_only_enabled if self.merge_requests_ff_only_enabled
:ff :ff
...@@ -1589,7 +1616,7 @@ class Project < ActiveRecord::Base ...@@ -1589,7 +1616,7 @@ class Project < ActiveRecord::Base
def storage def storage
@storage ||= @storage ||=
if self.storage_version && self.storage_version >= 1 if hashed_storage?
Storage::HashedProject.new(self) Storage::HashedProject.new(self)
else else
Storage::LegacyProject.new(self) Storage::LegacyProject.new(self)
...@@ -1602,6 +1629,14 @@ class Project < ActiveRecord::Base ...@@ -1602,6 +1629,14 @@ class Project < ActiveRecord::Base
end end
end end
def repo_reference_count
Gitlab::ReferenceCounter.new(gl_repository(is_wiki: false)).value
end
def wiki_reference_count
Gitlab::ReferenceCounter.new(gl_repository(is_wiki: true)).value
end
# set last_activity_at to the same as created_at # set last_activity_at to the same as created_at
def set_last_activity_at def set_last_activity_at
update_column(:last_activity_at, self.created_at) update_column(:last_activity_at, self.created_at)
......
...@@ -541,10 +541,13 @@ class Repository ...@@ -541,10 +541,13 @@ class Repository
cache_method :tag_count, fallback: 0 cache_method :tag_count, fallback: 0
def avatar def avatar
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38327
Gitlab::GitalyClient.allow_n_plus_1_calls do
if tree = file_on_head(:avatar) if tree = file_on_head(:avatar)
tree.path tree.path
end end
end end
end
cache_method :avatar cache_method :avatar
def readme def readme
......
...@@ -4,6 +4,7 @@ module Storage ...@@ -4,6 +4,7 @@ module Storage
delegate :gitlab_shell, :repository_storage_path, to: :project delegate :gitlab_shell, :repository_storage_path, to: :project
ROOT_PATH_PREFIX = '@hashed'.freeze ROOT_PATH_PREFIX = '@hashed'.freeze
STORAGE_VERSION = 1
def initialize(project) def initialize(project)
@project = project @project = project
......
...@@ -63,7 +63,7 @@ class User < ActiveRecord::Base ...@@ -63,7 +63,7 @@ class User < ActiveRecord::Base
lease = Gitlab::ExclusiveLease.new("user_update_tracked_fields:#{id}", timeout: 1.hour.to_i) lease = Gitlab::ExclusiveLease.new("user_update_tracked_fields:#{id}", timeout: 1.hour.to_i)
return unless lease.try_obtain return unless lease.try_obtain
Users::UpdateService.new(self).execute(validate: false) Users::UpdateService.new(self, user: self).execute(validate: false)
end end
attr_accessor :force_random_password attr_accessor :force_random_password
...@@ -545,8 +545,8 @@ class User < ActiveRecord::Base ...@@ -545,8 +545,8 @@ class User < ActiveRecord::Base
def update_emails_with_primary_email def update_emails_with_primary_email
primary_email_record = emails.find_by(email: email) primary_email_record = emails.find_by(email: email)
if primary_email_record if primary_email_record
Emails::DestroyService.new(self, email: email).execute Emails::DestroyService.new(self, user: self, email: email).execute
Emails::CreateService.new(self, email: email_was).execute Emails::CreateService.new(self, user: self, email: email_was).execute
end end
end end
...@@ -1023,7 +1023,7 @@ class User < ActiveRecord::Base ...@@ -1023,7 +1023,7 @@ class User < ActiveRecord::Base
if attempts_exceeded? if attempts_exceeded?
lock_access! unless access_locked? lock_access! unless access_locked?
else else
Users::UpdateService.new(self).execute(validate: false) Users::UpdateService.new(self, user: self).execute(validate: false)
end end
end end
...@@ -1209,7 +1209,7 @@ class User < ActiveRecord::Base ...@@ -1209,7 +1209,7 @@ class User < ActiveRecord::Base
&creation_block &creation_block
) )
Users::UpdateService.new(user).execute(validate: false) Users::UpdateService.new(user, user: user).execute(validate: false)
user user
ensure ensure
Gitlab::ExclusiveLease.cancel(lease_key, uuid) Gitlab::ExclusiveLease.cancel(lease_key, uuid)
......
...@@ -2,114 +2,57 @@ module Ci ...@@ -2,114 +2,57 @@ module Ci
class CreatePipelineService < BaseService class CreatePipelineService < BaseService
attr_reader :pipeline attr_reader :pipeline
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, mirror_update: false) SEQUENCE = [Gitlab::Ci::Pipeline::Chain::Validate::Abilities,
Gitlab::Ci::Pipeline::Chain::Validate::Repository,
Gitlab::Ci::Pipeline::Chain::Validate::Config,
Gitlab::Ci::Pipeline::Chain::Skip,
Gitlab::Ci::Pipeline::Chain::Create].freeze
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(
source: source, source: source,
project: project, project: project,
ref: ref, ref: ref,
sha: sha, sha: sha,
before_sha: before_sha, before_sha: before_sha,
tag: tag?, tag: tag_exists?,
trigger_requests: Array(trigger_request), trigger_requests: Array(trigger_request),
user: current_user, user: current_user,
pipeline_schedule: schedule, pipeline_schedule: schedule,
protected: project.protected_for?(ref) protected: project.protected_for?(ref)
) )
result = validate_project_and_git_items(mirror_update: mirror_update) || # VALIDATE mirror_update!
validate_pipeline(ignore_skip_ci: ignore_skip_ci, command = OpenStruct.new(ignore_skip_ci: ignore_skip_ci,
save_on_errors: save_on_errors) save_incompleted: save_on_errors,
allow_mirror_update: mirror_update,
return result if result seeds_block: block,
project: project,
begin current_user: current_user)
Ci::Pipeline.transaction do
pipeline.save!
yield(pipeline) if block_given?
Ci::CreatePipelineStagesService sequence = Gitlab::Ci::Pipeline::Chain::Sequence
.new(project, current_user) .new(pipeline, command, SEQUENCE)
.execute(pipeline)
end
rescue ActiveRecord::RecordInvalid => e
return error("Failed to persist the pipeline: #{e}")
end
update_merge_requests_head_pipeline sequence.build! do |pipeline, sequence|
update_merge_requests_head_pipeline if pipeline.persisted?
if sequence.complete?
cancel_pending_pipelines if project.auto_cancel_pending_pipelines? cancel_pending_pipelines if project.auto_cancel_pending_pipelines?
pipeline_created_counter.increment(source: source) pipeline_created_counter.increment(source: source)
pipeline.tap(&:process!) pipeline.process!
end
private
def validate_project_and_git_items(mirror_update: false)
unless project.builds_enabled?
return error('Pipeline is disabled')
end
if mirror_update && !project.mirror_trigger_builds?
return error('Pipeline is disabled for mirror updates')
end
unless allowed_to_trigger_pipeline?
if can?(current_user, :create_pipeline, project)
return error("Insufficient permissions for protected ref '#{ref}'")
else
return error('Insufficient permissions to create a new pipeline')
end
end
unless branch? || tag?
return error('Reference not found')
end
unless commit
return error('Commit not found')
end end
end end
def validate_pipeline(ignore_skip_ci:, save_on_errors:)
unless pipeline.config_processor
unless pipeline.ci_yaml_file
return error("Missing #{pipeline.ci_yaml_file_path} file")
end
return error(pipeline.yaml_errors, save: save_on_errors)
end
if !ignore_skip_ci && skip_ci?
pipeline.skip if save_on_errors
return pipeline
end end
unless pipeline.has_stage_seeds? private
return error('No stages / jobs for this pipeline.')
end
end
def allowed_to_trigger_pipeline? def commit
if current_user @commit ||= project.commit(origin_sha || origin_ref)
allowed_to_create?
else # legacy triggers don't have a corresponding user
!project.protected_for?(ref)
end
end end
def allowed_to_create? def sha
return unless can?(current_user, :create_pipeline, project) commit.try(:id)
access = Gitlab::UserAccess.new(current_user, project: project)
if branch?
access.can_update_branch?(ref)
elsif tag?
access.can_create_tag?(ref)
else
true # Allow it for now and we'll reject when we check ref existence
end
end end
def update_merge_requests_head_pipeline def update_merge_requests_head_pipeline
...@@ -119,11 +62,6 @@ module Ci ...@@ -119,11 +62,6 @@ module Ci
.update_all(head_pipeline_id: @pipeline.id) .update_all(head_pipeline_id: @pipeline.id)
end end
def skip_ci?
return false unless pipeline.git_commit_message
pipeline.git_commit_message =~ /\[(ci[ _-]skip|skip[ _-]ci)\]/i
end
def cancel_pending_pipelines def cancel_pending_pipelines
Gitlab::OptimisticLocking.retry_lock(auto_cancelable_pipelines) do |cancelables| Gitlab::OptimisticLocking.retry_lock(auto_cancelable_pipelines) do |cancelables|
cancelables.find_each do |cancelable| cancelables.find_each do |cancelable|
...@@ -140,14 +78,6 @@ module Ci ...@@ -140,14 +78,6 @@ module Ci
.created_or_pending .created_or_pending
end end
def commit
@commit ||= project.commit(origin_sha || origin_ref)
end
def sha
commit.try(:id)
end
def before_sha def before_sha
params[:checkout_sha] || params[:before] || Gitlab::Git::BLANK_SHA params[:checkout_sha] || params[:before] || Gitlab::Git::BLANK_SHA
end end
...@@ -160,41 +90,17 @@ module Ci ...@@ -160,41 +90,17 @@ module Ci
params[:ref] params[:ref]
end end
def branch? def tag_exists?
return @is_branch if defined?(@is_branch) project.repository.tag_exists?(ref)
@is_branch =
project.repository.ref_exists?(Gitlab::Git::BRANCH_REF_PREFIX + ref)
end
def tag?
return @is_tag if defined?(@is_tag)
@is_tag =
project.repository.ref_exists?(Gitlab::Git::TAG_REF_PREFIX + ref)
end end
def ref def ref
@ref ||= Gitlab::Git.ref_name(origin_ref) @ref ||= Gitlab::Git.ref_name(origin_ref)
end end
def valid_sha?
origin_sha && origin_sha != Gitlab::Git::BLANK_SHA
end
def error(message, save: false)
pipeline.tap do
pipeline.errors.add(:base, message)
if save
pipeline.drop
update_merge_requests_head_pipeline
end
end
end
def pipeline_created_counter def pipeline_created_counter
@pipeline_created_counter ||= Gitlab::Metrics.counter(:pipelines_created_total, "Counter of pipelines created") @pipeline_created_counter ||= Gitlab::Metrics
.counter(:pipelines_created_total, "Counter of pipelines created")
end end
end end
end end
module Emails module Emails
class BaseService class BaseService
def initialize(user, opts) def initialize(current_user, opts)
@user = user @current_user = current_user
@user = opts.delete(:user)
@email = opts[:email] @email = opts[:email]
end end
end end
......
module Emails module Emails
class CreateService < ::Emails::BaseService class CreateService < ::Emails::BaseService
prepend ::EE::Emails::CreateService
def execute def execute
@user.emails.create(email: @email) @user.emails.create(email: @email)
end end
......
module Emails module Emails
class DestroyService < ::Emails::BaseService class DestroyService < ::Emails::BaseService
prepend ::EE::Emails::DestroyService
def execute def execute
Email.find_by_email!(@email).destroy && update_secondary_emails! update_secondary_emails! if Email.find_by_email!(@email).destroy
end end
private private
def update_secondary_emails! def update_secondary_emails!
result = ::Users::UpdateService.new(@user).execute do |user| result = ::Users::UpdateService.new(@current_user, user: @user).execute do |user|
user.update_secondary_emails! user.update_secondary_emails!
end end
......
...@@ -10,14 +10,18 @@ module Geo ...@@ -10,14 +10,18 @@ module Geo
repository_storage_name: project.repository.storage, repository_storage_name: project.repository.storage,
repository_storage_path: project.repository_storage_path, repository_storage_path: project.repository_storage_path,
old_path_with_namespace: old_path_with_namespace, old_path_with_namespace: old_path_with_namespace,
new_path_with_namespace: project.full_path, new_path_with_namespace: project.disk_path,
old_wiki_path_with_namespace: old_wiki_path_with_namespace, old_wiki_path_with_namespace: old_wiki_path_with_namespace,
new_wiki_path_with_namespace: new_wiki_path_with_namespace, new_wiki_path_with_namespace: new_wiki_path_with_namespace,
old_path: params.fetch(:old_path), old_path: old_path,
new_path: project.path new_path: project.path
) )
end end
def old_path
params.fetch(:old_path)
end
def old_path_with_namespace def old_path_with_namespace
params.fetch(:old_path_with_namespace) params.fetch(:old_path_with_namespace)
end end
...@@ -27,7 +31,7 @@ module Geo ...@@ -27,7 +31,7 @@ module Geo
end end
def new_wiki_path_with_namespace def new_wiki_path_with_namespace
project.wiki.full_path "#{project.disk_path}.wiki"
end end
end end
end end
...@@ -19,7 +19,7 @@ module MergeRequests ...@@ -19,7 +19,7 @@ module MergeRequests
@merge_request = merge_request @merge_request = merge_request
unless @merge_request.mergeable? unless @merge_request.mergeable?
return log_merge_error('Merge request is not mergeable', save_message_on_model: true) return handle_merge_error(log_message: 'Merge request is not mergeable', save_message_on_model: true)
end end
check_size_limit check_size_limit
...@@ -27,7 +27,7 @@ module MergeRequests ...@@ -27,7 +27,7 @@ module MergeRequests
@source = find_merge_source @source = find_merge_source
unless @source unless @source
return log_merge_error('No source for merge', save_message_on_model: true) return handle_merge_error(log_message: 'No source for merge', save_message_on_model: true)
end end
merge_request.in_locked_state do merge_request.in_locked_state do
...@@ -38,8 +38,7 @@ module MergeRequests ...@@ -38,8 +38,7 @@ module MergeRequests
end end
end end
rescue MergeError => e rescue MergeError => e
clean_merge_jid handle_merge_error(log_message: e.message, save_message_on_model: true)
log_merge_error(e.message, save_message_on_model: true)
end end
def hooks_validation_pass?(merge_request) def hooks_validation_pass?(merge_request)
...@@ -52,12 +51,12 @@ module MergeRequests ...@@ -52,12 +51,12 @@ module MergeRequests
return true unless push_rule return true unless push_rule
unless push_rule.commit_message_allowed?(params[:commit_message]) unless push_rule.commit_message_allowed?(params[:commit_message])
log_merge_error("Commit message does not follow the pattern '#{push_rule.commit_message_regex}'", save_message_on_model: true) handle_merge_error(log_message: "Commit message does not follow the pattern '#{push_rule.commit_message_regex}'", save_message_on_model: true)
return false return false
end end
unless push_rule.author_email_allowed?(current_user.email) unless push_rule.author_email_allowed?(current_user.email)
log_merge_error("Commit author's email '#{current_user.email}' does not follow the pattern '#{push_rule.author_email_regex}'", save_message_on_model: true) handle_merge_error(log_message: "Commit author's email '#{current_user.email}' does not follow the pattern '#{push_rule.author_email_regex}'", save_message_on_model: true)
return false return false
end end
...@@ -103,10 +102,16 @@ module MergeRequests ...@@ -103,10 +102,16 @@ module MergeRequests
@merge_request.force_remove_source_branch? ? @merge_request.author : current_user @merge_request.force_remove_source_branch? ? @merge_request.author : current_user
end end
def log_merge_error(message, save_message_on_model: false) # Logs merge error message and cleans `MergeRequest#merge_jid`.
Rails.logger.error("MergeService ERROR: #{merge_request_info} - #{message}") #
def handle_merge_error(log_message:, save_message_on_model: false)
Rails.logger.error("MergeService ERROR: #{merge_request_info} - #{log_message}")
@merge_request.update(merge_error: message) if save_message_on_model if save_message_on_model
@merge_request.update(merge_error: log_message, merge_jid: nil)
else
clean_merge_jid
end
end end
def merge_request_info def merge_request_info
......
module Projects
class HashedStorageMigrationService < BaseService
include Gitlab::ShellAdapter
prepend ::EE::Projects::HashedStorageMigrationService
attr_reader :old_disk_path, :new_disk_path
def initialize(project, logger = nil)
@project = project
@logger ||= Rails.logger
end
def execute
return if project.hashed_storage?
@old_disk_path = project.disk_path
has_wiki = project.wiki.repository_exists?
project.storage_version = Storage::HashedProject::STORAGE_VERSION
project.ensure_storage_path_exists
@new_disk_path = project.disk_path
result = move_repository(@old_disk_path, @new_disk_path)
if has_wiki
result &&= move_repository("#{@old_disk_path}.wiki", "#{@new_disk_path}.wiki")
end
unless result
rollback_folder_move
return
end
project.repository_read_only = false
project.save!
block_given? ? yield : result
end
private
def move_repository(from_name, to_name)
from_exists = gitlab_shell.exists?(project.repository_storage_path, "#{from_name}.git")
to_exists = gitlab_shell.exists?(project.repository_storage_path, "#{to_name}.git")
# If we don't find the repository on either original or target we should log that as it could be an issue if the
# project was not originally empty.
if !from_exists && !to_exists
logger.warn "Can't find a repository on either source or target paths for #{project.full_path} (ID=#{project.id}) ..."
return false
elsif !from_exists
# Repository have been moved already.
return true
end
gitlab_shell.mv_repository(project.repository_storage_path, from_name, to_name)
end
def rollback_folder_move
move_repository(@new_disk_path, @old_disk_path)
move_repository("#{@new_disk_path}.wiki", "#{@old_disk_path}.wiki")
end
def logger
@logger
end
end
end
module Users module Users
class UpdateService < BaseService class UpdateService < BaseService
include NewUserNotifier include NewUserNotifier
prepend EE::Users::UpdateService
def initialize(user, params = {}) def initialize(current_user, params = {})
@user = user @current_user = current_user
@user = params.delete(:user)
@params = params.dup @params = params.dup
end end
...@@ -15,9 +17,7 @@ module Users ...@@ -15,9 +17,7 @@ module Users
user_exists = @user.persisted? user_exists = @user.persisted?
if @user.save(validate: validate) if @user.save(validate: validate)
notify_new_user(@user, nil) unless user_exists notify_success(user_exists)
success
else else
error(@user.errors.full_messages.uniq.join('. ')) error(@user.errors.full_messages.uniq.join('. '))
end end
...@@ -31,6 +31,14 @@ module Users ...@@ -31,6 +31,14 @@ module Users
true true
end end
protected
def notify_success(user_exists)
notify_new_user(@user, nil) unless user_exists
success
end
private private
def assign_attributes(&block) def assign_attributes(&block)
......
# LdapFilteralidator
#
# Custom validator for LDAP filters
#
# Example:
#
# class LdapGroupLink < ActiveRecord::Base
# validates :filter, ldap_filter: true
# end
#
class LdapFilterValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
Net::LDAP::Filter::FilterParser.parse(value)
rescue Net::LDAP::FilterSyntaxInvalidError
record.errors.add(attribute, 'must be a valid filter')
end
end
...@@ -37,6 +37,6 @@ ...@@ -37,6 +37,6 @@
= link_to 'Cancel', admin_group_path(@group), class: "btn btn-cancel" = link_to 'Cancel', admin_group_path(@group), class: "btn btn-cancel"
- if ldap_enabled? && @group.persisted? - if ldap_enabled? && @group.persisted?
%h3.page-title Linked LDAP groups %h3.page-title LDAP synchronizations
= render 'ldap_group_links/form', group: @group = render 'ldap_group_links/form', group: @group
= render 'ldap_group_links/ldap_group_links', group: @group = render 'ldap_group_links/ldap_group_links', group: @group
...@@ -63,13 +63,12 @@ ...@@ -63,13 +63,12 @@
= render partial: "namespaces/shared_runner_status", locals: { namespace: @group } = render partial: "namespaces/shared_runner_status", locals: { namespace: @group }
.panel.panel-default .panel.panel-default
.panel-heading Linked LDAP groups .panel-heading Active synchronizations
%ul.well-list %ul.well-list
- if @group.ldap_group_links.any? - if @group.ldap_group_links.any?
- @group.ldap_group_links.each do |ldap_group_link| - @group.ldap_group_links.each do |ldap_group_link|
%li %li
cn: %strong= ldap_group_link.cn ? "Group: #{ldap_group_link.cn}" : "Filter: #{truncate(ldap_group_link.filter, length: 40)}"
%strong= ldap_group_link.cn
as as
%strong= ldap_group_link.human_access %strong= ldap_group_link.human_access
......
.top-area
%ul.nav-links
= nav_link(html_options: { class: ("active" unless params[:personal].present?) }) do
= link_to s_('DashboardProjects|All'), dashboard_projects_path
= nav_link(html_options: { class: ("active" if params[:personal].present?) }) do
= link_to s_('DashboardProjects|Personal'), filter_projects_path(personal: true)
...@@ -10,8 +10,9 @@ ...@@ -10,8 +10,9 @@
= render "projects/last_push" = render "projects/last_push"
%div{ class: container_class } %div{ class: container_class }
- if has_projects_or_name?(@projects, params) - if show_projects?(@projects, params)
= render 'dashboard/projects_head' = render 'dashboard/projects_head'
= render 'nav'
= render 'projects' = render 'projects'
- else - else
= render "zero_authorized_projects" = render "zero_authorized_projects"
...@@ -5,8 +5,8 @@ ...@@ -5,8 +5,8 @@
%ul %ul
- @group.ldap_group_links.each do |ldap_group_link| - @group.ldap_group_links.each do |ldap_group_link|
%li %li
People in cn People in
%code= ldap_group_link.cn %code= ldap_group_link.cn ? "cn: #{ldap_group_link.cn}" : "filter: #{truncate(ldap_group_link.filter, length: 70)}"
are given are given
%code= ldap_group_link.human_access %code= ldap_group_link.human_access
access. access.
......
- page_title "LDAP Groups" - page_title 'LDAP Syncrhonizations'
%h3.page-title Linked LDAP groups %h3.page-title LDAP synchronizations
= render 'ldap_group_links/form', group: @group = render 'ldap_group_links/form', group: @group
= render 'ldap_group_links/ldap_group_links', group: @group = render 'ldap_group_links/ldap_group_links', group: @group
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'ldap_group_links'
%section.ldap-group-links %section.ldap-group-links
= form_for [group, LdapGroupLink.new], html: { class: 'form-horizontal' } do |f| = form_for [group, LdapGroupLink.new], html: { class: 'form-horizontal' } do |f|
.form-holder .form-holder
.form-group.clearfix .form-group.row
= f.label :cn, class: 'control-label' do = f.label :cn, class: 'control-label col-sm-2' do
LDAP Server LDAP Server
.col-sm-10 .col-sm-10
= f.select :provider, ldap_server_select_options, {}, class: 'form-control' = f.select :provider, ldap_server_select_options, {}, class: 'form-control'
.form-group.clearfix
= f.label :cn, class: 'control-label' do .form-group.row
= f.label :cn, class: 'control-label col-sm-2' do
Sync method
.col-sm-10
.radio
= label_tag :sync_method_group do
= radio_button_tag :sync_method, :group, true
LDAP Group cn
.radio
= label_tag :sync_method_filter do
= radio_button_tag :sync_method, :filter
LDAP user filter
.form-group.row.cn-link
= f.label :cn, class: 'control-label col-sm-2' do
LDAP Group cn LDAP Group cn
.col-sm-10 .col-sm-10
= f.hidden_field :cn, placeholder: "Ex. QA group", class: "xxlarge ajax-ldap-groups-select input-mn-300" = f.hidden_field :cn, placeholder: 'Ex. QA group', class: 'xxlarge ajax-ldap-groups-select input-mn-300'
.help-block .help-block
Synchronize #{group.name}'s members with this LDAP group. Synchronize #{group.name}'s members with this LDAP group.
%br %br
If you select an LDAP group you do not belong to you will lose ownership of #{group.name}. If you select an LDAP group you do not belong to you will lose ownership of #{group.name}.
.form-group.clearfix .form-group.row.filter-link
= f.label :group_access, class: 'control-label' do = f.label :filter, class: 'control-label col-sm-2' do
LDAP User filter
.col-sm-10
= f.text_field :filter, placeholder: 'Ex. (&(objectCategory=person)(objectClass=developer))', class: 'form-control xxlarge input-mn-300'
.help-block
- ldap_link = link_to 'LDAP Search Filter Syntax', 'https://msdn.microsoft.com/en-us/library/aa746475(v=vs.85).aspx'
This query must use valid #{ldap_link}. Synchronize #{group.name}'s members with this LDAP user filter.
%br
If you do not belong to this LDAP user filter you will lose ownership of #{group.name}.
.form-group.row
= f.label :group_access, class: 'control-label col-sm-2' do
LDAP Access LDAP Access
.col-sm-10 .col-sm-10
= f.select :group_access, options_for_select(GroupMember.access_level_roles), {}, class: 'form-control' = f.select :group_access, options_for_select(GroupMember.access_level_roles), {}, class: 'form-control'
...@@ -27,4 +54,4 @@ ...@@ -27,4 +54,4 @@
You can manage permission levels for individual group members in the Members tab. You can manage permission levels for individual group members in the Members tab.
.form-actions .form-actions
= f.submit 'Add synchronization', class: "btn btn-create" = f.submit 'Add synchronization', class: 'btn btn-create'
%li %li
.pull-right .pull-right
= link_to group_ldap_group_link_path(group, ldap_group_link), method: :delete, class: 'btn btn-danger btn-sm' do = link_to group_ldap_group_link_path(group, ldap_group_link), method: :delete, class: 'btn btn-danger btn-sm' do
= fa_icon('unlink', text: 'unlink') = fa_icon('unlink', text: 'Remove')
%strong= ldap_group_link.cn %strong= ldap_group_link.cn ? "Group: #{ldap_group_link.cn}" : "Filter: #{truncate(ldap_group_link.filter, length: 70)}"
- if ldap_group_link.config - if ldap_group_link.config
.light .light
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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