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 @@
karma.config.js
webpack.config.js
svg.config.js
/app/assets/javascripts/locale/**/*.js
/app/assets/javascripts/locale/**/app.js
......@@ -2,6 +2,13 @@
documentation](doc/development/changelog.md) for instructions on adding your own
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)
- [FIXED] Fix duplicate key errors in PostDeployMigrateUserExternalMailData migration.
......
......@@ -26,7 +26,7 @@ gem 'doorkeeper', '~> 4.2.0'
gem 'doorkeeper-openid_connect', '~> 1.1.0'
gem 'omniauth', '~> 1.4.2'
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-facebook', '~> 4.0.0'
gem 'omniauth-github', '~> 1.1.1'
......
......@@ -512,10 +512,10 @@ GEM
omniauth-oauth2 (~> 1.1)
omniauth-authentiq (0.3.1)
omniauth-oauth2 (~> 1.3, >= 1.3.1)
omniauth-azure-oauth2 (0.0.6)
omniauth-azure-oauth2 (0.0.9)
jwt (~> 1.0)
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.1)
omniauth-oauth2 (~> 1.4)
omniauth-cas3 (1.1.4)
addressable (~> 2.3)
nokogiri (~> 1.7, >= 1.7.1)
......@@ -541,7 +541,7 @@ GEM
omniauth-oauth (1.1.0)
oauth
omniauth (~> 1.0)
omniauth-oauth2 (1.3.1)
omniauth-oauth2 (1.4.0)
oauth2 (~> 1.0)
omniauth (~> 1.2)
omniauth-oauth2-generic (0.2.2)
......@@ -1077,7 +1077,7 @@ DEPENDENCIES
omniauth (~> 1.4.2)
omniauth-auth0 (~> 1.4.1)
omniauth-authentiq (~> 0.3.1)
omniauth-azure-oauth2 (~> 0.0.6)
omniauth-azure-oauth2 (~> 0.0.9)
omniauth-cas3 (~> 1.1.4)
omniauth-facebook (~> 4.0.0)
omniauth-github (~> 1.1.1)
......
File mode changed from 100755 to 100644
......@@ -77,9 +77,6 @@ $(() => {
});
Store.rootPath = this.boardsEndpoint;
this.filterManager = new FilteredSearchBoards(Store.filter, true);
this.filterManager.setup();
// Listen for updateTokens event
eventHub.$on('updateTokens', this.updateTokens);
},
......@@ -87,6 +84,9 @@ $(() => {
eventHub.$off('updateTokens', this.updateTokens);
},
mounted () {
this.filterManager = new FilteredSearchBoards(Store.filter, true);
this.filterManager.setup();
Store.disabled = this.disabled;
gl.boardService.all()
.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 @@
import Vue from 'vue';
import Cookies from 'js-cookie';
import Translate from '../vue_shared/translate';
import LimitWarningComponent from './components/limit_warning_component';
import './components/stage_code_component';
import './components/stage_issue_component';
import './components/stage_plan_component';
import './components/stage_production_component';
import './components/stage_review_component';
import './components/stage_staging_component';
import './components/stage_test_component';
import './components/total_time_component';
import './cycle_analytics_service';
import './cycle_analytics_store';
import limitWarningComponent from './components/limit_warning_component.vue';
import stageCodeComponent from './components/stage_code_component.vue';
import stagePlanComponent from './components/stage_plan_component.vue';
import stageComponent from './components/stage_component.vue';
import stageReviewComponent from './components/stage_review_component.vue';
import stageStagingComponent from './components/stage_staging_component.vue';
import stageTestComponent from './components/stage_test_component.vue';
import totalTime from './components/total_time_component.vue';
import CycleAnalyticsService from './cycle_analytics_service';
import CycleAnalyticsStore from './cycle_analytics_store';
Vue.use(Translate);
$(() => {
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({
el: '#cycle-analytics',
name: 'CycleAnalytics',
data: {
state: cycleAnalyticsStore.state,
isLoading: false,
isLoadingStage: false,
isEmptyStage: false,
hasError: false,
startDate: 30,
isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE),
data() {
const cycleAnalyticsEl = document.querySelector('#cycle-analytics');
const cycleAnalyticsService = new CycleAnalyticsService({
requestPath: cycleAnalyticsEl.dataset.requestPath,
});
return {
store: CycleAnalyticsStore,
state: CycleAnalyticsStore.state,
isLoading: false,
isLoadingStage: false,
isEmptyStage: false,
hasError: false,
startDate: 30,
isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE),
service: cycleAnalyticsService,
};
},
computed: {
currentStage() {
return cycleAnalyticsStore.currentActiveStage();
return this.store.currentActiveStage();
},
},
components: {
'stage-issue-component': gl.cycleAnalytics.StageIssueComponent,
'stage-plan-component': gl.cycleAnalytics.StagePlanComponent,
'stage-code-component': gl.cycleAnalytics.StageCodeComponent,
'stage-test-component': gl.cycleAnalytics.StageTestComponent,
'stage-review-component': gl.cycleAnalytics.StageReviewComponent,
'stage-staging-component': gl.cycleAnalytics.StageStagingComponent,
'stage-production-component': gl.cycleAnalytics.StageProductionComponent,
'stage-issue-component': stageComponent,
'stage-plan-component': stagePlanComponent,
'stage-code-component': stageCodeComponent,
'stage-test-component': stageTestComponent,
'stage-review-component': stageReviewComponent,
'stage-staging-component': stageStagingComponent,
'stage-production-component': stageComponent,
},
created() {
this.fetchCycleAnalyticsData();
},
methods: {
handleError() {
cycleAnalyticsStore.setErrorState(true);
this.store.setErrorState(true);
return new Flash('There was an error while fetching cycle analytics data.');
},
initDropdown() {
......@@ -77,17 +80,17 @@ $(() => {
this.isLoading = true;
cycleAnalyticsService
this.service
.fetchCycleAnalyticsData(fetchOptions)
.done((response) => {
cycleAnalyticsStore.setCycleAnalyticsData(response);
.then(resp => resp.json())
.then((response) => {
this.store.setCycleAnalyticsData(response);
this.selectDefaultStage();
this.initDropdown();
this.isLoading = false;
})
.error(() => {
.catch(() => {
this.handleError();
})
.always(() => {
this.isLoading = false;
});
},
......@@ -100,27 +103,27 @@ $(() => {
if (this.currentStage === stage) return;
if (!stage.isUserAllowed) {
cycleAnalyticsStore.setActiveStage(stage);
this.store.setActiveStage(stage);
return;
}
this.isLoadingStage = true;
cycleAnalyticsStore.setStageEvents([], stage);
cycleAnalyticsStore.setActiveStage(stage);
this.store.setStageEvents([], stage);
this.store.setActiveStage(stage);
cycleAnalyticsService
this.service
.fetchStageData({
stage,
startDate: this.startDate,
})
.done((response) => {
.then(resp => resp.json())
.then((response) => {
this.isEmptyStage = !response.events.length;
cycleAnalyticsStore.setStageEvents(response.events, stage);
this.store.setStageEvents(response.events, stage);
this.isLoadingStage = false;
})
.error(() => {
.catch(() => {
this.isEmptyStage = true;
})
.always(() => {
this.isLoadingStage = false;
});
},
......@@ -132,6 +135,6 @@ $(() => {
});
// Register global components
Vue.component('limit-warning', LimitWarningComponent);
Vue.component('total-time', gl.cycleAnalytics.TotalTimeComponent);
Vue.component('limit-warning', limitWarningComponent);
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 = {});
global.cycleAnalytics = global.cycleAnalytics || {};
Vue.use(VueResource);
class CycleAnalyticsService {
export default class CycleAnalyticsService {
constructor(options) {
this.requestPath = options.requestPath;
this.cycleAnalytics = Vue.resource(this.requestPath);
}
fetchCycleAnalyticsData(options) {
options = options || { startDate: 30 };
return $.ajax({
url: this.requestPath,
method: 'GET',
dataType: 'json',
contentType: 'application/json',
data: {
cycle_analytics: {
start_date: options.startDate,
},
},
});
fetchCycleAnalyticsData(options = { startDate: 30 }) {
return this.cycleAnalytics.get({ cycle_analytics: { start_date: options.startDate } });
}
fetchStageData(options) {
......@@ -30,12 +19,12 @@ class CycleAnalyticsService {
startDate,
} = options;
return $.get(`${this.requestPath}/events/${stage.name}.json`, {
cycle_analytics: {
start_date: startDate,
return Vue.http.get(`${this.requestPath}/events/${stage.name}.json`, {
params: {
cycle_analytics: {
start_date: startDate,
},
},
});
}
}
global.cycleAnalytics.CycleAnalyticsService = CycleAnalyticsService;
......@@ -4,9 +4,6 @@ import { __ } from '../locale';
import '../lib/utils/text_utility';
import DEFAULT_EVENT_OBJECTS from './default_event_objects';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
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.'),
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 = {
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: {
summary: '',
stats: '',
......
......@@ -7,6 +7,8 @@
* causes reflows, visit https://gist.github.com/paulirish/5d52fb081b3570c81e3a
*/
import Cookies from 'js-cookie';
const LINE_NUMBER_CLASS = 'diff-line-num';
const UNFOLDABLE_LINE_CLASS = 'js-unfold';
const NO_COMMENT_CLASS = 'no-comment-btn';
......@@ -27,9 +29,7 @@ export default {
this.userCanCreateNote = $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('can-create-note') === '';
}
if (typeof notes !== 'undefined' && !this.isParallelView) {
this.isParallelView = notes.isParallelView && notes.isParallelView();
}
this.isParallelView = Cookies.get('diff_view') === 'parallel';
if (this.userCanCreateNote) {
$diffFile.on('mouseover', LINE_COLUMN_CLASSES, e => this.showButton(this.isParallelView, e))
......
......@@ -34,7 +34,7 @@ export const canShowActiveSubItems = (el) => {
export const canShowSubItems = () => bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md' || bp.getBreakpointSize() === 'lg';
export const getHideSubItemsInterval = () => {
if (!currentOpenMenu) return 0;
if (!currentOpenMenu || !mousePos.length) return 0;
const currentMousePos = mousePos[mousePos.length - 1];
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);
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');
} 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');
if (insertPlaceholder && el.nextElementSibling && el.nextElementSibling.classList.contains('sticky-placeholder')) {
el.nextElementSibling.remove();
}
}
};
export default (el) => {
export default (el, insertPlaceholder = true) => {
if (!el) return;
const computedStyle = window.getComputedStyle(el);
......@@ -17,7 +37,7 @@ export default (el) => {
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,
});
};
......@@ -28,148 +28,149 @@
// </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 $fileHolder = $('.file-holder');
$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);
$.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]);
}
};
const LineHighlighter = function(options = {}) {
options.highlightLineClass = options.highlightLineClass || 'hll';
options.fileHolderSelector = options.fileHolderSelector || '.file-holder';
options.scrollFileHolder = options.scrollFileHolder || false;
options.hash = options.hash || location.hash;
// Set the URL hash string
LineHighlighter.prototype.setHash = function(firstLineNumber, lastLineNumber) {
var hash;
if (lastLineNumber) {
hash = "#L" + firstLineNumber + "-" + lastLineNumber;
this.options = options;
this._hash = options.hash;
this.highlightLineClass = options.highlightLineClass;
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.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 {
hash = "#L" + firstLineNumber;
$.scrollTo(lineSelector, scrollOptions);
}
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);
};
return LineHighlighter;
})();
}).call(window);
}
}
};
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.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) => {
return data;
}, {});
let lang = document.querySelector('html').getAttribute('lang') || 'en';
lang = lang.replace(/-/g, '_');
const langAttribute = document.querySelector('html').getAttribute('lang');
const lang = (langAttribute || 'en').replace(/-/g, '_');
const locale = new Jed(locales[lang]);
/**
......
......@@ -302,7 +302,10 @@ $(function () {
return $container.remove();
// 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
$body.on('click', '.js-toggle-diff-comments', function (e) {
var $this = $(this);
......
......@@ -352,7 +352,7 @@ import {
}
expandViewContainer() {
const $wrapper = $('.content-wrapper .container-fluid');
const $wrapper = $('.content-wrapper .container-fluid').not('.breadcrumbs');
if (this.fixedLayoutPref === null) {
this.fixedLayoutPref = $wrapper.hasClass('container-limited');
}
......
......@@ -73,7 +73,8 @@ import _ from 'underscore';
aspectRatio: 1,
modal: true,
scalable: false,
rotatable: false,
rotatable: true,
checkOrientation: true,
zoomable: true,
dragMode: 'move',
guides: false,
......
<script>
/* global LineHighlighter */
import Store from '../stores/repo_store';
export default {
data: () => Store,
mounted() {
this.highlightFile();
},
computed: {
html() {
return this.activeFile.html;
},
},
methods: {
highlightFile() {
$(this.$el).find('.file-content').syntaxHighlight();
},
},
mounted() {
this.highlightFile();
this.lineHighlighter = new LineHighlighter({
fileHolderSelector: '.blob-viewer-container',
scrollFileHolder: true,
});
},
watch: {
html() {
this.$nextTick(() => {
......
......@@ -178,8 +178,8 @@ const RepoHelper = {
setFile(data, file) {
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') {
newFile.tooLarge = true;
}
......
......@@ -43,6 +43,8 @@ import Cookies from 'js-cookie';
$allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right');
$('aside.right-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) {
return Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'));
......
......@@ -31,10 +31,12 @@
@import "framework/mobile";
@import "framework/modal";
@import "framework/nav";
@import "framework/new-nav";
@import "framework/pagination";
@import "framework/panels";
@import "framework/selects";
@import "framework/sidebar";
@import "framework/new-sidebar";
@import "framework/tables";
@import "framework/notes";
@import "framework/timeline";
......
......@@ -260,7 +260,7 @@
position: relative;
border: 1px solid $blue-300;
border-radius: $border-radius-default;
background-color: $blue-25;
background-color: $blue-50;
justify-content: center;
.dismiss-button {
......
......@@ -779,6 +779,14 @@
white-space: normal;
width: 100%;
&.dropdown-menu-user-link {
white-space: nowrap;
.dropdown-menu-user-username {
display: block;
}
}
// make sure the text color is not overriden
&.text-danger {
color: $brand-danger;
......
......@@ -6,7 +6,7 @@
// Header
header.navbar-gitlab-new {
background: linear-gradient(to right, $color-900, $color-800);
background-color: $color-900;
.navbar-collapse {
color: $color-200;
......@@ -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);
header.navbar-gitlab-new {
background: $theme-gray-100;
background-color: $theme-gray-100;
box-shadow: 0 2px 0 0 $border-color;
.logo-text svg {
......@@ -242,10 +242,10 @@ body {
&:hover {
background-color: $white-light;
box-shadow: inset 0 0 0 1px $blue-100;
box-shadow: inset 0 0 0 1px $blue-200;
.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 {
.search-icon {
color: $theme-gray-200;
}
.search-input {
color: $gl-text-color;
}
}
.location-badge {
......
......@@ -142,5 +142,41 @@
}
@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 {
display: flex;
......
......@@ -295,75 +295,6 @@ header.navbar-gitlab-new {
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 {
display: flex;
min-height: 48px;
......@@ -375,6 +306,8 @@ header.navbar-gitlab-new {
display: flex;
width: 100%;
position: relative;
padding-top: $gl-padding / 2;
padding-bottom: $gl-padding / 2;
align-items: center;
border-bottom: 1px solid $border-color;
}
......@@ -386,11 +319,6 @@ header.navbar-gitlab-new {
align-self: center;
color: $gl-text-color-secondary;
@media (max-width: $screen-xs-max) {
padding-left: 17px;
border-left: 1px solid $gl-text-color-quaternary;
}
.avatar-tile {
margin-right: 4px;
border: 1px solid $border-color;
......@@ -420,6 +348,7 @@ header.navbar-gitlab-new {
display: flex;
align-items: center;
position: relative;
padding: 2px 0;
&:not(:last-child) {
margin-right: 20px;
......@@ -455,7 +384,7 @@ header.navbar-gitlab-new {
margin: 0;
font-size: 12px;
font-weight: 600;
line-height: 1;
line-height: 16px;
a {
color: $gl-text-color;
......
......@@ -461,6 +461,13 @@ $new-sidebar-collapsed-width: 50px;
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) {
......
......@@ -137,7 +137,7 @@ $well-border: #eee;
//##
$code-color: $red-600;
$code-bg: lighten($red-50, 2%);
$code-bg: lighten($red-100, 2%);
$kbd-color: $white-light;
$kbd-bg: #333;
......
......@@ -6,6 +6,8 @@ $gutter_width: 290px;
$gutter_inner_width: 250px;
$sidebar-transition-duration: .15s;
$sidebar-breakpoint: 1024px;
$default-transition-duration: .15s;
$right-sidebar-transition-duration: .3s;
/*
* Color schema
......@@ -27,46 +29,45 @@ $gray-dark: darken($gray-light, $darken-dark-factor);
$gray-darker: #eee;
$gray-darkest: #c4c4c4;
$green-25: #f6fcf8;
$green-50: #e4f5eb;
$green-100: #bae6cc;
$green-200: #8dd5aa;
$green-300: #5fc488;
$green-400: #3cb76f;
$green-50: #f1fdf6;
$green-100: #dcf5e7;
$green-200: #b3e6c8;
$green-300: #75d09b;
$green-400: #37b96d;
$green-500: #1aaa55;
$green-600: #168f48;
$green-700: #12753a;
$green-800: #0e5a2d;
$green-900: #0a4020;
$green-950: #072b15;
$blue-25: #f6fafd;
$blue-50: #e4eff9;
$blue-100: #bcd7f1;
$blue-200: #8fbce8;
$blue-300: #62a1df;
$blue-400: #418cd8;
$blue-50: #f6fafe;
$blue-100: #e4f0fb;
$blue-200: #b8d6f4;
$blue-300: #73afea;
$blue-400: #2e87e0;
$blue-500: #1f78d1;
$blue-600: #1b69b6;
$blue-700: #17599c;
$blue-800: #134a81;
$blue-900: #0f3b66;
$blue-950: #0a2744;
$orange-25: #fffcf8;
$orange-50: #fff2e1;
$orange-100: #fedfb3;
$orange-200: #feca81;
$orange-300: #fdb44f;
$orange-400: #fca429;
$orange-50: #fffaf4;
$orange-100: #fff1de;
$orange-200: #fed69f;
$orange-300: #fdbc60;
$orange-400: #fca121;
$orange-500: #fc9403;
$orange-600: #de7e00;
$orange-700: #c26700;
$orange-800: #a35100;
$orange-900: #853b00;
$orange-800: #a35200;
$orange-900: #853c00;
$orange-950: #592800;
$red-25: #fef7f6;
$red-50: #fbe7e4;
$red-100: #f4c4bc;
$red-200: #ed9d90;
$red-50: #fef6f5;
$red-100: #fbe5e1;
$red-200: #f2b4a9;
$red-300: #e67664;
$red-400: #e05842;
$red-500: #db3b21;
......@@ -74,6 +75,7 @@ $red-600: #c0341d;
$red-700: #a62d19;
$red-800: #8b2615;
$red-900: #711e11;
$red-950: #4b140b;
// GitLab themes
......@@ -184,8 +186,8 @@ $list-text-disabled-color: $gl-text-color-tertiary;
$list-border-light: #eee;
$list-border: rgba(0, 0, 0, 0.05);
$list-text-height: 42px;
$list-warning-row-bg: $orange-50;
$list-warning-row-border: $orange-100;
$list-warning-row-bg: $orange-100;
$list-warning-row-border: $orange-200;
$list-warning-row-color: $orange-700;
/*
......@@ -214,8 +216,8 @@ $gl-sidebar-padding: 22px;
/*
* Misc
*/
$row-hover: $blue-25;
$row-hover-border: $blue-100;
$row-hover: $blue-50;
$row-hover-border: $blue-200;
$progress-color: #c0392b;
$header-height: 50px;
$new-navbar-height: 40px;
......@@ -265,8 +267,8 @@ $time-color: #999;
$project-member-show-color: #aaa;
$gl-promo-color: #aaa;
$error-bg: $red-400;
$warning-message-bg: $orange-50;
$warning-message-border: $orange-100;
$warning-message-bg: $orange-100;
$warning-message-border: $orange-200;
$warning-message-color: $orange-700;
$control-group-descr-color: #666;
$table-permission-x-bg: #d9edf7;
......@@ -451,17 +453,17 @@ $builds-trace-bg: #111;
/*
* Callout
*/
$callout-danger-bg: $red-50;
$callout-danger-border: $red-100;
$callout-danger-bg: $red-100;
$callout-danger-border: $red-200;
$callout-danger-color: $red-700;
$callout-warning-bg: $orange-50;
$callout-warning-border: $orange-100;
$callout-warning-bg: $orange-100;
$callout-warning-border: $orange-200;
$callout-warning-color: $orange-700;
$callout-info-bg: $blue-50;
$callout-info-border: $blue-100;
$callout-info-bg: $blue-100;
$callout-info-border: $blue-200;
$callout-info-color: $blue-700;
$callout-success-bg: $green-50;
$callout-success-border: $green-100;
$callout-success-bg: $green-100;
$callout-success-border: $green-200;
$callout-success-color: $green-700;
/*
......
......@@ -55,6 +55,15 @@
.boards-app {
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 {
......@@ -78,11 +87,6 @@
height: calc(100vh - 222px);
// scss-lint:enable DuplicateProperty
min-height: 475px;
transition: width .2s;
&.is-compact {
width: calc(100% - 290px);
}
}
}
......@@ -412,14 +416,6 @@
.page-with-layout-nav.page-with-sub-nav .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 {
position: relative;
}
......@@ -457,8 +453,8 @@
.right-sidebar.right-sidebar-expanded {
&.boards-sidebar-slide-enter-active,
&.boards-sidebar-slide-leave-active {
transition: width .2s,
padding .2s;
transition: width $right-sidebar-transition-duration,
padding $right-sidebar-transition-duration;
}
&.boards-sidebar-slide-enter,
......
......@@ -83,7 +83,7 @@ $space-between-cards: 8px;
border-top-color: $color-low-score;
.card-score-big {
background-color: $red-25;
background-color: $red-50;
}
}
......@@ -91,7 +91,7 @@ $space-between-cards: 8px;
border-top-color: $color-average-score;
.card-score-big {
background-color: $orange-25;
background-color: $orange-50;
}
}
......@@ -99,7 +99,7 @@ $space-between-cards: 8px;
border-top-color: $color-high-score;
.card-score-big {
background-color: $green-25;
background-color: $green-50;
}
}
......
......@@ -451,7 +451,7 @@
}
.files {
margin-top: -1px;
margin-top: 1px;
.diff-file:last-child {
margin-bottom: 0;
......@@ -535,7 +535,6 @@
}
.diff-notes-collapse {
position: relative;
width: 19px;
height: 19px;
padding: 0;
......@@ -543,11 +542,7 @@
z-index: 100;
svg {
position: absolute;
left: 50%;
top: 50%;
margin-left: -5.5px;
margin-top: -5.5px;
vertical-align: text-top;
}
path {
......@@ -586,11 +581,6 @@
top: 76px;
}
+ .files,
+ .alert {
margin-top: 1px;
}
&:not(.is-stuck) .diff-stats-additions-deletions-collapsed {
display: none;
}
......@@ -605,11 +595,6 @@
.inline-parallel-buttons {
display: none;
}
+ .files,
+ .alert {
margin-top: 32px;
}
}
}
}
......
......@@ -7,7 +7,7 @@
.is-confidential {
color: $orange-600;
background-color: $orange-50;
background-color: $orange-100;
border-radius: $border-radius-default;
padding: 5px;
margin: 0 3px 0 -4px;
......@@ -223,14 +223,14 @@
top: $new-navbar-height;
bottom: 0;
right: 0;
transition: width .3s;
transition: width $right-sidebar-transition-duration;
background: $gray-light;
z-index: 200;
overflow: hidden;
.issuable-sidebar {
width: calc(100% + 100px);
height: calc(100% - #{$new-navbar-height});
height: 100%;
overflow-y: scroll;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
......
......@@ -255,7 +255,7 @@ $colors: (
&.saved {
.editor {
border-top: solid 2px $green-200;
border-top: solid 2px $green-300;
}
}
......
......@@ -103,7 +103,7 @@
.confidential-issue-warning {
color: $orange-600;
background-color: $orange-50;
background-color: $orange-100;
border-radius: $border-radius-default $border-radius-default 0 0;
border: 1px solid $border-gray-normal;
border-bottom: none;
......
......@@ -644,20 +644,20 @@ button.mini-pipeline-graph-dropdown-toggle {
// Dropdown button animation in mini pipeline graph
&.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 {
@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-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 {
@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,
......
......@@ -291,7 +291,7 @@ table.u2f-registrations {
.bordered-box {
border: 1px solid $blue-300;
border-radius: $border-radius-default;
background-color: $blue-25;
background-color: $blue-50;
position: relative;
display: flex;
justify-content: center;
......@@ -379,7 +379,7 @@ table.u2f-registrations {
.nav-wip {
border: 1px solid $blue-500;
background: $blue-25;
background: $blue-50;
padding: $gl-padding;
margin-bottom: $gl-padding;
......
......@@ -54,6 +54,10 @@
border-radius: $border-radius-default;
color: $almost-black;
.code.white pre .hll {
background-color: $well-light-border !important;
}
.tree-content-holder {
display: flex;
min-height: 300px;
......
......@@ -28,9 +28,7 @@ input[type="checkbox"]:hover {
}
.search {
margin-right: 10px;
margin-left: 10px;
margin-top: ($header-height - 35) / 2;
margin: 4px 8px 0;
form {
@extend .form-control;
......@@ -38,15 +36,23 @@ input[type="checkbox"]:hover {
padding: 4px;
width: $search-input-width;
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 {
border-color: lighten($dropdown-input-focus-border, 20%);
box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%);
box-shadow: none;
}
}
.location-text {
font-style: normal;
.location-badge {
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 {
......@@ -56,41 +62,26 @@ input[type="checkbox"]:hover {
margin-left: 5px;
line-height: 25px;
width: 98%;
color: $white-light;
background: none;
transition: color ease-in-out $default-transition-duration;
}
.location-badge {
line-height: 25px;
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::placeholder {
transition: color ease-in-out $default-transition-duration;
}
.search-input-container {
display: -webkit-flex;
display: flex;
position: relative;
}
.search-input-wrap {
// Fallback if flexbox is not supported
display: inline-block;
}
.search-input-wrap {
width: 100%;
.search-icon,
.clear-icon {
position: absolute;
right: 5px;
top: 0;
color: $location-icon-color;
&::before {
font-family: FontAwesome;
......@@ -101,7 +92,7 @@ input[type="checkbox"]:hover {
.search-icon {
@extend .fa-search;
transition: color 0.15s;
transition: color $default-transition-duration;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
......@@ -148,21 +139,32 @@ input[type="checkbox"]:hover {
form {
@extend .form-control:focus;
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 {
transition: all 0.15s;
background-color: $location-badge-active-bg;
color: $white-light;
}
.search-input {
color: $gl-text-color;
transition: color ease-in-out $default-transition-duration;
}
.search-input-wrap {
i {
color: $layout-link-gray;
.search-input::placeholder {
color: $gl-text-color-tertiary;
}
}
.location-badge {
transition: all $default-transition-duration;
background-color: $nav-badge-bg;
border-color: $border-color;
}
.dropdown-menu {
transition-duration: 100ms, 75ms;
transition-delay: 75ms, 100ms;
......
......@@ -18,7 +18,7 @@
}
&.ci-failed {
@include status-color($red-50, $red-500, $red-600);
@include status-color($red-100, $red-500, $red-600);
}
&.ci-success {
......@@ -39,12 +39,12 @@
&.ci-pending,
&.ci-failed_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-running {
@include status-color($blue-50, $blue-500, $blue-600);
@include status-color($blue-100, $blue-500, $blue-600);
}
&.ci-created,
......
......@@ -15,3 +15,9 @@
-ms-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
@application = Doorkeeper::Application.new(application_params)
if @application.save
flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
redirect_to admin_application_url(@application)
redirect_to_admin_page
else
render :new
end
......@@ -42,6 +41,13 @@ class Admin::ApplicationsController < Admin::ApplicationController
redirect_to admin_applications_url, status: 302, notice: 'Application was successfully destroyed.'
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
def set_application
......
......@@ -128,7 +128,7 @@ class Admin::UsersController < Admin::ApplicationController
end
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!
end
......@@ -155,7 +155,7 @@ class Admin::UsersController < Admin::ApplicationController
def remove_email
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|
if success
......@@ -219,7 +219,7 @@ class Admin::UsersController < Admin::ApplicationController
end
def update_user(&block)
result = Users::UpdateService.new(user).execute(&block)
result = Users::UpdateService.new(current_user, user: user).execute(&block)
result[:status] == :success
end
......
......@@ -106,7 +106,7 @@ module IssuableCollections
# @filter_params[:authorized_only] = true
end
@filter_params
@filter_params.permit(IssuableFinder::VALID_PARAMS)
end
def set_default_state
......@@ -117,19 +117,32 @@ module IssuableCollections
key = 'issuable_sort'
cookies[key] = params[:sort] if params[:sort].present?
# 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'
cookies[key] = update_cookie_value(cookies[key])
params[:sort] = cookies[key]
end
def default_sort_order
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
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
......@@ -12,10 +12,14 @@ class ConfirmationsController < Devise::ConfirmationsController
def after_confirmation_path_for(resource_name, resource)
if signed_in?(resource_name)
after_sign_in_path_for(resource)
after_sign_in(resource)
else
flash[:notice] += " Please sign in."
new_session_path(resource_name)
end
end
def after_sign_in(resource)
after_sign_in_path_for(resource)
end
end
......@@ -21,14 +21,20 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
@application.owner = current_user
if @application.save
flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
redirect_to oauth_application_url(@application)
redirect_to_oauth_application_page
else
set_index_vars
render :index
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
def verify_user_oauth_applications_enabled
......
......@@ -2,7 +2,7 @@ class Profiles::AvatarsController < Profiles::ApplicationController
def destroy
@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
end
......
......@@ -5,7 +5,7 @@ class Profiles::EmailsController < Profiles::ApplicationController
end
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?
NotificationService.new.new_email(@email)
......@@ -19,7 +19,7 @@ class Profiles::EmailsController < Profiles::ApplicationController
def destroy
@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|
format.html { redirect_to profile_emails_url, status: 302 }
......
......@@ -14,7 +14,7 @@ class Profiles::KeysController < Profiles::ApplicationController
@key = Keys::CreateService.new(current_user, key_params).execute
if @key.persisted?
redirect_to profile_key_path(@key)
redirect_to_profile_key_path
else
@keys = current_user.keys.select(&:persisted?)
render :index
......@@ -50,6 +50,12 @@ class Profiles::KeysController < Profiles::ApplicationController
end
end
protected
def redirect_to_profile_key_path
redirect_to profile_key_path(@key)
end
private
def key_params
......
......@@ -7,7 +7,7 @@ class Profiles::NotificationsController < Profiles::ApplicationController
end
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
flash[:notice] = "Notification settings saved"
......
......@@ -21,10 +21,10 @@ class Profiles::PasswordsController < Profiles::ApplicationController
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
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'
else
......@@ -46,7 +46,7 @@ class Profiles::PasswordsController < Profiles::ApplicationController
return
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
flash[:notice] = "Password was successfully updated. Please login with it"
......
......@@ -6,7 +6,7 @@ class Profiles::PreferencesController < Profiles::ApplicationController
def update
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
flash[:notice] = 'Preferences saved.'
......
......@@ -10,7 +10,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
current_user.otp_grace_period_started_at = Time.current
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?
two_factor_authentication_reason(
......@@ -41,7 +41,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
def create
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!
end
......@@ -70,7 +70,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
end
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!
end
end
......
......@@ -10,7 +10,7 @@ class ProfilesController < Profiles::ApplicationController
def update
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
message = "Profile was successfully updated"
......@@ -25,7 +25,7 @@ class ProfilesController < Profiles::ApplicationController
end
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!
end
......@@ -35,7 +35,7 @@ class ProfilesController < Profiles::ApplicationController
end
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!
end
......@@ -45,7 +45,7 @@ class ProfilesController < Profiles::ApplicationController
end
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!
end
......@@ -61,7 +61,7 @@ class ProfilesController < Profiles::ApplicationController
end
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
{ notice: "Username successfully changed" }
......
......@@ -55,7 +55,7 @@ class SessionsController < Devise::SessionsController
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
end
......
......@@ -25,6 +25,28 @@ class IssuableFinder
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
def initialize(current_user, params = {})
......
......@@ -13,22 +13,29 @@ module AvatarsHelper
user_name = options[:user].try(:name) || options[:user_name]
avatar_url = options[:url] || avatar_icon(options[:user] || options[:user_email], avatar_size)
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])
if has_tooltip
css_class.push('has-tooltip')
data_attributes = { container: 'body' }
data_attributes[:container] = 'body'
end
image_tag(
avatar_url,
if options[:lazy]
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,
alt: "#{user_name}'s avatar",
title: user_name,
data: data_attributes,
lazy: true
)
title: user_name
}
tag(:img, image_options)
end
def user_avatar(options = {})
......
......@@ -79,6 +79,6 @@ module BoardsHelper
end
def boards_link_text
_("Board")
s_("IssueBoards|Board")
end
end
......@@ -239,8 +239,8 @@ module ProjectsHelper
end
end
def has_projects_or_name?(projects, params)
!!(params[:name] || any_projects?(projects))
def show_projects?(projects, params)
!!(params[:personal] || params[:name] || any_projects?(projects))
end
private
......
This diff is collapsed.
......@@ -434,7 +434,7 @@ module Ci
def update_duration
return unless started_at
self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
self.duration = Gitlab::Ci::Pipeline::Duration.from_pipeline(self)
end
def execute_hooks
......
......@@ -174,7 +174,7 @@ module Ci
end
def assignable_for?(project)
!locked? || projects.exists?(id: project.id)
is_shared? || projects.exists?(id: project.id)
end
def accepting_tags?(build)
......
......@@ -25,8 +25,8 @@ class Commit
DIFF_HARD_LIMIT_FILES = 1000
DIFF_HARD_LIMIT_LINES = 50000
# The SHA can be between 7 and 40 hex characters.
COMMIT_SHA_PATTERN = '\h{7,40}'.freeze
MIN_SHA_LENGTH = 7
COMMIT_SHA_PATTERN = /\h{#{MIN_SHA_LENGTH},40}/.freeze
def banzai_render_context(field)
context = { pipeline: :single_line, project: self.project }
......@@ -53,7 +53,7 @@ class Commit
# Truncate sha to 8 characters
def truncate_sha(sha)
sha[0..7]
sha[0..MIN_SHA_LENGTH]
end
def max_diff_options
......@@ -100,7 +100,7 @@ class Commit
def self.reference_pattern
@reference_pattern ||= %r{
(?:#{Project.reference_pattern}#{reference_prefix})?
(?<commit>\h{7,40})
(?<commit>#{COMMIT_SHA_PATTERN})
}x
end
......@@ -216,9 +216,8 @@ class Commit
@raw.respond_to?(method, include_private) || super
end
# Truncate sha to 8 characters
def short_id
@raw.short_id(7)
@raw.short_id(MIN_SHA_LENGTH)
end
def diff_refs
......
......@@ -143,16 +143,18 @@ module Issuable
end
def sort(method, excluded_labels: [])
sorted = case method.to_s
when 'milestone_due_asc' then order_milestone_due_asc
when 'milestone_due_desc' then order_milestone_due_desc
when 'downvotes_desc' then order_downvotes_desc
when 'upvotes_desc' then order_upvotes_desc
when 'label_priority' then order_labels_priority(excluded_labels: excluded_labels)
when 'priority' then order_due_date_and_labels_priority(excluded_labels: excluded_labels)
else
order_by(method)
end
sorted =
case method.to_s
when 'downvotes_desc' then order_downvotes_desc
when 'label_priority' then order_labels_priority(excluded_labels: excluded_labels)
when 'milestone' then order_milestone_due_asc
when 'milestone_due_asc' then order_milestone_due_asc
when 'milestone_due_desc' then order_milestone_due_desc
when 'popularity' then order_upvotes_desc
when 'priority' then order_due_date_and_labels_priority(excluded_labels: excluded_labels)
when 'upvotes_desc' then order_upvotes_desc
else order_by(method)
end
# Break ties with the ID column for pagination
sorted.order(id: :desc)
......@@ -214,7 +216,7 @@ module Issuable
def grouping_columns(sort)
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
grouping_columns << milestone_table[:id]
grouping_columns << milestone_table[:due_date]
......
......@@ -19,14 +19,15 @@ module Sortable
module ClassMethods
def order_by(method)
case method.to_s
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
when 'created_asc' then order_created_asc
when 'created_asc' then order_created_asc
when 'created_date' 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
all
end
......
......@@ -116,7 +116,8 @@ class Issue < ActiveRecord::Base
def self.sort(method, excluded_labels: [])
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
else
super
......
......@@ -489,13 +489,7 @@ class Repository
def exists?
return false unless full_path
Gitlab::GitalyClient.migrate(:repository_exists) do |enabled|
if enabled
raw_repository.exists?
else
refs_directory_exists?
end
end
raw_repository.exists?
end
cache_method :exists?
......@@ -534,8 +528,11 @@ class Repository
cache_method :tag_count, fallback: 0
def avatar
if tree = file_on_head(:avatar)
tree.path
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38327
Gitlab::GitalyClient.allow_n_plus_1_calls do
if tree = file_on_head(:avatar)
tree.path
end
end
end
cache_method :avatar
......@@ -1060,12 +1057,6 @@ class Repository
blob.data
end
def refs_directory_exists?
circuit_breaker.perform do
File.exist?(File.join(path_to_repo, 'refs'))
end
end
def cache
# TODO: should we use UUIDs here? We could move repositories without clearing this cache
@cache ||= RepositoryCache.new(full_path, @project.id)
......@@ -1117,10 +1108,6 @@ class Repository
Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', Gitlab::GlRepository.gl_repository(project, false))
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)
ref ||= root_ref
......
......@@ -60,7 +60,7 @@ class User < ActiveRecord::Base
lease = Gitlab::ExclusiveLease.new("user_update_tracked_fields:#{id}", timeout: 1.hour.to_i)
return unless lease.try_obtain
Users::UpdateService.new(self).execute(validate: false)
Users::UpdateService.new(self, user: self).execute(validate: false)
end
attr_accessor :force_random_password
......@@ -526,8 +526,8 @@ class User < ActiveRecord::Base
def update_emails_with_primary_email
primary_email_record = emails.find_by(email: email)
if primary_email_record
Emails::DestroyService.new(self, email: email).execute
Emails::CreateService.new(self, email: email_was).execute
Emails::DestroyService.new(self, user: self, email: email).execute
Emails::CreateService.new(self, user: self, email: email_was).execute
end
end
......@@ -1000,7 +1000,7 @@ class User < ActiveRecord::Base
if attempts_exceeded?
lock_access! unless access_locked?
else
Users::UpdateService.new(self).execute(validate: false)
Users::UpdateService.new(self, user: self).execute(validate: false)
end
end
......@@ -1186,7 +1186,7 @@ class User < ActiveRecord::Base
&creation_block
)
Users::UpdateService.new(user).execute(validate: false)
Users::UpdateService.new(user, user: user).execute(validate: false)
user
ensure
Gitlab::ExclusiveLease.cancel(lease_key, uuid)
......
......@@ -2,110 +2,55 @@ module Ci
class CreatePipelineService < BaseService
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(
source: source,
project: project,
ref: ref,
sha: sha,
before_sha: before_sha,
tag: tag?,
tag: tag_exists?,
trigger_requests: Array(trigger_request),
user: current_user,
pipeline_schedule: schedule,
protected: project.protected_for?(ref)
)
result = validate_project_and_git_items ||
validate_pipeline(ignore_skip_ci: ignore_skip_ci,
save_on_errors: save_on_errors)
command = OpenStruct.new(ignore_skip_ci: ignore_skip_ci,
save_incompleted: 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
Ci::Pipeline.transaction do
pipeline.save!
sequence.build! do |pipeline, sequence|
update_merge_requests_head_pipeline if pipeline.persisted?
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
.new(project, current_user)
.execute(pipeline)
pipeline.process!
end
rescue ActiveRecord::RecordInvalid => e
return error("Failed to persist the pipeline: #{e}")
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
private
def validate_project_and_git_items
unless project.builds_enabled?
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
def commit
@commit ||= project.commit(origin_sha || origin_ref)
end
def allowed_to_create?
return unless can?(current_user, :create_pipeline, project)
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
def sha
commit.try(:id)
end
def update_merge_requests_head_pipeline
......@@ -115,11 +60,6 @@ module Ci
.update_all(head_pipeline_id: @pipeline.id)
end
def skip_ci?
return false unless pipeline.git_commit_message
pipeline.git_commit_message =~ /\[(ci[ _-]skip|skip[ _-]ci)\]/i
end
def cancel_pending_pipelines
Gitlab::OptimisticLocking.retry_lock(auto_cancelable_pipelines) do |cancelables|
cancelables.find_each do |cancelable|
......@@ -136,14 +76,6 @@ module Ci
.created_or_pending
end
def commit
@commit ||= project.commit(origin_sha || origin_ref)
end
def sha
commit.try(:id)
end
def before_sha
params[:checkout_sha] || params[:before] || Gitlab::Git::BLANK_SHA
end
......@@ -156,41 +88,17 @@ module Ci
params[:ref]
end
def branch?
return @is_branch if defined?(@is_branch)
@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)
def tag_exists?
project.repository.tag_exists?(ref)
end
def ref
@ref ||= Gitlab::Git.ref_name(origin_ref)
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
@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
module Emails
class BaseService
def initialize(user, opts)
@user = user
def initialize(current_user, opts)
@current_user = current_user
@user = opts.delete(:user)
@email = opts[:email]
end
end
......
module Emails
class DestroyService < ::Emails::BaseService
def execute
Email.find_by_email!(@email).destroy && update_secondary_emails!
update_secondary_emails! if Email.find_by_email!(@email).destroy
end
private
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!
end
......
......@@ -14,13 +14,13 @@ module MergeRequests
@merge_request = merge_request
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
@source = find_merge_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
merge_request.in_locked_state do
......@@ -31,8 +31,7 @@ module MergeRequests
end
end
rescue MergeError => e
clean_merge_jid
log_merge_error(e.message, save_message_on_model: true)
handle_merge_error(log_message: e.message, save_message_on_model: true)
end
private
......@@ -74,10 +73,16 @@ module MergeRequests
@merge_request.force_remove_source_branch? ? @merge_request.author : current_user
end
def log_merge_error(message, save_message_on_model: false)
Rails.logger.error("MergeService ERROR: #{merge_request_info} - #{message}")
# Logs merge error message and cleans `MergeRequest#merge_jid`.
#
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
def merge_request_info
......
......@@ -2,22 +2,21 @@ module Users
class UpdateService < BaseService
include NewUserNotifier
def initialize(user, params = {})
@user = user
def initialize(current_user, params = {})
@current_user = current_user
@user = params.delete(:user)
@params = params.dup
end
def execute(validate: true, &block)
yield(@user) if block_given?
assign_attributes(&block)
user_exists = @user.persisted?
if @user.save(validate: validate)
notify_new_user(@user, nil) unless user_exists
assign_attributes(&block)
success
if @user.save(validate: validate)
notify_success(user_exists)
else
error(@user.errors.full_messages.uniq.join('. '))
end
......@@ -33,6 +32,12 @@ module Users
private
def notify_success(user_exists)
notify_new_user(@user, nil) unless user_exists
success
end
def assign_attributes(&block)
if @user.user_synced_attributes_metadata
params.except!(*@user.user_synced_attributes_metadata.read_only_attributes)
......
- @no_container = true
- page_title "Background Jobs"
= render 'admin/monitoring/head'
%div{ class: container_class }
%h3.page-title Background Jobs
......
- breadcrumb_title "Cohorts"
- @no_container = true
= render "admin/dashboard/head"
%div{ class: container_class }
- if @cohorts
......
- @no_container = true
- page_title 'ConvDev Index'
= render 'admin/monitoring/head'
.container
- if show_callout?('convdev_intro_callout_dismissed')
= 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
- breadcrumb_title "Dashboard"
= render "admin/dashboard/head"
%div{ class: container_class }
.admin-dashboard.prepend-top-default
......
- @no_container = true
- page_title "Groups"
= render "admin/dashboard/head"
%div{ class: container_class }
.top-area
......
- @no_container = true
- page_title _('Health Check')
- no_errors = @errors.blank? && @failing_storage_statuses.blank?
= render 'admin/monitoring/head'
%div{ class: container_class }
%h3.page-title= page_title
......
- breadcrumb_title "Jobs"
- @no_container = true
= render "admin/dashboard/head"
%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