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

Merge remote-tracking branch 'upstream/master' into...

Merge remote-tracking branch 'upstream/master' into gem/sm/bump-google-api-client-gem-from-0-8-6-to-0-13-6

* upstream/master: (130 commits)
  Handle error when fetching ref for MR with deleted source branch
  refactor emails service
  fix update service
  refactor users update service
  refactor keys controller
  refactor some controllers to make them EE friendly
  fix specs
  fix users update service
  update initializers
  refactor services to match EE signature
  Change recommended MySQL version to 5.6
  Clarify artifact download via the API only accepts branch or tag name for ref
  Clean merge_jid whenever necessary on the merge process
  Rolling back change to n+1 detection
  Breadcrumbs receives padding when double lined
  karma spec fixes
  Add EEP to headings
  Added some Gitaly docs to `docs/development`
  Check orientation of profile image
  Fixed missing namespaces on navigation translations
  ...
parents 028712fb 576425f0
...@@ -8,4 +8,4 @@ ...@@ -8,4 +8,4 @@
karma.config.js karma.config.js
webpack.config.js webpack.config.js
svg.config.js svg.config.js
/app/assets/javascripts/locale/**/*.js /app/assets/javascripts/locale/**/app.js
...@@ -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.
......
...@@ -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'
......
...@@ -512,10 +512,10 @@ GEM ...@@ -512,10 +512,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)
...@@ -541,7 +541,7 @@ GEM ...@@ -541,7 +541,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)
...@@ -1077,7 +1077,7 @@ DEPENDENCIES ...@@ -1077,7 +1077,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)
......
File mode changed from 100755 to 100644
...@@ -77,9 +77,6 @@ $(() => { ...@@ -77,9 +77,6 @@ $(() => {
}); });
Store.rootPath = this.boardsEndpoint; Store.rootPath = this.boardsEndpoint;
this.filterManager = new FilteredSearchBoards(Store.filter, true);
this.filterManager.setup();
// Listen for updateTokens event // Listen for updateTokens event
eventHub.$on('updateTokens', this.updateTokens); eventHub.$on('updateTokens', this.updateTokens);
}, },
...@@ -87,6 +84,9 @@ $(() => { ...@@ -87,6 +84,9 @@ $(() => {
eventHub.$off('updateTokens', this.updateTokens); eventHub.$off('updateTokens', this.updateTokens);
}, },
mounted () { mounted () {
this.filterManager = new FilteredSearchBoards(Store.filter, true);
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');
isLoading: false, const cycleAnalyticsService = new CycleAnalyticsService({
isLoadingStage: false, requestPath: cycleAnalyticsEl.dataset.requestPath,
isEmptyStage: false, });
hasError: false,
startDate: 30, return {
isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE), store: CycleAnalyticsStore,
state: CycleAnalyticsStore.state,
isLoading: false,
isLoadingStage: false,
isEmptyStage: false,
hasError: false,
startDate: 30,
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`, {
cycle_analytics: { params: {
start_date: startDate, cycle_analytics: {
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: '',
......
...@@ -7,6 +7,8 @@ ...@@ -7,6 +7,8 @@
* causes reflows, visit https://gist.github.com/paulirish/5d52fb081b3570c81e3a * causes reflows, visit https://gist.github.com/paulirish/5d52fb081b3570c81e3a
*/ */
import Cookies from 'js-cookie';
const LINE_NUMBER_CLASS = 'diff-line-num'; const LINE_NUMBER_CLASS = 'diff-line-num';
const UNFOLDABLE_LINE_CLASS = 'js-unfold'; const UNFOLDABLE_LINE_CLASS = 'js-unfold';
const NO_COMMENT_CLASS = 'no-comment-btn'; const NO_COMMENT_CLASS = 'no-comment-btn';
...@@ -27,9 +29,7 @@ export default { ...@@ -27,9 +29,7 @@ export default {
this.userCanCreateNote = $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('can-create-note') === ''; this.userCanCreateNote = $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('can-create-note') === '';
} }
if (typeof notes !== 'undefined' && !this.isParallelView) { this.isParallelView = Cookies.get('diff_view') === 'parallel';
this.isParallelView = notes.isParallelView && notes.isParallelView();
}
if (this.userCanCreateNote) { if (this.userCanCreateNote) {
$diffFile.on('mouseover', LINE_COLUMN_CLASSES, e => this.showButton(this.isParallelView, e)) $diffFile.on('mouseover', LINE_COLUMN_CLASSES, e => this.showButton(this.isParallelView, e))
......
...@@ -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];
......
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,148 +28,149 @@ ...@@ -28,148 +28,149 @@
// </div> // </div>
// </div> // </div>
// //
(function() {
this.LineHighlighter = (function() {
// CSS class applied to highlighted lines
LineHighlighter.prototype.highlightClass = 'hll';
// Internal copy of location.hash so we're not dependent on `location` in tests
LineHighlighter.prototype._hash = '';
function LineHighlighter(hash) {
if (hash == null) {
// Initialize a LineHighlighter object
//
// hash - String URL hash for dependency injection in tests
hash = location.hash;
}
this.setHash = this.setHash.bind(this);
this.highlightLine = this.highlightLine.bind(this);
this.clickHandler = this.clickHandler.bind(this);
this.highlightHash = this.highlightHash.bind(this);
this._hash = hash;
this.bindEvents();
this.highlightHash();
}
LineHighlighter.prototype.bindEvents = function() { const LineHighlighter = function(options = {}) {
const $fileHolder = $('.file-holder'); options.highlightLineClass = options.highlightLineClass || 'hll';
$fileHolder.on('click', 'a[data-line-number]', this.clickHandler); options.fileHolderSelector = options.fileHolderSelector || '.file-holder';
$fileHolder.on('highlight:line', this.highlightHash); options.scrollFileHolder = options.scrollFileHolder || false;
}; options.hash = options.hash || location.hash;
LineHighlighter.prototype.highlightHash = function() {
var range;
if (this._hash !== '') {
range = this.hashToRange(this._hash);
if (range[0]) {
this.highlightRange(range);
$.scrollTo("#L" + range[0], {
// Scroll to the first highlighted line on initial load
// Offset -50 for the sticky top bar, and another -100 for some context
offset: -150
});
}
}
};
LineHighlighter.prototype.clickHandler = function(event) {
var current, lineNumber, range;
event.preventDefault();
this.clearHighlight();
lineNumber = $(event.target).closest('a').data('line-number');
current = this.hashToRange(this._hash);
if (!(current[0] && event.shiftKey)) {
// If there's no current selection, or there is but Shift wasn't held,
// treat this like a single-line selection.
this.setHash(lineNumber);
return this.highlightLine(lineNumber);
} else if (event.shiftKey) {
if (lineNumber < current[0]) {
range = [lineNumber, current[0]];
} else {
range = [current[0], lineNumber];
}
this.setHash(range[0], range[1]);
return this.highlightRange(range);
}
};
LineHighlighter.prototype.clearHighlight = function() {
return $("." + this.highlightClass).removeClass(this.highlightClass);
// Unhighlight previously highlighted lines
};
// Convert a URL hash String into line numbers
//
// hash - Hash String
//
// Examples:
//
// hashToRange('#L5') # => [5, null]
// hashToRange('#L5-15') # => [5, 15]
// hashToRange('#foo') # => [null, null]
//
// Returns an Array
LineHighlighter.prototype.hashToRange = function(hash) {
var first, last, matches;
// ?L(\d+)(?:-(\d+))?$/)
matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/);
if (matches && matches.length) {
first = parseInt(matches[1], 10);
last = matches[2] ? parseInt(matches[2], 10) : null;
return [first, last];
} else {
return [null, null];
}
};
// Highlight a single line
//
// lineNumber - Line number to highlight
LineHighlighter.prototype.highlightLine = function(lineNumber) {
return $("#LC" + lineNumber).addClass(this.highlightClass);
};
// Highlight all lines within a range
//
// range - Array containing the starting and ending line numbers
LineHighlighter.prototype.highlightRange = function(range) {
var i, lineNumber, ref, ref1, results;
if (range[1]) {
results = [];
for (lineNumber = i = ref = range[0], ref1 = range[1]; ref <= ref1 ? i <= ref1 : i >= ref1; lineNumber = ref <= ref1 ? (i += 1) : (i -= 1)) {
results.push(this.highlightLine(lineNumber));
}
return results;
} else {
return this.highlightLine(range[0]);
}
};
// Set the URL hash string this.options = options;
LineHighlighter.prototype.setHash = function(firstLineNumber, lastLineNumber) { this._hash = options.hash;
var hash; this.highlightLineClass = options.highlightLineClass;
if (lastLineNumber) { this.setHash = this.setHash.bind(this);
hash = "#L" + firstLineNumber + "-" + lastLineNumber; this.highlightLine = this.highlightLine.bind(this);
this.clickHandler = this.clickHandler.bind(this);
this.highlightHash = this.highlightHash.bind(this);
this.bindEvents();
this.highlightHash();
};
LineHighlighter.prototype.bindEvents = function() {
const $fileHolder = $(this.options.fileHolderSelector);
$fileHolder.on('click', 'a[data-line-number]', this.clickHandler);
$fileHolder.on('highlight:line', this.highlightHash);
};
LineHighlighter.prototype.highlightHash = function() {
var range;
if (this._hash !== '') {
range = this.hashToRange(this._hash);
if (range[0]) {
this.highlightRange(range);
const lineSelector = `#L${range[0]}`;
const scrollOptions = {
// Scroll to the first highlighted line on initial load
// Offset -50 for the sticky top bar, and another -100 for some context
offset: -150
};
if (this.options.scrollFileHolder) {
$(this.options.fileHolderSelector).scrollTo(lineSelector, scrollOptions);
} else { } else {
hash = "#L" + firstLineNumber; $.scrollTo(lineSelector, scrollOptions);
} }
this._hash = hash; }
return this.__setLocationHash__(hash); }
}; };
// Make the actual hash change in the browser LineHighlighter.prototype.clickHandler = function(event) {
// var current, lineNumber, range;
// This method is stubbed in tests. event.preventDefault();
LineHighlighter.prototype.__setLocationHash__ = function(value) { this.clearHighlight();
return history.pushState({ lineNumber = $(event.target).closest('a').data('line-number');
url: value current = this.hashToRange(this._hash);
// We're using pushState instead of assigning location.hash directly to if (!(current[0] && event.shiftKey)) {
// prevent the page from scrolling on the hashchange event // If there's no current selection, or there is but Shift wasn't held,
}, document.title, value); // treat this like a single-line selection.
}; this.setHash(lineNumber);
return this.highlightLine(lineNumber);
return LineHighlighter; } else if (event.shiftKey) {
})(); if (lineNumber < current[0]) {
}).call(window); range = [lineNumber, current[0]];
} else {
range = [current[0], lineNumber];
}
this.setHash(range[0], range[1]);
return this.highlightRange(range);
}
};
LineHighlighter.prototype.clearHighlight = function() {
return $("." + this.highlightLineClass).removeClass(this.highlightLineClass);
};
// Convert a URL hash String into line numbers
//
// hash - Hash String
//
// Examples:
//
// hashToRange('#L5') # => [5, null]
// hashToRange('#L5-15') # => [5, 15]
// hashToRange('#foo') # => [null, null]
//
// Returns an Array
LineHighlighter.prototype.hashToRange = function(hash) {
var first, last, matches;
// ?L(\d+)(?:-(\d+))?$/)
matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/);
if (matches && matches.length) {
first = parseInt(matches[1], 10);
last = matches[2] ? parseInt(matches[2], 10) : null;
return [first, last];
} else {
return [null, null];
}
};
// Highlight a single line
//
// lineNumber - Line number to highlight
LineHighlighter.prototype.highlightLine = function(lineNumber) {
return $("#LC" + lineNumber).addClass(this.highlightLineClass);
};
// Highlight all lines within a range
//
// range - Array containing the starting and ending line numbers
LineHighlighter.prototype.highlightRange = function(range) {
var i, lineNumber, ref, ref1, results;
if (range[1]) {
results = [];
for (lineNumber = i = ref = range[0], ref1 = range[1]; ref <= ref1 ? i <= ref1 : i >= ref1; lineNumber = ref <= ref1 ? (i += 1) : (i -= 1)) {
results.push(this.highlightLine(lineNumber));
}
return results;
} else {
return this.highlightLine(range[0]);
}
};
// Set the URL hash string
LineHighlighter.prototype.setHash = function(firstLineNumber, lastLineNumber) {
var hash;
if (lastLineNumber) {
hash = "#L" + firstLineNumber + "-" + lastLineNumber;
} else {
hash = "#L" + firstLineNumber;
}
this._hash = hash;
return this.__setLocationHash__(hash);
};
// Make the actual hash change in the browser
//
// This method is stubbed in tests.
LineHighlighter.prototype.__setLocationHash__ = function(value) {
return history.pushState({
url: value
// We're using pushState instead of assigning location.hash directly to
// prevent the page from scrolling on the hashchange event
}, document.title, value);
};
window.LineHighlighter = LineHighlighter;
...@@ -16,9 +16,8 @@ const locales = allLocales.reduce((d, obj) => { ...@@ -16,9 +16,8 @@ const locales = allLocales.reduce((d, obj) => {
return data; return data;
}, {}); }, {});
let lang = document.querySelector('html').getAttribute('lang') || 'en'; const langAttribute = document.querySelector('html').getAttribute('lang');
lang = lang.replace(/-/g, '_'); const lang = (langAttribute || 'en').replace(/-/g, '_');
const locale = new Jed(locales[lang]); const locale = new Jed(locales[lang]);
/** /**
......
...@@ -302,7 +302,10 @@ $(function () { ...@@ -302,7 +302,10 @@ $(function () {
return $container.remove(); return $container.remove();
// Commit show suppressed diff // Commit show suppressed diff
}); });
$('.navbar-toggle').on('click', () => $('.header-content').toggleClass('menu-expanded')); $('.navbar-toggle').on('click', () => {
$('.header-content').toggleClass('menu-expanded');
gl.lazyLoader.loadCheck();
});
// Show/hide comments on diff // Show/hide comments on diff
$body.on('click', '.js-toggle-diff-comments', function (e) { $body.on('click', '.js-toggle-diff-comments', function (e) {
var $this = $(this); var $this = $(this);
......
...@@ -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');
} }
......
...@@ -73,7 +73,8 @@ import _ from 'underscore'; ...@@ -73,7 +73,8 @@ import _ from 'underscore';
aspectRatio: 1, aspectRatio: 1,
modal: true, modal: true,
scalable: false, scalable: false,
rotatable: false, rotatable: true,
checkOrientation: true,
zoomable: true, zoomable: true,
dragMode: 'move', dragMode: 'move',
guides: false, guides: false,
......
<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;
} }
......
...@@ -43,6 +43,8 @@ import Cookies from 'js-cookie'; ...@@ -43,6 +43,8 @@ import Cookies from 'js-cookie';
$allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right'); $allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right');
$('aside.right-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); $('aside.right-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
$('.page-with-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); $('.page-with-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
if (gl.lazyLoader) gl.lazyLoader.loadCheck();
} }
if (!triggered) { if (!triggered) {
return Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed')); return Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'));
......
...@@ -31,10 +31,12 @@ ...@@ -31,10 +31,12 @@
@import "framework/mobile"; @import "framework/mobile";
@import "framework/modal"; @import "framework/modal";
@import "framework/nav"; @import "framework/nav";
@import "framework/new-nav";
@import "framework/pagination"; @import "framework/pagination";
@import "framework/panels"; @import "framework/panels";
@import "framework/selects"; @import "framework/selects";
@import "framework/sidebar"; @import "framework/sidebar";
@import "framework/new-sidebar";
@import "framework/tables"; @import "framework/tables";
@import "framework/notes"; @import "framework/notes";
@import "framework/timeline"; @import "framework/timeline";
......
...@@ -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;
} }
} }
} }
...@@ -254,6 +254,10 @@ body { ...@@ -254,6 +254,10 @@ body {
.search-icon { .search-icon {
color: $theme-gray-200; color: $theme-gray-200;
} }
.search-input {
color: $gl-text-color;
}
} }
.location-badge { .location-badge {
......
...@@ -142,5 +142,41 @@ ...@@ -142,5 +142,41 @@
} }
@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) {
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;
}
}
.nav-links { .nav-links {
display: flex; display: flex;
......
...@@ -295,75 +295,6 @@ header.navbar-gitlab-new { ...@@ -295,75 +295,6 @@ header.navbar-gitlab-new {
margin-top: 4px; margin-top: 4px;
} }
.search {
margin: 4px 8px 0;
form {
height: 32px;
border: 0;
border-radius: $border-radius-default;
transition: border-color ease-in-out 0.15s, background-color ease-in-out 0.15s;
&:hover {
box-shadow: none;
}
}
&.search-active form {
box-shadow: none;
.search-input {
color: $gl-text-color;
transition: color ease-in-out 0.15s;
}
.search-input::placeholder {
color: $gl-text-color-tertiary;
}
.search-input-wrap {
.search-icon,
.clear-icon {
color: $gl-text-color-tertiary;
transition: color ease-in-out 0.15s;
}
}
}
.search-input {
color: $white-light;
background: none;
transition: color ease-in-out 0.15s;
}
.search-input::placeholder {
transition: color ease-in-out 0.15s;
}
.location-badge {
font-size: 12px;
margin: -4px 4px -4px -4px;
line-height: 25px;
padding: 4px 8px;
border-radius: 2px 0 0 2px;
height: 32px;
transition: border-color ease-in-out 0.15s;
}
&.search-active {
.location-badge {
background-color: $nav-badge-bg;
border-color: $border-color;
}
.search-input-wrap {
.clear-icon {
color: $white-light;
}
}
}
}
.breadcrumbs { .breadcrumbs {
display: flex; display: flex;
min-height: 48px; min-height: 48px;
...@@ -375,6 +306,8 @@ header.navbar-gitlab-new { ...@@ -375,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;
} }
...@@ -386,11 +319,6 @@ header.navbar-gitlab-new { ...@@ -386,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;
...@@ -420,6 +348,7 @@ header.navbar-gitlab-new { ...@@ -420,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;
...@@ -455,7 +384,7 @@ header.navbar-gitlab-new { ...@@ -455,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;
......
...@@ -6,6 +6,8 @@ $gutter_width: 290px; ...@@ -6,6 +6,8 @@ $gutter_width: 290px;
$gutter_inner_width: 250px; $gutter_inner_width: 250px;
$sidebar-transition-duration: .15s; $sidebar-transition-duration: .15s;
$sidebar-breakpoint: 1024px; $sidebar-breakpoint: 1024px;
$default-transition-duration: .15s;
$right-sidebar-transition-duration: .3s;
/* /*
* Color schema * Color schema
...@@ -27,46 +29,45 @@ $gray-dark: darken($gray-light, $darken-dark-factor); ...@@ -27,46 +29,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;
...@@ -74,6 +75,7 @@ $red-600: #c0341d; ...@@ -74,6 +75,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
...@@ -184,8 +186,8 @@ $list-text-disabled-color: $gl-text-color-tertiary; ...@@ -184,8 +186,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;
/* /*
...@@ -214,8 +216,8 @@ $gl-sidebar-padding: 22px; ...@@ -214,8 +216,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;
...@@ -265,8 +267,8 @@ $time-color: #999; ...@@ -265,8 +267,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;
...@@ -451,17 +453,17 @@ $builds-trace-bg: #111; ...@@ -451,17 +453,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;
/* /*
......
...@@ -55,6 +55,15 @@ ...@@ -55,6 +55,15 @@
.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 {
...@@ -78,11 +87,6 @@ ...@@ -78,11 +87,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);
}
} }
} }
...@@ -412,14 +416,6 @@ ...@@ -412,14 +416,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;
} }
...@@ -457,8 +453,8 @@ ...@@ -457,8 +453,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;
...@@ -535,7 +535,6 @@ ...@@ -535,7 +535,6 @@
} }
.diff-notes-collapse { .diff-notes-collapse {
position: relative;
width: 19px; width: 19px;
height: 19px; height: 19px;
padding: 0; padding: 0;
...@@ -543,11 +542,7 @@ ...@@ -543,11 +542,7 @@
z-index: 100; z-index: 100;
svg { svg {
position: absolute; vertical-align: text-top;
left: 50%;
top: 50%;
margin-left: -5.5px;
margin-top: -5.5px;
} }
path { path {
...@@ -586,11 +581,6 @@ ...@@ -586,11 +581,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 +595,6 @@ ...@@ -605,11 +595,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;
......
...@@ -644,20 +644,20 @@ button.mini-pipeline-graph-dropdown-toggle { ...@@ -644,20 +644,20 @@ button.mini-pipeline-graph-dropdown-toggle {
// 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,
......
...@@ -291,7 +291,7 @@ table.u2f-registrations { ...@@ -291,7 +291,7 @@ table.u2f-registrations {
.bordered-box { .bordered-box {
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;
...@@ -379,7 +379,7 @@ table.u2f-registrations { ...@@ -379,7 +379,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;
......
...@@ -28,9 +28,7 @@ input[type="checkbox"]:hover { ...@@ -28,9 +28,7 @@ input[type="checkbox"]:hover {
} }
.search { .search {
margin-right: 10px; margin: 4px 8px 0;
margin-left: 10px;
margin-top: ($header-height - 35) / 2;
form { form {
@extend .form-control; @extend .form-control;
...@@ -38,15 +36,23 @@ input[type="checkbox"]:hover { ...@@ -38,15 +36,23 @@ input[type="checkbox"]:hover {
padding: 4px; padding: 4px;
width: $search-input-width; width: $search-input-width;
line-height: 24px; line-height: 24px;
height: 32px;
border: 0;
border-radius: $border-radius-default;
transition: border-color ease-in-out $default-transition-duration, background-color ease-in-out $default-transition-duration;
&:hover { &:hover {
border-color: lighten($dropdown-input-focus-border, 20%); box-shadow: none;
box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%);
} }
} }
.location-text { .location-badge {
font-style: normal; font-size: 12px;
margin: -4px 4px -4px -4px;
line-height: 25px;
padding: 4px 8px;
border-radius: $border-radius-default 0 0 $border-radius-default;
transition: border-color ease-in-out $default-transition-duration;
} }
.search-input { .search-input {
...@@ -56,41 +62,26 @@ input[type="checkbox"]:hover { ...@@ -56,41 +62,26 @@ input[type="checkbox"]:hover {
margin-left: 5px; margin-left: 5px;
line-height: 25px; line-height: 25px;
width: 98%; width: 98%;
color: $white-light;
background: none;
transition: color ease-in-out $default-transition-duration;
} }
.location-badge { .search-input::placeholder {
line-height: 25px; transition: color ease-in-out $default-transition-duration;
padding: 0 5px;
border-radius: $border-radius-default;
font-size: 14px;
font-style: normal;
color: $note-disabled-comment-color;
display: inline-block;
background-color: $gray-normal;
vertical-align: top;
cursor: default;
} }
.search-input-container { .search-input-container {
display: -webkit-flex;
display: flex; display: flex;
position: relative; position: relative;
} }
.search-input-wrap { .search-input-wrap {
// Fallback if flexbox is not supported
display: inline-block;
}
.search-input-wrap {
width: 100%;
.search-icon, .search-icon,
.clear-icon { .clear-icon {
position: absolute; position: absolute;
right: 5px; right: 5px;
top: 0; top: 0;
color: $location-icon-color;
&::before { &::before {
font-family: FontAwesome; font-family: FontAwesome;
...@@ -101,7 +92,7 @@ input[type="checkbox"]:hover { ...@@ -101,7 +92,7 @@ input[type="checkbox"]:hover {
.search-icon { .search-icon {
@extend .fa-search; @extend .fa-search;
transition: color 0.15s; transition: color $default-transition-duration;
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
-ms-user-select: none; -ms-user-select: none;
...@@ -148,21 +139,32 @@ input[type="checkbox"]:hover { ...@@ -148,21 +139,32 @@ input[type="checkbox"]:hover {
form { form {
@extend .form-control:focus; @extend .form-control:focus;
border-color: $dropdown-input-focus-border; border-color: $dropdown-input-focus-border;
box-shadow: 0 0 4px $search-input-focus-shadow-color; box-shadow: none;
}
.search-input-wrap {
.search-icon,
.clear-icon {
color: $gl-text-color-tertiary;
transition: color ease-in-out $default-transition-duration;
}
}
.location-badge { .search-input {
transition: all 0.15s; color: $gl-text-color;
background-color: $location-badge-active-bg; transition: color ease-in-out $default-transition-duration;
color: $white-light; }
}
.search-input-wrap { .search-input::placeholder {
i { color: $gl-text-color-tertiary;
color: $layout-link-gray;
} }
} }
.location-badge {
transition: all $default-transition-duration;
background-color: $nav-badge-bg;
border-color: $border-color;
}
.dropdown-menu { .dropdown-menu {
transition-duration: 100ms, 75ms; transition-duration: 100ms, 75ms;
transition-delay: 75ms, 100ms; transition-delay: 75ms, 100ms;
......
...@@ -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;
}
...@@ -22,8 +22,7 @@ class Admin::ApplicationsController < Admin::ApplicationController ...@@ -22,8 +22,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 +41,13 @@ class Admin::ApplicationsController < Admin::ApplicationController ...@@ -42,6 +41,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
...@@ -219,7 +219,7 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -219,7 +219,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
......
...@@ -106,7 +106,7 @@ module IssuableCollections ...@@ -106,7 +106,7 @@ module IssuableCollections
# @filter_params[:authorized_only] = true # @filter_params[:authorized_only] = true
end end
@filter_params @filter_params.permit(IssuableFinder::VALID_PARAMS)
end end
def set_default_state def set_default_state
...@@ -117,19 +117,32 @@ module IssuableCollections ...@@ -117,19 +117,32 @@ module IssuableCollections
key = 'issuable_sort' key = 'issuable_sort'
cookies[key] = params[:sort] if params[:sort].present? cookies[key] = params[:sort] if params[:sort].present?
cookies[key] = update_cookie_value(cookies[key])
# id_desc and id_asc are old values for these two.
cookies[key] = sort_value_recently_created if cookies[key] == 'id_desc'
cookies[key] = sort_value_oldest_created if cookies[key] == 'id_asc'
params[:sort] = cookies[key] params[:sort] = cookies[key]
end end
def default_sort_order def default_sort_order
case params[:state] case params[:state]
when 'opened', 'all' then sort_value_recently_created when 'opened', 'all' then sort_value_created_date
when 'merged', 'closed' then sort_value_recently_updated when 'merged', 'closed' then sort_value_recently_updated
else sort_value_recently_created else sort_value_created_date
end
end
# Update old values to the actual ones.
def update_cookie_value(value)
case value
when 'id_asc' then sort_value_oldest_created
when 'id_desc' then sort_value_recently_created
when 'created_asc' then sort_value_created_date
when 'created_desc' then sort_value_created_date
when 'due_date_asc' then sort_value_due_date
when 'due_date_desc' then sort_value_due_date
when 'milestone_due_asc' then sort_value_milestone
when 'milestone_due_desc' then sort_value_milestone
when 'downvotes_asc' then sort_value_popularity
when 'downvotes_desc' then sort_value_popularity
else value
end end
end end
end end
...@@ -12,10 +12,14 @@ class ConfirmationsController < Devise::ConfirmationsController ...@@ -12,10 +12,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
...@@ -21,14 +21,20 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController ...@@ -21,14 +21,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
......
...@@ -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 }
......
...@@ -14,7 +14,7 @@ class Profiles::KeysController < Profiles::ApplicationController ...@@ -14,7 +14,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 +50,12 @@ class Profiles::KeysController < Profiles::ApplicationController ...@@ -50,6 +50,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" }
......
...@@ -55,7 +55,7 @@ class SessionsController < Devise::SessionsController ...@@ -55,7 +55,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
......
...@@ -25,6 +25,28 @@ class IssuableFinder ...@@ -25,6 +25,28 @@ class IssuableFinder
NONE = '0'.freeze NONE = '0'.freeze
SCALAR_PARAMS = %i[
assignee_id
assignee_username
author_id
author_username
authorized_only
due_date
group_id
iids
label_name
milestone_title
non_archived
project_id
scope
search
sort
state
].freeze
ARRAY_PARAMS = { label_name: [], iids: [], assignee_username: [] }.freeze
VALID_PARAMS = (SCALAR_PARAMS + [ARRAY_PARAMS]).freeze
attr_accessor :current_user, :params attr_accessor :current_user, :params
def initialize(current_user, params = {}) def initialize(current_user, params = {})
......
...@@ -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'
data_attributes[:src] = avatar_url
avatar_url = LazyImageTagHelper.placeholder_image
end
image_options = {
alt: "#{user_name}'s avatar",
src: avatar_url,
data: data_attributes,
class: css_class, class: css_class,
alt: "#{user_name}'s avatar", title: user_name
title: user_name, }
data: data_attributes,
lazy: true tag(:img, image_options)
)
end end
def user_avatar(options = {}) def user_avatar(options = {})
......
...@@ -79,6 +79,6 @@ module BoardsHelper ...@@ -79,6 +79,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
......
This diff is collapsed.
...@@ -434,7 +434,7 @@ module Ci ...@@ -434,7 +434,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
......
...@@ -174,7 +174,7 @@ module Ci ...@@ -174,7 +174,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
......
...@@ -143,16 +143,18 @@ module Issuable ...@@ -143,16 +143,18 @@ module Issuable
end end
def sort(method, excluded_labels: []) def sort(method, excluded_labels: [])
sorted = case method.to_s sorted =
when 'milestone_due_asc' then order_milestone_due_asc case method.to_s
when 'milestone_due_desc' then order_milestone_due_desc when 'downvotes_desc' then order_downvotes_desc
when 'downvotes_desc' then order_downvotes_desc when 'label_priority' then order_labels_priority(excluded_labels: excluded_labels)
when 'upvotes_desc' then order_upvotes_desc when 'milestone' then order_milestone_due_asc
when 'label_priority' then order_labels_priority(excluded_labels: excluded_labels) when 'milestone_due_asc' then order_milestone_due_asc
when 'priority' then order_due_date_and_labels_priority(excluded_labels: excluded_labels) when 'milestone_due_desc' then order_milestone_due_desc
else when 'popularity' then order_upvotes_desc
order_by(method) when 'priority' then order_due_date_and_labels_priority(excluded_labels: excluded_labels)
end when 'upvotes_desc' then order_upvotes_desc
else order_by(method)
end
# Break ties with the ID column for pagination # Break ties with the ID column for pagination
sorted.order(id: :desc) sorted.order(id: :desc)
...@@ -214,7 +216,7 @@ module Issuable ...@@ -214,7 +216,7 @@ module Issuable
def grouping_columns(sort) def grouping_columns(sort)
grouping_columns = [arel_table[:id]] grouping_columns = [arel_table[:id]]
if %w(milestone_due_desc milestone_due_asc).include?(sort) if %w(milestone_due_desc milestone_due_asc milestone).include?(sort)
milestone_table = Milestone.arel_table milestone_table = Milestone.arel_table
grouping_columns << milestone_table[:id] grouping_columns << milestone_table[:id]
grouping_columns << milestone_table[:due_date] grouping_columns << milestone_table[:due_date]
......
...@@ -19,14 +19,15 @@ module Sortable ...@@ -19,14 +19,15 @@ module Sortable
module ClassMethods module ClassMethods
def order_by(method) def order_by(method)
case method.to_s case method.to_s
when 'name_asc' then order_name_asc when 'created_asc' then order_created_asc
when 'name_desc' then order_name_desc when 'created_date' then order_created_desc
when 'updated_asc' then order_updated_asc
when 'updated_desc' then order_updated_desc
when 'created_asc' then order_created_asc
when 'created_desc' then order_created_desc when 'created_desc' then order_created_desc
when 'id_desc' then order_id_desc when 'id_asc' then order_id_asc
when 'id_asc' then order_id_asc when 'id_desc' then order_id_desc
when 'name_asc' then order_name_asc
when 'name_desc' then order_name_desc
when 'updated_asc' then order_updated_asc
when 'updated_desc' then order_updated_desc
else else
all all
end end
......
...@@ -116,7 +116,8 @@ class Issue < ActiveRecord::Base ...@@ -116,7 +116,8 @@ class Issue < ActiveRecord::Base
def self.sort(method, excluded_labels: []) def self.sort(method, excluded_labels: [])
case method.to_s case method.to_s
when 'due_date_asc' then order_due_date_asc when 'due_date' then order_due_date_asc
when 'due_date_asc' then order_due_date_asc
when 'due_date_desc' then order_due_date_desc when 'due_date_desc' then order_due_date_desc
else else
super super
......
...@@ -489,13 +489,7 @@ class Repository ...@@ -489,13 +489,7 @@ class Repository
def exists? def exists?
return false unless full_path return false unless full_path
Gitlab::GitalyClient.migrate(:repository_exists) do |enabled| raw_repository.exists?
if enabled
raw_repository.exists?
else
refs_directory_exists?
end
end
end end
cache_method :exists? cache_method :exists?
...@@ -534,8 +528,11 @@ class Repository ...@@ -534,8 +528,11 @@ class Repository
cache_method :tag_count, fallback: 0 cache_method :tag_count, fallback: 0
def avatar def avatar
if tree = file_on_head(:avatar) # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38327
tree.path Gitlab::GitalyClient.allow_n_plus_1_calls do
if tree = file_on_head(:avatar)
tree.path
end
end end
end end
cache_method :avatar cache_method :avatar
...@@ -1060,12 +1057,6 @@ class Repository ...@@ -1060,12 +1057,6 @@ class Repository
blob.data blob.data
end end
def refs_directory_exists?
circuit_breaker.perform do
File.exist?(File.join(path_to_repo, 'refs'))
end
end
def cache def cache
# TODO: should we use UUIDs here? We could move repositories without clearing this cache # TODO: should we use UUIDs here? We could move repositories without clearing this cache
@cache ||= RepositoryCache.new(full_path, @project.id) @cache ||= RepositoryCache.new(full_path, @project.id)
...@@ -1117,10 +1108,6 @@ class Repository ...@@ -1117,10 +1108,6 @@ class Repository
Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', Gitlab::GlRepository.gl_repository(project, false)) Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', Gitlab::GlRepository.gl_repository(project, false))
end end
def circuit_breaker
@circuit_breaker ||= Gitlab::Git::Storage::CircuitBreaker.for_storage(project.repository_storage)
end
def find_commits_by_message_by_shelling_out(query, ref, path, limit, offset) def find_commits_by_message_by_shelling_out(query, ref, path, limit, offset)
ref ||= root_ref ref ||= root_ref
......
...@@ -60,7 +60,7 @@ class User < ActiveRecord::Base ...@@ -60,7 +60,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
...@@ -526,8 +526,8 @@ class User < ActiveRecord::Base ...@@ -526,8 +526,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
...@@ -1000,7 +1000,7 @@ class User < ActiveRecord::Base ...@@ -1000,7 +1000,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
...@@ -1186,7 +1186,7 @@ class User < ActiveRecord::Base ...@@ -1186,7 +1186,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,110 +2,55 @@ module Ci ...@@ -2,110 +2,55 @@ 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) 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, &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 || command = OpenStruct.new(ignore_skip_ci: ignore_skip_ci,
validate_pipeline(ignore_skip_ci: ignore_skip_ci, save_incompleted: save_on_errors,
save_on_errors: save_on_errors) seeds_block: block,
project: project,
current_user: current_user)
return result if result sequence = Gitlab::Ci::Pipeline::Chain::Sequence
.new(pipeline, command, SEQUENCE)
begin sequence.build! do |pipeline, sequence|
Ci::Pipeline.transaction do update_merge_requests_head_pipeline if pipeline.persisted?
pipeline.save!
yield(pipeline) if block_given? if sequence.complete?
cancel_pending_pipelines if project.auto_cancel_pending_pipelines?
pipeline_created_counter.increment(source: source)
Ci::CreatePipelineStagesService pipeline.process!
.new(project, current_user)
.execute(pipeline)
end end
rescue ActiveRecord::RecordInvalid => e
return error("Failed to persist the pipeline: #{e}")
end end
update_merge_requests_head_pipeline
cancel_pending_pipelines if project.auto_cancel_pending_pipelines?
pipeline_created_counter.increment(source: source)
pipeline.tap(&:process!)
end end
private private
def validate_project_and_git_items def commit
unless project.builds_enabled? @commit ||= project.commit(origin_sha || origin_ref)
return error('Pipeline is disabled')
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
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
unless pipeline.has_stage_seeds?
return error('No stages / jobs for this pipeline.')
end
end
def allowed_to_trigger_pipeline?
if current_user
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
...@@ -115,11 +60,6 @@ module Ci ...@@ -115,11 +60,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|
...@@ -136,14 +76,6 @@ module Ci ...@@ -136,14 +76,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
...@@ -156,41 +88,17 @@ module Ci ...@@ -156,41 +88,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 DestroyService < ::Emails::BaseService class DestroyService < ::Emails::BaseService
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
......
...@@ -14,13 +14,13 @@ module MergeRequests ...@@ -14,13 +14,13 @@ 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
@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
...@@ -31,8 +31,7 @@ module MergeRequests ...@@ -31,8 +31,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
private private
...@@ -74,10 +73,16 @@ module MergeRequests ...@@ -74,10 +73,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
......
...@@ -2,22 +2,21 @@ module Users ...@@ -2,22 +2,21 @@ module Users
class UpdateService < BaseService class UpdateService < BaseService
include NewUserNotifier include NewUserNotifier
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
def execute(validate: true, &block) def execute(validate: true, &block)
yield(@user) if block_given? yield(@user) if block_given?
assign_attributes(&block)
user_exists = @user.persisted? user_exists = @user.persisted?
if @user.save(validate: validate) assign_attributes(&block)
notify_new_user(@user, nil) unless user_exists
success if @user.save(validate: validate)
notify_success(user_exists)
else else
error(@user.errors.full_messages.uniq.join('. ')) error(@user.errors.full_messages.uniq.join('. '))
end end
...@@ -33,6 +32,12 @@ module Users ...@@ -33,6 +32,12 @@ module Users
private private
def notify_success(user_exists)
notify_new_user(@user, nil) unless user_exists
success
end
def assign_attributes(&block) def assign_attributes(&block)
if @user.user_synced_attributes_metadata if @user.user_synced_attributes_metadata
params.except!(*@user.user_synced_attributes_metadata.read_only_attributes) params.except!(*@user.user_synced_attributes_metadata.read_only_attributes)
......
- @no_container = true - @no_container = true
- page_title "Background Jobs" - page_title "Background Jobs"
= render 'admin/monitoring/head'
%div{ class: container_class } %div{ class: container_class }
%h3.page-title Background Jobs %h3.page-title Background Jobs
......
- breadcrumb_title "Cohorts" - breadcrumb_title "Cohorts"
- @no_container = true - @no_container = true
= render "admin/dashboard/head"
%div{ class: container_class } %div{ class: container_class }
- if @cohorts - if @cohorts
......
- @no_container = true - @no_container = true
- page_title 'ConvDev Index' - page_title 'ConvDev Index'
= render 'admin/monitoring/head'
.container .container
- if show_callout?('convdev_intro_callout_dismissed') - if show_callout?('convdev_intro_callout_dismissed')
= render 'callout' = render 'callout'
......
= content_for :sub_nav do
.scrolling-tabs-container.sub-nav-scroll
= render 'shared/nav_scroll'
.nav-links.sub-nav.scrolling-tabs
%ul{ class: (container_class) }
= nav_link(controller: :dashboard, html_options: {class: 'home'}) do
= link_to admin_root_path, title: 'Overview' do
%span
Overview
= nav_link(controller: [:admin, :projects]) do
= link_to admin_projects_path, title: 'Projects' do
%span
Projects
= nav_link(controller: :users) do
= link_to admin_users_path, title: 'Users' do
%span
Users
= nav_link(controller: :groups) do
= link_to admin_groups_path, title: 'Groups' do
%span
Groups
= nav_link path: 'builds#index' do
= link_to admin_jobs_path, title: 'Jobs' do
%span
Jobs
= nav_link path: ['runners#index', 'runners#show'] do
= link_to admin_runners_path, title: 'Runners' do
%span
Runners
= nav_link path: 'cohorts#index' do
= link_to admin_cohorts_path, title: 'Cohorts' do
%span
Cohorts
= nav_link(controller: :conversational_development_index) do
= link_to admin_conversational_development_index_path, title: 'ConvDev Index' do
%span
ConvDev Index
- @no_container = true - @no_container = true
- breadcrumb_title "Dashboard" - breadcrumb_title "Dashboard"
= render "admin/dashboard/head"
%div{ class: container_class } %div{ class: container_class }
.admin-dashboard.prepend-top-default .admin-dashboard.prepend-top-default
......
- @no_container = true - @no_container = true
- page_title "Groups" - page_title "Groups"
= render "admin/dashboard/head"
%div{ class: container_class } %div{ class: container_class }
.top-area .top-area
......
- @no_container = true - @no_container = true
- page_title _('Health Check') - page_title _('Health Check')
- no_errors = @errors.blank? && @failing_storage_statuses.blank? - no_errors = @errors.blank? && @failing_storage_statuses.blank?
= render 'admin/monitoring/head'
%div{ class: container_class } %div{ class: container_class }
%h3.page-title= page_title %h3.page-title= page_title
......
- breadcrumb_title "Jobs" - breadcrumb_title "Jobs"
- @no_container = true - @no_container = true
= render "admin/dashboard/head"
%div{ class: container_class } %div{ class: container_class }
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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.
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.
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