Commit dfef847d authored by Valery Sizov's avatar Valery Sizov

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into ce_upstream

parents 93044dec b187a3f2
<<<<<<< HEAD
5.9.1 5.9.1
=======
5.9.2
>>>>>>> b187a3f2bb00a0a0a6e5f8369edf2d6430a7af6e
export default {
props: {
count: {
type: Number,
required: true,
},
},
template: `
<span v-if="count === 50" class="events-info pull-right">
<i class="fa fa-warning has-tooltip"
aria-hidden="true"
:title="n__('Limited to showing %d event at most', 'Limited to showing %d events at most', 50)"
data-placement="top"></i>
{{ n__('Showing %d event', 'Showing %d events', 50) }}
</span>
`,
};
<script>
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
count: {
type: Number,
required: true,
},
},
directives: {
tooltip,
},
};
</script>
<template>
<span v-if="count === 50" class="events-info pull-right">
<i
class="fa fa-warning"
v-tooltip
aria-hidden="true"
:title="n__('Limited to showing %d event at most', 'Limited to showing %d events at most', 50)"
data-placement="top"></i>
{{ n__('Showing %d event', 'Showing %d events', 50) }}
</span>
</template>
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageCodeComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item">
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="mergeRequest.author.avatarUrl"/>
<h5 class="item-title merge-merquest-title">
<a :href="mergeRequest.url">
{{ mergeRequest.title }}
</a>
</h5>
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
&middot;
<span>
{{ s__('OpenedNDaysAgo|Opened') }}
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
</span>
<span>
{{ s__('ByAuthor|by') }}
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
</span>
</div>
<div class="item-time">
<total-time :time="mergeRequest.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
<script>
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
export default {
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
};
</script>
<template>
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item">
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="mergeRequest.author.avatarUrl"/>
<h5 class="item-title merge-merquest-title">
<a :href="mergeRequest.url">
{{ mergeRequest.title }}
</a>
</h5>
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
&middot;
<span>
{{ s__('OpenedNDaysAgo|Opened') }}
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
</span>
<span>
{{ s__('ByAuthor|by') }}
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
</span>
</div>
<div class="item-time">
<total-time :time="mergeRequest.totalTime"></total-time>
</div>
</li>
</ul>
</div>
</template>
<script>
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
export default {
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
};
</script>
<template>
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li
v-for="(issue, i) in items"
:key="i"
class="stage-event-item">
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="issue.author.avatarUrl"/>
<h5 class="item-title issue-title">
<a class="issue-title" :href="issue.url">
{{ issue.title }}
</a>
</h5>
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
&middot;
<span>
{{ s__('OpenedNDaysAgo|Opened') }}
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
</span>
<span>
{{ s__('ByAuthor|by') }}
<a :href="issue.author.webUrl" class="issue-author-link">
{{ issue.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="issue.totalTime"/>
</div>
</li>
</ul>
</div>
</template>
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageIssueComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item">
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="issue.author.avatarUrl"/>
<h5 class="item-title issue-title">
<a class="issue-title" :href="issue.url">
{{ issue.title }}
</a>
</h5>
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
&middot;
<span>
{{ s__('OpenedNDaysAgo|Opened') }}
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
</span>
<span>
{{ s__('ByAuthor|by') }}
<a :href="issue.author.webUrl" class="issue-author-link">
{{ issue.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="issue.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import iconCommit from '../svg/icon_commit.svg';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StagePlanComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
data() {
return { iconCommit };
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="commit in items" class="stage-event-item">
<div class="item-details item-conmmit-component">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="commit.author.avatarUrl"/>
<h5 class="item-title commit-title">
<a :href="commit.commitUrl">
{{ commit.title }}
</a>
</h5>
<span>
{{ s__('FirstPushedBy|First') }}
<span class="commit-icon">${iconCommit}</span>
<a :href="commit.commitUrl" class="commit-hash-link commit-sha">{{ commit.shortSha }}</a>
{{ s__('FirstPushedBy|pushed by') }}
<a :href="commit.author.webUrl" class="commit-author-link">
{{ commit.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="commit.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
<script>
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import iconCommit from '../svg/icon_commit.svg';
export default {
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
computed: {
iconCommit() {
return iconCommit;
},
},
};
</script>
<template>
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li
v-for="(commit, i) in items"
:key="i"
class="stage-event-item">
<div class="item-details item-conmmit-component">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="commit.author.avatarUrl"/>
<h5 class="item-title commit-title">
<a :href="commit.commitUrl">
{{ commit.title }}
</a>
</h5>
<span>
{{ s__('FirstPushedBy|First') }}
<span class="commit-icon" v-html="iconCommit"></span>
<a :href="commit.commitUrl" class="commit-hash-link commit-sha">{{ commit.shortSha }}</a>
{{ s__('FirstPushedBy|pushed by') }}
<a :href="commit.author.webUrl" class="commit-author-link">
{{ commit.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="commit.totalTime" />
</div>
</li>
</ul>
</div>
</template>
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageProductionComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item">
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="issue.author.avatarUrl"/>
<h5 class="item-title issue-title">
<a class="issue-title" :href="issue.url">
{{ issue.title }}
</a>
</h5>
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
&middot;
<span>
{{ s__('OpenedNDaysAgo|Opened') }}
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
</span>
<span>
{{ s__('ByAuthor|by') }}
<a :href="issue.author.webUrl" class="issue-author-link">
{{ issue.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="issue.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageReviewComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item">
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="mergeRequest.author.avatarUrl"/>
<h5 class="item-title merge-merquest-title">
<a :href="mergeRequest.url">
{{ mergeRequest.title }}
</a>
</h5>
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
&middot;
<span>
{{ s__('OpenedNDaysAgo|Opened') }}
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
</span>
<span>
{{ s__('ByAuthor|by') }}
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
</span>
<template v-if="mergeRequest.state === 'closed'">
<span class="merge-request-state">
<i class="fa fa-ban"></i>
{{ mergeRequest.state.toUpperCase() }}
</span>
</template>
<template v-else>
<span class="merge-request-branch" v-if="mergeRequest.branch">
<i class= "fa fa-code-fork"></i>
<a :href="mergeRequest.branch.url">{{ mergeRequest.branch.name }}</a>
</span>
</template>
</div>
<div class="item-time">
<total-time :time="mergeRequest.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
<script>
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
export default {
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
};
</script>
<template>
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li
v-for="(mergeRequest, i) in items"
:key="i"
class="stage-event-item">
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="mergeRequest.author.avatarUrl"/>
<h5 class="item-title merge-merquest-title">
<a :href="mergeRequest.url">
{{ mergeRequest.title }}
</a>
</h5>
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
&middot;
<span>
{{ s__('OpenedNDaysAgo|Opened') }}
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
</span>
<span>
{{ s__('ByAuthor|by') }}
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
</span>
<template v-if="mergeRequest.state === 'closed'">
<span class="merge-request-state">
<i class="fa fa-ban"></i>
{{ mergeRequest.state.toUpperCase() }}
</span>
</template>
<template v-else>
<span class="merge-request-branch" v-if="mergeRequest.branch">
<i class= "fa fa-code-fork"></i>
<a :href="mergeRequest.branch.url">{{ mergeRequest.branch.name }}</a>
</span>
</template>
</div>
<div class="item-time">
<total-time :time="mergeRequest.totalTime"/>
</div>
</li>
</ul>
</div>
</template>
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import iconBranch from '../svg/icon_branch.svg';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageStagingComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
data() {
return { iconBranch };
},
components: {
userAvatarImage,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="build in items" class="stage-event-item item-build-component">
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="build.author.avatarUrl"/>
<h5 class="item-title">
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
<i class="fa fa-code-fork"></i>
<a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a>
<span class="icon-branch">${iconBranch}</span>
<a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a>
</h5>
<span>
<a :href="build.url" class="build-date">{{ build.date }}</a>
{{ s__('ByAuthor|by') }}
<a :href="build.author.webUrl" class="issue-author-link">
{{ build.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="build.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
<script>
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import iconBranch from '../svg/icon_branch.svg';
export default {
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
computed: {
iconBranch() {
return iconBranch;
},
},
};
</script>
<template>
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li
v-for="(build, i) in items"
class="stage-event-item item-build-component"
:key="i">
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="build.author.avatarUrl"/>
<h5 class="item-title">
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
<i class="fa fa-code-fork"></i>
<a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a>
<span class="icon-branch" v-html="iconBranch"></span>
<a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a>
</h5>
<span>
<a :href="build.url" class="build-date">{{ build.date }}</a>
{{ s__('ByAuthor|by') }}
<a :href="build.author.webUrl" class="issue-author-link">
{{ build.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="build.totalTime"/>
</div>
</li>
</ul>
</div>
</template>
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import iconBuildStatus from '../svg/icon_build_status.svg';
import iconBranch from '../svg/icon_branch.svg';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageTestComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
data() {
return { iconBuildStatus, iconBranch };
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="build in items" class="stage-event-item item-build-component">
<div class="item-details">
<h5 class="item-title">
<span class="icon-build-status">${iconBuildStatus}</span>
<a :href="build.url" class="item-build-name">{{ build.name }}</a>
&middot;
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
<i class="fa fa-code-fork"></i>
<a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a>
<span class="icon-branch">${iconBranch}</span>
<a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a>
</h5>
<span>
<a :href="build.url" class="issue-date">
{{ build.date }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="build.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
<script>
import iconBuildStatus from '../svg/icon_build_status.svg';
import iconBranch from '../svg/icon_branch.svg';
export default {
props: {
items: Array,
stage: Object,
},
computed: {
iconBuildStatus() {
return iconBuildStatus;
},
iconBranch() {
return iconBranch;
},
},
};
</script>
<template>
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li
v-for="(build, i) in items"
:key="i"
class="stage-event-item item-build-component">
<div class="item-details">
<h5 class="item-title">
<span class="icon-build-status" v-html="iconBuildStatus"></span>
<a :href="build.url" class="item-build-name">{{ build.name }}</a>
&middot;
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
<i class="fa fa-code-fork"></i>
<a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a>
<span class="icon-branch" v-html="iconBranch"></span>
<a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a>
</h5>
<span>
<a :href="build.url" class="issue-date">
{{ build.date }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="build.totalTime"/>
</div>
</li>
</ul>
</div>
</template>
/* eslint-disable no-param-reassign */
import Vue from 'vue';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.TotalTimeComponent = Vue.extend({
props: {
time: Object,
},
template: `
<span class="total-time">
<template v-if="Object.keys(time).length">
<template v-if="time.days">{{ time.days }} <span>{{ n__('day', 'days', time.days) }}</span></template>
<template v-if="time.hours">{{ time.hours }} <span>{{ n__('Time|hr', 'Time|hrs', time.hours) }}</span></template>
<template v-if="time.mins && !time.days">{{ time.mins }} <span>{{ n__('Time|min', 'Time|mins', time.mins) }}</span></template>
<template v-if="time.seconds && Object.keys(time).length === 1 || time.seconds === 0">{{ time.seconds }} <span>{{ s__('Time|s') }}</span></template>
</template>
<template v-else>
--
</template>
</span>
`,
});
<script>
export default {
props: {
time: {
type: Object,
required: false,
default: () => ({}),
},
},
computed: {
hasData() {
return Object.keys(this.time).length;
},
},
};
</script>
<template>
<span class="total-time">
<template v-if="hasData">
<template v-if="time.days">{{ time.days }} <span>{{ n__('day', 'days', time.days) }}</span></template>
<template v-if="time.hours">{{ time.hours }} <span>{{ n__('Time|hr', 'Time|hrs', time.hours) }}</span></template>
<template v-if="time.mins && !time.days">{{ time.mins }} <span>{{ n__('Time|min', 'Time|mins', time.mins) }}</span></template>
<template v-if="time.seconds && hasDa === 1 || time.seconds === 0">{{ time.seconds }} <span>{{ s__('Time|s') }}</span></template>
</template>
<template v-else>
--
</template>
</span>
</template>
...@@ -3,60 +3,63 @@ ...@@ -3,60 +3,63 @@
import Vue from 'vue'; import Vue from 'vue';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import Translate from '../vue_shared/translate'; import Translate from '../vue_shared/translate';
import LimitWarningComponent from './components/limit_warning_component'; import limitWarningComponent from './components/limit_warning_component.vue';
import './components/stage_code_component'; import stageCodeComponent from './components/stage_code_component.vue';
import './components/stage_issue_component'; import stagePlanComponent from './components/stage_plan_component.vue';
import './components/stage_plan_component'; import stageComponent from './components/stage_component.vue';
import './components/stage_production_component'; import stageReviewComponent from './components/stage_review_component.vue';
import './components/stage_review_component'; import stageStagingComponent from './components/stage_staging_component.vue';
import './components/stage_staging_component'; import stageTestComponent from './components/stage_test_component.vue';
import './components/stage_test_component'; import totalTime from './components/total_time_component.vue';
import './components/total_time_component'; import CycleAnalyticsService from './cycle_analytics_service';
import './cycle_analytics_service'; import CycleAnalyticsStore from './cycle_analytics_store';
import './cycle_analytics_store';
Vue.use(Translate); Vue.use(Translate);
$(() => { $(() => {
const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed'; const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
const cycleAnalyticsEl = document.querySelector('#cycle-analytics');
const cycleAnalyticsStore = gl.cycleAnalytics.CycleAnalyticsStore;
const cycleAnalyticsService = new gl.cycleAnalytics.CycleAnalyticsService({
requestPath: cycleAnalyticsEl.dataset.requestPath,
});
gl.cycleAnalyticsApp = new Vue({ gl.cycleAnalyticsApp = new Vue({
el: '#cycle-analytics', el: '#cycle-analytics',
name: 'CycleAnalytics', name: 'CycleAnalytics',
data: { data() {
state: cycleAnalyticsStore.state, const cycleAnalyticsEl = document.querySelector('#cycle-analytics');
isLoading: false, const cycleAnalyticsService = new CycleAnalyticsService({
isLoadingStage: false, requestPath: cycleAnalyticsEl.dataset.requestPath,
isEmptyStage: false, });
hasError: false,
startDate: 30, return {
isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE), store: CycleAnalyticsStore,
state: CycleAnalyticsStore.state,
isLoading: false,
isLoadingStage: false,
isEmptyStage: false,
hasError: false,
startDate: 30,
isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE),
service: cycleAnalyticsService,
};
}, },
computed: { computed: {
currentStage() { currentStage() {
return cycleAnalyticsStore.currentActiveStage(); return this.store.currentActiveStage();
}, },
}, },
components: { components: {
'stage-issue-component': gl.cycleAnalytics.StageIssueComponent, 'stage-issue-component': stageComponent,
'stage-plan-component': gl.cycleAnalytics.StagePlanComponent, 'stage-plan-component': stagePlanComponent,
'stage-code-component': gl.cycleAnalytics.StageCodeComponent, 'stage-code-component': stageCodeComponent,
'stage-test-component': gl.cycleAnalytics.StageTestComponent, 'stage-test-component': stageTestComponent,
'stage-review-component': gl.cycleAnalytics.StageReviewComponent, 'stage-review-component': stageReviewComponent,
'stage-staging-component': gl.cycleAnalytics.StageStagingComponent, 'stage-staging-component': stageStagingComponent,
'stage-production-component': gl.cycleAnalytics.StageProductionComponent, 'stage-production-component': stageComponent,
}, },
created() { created() {
this.fetchCycleAnalyticsData(); this.fetchCycleAnalyticsData();
}, },
methods: { methods: {
handleError() { handleError() {
cycleAnalyticsStore.setErrorState(true); this.store.setErrorState(true);
return new Flash('There was an error while fetching cycle analytics data.'); return new Flash('There was an error while fetching cycle analytics data.');
}, },
initDropdown() { initDropdown() {
...@@ -77,17 +80,17 @@ $(() => { ...@@ -77,17 +80,17 @@ $(() => {
this.isLoading = true; this.isLoading = true;
cycleAnalyticsService this.service
.fetchCycleAnalyticsData(fetchOptions) .fetchCycleAnalyticsData(fetchOptions)
.done((response) => { .then(resp => resp.json())
cycleAnalyticsStore.setCycleAnalyticsData(response); .then((response) => {
this.store.setCycleAnalyticsData(response);
this.selectDefaultStage(); this.selectDefaultStage();
this.initDropdown(); this.initDropdown();
this.isLoading = false;
}) })
.error(() => { .catch(() => {
this.handleError(); this.handleError();
})
.always(() => {
this.isLoading = false; this.isLoading = false;
}); });
}, },
...@@ -100,27 +103,27 @@ $(() => { ...@@ -100,27 +103,27 @@ $(() => {
if (this.currentStage === stage) return; if (this.currentStage === stage) return;
if (!stage.isUserAllowed) { if (!stage.isUserAllowed) {
cycleAnalyticsStore.setActiveStage(stage); this.store.setActiveStage(stage);
return; return;
} }
this.isLoadingStage = true; this.isLoadingStage = true;
cycleAnalyticsStore.setStageEvents([], stage); this.store.setStageEvents([], stage);
cycleAnalyticsStore.setActiveStage(stage); this.store.setActiveStage(stage);
cycleAnalyticsService this.service
.fetchStageData({ .fetchStageData({
stage, stage,
startDate: this.startDate, startDate: this.startDate,
}) })
.done((response) => { .then(resp => resp.json())
.then((response) => {
this.isEmptyStage = !response.events.length; this.isEmptyStage = !response.events.length;
cycleAnalyticsStore.setStageEvents(response.events, stage); this.store.setStageEvents(response.events, stage);
this.isLoadingStage = false;
}) })
.error(() => { .catch(() => {
this.isEmptyStage = true; this.isEmptyStage = true;
})
.always(() => {
this.isLoadingStage = false; this.isLoadingStage = false;
}); });
}, },
...@@ -132,6 +135,6 @@ $(() => { ...@@ -132,6 +135,6 @@ $(() => {
}); });
// Register global components // Register global components
Vue.component('limit-warning', LimitWarningComponent); Vue.component('limit-warning', limitWarningComponent);
Vue.component('total-time', gl.cycleAnalytics.TotalTimeComponent); Vue.component('total-time', totalTime);
}); });
/* eslint-disable no-param-reassign */ import Vue from 'vue';
import VueResource from 'vue-resource';
const global = window.gl || (window.gl = {}); Vue.use(VueResource);
global.cycleAnalytics = global.cycleAnalytics || {};
class CycleAnalyticsService { export default class CycleAnalyticsService {
constructor(options) { constructor(options) {
this.requestPath = options.requestPath; this.requestPath = options.requestPath;
this.cycleAnalytics = Vue.resource(this.requestPath);
} }
fetchCycleAnalyticsData(options) { fetchCycleAnalyticsData(options = { startDate: 30 }) {
options = options || { startDate: 30 }; return this.cycleAnalytics.get({ cycle_analytics: { start_date: options.startDate } });
return $.ajax({
url: this.requestPath,
method: 'GET',
dataType: 'json',
contentType: 'application/json',
data: {
cycle_analytics: {
start_date: options.startDate,
},
},
});
} }
fetchStageData(options) { fetchStageData(options) {
...@@ -30,12 +19,12 @@ class CycleAnalyticsService { ...@@ -30,12 +19,12 @@ class CycleAnalyticsService {
startDate, startDate,
} = options; } = options;
return $.get(`${this.requestPath}/events/${stage.name}.json`, { return Vue.http.get(`${this.requestPath}/events/${stage.name}.json`, {
cycle_analytics: { params: {
start_date: startDate, cycle_analytics: {
start_date: startDate,
},
}, },
}); });
} }
} }
global.cycleAnalytics.CycleAnalyticsService = CycleAnalyticsService;
...@@ -4,9 +4,6 @@ import { __ } from '../locale'; ...@@ -4,9 +4,6 @@ import { __ } from '../locale';
import '../lib/utils/text_utility'; import '../lib/utils/text_utility';
import DEFAULT_EVENT_OBJECTS from './default_event_objects'; import DEFAULT_EVENT_OBJECTS from './default_event_objects';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
const EMPTY_STAGE_TEXTS = { const EMPTY_STAGE_TEXTS = {
issue: __('The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.'), issue: __('The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.'),
plan: __('The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.'), plan: __('The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.'),
...@@ -17,7 +14,7 @@ const EMPTY_STAGE_TEXTS = { ...@@ -17,7 +14,7 @@ const EMPTY_STAGE_TEXTS = {
production: __('The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.'), production: __('The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.'),
}; };
global.cycleAnalytics.CycleAnalyticsStore = { export default {
state: { state: {
summary: '', summary: '',
stats: '', stats: '',
......
...@@ -34,7 +34,7 @@ export const canShowActiveSubItems = (el) => { ...@@ -34,7 +34,7 @@ export const canShowActiveSubItems = (el) => {
export const canShowSubItems = () => bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md' || bp.getBreakpointSize() === 'lg'; export const canShowSubItems = () => bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md' || bp.getBreakpointSize() === 'lg';
export const getHideSubItemsInterval = () => { export const getHideSubItemsInterval = () => {
if (!currentOpenMenu) return 0; if (!currentOpenMenu || !mousePos.length) return 0;
const currentMousePos = mousePos[mousePos.length - 1]; const currentMousePos = mousePos[mousePos.length - 1];
const prevMousePos = mousePos[0]; const prevMousePos = mousePos[0];
......
export const isSticky = (el, scrollY, stickyTop) => { export const createPlaceholder = () => {
const placeholder = document.createElement('div');
placeholder.classList.add('sticky-placeholder');
return placeholder;
};
export const isSticky = (el, scrollY, stickyTop, insertPlaceholder) => {
const top = Math.floor(el.offsetTop - scrollY); const top = Math.floor(el.offsetTop - scrollY);
if (top <= stickyTop) { if (top <= stickyTop && !el.classList.contains('is-stuck')) {
const placeholder = insertPlaceholder ? createPlaceholder() : null;
const heightBefore = el.offsetHeight;
el.classList.add('is-stuck'); el.classList.add('is-stuck');
} else {
if (insertPlaceholder) {
el.parentNode.insertBefore(placeholder, el.nextElementSibling);
placeholder.style.height = `${heightBefore - el.offsetHeight}px`;
}
} else if (top > stickyTop && el.classList.contains('is-stuck')) {
el.classList.remove('is-stuck'); el.classList.remove('is-stuck');
if (insertPlaceholder && el.nextElementSibling && el.nextElementSibling.classList.contains('sticky-placeholder')) {
el.nextElementSibling.remove();
}
} }
}; };
export default (el) => { export default (el, insertPlaceholder = true) => {
if (!el) return; if (!el) return;
const computedStyle = window.getComputedStyle(el); const computedStyle = window.getComputedStyle(el);
...@@ -17,7 +37,7 @@ export default (el) => { ...@@ -17,7 +37,7 @@ export default (el) => {
const stickyTop = parseInt(computedStyle.top, 10); const stickyTop = parseInt(computedStyle.top, 10);
document.addEventListener('scroll', () => isSticky(el, window.scrollY, stickyTop), { document.addEventListener('scroll', () => isSticky(el, window.scrollY, stickyTop, insertPlaceholder), {
passive: true, passive: true,
}); });
}; };
...@@ -178,8 +178,8 @@ const RepoHelper = { ...@@ -178,8 +178,8 @@ const RepoHelper = {
setFile(data, file) { setFile(data, file) {
const newFile = data; const newFile = data;
newFile.url = file.url || Service.url; // Grab the URL from service, happens on page refresh.
newFile.url = file.url;
if (newFile.render_error === 'too_large' || newFile.render_error === 'collapsed') { if (newFile.render_error === 'too_large' || newFile.render_error === 'collapsed') {
newFile.tooLarge = true; newFile.tooLarge = true;
} }
......
...@@ -260,7 +260,7 @@ ...@@ -260,7 +260,7 @@
position: relative; position: relative;
border: 1px solid $blue-300; border: 1px solid $blue-300;
border-radius: $border-radius-default; border-radius: $border-radius-default;
background-color: $blue-25; background-color: $blue-50;
justify-content: center; justify-content: center;
.dismiss-button { .dismiss-button {
......
...@@ -779,6 +779,14 @@ ...@@ -779,6 +779,14 @@
white-space: normal; white-space: normal;
width: 100%; width: 100%;
&.dropdown-menu-user-link {
white-space: nowrap;
.dropdown-menu-user-username {
display: block;
}
}
// make sure the text color is not overriden // make sure the text color is not overriden
&.text-danger { &.text-danger {
color: $brand-danger; color: $brand-danger;
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
// Header // Header
header.navbar-gitlab-new { header.navbar-gitlab-new {
background: linear-gradient(to right, $color-900, $color-800); background-color: $color-900;
.navbar-collapse { .navbar-collapse {
color: $color-200; color: $color-200;
...@@ -201,7 +201,7 @@ body { ...@@ -201,7 +201,7 @@ body {
@include gitlab-theme($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-700, $theme-gray-700, $theme-gray-100, $theme-gray-700); @include gitlab-theme($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-700, $theme-gray-700, $theme-gray-100, $theme-gray-700);
header.navbar-gitlab-new { header.navbar-gitlab-new {
background: $theme-gray-100; background-color: $theme-gray-100;
box-shadow: 0 2px 0 0 $border-color; box-shadow: 0 2px 0 0 $border-color;
.logo-text svg { .logo-text svg {
...@@ -242,10 +242,10 @@ body { ...@@ -242,10 +242,10 @@ body {
&:hover { &:hover {
background-color: $white-light; background-color: $white-light;
box-shadow: inset 0 0 0 1px $blue-100; box-shadow: inset 0 0 0 1px $blue-200;
.location-badge { .location-badge {
box-shadow: inset 0 0 0 1px $blue-100; box-shadow: inset 0 0 0 1px $blue-200;
} }
} }
} }
......
...@@ -142,7 +142,43 @@ ...@@ -142,7 +142,43 @@
} }
@mixin green-status-color { @mixin green-status-color {
@include status-color($green-50, $green-500, $green-700); @include status-color($green-100, $green-500, $green-700);
}
@mixin fade($gradient-direction, $gradient-color) {
visibility: hidden;
opacity: 0;
z-index: 2;
position: absolute;
bottom: 12px;
width: 43px;
height: 30px;
transition-duration: .3s;
-webkit-transform: translateZ(0);
background: linear-gradient(to $gradient-direction, $gradient-color 45%, rgba($gradient-color, 0.4));
&.scrolling {
visibility: visible;
opacity: 1;
transition-duration: .3s;
}
.fa {
position: relative;
top: 5px;
font-size: 18px;
}
}
@mixin scrolling-links() {
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
display: flex;
&::-webkit-scrollbar {
display: none;
}
} }
@mixin fade($gradient-direction, $gradient-color) { @mixin fade($gradient-direction, $gradient-color) {
......
...@@ -137,7 +137,7 @@ $well-border: #eee; ...@@ -137,7 +137,7 @@ $well-border: #eee;
//## //##
$code-color: $red-600; $code-color: $red-600;
$code-bg: lighten($red-50, 2%); $code-bg: lighten($red-100, 2%);
$kbd-color: $white-light; $kbd-color: $white-light;
$kbd-bg: #333; $kbd-bg: #333;
......
...@@ -29,46 +29,45 @@ $gray-dark: darken($gray-light, $darken-dark-factor); ...@@ -29,46 +29,45 @@ $gray-dark: darken($gray-light, $darken-dark-factor);
$gray-darker: #eee; $gray-darker: #eee;
$gray-darkest: #c4c4c4; $gray-darkest: #c4c4c4;
$green-25: #f6fcf8; $green-50: #f1fdf6;
$green-50: #e4f5eb; $green-100: #dcf5e7;
$green-100: #bae6cc; $green-200: #b3e6c8;
$green-200: #8dd5aa; $green-300: #75d09b;
$green-300: #5fc488; $green-400: #37b96d;
$green-400: #3cb76f;
$green-500: #1aaa55; $green-500: #1aaa55;
$green-600: #168f48; $green-600: #168f48;
$green-700: #12753a; $green-700: #12753a;
$green-800: #0e5a2d; $green-800: #0e5a2d;
$green-900: #0a4020; $green-900: #0a4020;
$green-950: #072b15;
$blue-25: #f6fafd; $blue-50: #f6fafe;
$blue-50: #e4eff9; $blue-100: #e4f0fb;
$blue-100: #bcd7f1; $blue-200: #b8d6f4;
$blue-200: #8fbce8; $blue-300: #73afea;
$blue-300: #62a1df; $blue-400: #2e87e0;
$blue-400: #418cd8;
$blue-500: #1f78d1; $blue-500: #1f78d1;
$blue-600: #1b69b6; $blue-600: #1b69b6;
$blue-700: #17599c; $blue-700: #17599c;
$blue-800: #134a81; $blue-800: #134a81;
$blue-900: #0f3b66; $blue-900: #0f3b66;
$blue-950: #0a2744;
$orange-25: #fffcf8; $orange-50: #fffaf4;
$orange-50: #fff2e1; $orange-100: #fff1de;
$orange-100: #fedfb3; $orange-200: #fed69f;
$orange-200: #feca81; $orange-300: #fdbc60;
$orange-300: #fdb44f; $orange-400: #fca121;
$orange-400: #fca429;
$orange-500: #fc9403; $orange-500: #fc9403;
$orange-600: #de7e00; $orange-600: #de7e00;
$orange-700: #c26700; $orange-700: #c26700;
$orange-800: #a35100; $orange-800: #a35200;
$orange-900: #853b00; $orange-900: #853c00;
$orange-950: #592800;
$red-25: #fef7f6; $red-50: #fef6f5;
$red-50: #fbe7e4; $red-100: #fbe5e1;
$red-100: #f4c4bc; $red-200: #f2b4a9;
$red-200: #ed9d90;
$red-300: #e67664; $red-300: #e67664;
$red-400: #e05842; $red-400: #e05842;
$red-500: #db3b21; $red-500: #db3b21;
...@@ -76,6 +75,7 @@ $red-600: #c0341d; ...@@ -76,6 +75,7 @@ $red-600: #c0341d;
$red-700: #a62d19; $red-700: #a62d19;
$red-800: #8b2615; $red-800: #8b2615;
$red-900: #711e11; $red-900: #711e11;
$red-950: #4b140b;
// GitLab themes // GitLab themes
...@@ -186,8 +186,8 @@ $list-text-disabled-color: $gl-text-color-tertiary; ...@@ -186,8 +186,8 @@ $list-text-disabled-color: $gl-text-color-tertiary;
$list-border-light: #eee; $list-border-light: #eee;
$list-border: rgba(0, 0, 0, 0.05); $list-border: rgba(0, 0, 0, 0.05);
$list-text-height: 42px; $list-text-height: 42px;
$list-warning-row-bg: $orange-50; $list-warning-row-bg: $orange-100;
$list-warning-row-border: $orange-100; $list-warning-row-border: $orange-200;
$list-warning-row-color: $orange-700; $list-warning-row-color: $orange-700;
/* /*
...@@ -216,8 +216,8 @@ $gl-sidebar-padding: 22px; ...@@ -216,8 +216,8 @@ $gl-sidebar-padding: 22px;
/* /*
* Misc * Misc
*/ */
$row-hover: $blue-25; $row-hover: $blue-50;
$row-hover-border: $blue-100; $row-hover-border: $blue-200;
$progress-color: #c0392b; $progress-color: #c0392b;
$header-height: 50px; $header-height: 50px;
$new-navbar-height: 40px; $new-navbar-height: 40px;
...@@ -272,8 +272,8 @@ $time-color: #999; ...@@ -272,8 +272,8 @@ $time-color: #999;
$project-member-show-color: #aaa; $project-member-show-color: #aaa;
$gl-promo-color: #aaa; $gl-promo-color: #aaa;
$error-bg: $red-400; $error-bg: $red-400;
$warning-message-bg: $orange-50; $warning-message-bg: $orange-100;
$warning-message-border: $orange-100; $warning-message-border: $orange-200;
$warning-message-color: $orange-700; $warning-message-color: $orange-700;
$control-group-descr-color: #666; $control-group-descr-color: #666;
$table-permission-x-bg: #d9edf7; $table-permission-x-bg: #d9edf7;
...@@ -459,17 +459,17 @@ $builds-trace-bg: #111; ...@@ -459,17 +459,17 @@ $builds-trace-bg: #111;
/* /*
* Callout * Callout
*/ */
$callout-danger-bg: $red-50; $callout-danger-bg: $red-100;
$callout-danger-border: $red-100; $callout-danger-border: $red-200;
$callout-danger-color: $red-700; $callout-danger-color: $red-700;
$callout-warning-bg: $orange-50; $callout-warning-bg: $orange-100;
$callout-warning-border: $orange-100; $callout-warning-border: $orange-200;
$callout-warning-color: $orange-700; $callout-warning-color: $orange-700;
$callout-info-bg: $blue-50; $callout-info-bg: $blue-100;
$callout-info-border: $blue-100; $callout-info-border: $blue-200;
$callout-info-color: $blue-700; $callout-info-color: $blue-700;
$callout-success-bg: $green-50; $callout-success-bg: $green-100;
$callout-success-border: $green-100; $callout-success-border: $green-200;
$callout-success-color: $green-700; $callout-success-color: $green-700;
/* /*
......
...@@ -83,7 +83,7 @@ $space-between-cards: 8px; ...@@ -83,7 +83,7 @@ $space-between-cards: 8px;
border-top-color: $color-low-score; border-top-color: $color-low-score;
.card-score-big { .card-score-big {
background-color: $red-25; background-color: $red-50;
} }
} }
...@@ -91,7 +91,7 @@ $space-between-cards: 8px; ...@@ -91,7 +91,7 @@ $space-between-cards: 8px;
border-top-color: $color-average-score; border-top-color: $color-average-score;
.card-score-big { .card-score-big {
background-color: $orange-25; background-color: $orange-50;
} }
} }
...@@ -99,7 +99,7 @@ $space-between-cards: 8px; ...@@ -99,7 +99,7 @@ $space-between-cards: 8px;
border-top-color: $color-high-score; border-top-color: $color-high-score;
.card-score-big { .card-score-big {
background-color: $green-25; background-color: $green-50;
} }
} }
......
...@@ -451,7 +451,7 @@ ...@@ -451,7 +451,7 @@
} }
.files { .files {
margin-top: -1px; margin-top: 1px;
.diff-file:last-child { .diff-file:last-child {
margin-bottom: 0; margin-bottom: 0;
...@@ -586,11 +586,6 @@ ...@@ -586,11 +586,6 @@
top: 76px; top: 76px;
} }
+ .files,
+ .alert {
margin-top: 1px;
}
&:not(.is-stuck) .diff-stats-additions-deletions-collapsed { &:not(.is-stuck) .diff-stats-additions-deletions-collapsed {
display: none; display: none;
} }
...@@ -605,11 +600,6 @@ ...@@ -605,11 +600,6 @@
.inline-parallel-buttons { .inline-parallel-buttons {
display: none; display: none;
} }
+ .files,
+ .alert {
margin-top: 32px;
}
} }
} }
} }
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
.is-confidential { .is-confidential {
color: $orange-600; color: $orange-600;
background-color: $orange-50; background-color: $orange-100;
border-radius: $border-radius-default; border-radius: $border-radius-default;
padding: 5px; padding: 5px;
margin: 0 3px 0 -4px; margin: 0 3px 0 -4px;
......
...@@ -255,7 +255,7 @@ $colors: ( ...@@ -255,7 +255,7 @@ $colors: (
&.saved { &.saved {
.editor { .editor {
border-top: solid 2px $green-200; border-top: solid 2px $green-300;
} }
} }
......
...@@ -103,7 +103,7 @@ ...@@ -103,7 +103,7 @@
.confidential-issue-warning { .confidential-issue-warning {
color: $orange-600; color: $orange-600;
background-color: $orange-50; background-color: $orange-100;
border-radius: $border-radius-default $border-radius-default 0 0; border-radius: $border-radius-default $border-radius-default 0 0;
border: 1px solid $border-gray-normal; border: 1px solid $border-gray-normal;
border-bottom: none; border-bottom: none;
......
...@@ -674,20 +674,20 @@ a.linked-pipeline-mini-item { ...@@ -674,20 +674,20 @@ a.linked-pipeline-mini-item {
// Dropdown button animation in mini pipeline graph // Dropdown button animation in mini pipeline graph
&.ci-status-icon-success { &.ci-status-icon-success {
@include mini-pipeline-graph-color($green-50, $green-500, $green-600); @include mini-pipeline-graph-color($green-100, $green-500, $green-600);
} }
&.ci-status-icon-failed { &.ci-status-icon-failed {
@include mini-pipeline-graph-color($red-50, $red-500, $red-600); @include mini-pipeline-graph-color($red-100, $red-500, $red-600);
} }
&.ci-status-icon-pending, &.ci-status-icon-pending,
&.ci-status-icon-success_with_warnings { &.ci-status-icon-success_with_warnings {
@include mini-pipeline-graph-color($orange-50, $orange-500, $orange-600); @include mini-pipeline-graph-color($orange-100, $orange-500, $orange-600);
} }
&.ci-status-icon-running { &.ci-status-icon-running {
@include mini-pipeline-graph-color($blue-50, $blue-400, $blue-600); @include mini-pipeline-graph-color($blue-100, $blue-400, $blue-600);
} }
&.ci-status-icon-canceled, &.ci-status-icon-canceled,
......
...@@ -292,7 +292,7 @@ table.u2f-registrations { ...@@ -292,7 +292,7 @@ table.u2f-registrations {
padding: 32px; padding: 32px;
border: 1px solid $blue-300; border: 1px solid $blue-300;
border-radius: $border-radius-default; border-radius: $border-radius-default;
background-color: $blue-25; background-color: $blue-50;
position: relative; position: relative;
display: flex; display: flex;
justify-content: center; justify-content: center;
...@@ -376,7 +376,7 @@ table.u2f-registrations { ...@@ -376,7 +376,7 @@ table.u2f-registrations {
.nav-wip { .nav-wip {
border: 1px solid $blue-500; border: 1px solid $blue-500;
background: $blue-25; background: $blue-50;
padding: $gl-padding; padding: $gl-padding;
margin-bottom: $gl-padding; margin-bottom: $gl-padding;
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
} }
&.ci-failed { &.ci-failed {
@include status-color($red-50, $red-500, $red-600); @include status-color($red-100, $red-500, $red-600);
} }
&.ci-success { &.ci-success {
...@@ -39,12 +39,12 @@ ...@@ -39,12 +39,12 @@
&.ci-pending, &.ci-pending,
&.ci-failed_with_warnings, &.ci-failed_with_warnings,
&.ci-success_with_warnings { &.ci-success_with_warnings {
@include status-color($orange-50, $orange-500, $orange-700); @include status-color($orange-100, $orange-500, $orange-700);
} }
&.ci-info, &.ci-info,
&.ci-running { &.ci-running {
@include status-color($blue-50, $blue-500, $blue-600); @include status-color($blue-100, $blue-500, $blue-600);
} }
&.ci-created, &.ci-created,
......
...@@ -15,3 +15,9 @@ ...@@ -15,3 +15,9 @@
-ms-animation: none !important; -ms-animation: none !important;
animation: none !important; animation: none !important;
} }
// Disable sticky changes bar for tests
.diff-files-changed {
position: relative !important;
top: 0 !important;
}
...@@ -142,8 +142,11 @@ module IssuableCollections ...@@ -142,8 +142,11 @@ module IssuableCollections
when 'milestone_due_desc' then sort_value_milestone when 'milestone_due_desc' then sort_value_milestone
when 'downvotes_asc' then sort_value_popularity when 'downvotes_asc' then sort_value_popularity
when 'downvotes_desc' then sort_value_popularity when 'downvotes_desc' then sort_value_popularity
<<<<<<< HEAD
when 'weight_asc' then sort_value_weight when 'weight_asc' then sort_value_weight
when 'weight_desc' then sort_value_weight when 'weight_desc' then sort_value_weight
=======
>>>>>>> b187a3f2bb00a0a0a6e5f8369edf2d6430a7af6e
else value else value
end end
end end
......
...@@ -13,22 +13,29 @@ module AvatarsHelper ...@@ -13,22 +13,29 @@ module AvatarsHelper
user_name = options[:user].try(:name) || options[:user_name] user_name = options[:user].try(:name) || options[:user_name]
avatar_url = options[:url] || avatar_icon(options[:user] || options[:user_email], avatar_size) avatar_url = options[:url] || avatar_icon(options[:user] || options[:user_email], avatar_size)
has_tooltip = options[:has_tooltip].nil? ? true : options[:has_tooltip] has_tooltip = options[:has_tooltip].nil? ? true : options[:has_tooltip]
data_attributes = {} data_attributes = options[:data] || {}
css_class = %W[avatar s#{avatar_size}].push(*options[:css_class]) css_class = %W[avatar s#{avatar_size}].push(*options[:css_class])
if has_tooltip if has_tooltip
css_class.push('has-tooltip') css_class.push('has-tooltip')
data_attributes = { container: 'body' } data_attributes[:container] = 'body'
end end
image_tag( if options[:lazy]
avatar_url, css_class << 'lazy'
data_attributes[:src] = avatar_url
avatar_url = LazyImageTagHelper.placeholder_image
end
image_options = {
alt: "#{user_name}'s avatar",
src: avatar_url,
data: data_attributes,
class: css_class, class: css_class,
alt: "#{user_name}'s avatar", title: user_name
title: user_name, }
data: data_attributes,
lazy: true tag(:img, image_options)
)
end end
def user_avatar(options = {}) def user_avatar(options = {})
......
...@@ -239,8 +239,8 @@ module ProjectsHelper ...@@ -239,8 +239,8 @@ module ProjectsHelper
end end
end end
def has_projects_or_name?(projects, params) def show_projects?(projects, params)
!!(params[:name] || any_projects?(projects)) !!(params[:personal] || params[:name] || any_projects?(projects))
end end
private private
......
...@@ -12,8 +12,11 @@ module SortingHelper ...@@ -12,8 +12,11 @@ module SortingHelper
sort_value_milestone => sort_title_milestone, sort_value_milestone => sort_title_milestone,
sort_value_milestone_later => sort_title_milestone_later, sort_value_milestone_later => sort_title_milestone_later,
sort_value_milestone_soon => sort_title_milestone_soon, sort_value_milestone_soon => sort_title_milestone_soon,
<<<<<<< HEAD
sort_value_less_weight => sort_title_less_weight, sort_value_less_weight => sort_title_less_weight,
sort_value_more_weight => sort_title_more_weight, sort_value_more_weight => sort_title_more_weight,
=======
>>>>>>> b187a3f2bb00a0a0a6e5f8369edf2d6430a7af6e
sort_value_name => sort_title_name, sort_value_name => sort_title_name,
sort_value_name_desc => sort_title_name_desc, sort_value_name_desc => sort_title_name_desc,
sort_value_oldest_created => sort_title_oldest_created, sort_value_oldest_created => sort_title_oldest_created,
...@@ -24,8 +27,12 @@ module SortingHelper ...@@ -24,8 +27,12 @@ module SortingHelper
sort_value_recently_updated => sort_title_recently_updated, sort_value_recently_updated => sort_title_recently_updated,
sort_value_popularity => sort_title_popularity, sort_value_popularity => sort_title_popularity,
sort_value_priority => sort_title_priority, sort_value_priority => sort_title_priority,
<<<<<<< HEAD
sort_value_upvotes => sort_title_upvotes, sort_value_upvotes => sort_title_upvotes,
sort_value_weight => sort_title_weight sort_value_weight => sort_title_weight
=======
sort_value_upvotes => sort_title_upvotes
>>>>>>> b187a3f2bb00a0a0a6e5f8369edf2d6430a7af6e
} }
end end
...@@ -87,6 +94,7 @@ module SortingHelper ...@@ -87,6 +94,7 @@ module SortingHelper
def sortable_item(item, path, sorted_by) def sortable_item(item, path, sorted_by)
link_to item, path, class: sorted_by == item ? 'is-active' : '' link_to item, path, class: sorted_by == item ? 'is-active' : ''
<<<<<<< HEAD
end end
# Titles. # Titles.
...@@ -116,12 +124,16 @@ module SortingHelper ...@@ -116,12 +124,16 @@ module SortingHelper
def sort_title_due_date_soon def sort_title_due_date_soon
s_('SortOptions|Due soon') s_('SortOptions|Due soon')
=======
>>>>>>> b187a3f2bb00a0a0a6e5f8369edf2d6430a7af6e
end end
def sort_title_label_priority # Titles.
s_('SortOptions|Label priority') def sort_title_access_level_asc
s_('SortOptions|Access level, ascending')
end end
<<<<<<< HEAD
def sort_title_largest_group def sort_title_largest_group
s_('SortOptions|Largest group') s_('SortOptions|Largest group')
end end
...@@ -132,24 +144,50 @@ module SortingHelper ...@@ -132,24 +144,50 @@ module SortingHelper
def sort_title_last_joined def sort_title_last_joined
s_('SortOptions|Last joined') s_('SortOptions|Last joined')
=======
def sort_title_access_level_desc
s_('SortOptions|Access level, descending')
end end
def sort_title_latest_activity def sort_title_created_date
s_('SortOptions|Last updated') s_('SortOptions|Created date')
end
def sort_title_downvotes
s_('SortOptions|Least popular')
>>>>>>> b187a3f2bb00a0a0a6e5f8369edf2d6430a7af6e
end
def sort_title_due_date
s_('SortOptions|Due date')
end end
<<<<<<< HEAD
def sort_title_less_weight def sort_title_less_weight
s_('SortOptions|Less weight') s_('SortOptions|Less weight')
end end
def sort_title_milestone def sort_title_milestone
s_('SortOptions|Milestone') s_('SortOptions|Milestone')
=======
def sort_title_due_date_later
s_('SortOptions|Due later')
end end
def sort_title_milestone_later def sort_title_due_date_soon
s_('SortOptions|Milestone due later') s_('SortOptions|Due soon')
end
def sort_title_label_priority
s_('SortOptions|Label priority')
>>>>>>> b187a3f2bb00a0a0a6e5f8369edf2d6430a7af6e
end
def sort_title_largest_group
s_('SortOptions|Largest group')
end end
<<<<<<< HEAD
def sort_title_milestone_soon def sort_title_milestone_soon
s_('SortOptions|Milestone due soon') s_('SortOptions|Milestone due soon')
end end
...@@ -164,12 +202,37 @@ module SortingHelper ...@@ -164,12 +202,37 @@ module SortingHelper
def sort_title_name_asc def sort_title_name_asc
s_('SortOptions|Name, ascending') s_('SortOptions|Name, ascending')
=======
def sort_title_largest_repo
s_('SortOptions|Largest repository')
end
def sort_title_last_joined
s_('SortOptions|Last joined')
end
def sort_title_latest_activity
s_('SortOptions|Last updated')
end
def sort_title_milestone
s_('SortOptions|Milestone')
end
def sort_title_milestone_later
s_('SortOptions|Milestone due later')
end
def sort_title_milestone_soon
s_('SortOptions|Milestone due soon')
>>>>>>> b187a3f2bb00a0a0a6e5f8369edf2d6430a7af6e
end end
def sort_title_name_desc def sort_title_name_desc
s_('SortOptions|Name, descending') s_('SortOptions|Name, descending')
end end
<<<<<<< HEAD
def sort_title_oldest_activity def sort_title_oldest_activity
s_('SortOptions|Oldest updated') s_('SortOptions|Oldest updated')
end end
...@@ -180,12 +243,25 @@ module SortingHelper ...@@ -180,12 +243,25 @@ module SortingHelper
def sort_title_oldest_joined def sort_title_oldest_joined
s_('SortOptions|Oldest joined') s_('SortOptions|Oldest joined')
=======
def sort_title_name_asc
s_('SortOptions|Name, ascending')
end end
def sort_title_oldest_signin def sort_title_name_desc
s_('SortOptions|Oldest sign in') s_('SortOptions|Name, descending')
end
def sort_title_oldest_activity
s_('SortOptions|Oldest updated')
>>>>>>> b187a3f2bb00a0a0a6e5f8369edf2d6430a7af6e
end
def sort_title_oldest_created
s_('SortOptions|Oldest created')
end end
<<<<<<< HEAD
def sort_title_oldest_updated def sort_title_oldest_updated
s_('SortOptions|Oldest updated') s_('SortOptions|Oldest updated')
end end
...@@ -226,6 +302,52 @@ module SortingHelper ...@@ -226,6 +302,52 @@ module SortingHelper
s_('SortOptions|Weight') s_('SortOptions|Weight')
end end
=======
def sort_title_oldest_joined
s_('SortOptions|Oldest joined')
end
def sort_title_oldest_signin
s_('SortOptions|Oldest sign in')
end
def sort_title_oldest_updated
s_('SortOptions|Oldest updated')
end
def sort_title_popularity
s_('SortOptions|Popularity')
end
def sort_title_priority
s_('SortOptions|Priority')
end
def sort_title_recently_created
s_('SortOptions|Last created')
end
def sort_title_recently_signin
s_('SortOptions|Recent sign in')
end
def sort_title_recently_updated
s_('SortOptions|Last updated')
end
def sort_title_start_date_later
s_('SortOptions|Start later')
end
def sort_title_start_date_soon
s_('SortOptions|Start soon')
end
def sort_title_upvotes
s_('SortOptions|Most popular')
end
>>>>>>> b187a3f2bb00a0a0a6e5f8369edf2d6430a7af6e
# Values. # Values.
def sort_value_access_level_asc def sort_value_access_level_asc
'access_level_asc' 'access_level_asc'
...@@ -275,6 +397,7 @@ module SortingHelper ...@@ -275,6 +397,7 @@ module SortingHelper
'latest_activity_desc' 'latest_activity_desc'
end end
<<<<<<< HEAD
def sort_value_less_weight def sort_value_less_weight
'weight_asc' 'weight_asc'
end end
...@@ -283,6 +406,12 @@ module SortingHelper ...@@ -283,6 +406,12 @@ module SortingHelper
'milestone' 'milestone'
end end
=======
def sort_value_milestone
'milestone'
end
>>>>>>> b187a3f2bb00a0a0a6e5f8369edf2d6430a7af6e
def sort_value_milestone_later def sort_value_milestone_later
'milestone_due_desc' 'milestone_due_desc'
end end
...@@ -291,6 +420,7 @@ module SortingHelper ...@@ -291,6 +420,7 @@ module SortingHelper
'milestone_due_asc' 'milestone_due_asc'
end end
<<<<<<< HEAD
def sort_value_more_weight def sort_value_more_weight
'weight_desc' 'weight_desc'
end end
...@@ -323,6 +453,36 @@ module SortingHelper ...@@ -323,6 +453,36 @@ module SortingHelper
'updated_asc' 'updated_asc'
end end
=======
def sort_value_name
'name_asc'
end
def sort_value_name_desc
'name_desc'
end
def sort_value_oldest_activity
'latest_activity_asc'
end
def sort_value_oldest_created
'created_asc'
end
def sort_value_oldest_signin
'oldest_sign_in'
end
def sort_value_oldest_joined
'oldest_joined'
end
def sort_value_oldest_updated
'updated_asc'
end
>>>>>>> b187a3f2bb00a0a0a6e5f8369edf2d6430a7af6e
def sort_value_popularity def sort_value_popularity
'popularity' 'popularity'
end end
......
...@@ -175,7 +175,7 @@ module Ci ...@@ -175,7 +175,7 @@ module Ci
end end
def assignable_for?(project) def assignable_for?(project)
!locked? || projects.exists?(id: project.id) is_shared? || projects.exists?(id: project.id)
end end
def accepting_tags?(build) def accepting_tags?(build)
......
.top-area
%ul.nav-links
= nav_link(html_options: { class: ("active" unless params[:personal].present?) }) do
= link_to s_('DashboardProjects|All'), dashboard_projects_path
= nav_link(html_options: { class: ("active" if params[:personal].present?) }) do
= link_to s_('DashboardProjects|Personal'), filter_projects_path(personal: true)
...@@ -10,8 +10,9 @@ ...@@ -10,8 +10,9 @@
= render "projects/last_push" = render "projects/last_push"
%div{ class: container_class } %div{ class: container_class }
- if has_projects_or_name?(@projects, params) - if show_projects?(@projects, params)
= render 'dashboard/projects_head' = render 'dashboard/projects_head'
= render 'nav'
= render 'projects' = render 'projects'
- else - else
= render "zero_authorized_projects" = render "zero_authorized_projects"
- @no_container = true - @no_container = true
- page_title _('Branches') - page_title _('Branches')
<<<<<<< HEAD
- add_to_breadcrumbs(_('Repository'), project_tree_path(@project)) - add_to_breadcrumbs(_('Repository'), project_tree_path(@project))
=======
>>>>>>> b187a3f2bb00a0a0a6e5f8369edf2d6430a7af6e
%div{ class: container_class } %div{ class: container_class }
.top-area.adjust .top-area.adjust
......
...@@ -3,10 +3,13 @@ ...@@ -3,10 +3,13 @@
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout
- expanded = Rails.env.test? - expanded = Rails.env.test?
<<<<<<< HEAD
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= webpack_bundle_tag('common_vue') = webpack_bundle_tag('common_vue')
= webpack_bundle_tag('service_desk') = webpack_bundle_tag('service_desk')
=======
>>>>>>> b187a3f2bb00a0a0a6e5f8369edf2d6430a7af6e
.project-edit-container .project-edit-container
%section.settings.general-settings %section.settings.general-settings
.settings-header .settings-header
......
- access = note_max_access_for_user(note)
- if note.has_special_role?(Note::SpecialRole::FIRST_TIME_CONTRIBUTOR) - if note.has_special_role?(Note::SpecialRole::FIRST_TIME_CONTRIBUTOR)
%span.note-role.note-role-special.has-tooltip{ title: _("This is the author's first Merge Request to this project. Handle with care.") } %span.note-role.note-role-special.has-tooltip{ title: _("This is the author's first Merge Request to this project. Handle with care.") }
= issuable_first_contribution_icon = issuable_first_contribution_icon
- if access = note_max_access_for_user(note) - if access.nonzero?
%span.note-role.note-role-access= Gitlab::Access.human_access(access) %span.note-role.note-role-access= Gitlab::Access.human_access(access)
- if note.resolvable? - if note.resolvable?
......
- @no_container = true - @no_container = true
- page_title "Pipelines" - page_title "Pipelines"
<<<<<<< HEAD
= content_for :flash_message do = content_for :flash_message do
= render 'shared/shared_runners_minutes_limit', project: @project = render 'shared/shared_runners_minutes_limit', project: @project
=======
>>>>>>> b187a3f2bb00a0a0a6e5f8369edf2d6430a7af6e
%div{ 'class' => container_class } %div{ 'class' => container_class }
- if show_auto_devops_callout?(@project) - if show_auto_devops_callout?(@project)
......
...@@ -26,7 +26,8 @@ ...@@ -26,7 +26,8 @@
%strong Disable Auto DevOps %strong Disable Auto DevOps
%br %br
%span.descr %span.descr
An explicit <code>.gitlab-ci.yml</code> needs to be specified before you can begin using Continious Integration and Delivery. An explicit <code>.gitlab-ci.yml</code> needs to be specified before you can begin using Continuous Integration and Delivery.
.radio .radio
= form.label :enabled_nil do = form.label :enabled_nil do
= form.radio_button :enabled, '' = form.radio_button :enabled, ''
......
...@@ -10,10 +10,13 @@ ...@@ -10,10 +10,13 @@
= sortable_item(sort_title_priority, page_filter_path(sort: sort_value_priority, label: true), sorted_by) = sortable_item(sort_title_priority, page_filter_path(sort: sort_value_priority, label: true), sorted_by)
= sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date, label: true), sorted_by) = sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date, label: true), sorted_by)
= sortable_item(sort_title_recently_updated, page_filter_path(sort: sort_value_recently_updated, label: true), sorted_by) = sortable_item(sort_title_recently_updated, page_filter_path(sort: sort_value_recently_updated, label: true), sorted_by)
<<<<<<< HEAD
- if viewing_issues && (@project || @group)&.feature_available?(:issue_weights) - if viewing_issues && (@project || @group)&.feature_available?(:issue_weights)
= sortable_item(sort_title_weight, page_filter_path(sort: sort_value_weight, label: true), sorted_by) = sortable_item(sort_title_weight, page_filter_path(sort: sort_value_weight, label: true), sorted_by)
=======
>>>>>>> b187a3f2bb00a0a0a6e5f8369edf2d6430a7af6e
= sortable_item(sort_title_milestone, page_filter_path(sort: sort_value_milestone, label: true), sorted_by) = sortable_item(sort_title_milestone, page_filter_path(sort: sort_value_milestone, label: true), sorted_by)
= sortable_item(sort_title_due_date, page_filter_path(sort: sort_value_due_date, label: true), sorted_by) if viewing_issues = sortable_item(sort_title_due_date, page_filter_path(sort: sort_value_due_date, label: true), sorted_by) if viewing_issues
= sortable_item(sort_title_popularity, page_filter_path(sort: sort_value_popularity, label: true), sorted_by) = sortable_item(sort_title_popularity, page_filter_path(sort: sort_value_popularity, label: true), sorted_by)
......
...@@ -12,7 +12,10 @@ ...@@ -12,7 +12,10 @@
%script#js-board-template{ type: "text/x-template" }= render "shared/boards/components/board" %script#js-board-template{ type: "text/x-template" }= render "shared/boards/components/board"
%script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal %script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal
<<<<<<< HEAD
%script#js-board-promotion{ type: "text/x-template" }= render "shared/promotions/promote_issue_board" %script#js-board-promotion{ type: "text/x-template" }= render "shared/promotions/promote_issue_board"
=======
>>>>>>> b187a3f2bb00a0a0a6e5f8369edf2d6430a7af6e
.hidden-xs.hidden-sm .hidden-xs.hidden-sm
= render 'shared/issuable/search_bar', type: :boards, board: board = render 'shared/issuable/search_bar', type: :boards, board: board
......
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
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
%li.filter-dropdown-item{ class: ('js-current-user' if user == current_user) } %li.filter-dropdown-item{ class: ('js-current-user' if user == current_user) }
%button.btn.btn-link.dropdown-user{ type: :button } %button.btn.btn-link.dropdown-user{ type: :button }
.avatar-container.s40 .avatar-container.s40
= user_avatar_without_link(user: user, lazy: avatar[:lazy], url: avatar[:url], size: 40, has_tooltip: false).gsub('/images/{{avatar_url}}','{{avatar_url}}').html_safe = user_avatar_without_link(user: user, lazy: avatar[:lazy], url: avatar[:url], size: 40, has_tooltip: false)
.dropdown-user-details .dropdown-user-details
%span %span
= user.name = user.name
......
---
title: Removes cycle analytics service and store from global namespace
merge_request:
author:
type: other
---
title: Notes will not show an empty bubble when the author isn't a member.
merge_request: 14450
author:
type: fixed
---
title: Some checks in `rake gitlab:check` were failling with 'undefined method `run_command`'
merge_request: 14469
author:
type: fixed
---
title: Make locked setting of Runner to not affect jobs scheduling
merge_request: 14483
author:
type: fixed
---
title: Added tabs to dashboard/projects to easily switch to personal projects
merge_request:
author:
type: added
---
title: Re-allow `name` attribute on user-provided anchor HTML
merge_request:
author:
type: fixed
...@@ -33,7 +33,7 @@ class MigrateUserExternalMailData < ActiveRecord::Migration ...@@ -33,7 +33,7 @@ class MigrateUserExternalMailData < ActiveRecord::Migration
SELECT true SELECT true
FROM user_synced_attributes_metadata FROM user_synced_attributes_metadata
WHERE user_id = users.id WHERE user_id = users.id
AND provider = users.email_provider OR (provider IS NULL AND users.email_provider IS NULL) AND (provider = users.email_provider OR (provider IS NULL AND users.email_provider IS NULL))
) )
AND id BETWEEN #{start_id} AND #{end_id} AND id BETWEEN #{start_id} AND #{end_id}
EOF EOF
......
...@@ -33,7 +33,7 @@ class PostDeployMigrateUserExternalMailData < ActiveRecord::Migration ...@@ -33,7 +33,7 @@ class PostDeployMigrateUserExternalMailData < ActiveRecord::Migration
SELECT true SELECT true
FROM user_synced_attributes_metadata FROM user_synced_attributes_metadata
WHERE user_id = users.id WHERE user_id = users.id
AND provider = users.email_provider OR (provider IS NULL AND users.email_provider IS NULL) AND (provider = users.email_provider OR (provider IS NULL AND users.email_provider IS NULL))
) )
AND id BETWEEN #{start_id} AND #{end_id} AND id BETWEEN #{start_id} AND #{end_id}
EOF EOF
......
...@@ -215,14 +215,29 @@ same time will ensure that both existing and new data is migrated. ...@@ -215,14 +215,29 @@ same time will ensure that both existing and new data is migrated.
In the next release we can remove the `after_commit` hooks and related code. We In the next release we can remove the `after_commit` hooks and related code. We
will also need to add a post-deployment migration that consumes any remaining will also need to add a post-deployment migration that consumes any remaining
jobs. Such a migration would look like this: jobs and manually run on any un-migrated rows. Such a migration would look like
this:
```ruby ```ruby
class ConsumeRemainingExtractServicesUrlJobs < ActiveRecord::Migration class ConsumeRemainingExtractServicesUrlJobs < ActiveRecord::Migration
disable_ddl_transaction! disable_ddl_transaction!
class Service < ActiveRecord::Base
include ::EachBatch
self.table_name = 'services'
end
def up def up
# This must be included
Gitlab::BackgroundMigration.steal('ExtractServicesUrl') Gitlab::BackgroundMigration.steal('ExtractServicesUrl')
# This should be included, but can be skipped - see below
Service.where(url: nil).each_batch(of: 50) do |batch|
range = batch.pluck('MIN(id)', 'MAX(id)').first
Gitlab::BackgroundMigration::ExtractServicesUrl.new.perform(*range)
end
end end
def down def down
...@@ -230,6 +245,15 @@ class ConsumeRemainingExtractServicesUrlJobs < ActiveRecord::Migration ...@@ -230,6 +245,15 @@ class ConsumeRemainingExtractServicesUrlJobs < ActiveRecord::Migration
end end
``` ```
The final step runs for any un-migrated rows after all of the jobs have been
processed. This is in case a Sidekiq process running the background migrations
received SIGKILL, leading to the jobs being lost. (See
[more reliable Sidekiq queue][reliable-sidekiq] for more information.)
If the application does not depend on the data being 100% migrated (for
instance, the data is advisory, and not mission-critical), then this final step
can be skipped.
This migration will then process any jobs for the ExtractServicesUrl migration This migration will then process any jobs for the ExtractServicesUrl migration
and continue once all jobs have been processed. Once done you can safely remove and continue once all jobs have been processed. Once done you can safely remove
the `services.properties` column. the `services.properties` column.
...@@ -254,6 +278,9 @@ for more details. ...@@ -254,6 +278,9 @@ for more details.
1. Make sure that background migration jobs are idempotent. 1. Make sure that background migration jobs are idempotent.
1. Make sure that tests you write are not false positives. 1. Make sure that tests you write are not false positives.
1. Make sure that if the data being migrated is critical and cannot be lost, the
clean-up migration also checks the final state of the data before completing.
[migrations-readme]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/migrations/README.md [migrations-readme]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/migrations/README.md
[issue-rspec-hooks]: https://gitlab.com/gitlab-org/gitlab-ce/issues/35351 [issue-rspec-hooks]: https://gitlab.com/gitlab-org/gitlab-ce/issues/35351
[reliable-sidekiq]: https://gitlab.com/gitlab-org/gitlab-ce/issues/36791
...@@ -148,13 +148,36 @@ helm install --name gitlab --set baseDomain=gitlab.io,baseIP=1.1.1.1,gitlab=ee,g ...@@ -148,13 +148,36 @@ helm install --name gitlab --set baseDomain=gitlab.io,baseIP=1.1.1.1,gitlab=ee,g
## Updating GitLab using the Helm Chart ## Updating GitLab using the Helm Chart
>**Note**: If you are upgrading from a previous version to 0.1.35 or above, you will need to change the access mode values for GitLab's storage. To do this, set the following in `values.yaml` or on the CLI:
```
gitlabDataAccessMode=ReadWriteMany
gitlabRegistryAccessMode=ReadWriteMany
gitlabConfigAccessMode=ReadWriteMany
```
Once your GitLab Chart is installed, configuration changes and chart updates Once your GitLab Chart is installed, configuration changes and chart updates
should we done using `helm upgrade`: should be done using `helm upgrade`:
```bash
helm upgrade -f values.yaml gitlab gitlab/gitlab-omnibus
```
## Upgrading from CE to EE using the Helm Chart
If you have installed the Community Edition using this chart, upgrading to Enterprise Edition is easy.
If you are using a `values.yaml` file to specify the configuration options, edit the file and set `gitlab=ee`. If you would like to run a specific version of GitLab EE, set `gitlabEEImage` to be the desired GitLab [docker image](https://hub.docker.com/r/gitlab/gitlab-ee/tags/). Then you can use `helm upgrade` to update your GitLab instance to EE:
```bash ```bash
helm upgrade -f values.yaml gitlab gitlab/gitlab-omnibus helm upgrade -f values.yaml gitlab gitlab/gitlab-omnibus
``` ```
You can also upgrade and specify these options via the command line:
```bash
helm upgrade gitlab --set gitlab=ee,gitlabEEImage=gitlab/gitlab-ee:9.5.5-ee.0 gitlab/gitlab-omnibus
```
## Uninstalling GitLab using the Helm Chart ## Uninstalling GitLab using the Helm Chart
To uninstall the GitLab Chart, run the following: To uninstall the GitLab Chart, run the following:
...@@ -163,5 +186,13 @@ To uninstall the GitLab Chart, run the following: ...@@ -163,5 +186,13 @@ To uninstall the GitLab Chart, run the following:
helm delete gitlab helm delete gitlab
``` ```
## Troubleshooting
### Storage errors when updating `gitlab-omnibus` versions prior to 0.1.35
Users upgrading `gitlab-omnibus` from a version prior to 0.1.35, may see an error like: `Error: UPGRADE FAILED: PersistentVolumeClaim "gitlab-gitlab-config-storage" is invalid: spec: Forbidden: field is immutable after creation`.
This is due to a change in the access mode for GitLab storage in version 0.1.35. To successfully upgrade, the access mode flags must be set to `ReadWriteMany` as detailed in the [update section](#updating-gitlab-using-the-helm-chart).
[kube-srv]: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services---service-types [kube-srv]: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services---service-types
[storageclass]: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#storageclasses [storageclass]: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#storageclasses
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
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
...@@ -45,8 +45,9 @@ module Banzai ...@@ -45,8 +45,9 @@ module Banzai
whitelist[:elements].push('abbr') whitelist[:elements].push('abbr')
whitelist[:attributes]['abbr'] = %w(title) whitelist[:attributes]['abbr'] = %w(title)
# Disallow `name` attribute globally # Disallow `name` attribute globally, allow on `a`
whitelist[:attributes][:all].delete('name') whitelist[:attributes][:all].delete('name')
whitelist[:attributes]['a'].push('name')
# Allow any protocol in `a` elements... # Allow any protocol in `a` elements...
whitelist[:protocols].delete('a') whitelist[:protocols].delete('a')
......
...@@ -386,7 +386,13 @@ module Gitlab ...@@ -386,7 +386,13 @@ module Gitlab
options[:limit] ||= 0 options[:limit] ||= 0
options[:offset] ||= 0 options[:offset] ||= 0
raw_log(options).map { |c| Commit.decorate(self, c) } gitaly_migrate(:find_commits) do |is_enabled|
if is_enabled
gitaly_commit_client.find_commits(options)
else
raw_log(options).map { |c| Commit.decorate(self, c) }
end
end
end end
# Used in gitaly-ruby # Used in gitaly-ruby
......
...@@ -228,10 +228,18 @@ module Gitlab ...@@ -228,10 +228,18 @@ module Gitlab
path.read.chomp path.read.chomp
end end
def self.timestamp(t)
Google::Protobuf::Timestamp.new(seconds: t.to_i)
end
def self.encode(s) def self.encode(s)
s.dup.force_encoding(Encoding::ASCII_8BIT) s.dup.force_encoding(Encoding::ASCII_8BIT)
end end
def self.encode_repeated(a)
Google::Protobuf::RepeatedField.new(:bytes, a.map { |s| self.encode(s) } )
end
# Count a stack. Used for n+1 detection # Count a stack. Used for n+1 detection
def self.count_stack def self.count_stack
return unless RequestStore.active? return unless RequestStore.active?
......
...@@ -230,6 +230,26 @@ module Gitlab ...@@ -230,6 +230,26 @@ module Gitlab
GitalyClient.call(@repository.storage, :commit_service, :commit_stats, request) GitalyClient.call(@repository.storage, :commit_service, :commit_stats, request)
end end
def find_commits(options)
request = Gitaly::FindCommitsRequest.new(
repository: @gitaly_repo,
limit: options[:limit],
offset: options[:offset],
follow: options[:follow],
skip_merges: options[:skip_merges],
disable_walk: options[:disable_walk]
)
request.after = GitalyClient.timestamp(options[:after]) if options[:after]
request.before = GitalyClient.timestamp(options[:before]) if options[:before]
request.revision = GitalyClient.encode(options[:ref]) if options[:ref]
request.paths = GitalyClient.encode_repeated(Array(options[:path])) if options[:path].present?
response = GitalyClient.call(@repository.storage, :commit_service, :find_commits, request)
consume_commits_response(response)
end
private private
def call_commit_diff(request_params, options = {}) def call_commit_diff(request_params, options = {})
......
module Gitlab
module Markdown
class Pipeline
def self.[](name)
name ||= :full
const_get("#{name.to_s.camelize}Pipeline")
end
def self.filters
[]
end
def self.transform_context(context)
context
end
def self.html_pipeline
@html_pipeline ||= HTML::Pipeline.new(filters)
end
class << self
%i(call to_document to_html).each do |meth|
define_method(meth) do |text, context|
context = transform_context(context)
html_pipeline.__send__(meth, text, context) # rubocop:disable GitlabSecurity/PublicSend
end
end
end
end
end
end
...@@ -9,7 +9,7 @@ module SystemCheck ...@@ -9,7 +9,7 @@ module SystemCheck
end end
def self.current_version def self.current_version
@current_version ||= Gitlab::VersionInfo.parse(run_command(%W(#{Gitlab.config.git.bin_path} --version))) @current_version ||= Gitlab::VersionInfo.parse(Gitlab::TaskHelpers.run_command(%W(#{Gitlab.config.git.bin_path} --version)))
end end
def check? def check?
......
...@@ -9,7 +9,7 @@ module SystemCheck ...@@ -9,7 +9,7 @@ module SystemCheck
end end
def self.current_version def self.current_version
@current_version ||= Gitlab::VersionInfo.parse(run_command(%w(ruby --version))) @current_version ||= Gitlab::VersionInfo.parse(Gitlab::TaskHelpers.run_command(%w(ruby --version)))
end end
def check? def check?
......
...@@ -3,15 +3,19 @@ ...@@ -3,15 +3,19 @@
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
# Use long options (e.g. --header instead of -H) for curl examples in documentation. # Use long options (e.g. --header instead of -H) for curl examples in documentation.
grep --extended-regexp --recursive --color=auto 'curl (.+ )?-[^- ].*' doc/ echo 'Checking for curl short options...'
grep --extended-regexp --recursive --color=auto 'curl (.+ )?-[^- ].*' doc/ >/dev/null 2>&1
if [ $? == 0 ] if [ $? == 0 ]
then then
echo '✖ ERROR: Short options should not be used in documentation!' >&2 echo '✖ ERROR: Short options for curl should not be used in documentation!
Use long options (e.g., --header instead of -H):' >&2
grep --extended-regexp --recursive --color=auto 'curl (.+ )?-[^- ].*' doc/
exit 1 exit 1
fi fi
# Ensure that the CHANGELOG.md does not contain duplicate versions # Ensure that the CHANGELOG.md does not contain duplicate versions
DUPLICATE_CHANGELOG_VERSIONS=$(grep --extended-regexp '^## .+' CHANGELOG.md | sed -E 's| \(.+\)||' | sort -r | uniq -d) DUPLICATE_CHANGELOG_VERSIONS=$(grep --extended-regexp '^## .+' CHANGELOG.md | sed -E 's| \(.+\)||' | sort -r | uniq -d)
echo 'Checking for CHANGELOG.md duplicate entries...'
if [ "${DUPLICATE_CHANGELOG_VERSIONS}" != "" ] if [ "${DUPLICATE_CHANGELOG_VERSIONS}" != "" ]
then then
echo '✖ ERROR: Duplicate versions in CHANGELOG.md:' >&2 echo '✖ ERROR: Duplicate versions in CHANGELOG.md:' >&2
...@@ -19,5 +23,15 @@ then ...@@ -19,5 +23,15 @@ then
exit 1 exit 1
fi fi
# Make sure no files in doc/ are executable
EXEC_PERM_COUNT=$(find doc/ app/ -type f -perm 755 | wc -l)
echo 'Checking for executable permissions...'
if [ "${EXEC_PERM_COUNT}" -ne 0 ]
then
echo '✖ ERROR: Executable permissions should not be used in documentation! Use `chmod 644` to the files in question:' >&2
find doc/ app/ -type f -perm 755
exit 1
fi
echo "✔ Linting passed" echo "✔ Linting passed"
exit 0 exit 0
...@@ -50,6 +50,25 @@ feature 'Dashboard Projects' do ...@@ -50,6 +50,25 @@ feature 'Dashboard Projects' do
end end
end end
context 'when on Your projects tab' do
it 'shows all projects by default' do
visit dashboard_projects_path
expect(page).to have_content(project.name)
end
it 'shows personal projects on personal projects tab', :js do
project3 = create(:project, namespace: user.namespace)
visit dashboard_projects_path
click_link 'Personal'
expect(page).not_to have_content(project.name)
expect(page).to have_content(project3.name)
end
end
context 'when on Starred projects tab' do context 'when on Starred projects tab' do
it 'shows only starred projects' do it 'shows only starred projects' do
user.toggle_star(project2) user.toggle_star(project2)
......
...@@ -26,12 +26,13 @@ describe AvatarsHelper do ...@@ -26,12 +26,13 @@ describe AvatarsHelper do
subject { helper.user_avatar_without_link(options) } subject { helper.user_avatar_without_link(options) }
it 'displays user avatar' do it 'displays user avatar' do
is_expected.to eq image_tag( is_expected.to eq tag(
LazyImageTagHelper.placeholder_image, :img,
class: 'avatar s16 has-tooltip lazy',
alt: "#{user.name}'s avatar", alt: "#{user.name}'s avatar",
title: user.name, src: avatar_icon(user, 16),
data: { container: 'body', src: avatar_icon(user, 16) } data: { container: 'body' },
class: 'avatar s16 has-tooltip',
title: user.name
) )
end end
...@@ -39,12 +40,13 @@ describe AvatarsHelper do ...@@ -39,12 +40,13 @@ describe AvatarsHelper do
let(:options) { { user: user, css_class: '.cat-pics' } } let(:options) { { user: user, css_class: '.cat-pics' } }
it 'uses provided css_class' do it 'uses provided css_class' do
is_expected.to eq image_tag( is_expected.to eq tag(
LazyImageTagHelper.placeholder_image, :img,
class: "avatar s16 #{options[:css_class]} has-tooltip lazy",
alt: "#{user.name}'s avatar", alt: "#{user.name}'s avatar",
title: user.name, src: avatar_icon(user, 16),
data: { container: 'body', src: avatar_icon(user, 16) } data: { container: 'body' },
class: "avatar s16 #{options[:css_class]} has-tooltip",
title: user.name
) )
end end
end end
...@@ -53,12 +55,13 @@ describe AvatarsHelper do ...@@ -53,12 +55,13 @@ describe AvatarsHelper do
let(:options) { { user: user, size: 99 } } let(:options) { { user: user, size: 99 } }
it 'uses provided size' do it 'uses provided size' do
is_expected.to eq image_tag( is_expected.to eq tag(
LazyImageTagHelper.placeholder_image, :img,
class: "avatar s#{options[:size]} has-tooltip lazy",
alt: "#{user.name}'s avatar", alt: "#{user.name}'s avatar",
title: user.name, src: avatar_icon(user, options[:size]),
data: { container: 'body', src: avatar_icon(user, options[:size]) } data: { container: 'body' },
class: "avatar s#{options[:size]} has-tooltip",
title: user.name
) )
end end
end end
...@@ -67,12 +70,28 @@ describe AvatarsHelper do ...@@ -67,12 +70,28 @@ describe AvatarsHelper do
let(:options) { { user: user, url: '/over/the/rainbow.png' } } let(:options) { { user: user, url: '/over/the/rainbow.png' } }
it 'uses provided url' do it 'uses provided url' do
is_expected.to eq image_tag( is_expected.to eq tag(
LazyImageTagHelper.placeholder_image, :img,
class: 'avatar s16 has-tooltip lazy',
alt: "#{user.name}'s avatar", alt: "#{user.name}'s avatar",
title: user.name, src: options[:url],
data: { container: 'body', src: options[:url] } data: { container: 'body' },
class: "avatar s16 has-tooltip",
title: user.name
)
end
end
context 'with lazy parameter' do
let(:options) { { user: user, lazy: true } }
it 'adds `lazy` class to class list, sets `data-src` with avatar URL and `src` with placeholder image' do
is_expected.to eq tag(
:img,
alt: "#{user.name}'s avatar",
src: LazyImageTagHelper.placeholder_image,
data: { container: 'body', src: avatar_icon(user, 16) },
class: "avatar s16 has-tooltip lazy",
title: user.name
) )
end end
end end
...@@ -82,12 +101,13 @@ describe AvatarsHelper do ...@@ -82,12 +101,13 @@ describe AvatarsHelper do
let(:options) { { user: user, has_tooltip: true } } let(:options) { { user: user, has_tooltip: true } }
it 'adds has-tooltip' do it 'adds has-tooltip' do
is_expected.to eq image_tag( is_expected.to eq tag(
LazyImageTagHelper.placeholder_image, :img,
class: 'avatar s16 has-tooltip lazy',
alt: "#{user.name}'s avatar", alt: "#{user.name}'s avatar",
title: user.name, src: avatar_icon(user, 16),
data: { container: 'body', src: avatar_icon(user, 16) } data: { container: 'body' },
class: "avatar s16 has-tooltip",
title: user.name
) )
end end
end end
...@@ -96,12 +116,12 @@ describe AvatarsHelper do ...@@ -96,12 +116,12 @@ describe AvatarsHelper do
let(:options) { { user: user, has_tooltip: false } } let(:options) { { user: user, has_tooltip: false } }
it 'does not add has-tooltip or data container' do it 'does not add has-tooltip or data container' do
is_expected.to eq image_tag( is_expected.to eq tag(
LazyImageTagHelper.placeholder_image, :img,
class: 'avatar s16 lazy',
alt: "#{user.name}'s avatar", alt: "#{user.name}'s avatar",
title: user.name, src: avatar_icon(user, 16),
data: { src: avatar_icon(user, 16) } class: "avatar s16",
title: user.name
) )
end end
end end
...@@ -114,23 +134,25 @@ describe AvatarsHelper do ...@@ -114,23 +134,25 @@ describe AvatarsHelper do
let(:options) { { user: user, user_name: 'Tinky Winky' } } let(:options) { { user: user, user_name: 'Tinky Winky' } }
it 'prefers user parameter' do it 'prefers user parameter' do
is_expected.to eq image_tag( is_expected.to eq tag(
LazyImageTagHelper.placeholder_image, :img,
class: 'avatar s16 has-tooltip lazy',
alt: "#{user.name}'s avatar", alt: "#{user.name}'s avatar",
title: user.name, src: avatar_icon(user, 16),
data: { container: 'body', src: avatar_icon(user, 16) } data: { container: 'body' },
class: "avatar s16 has-tooltip",
title: user.name
) )
end end
end end
it 'uses user_name and user_email parameter if user is not present' do it 'uses user_name and user_email parameter if user is not present' do
is_expected.to eq image_tag( is_expected.to eq tag(
LazyImageTagHelper.placeholder_image, :img,
class: 'avatar s16 has-tooltip lazy',
alt: "#{options[:user_name]}'s avatar", alt: "#{options[:user_name]}'s avatar",
title: options[:user_name], src: avatar_icon(options[:user_email], 16),
data: { container: 'body', src: avatar_icon(options[:user_email], 16) } data: { container: 'body' },
class: "avatar s16 has-tooltip",
title: options[:user_name]
) )
end end
end end
......
...@@ -429,22 +429,26 @@ describe ProjectsHelper do ...@@ -429,22 +429,26 @@ describe ProjectsHelper do
end end
end end
describe '#has_projects_or_name?' do describe '#show_projects' do
let(:projects) do let(:projects) do
create(:project) create(:project)
Project.all Project.all
end end
it 'returns true when there are projects' do it 'returns true when there are projects' do
expect(helper.has_projects_or_name?(projects, {})).to eq(true) expect(helper.show_projects?(projects, {})).to eq(true)
end end
it 'returns true when there are no projects but a name is given' do it 'returns true when there are no projects but a name is given' do
expect(helper.has_projects_or_name?(Project.none, name: 'foo')).to eq(true) expect(helper.show_projects?(Project.none, name: 'foo')).to eq(true)
end
it 'returns true when there are no projects but personal is present' do
expect(helper.show_projects?(Project.none, personal: 'true')).to eq(true)
end end
it 'returns false when there are no projects and there is no name' do it 'returns false when there are no projects and there is no name' do
expect(helper.has_projects_or_name?(Project.none, {})).to eq(false) expect(helper.show_projects?(Project.none, {})).to eq(false)
end end
end end
......
import Vue from 'vue'; import Vue from 'vue';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import limitWarningComp from '~/cycle_analytics/components/limit_warning_component'; import limitWarningComp from '~/cycle_analytics/components/limit_warning_component.vue';
Vue.use(Translate); Vue.use(Translate);
......
...@@ -73,6 +73,12 @@ describe('Fly out sidebar navigation', () => { ...@@ -73,6 +73,12 @@ describe('Fly out sidebar navigation', () => {
).toBe(0); ).toBe(0);
}); });
it('returns 0 if mousePos is empty', () => {
expect(
getHideSubItemsInterval(),
).toBe(0);
});
it('returns 0 when mouse above sub-items', () => { it('returns 0 when mouse above sub-items', () => {
showSubLevelItems(el); showSubLevelItems(el);
documentMouseMove({ documentMouseMove({
......
import { isSticky } from '~/lib/utils/sticky'; import { isSticky } from '~/lib/utils/sticky';
describe('sticky', () => { describe('sticky', () => {
const el = { let el;
offsetTop: 0,
classList: {},
};
beforeEach(() => { beforeEach(() => {
el.offsetTop = 0; document.body.innerHTML += `
el.classList.add = jasmine.createSpy('spy'); <div class="parent">
el.classList.remove = jasmine.createSpy('spy'); <div id="js-sticky"></div>
</div>
`;
el = document.getElementById('js-sticky');
}); });
describe('classList.remove', () => { afterEach(() => {
it('does not call classList.remove when stuck', () => { el.parentNode.remove();
isSticky(el, 0, 0); });
describe('when stuck', () => {
it('does not remove is-stuck class', () => {
isSticky(el, 0, el.offsetTop);
isSticky(el, 0, el.offsetTop);
expect( expect(
el.classList.remove, el.classList.contains('is-stuck'),
).not.toHaveBeenCalled(); ).toBeTruthy();
}); });
it('calls classList.remove when not stuck', () => { it('adds is-stuck class', () => {
el.offsetTop = 10; isSticky(el, 0, el.offsetTop);
isSticky(el, 0, 0);
expect( expect(
el.classList.remove, el.classList.contains('is-stuck'),
).toHaveBeenCalledWith('is-stuck'); ).toBeTruthy();
});
it('inserts placeholder element', () => {
isSticky(el, 0, el.offsetTop, true);
expect(
document.querySelector('.sticky-placeholder'),
).not.toBeNull();
}); });
}); });
describe('classList.add', () => { describe('when not stuck', () => {
it('calls classList.add when stuck', () => { it('removes is-stuck class', () => {
spyOn(el.classList, 'remove').and.callThrough();
isSticky(el, 0, el.offsetTop);
isSticky(el, 0, 0); isSticky(el, 0, 0);
expect( expect(
el.classList.add, el.classList.remove,
).toHaveBeenCalledWith('is-stuck'); ).toHaveBeenCalledWith('is-stuck');
expect(
el.classList.contains('is-stuck'),
).toBeFalsy();
}); });
it('does not call classList.add when not stuck', () => { it('does not add is-stuck class', () => {
el.offsetTop = 10;
isSticky(el, 0, 0); isSticky(el, 0, 0);
expect( expect(
el.classList.add, el.classList.contains('is-stuck'),
).not.toHaveBeenCalled(); ).toBeFalsy();
});
it('removes placeholder', () => {
isSticky(el, 0, el.offsetTop, true);
isSticky(el, 0, 0, true);
expect(
document.querySelector('.sticky-placeholder'),
).toBeNull();
}); });
}); });
}); });
...@@ -47,9 +47,11 @@ describe Banzai::Filter::SanitizationFilter do ...@@ -47,9 +47,11 @@ describe Banzai::Filter::SanitizationFilter do
describe 'custom whitelist' do describe 'custom whitelist' do
it 'customizes the whitelist only once' do it 'customizes the whitelist only once' do
instance = described_class.new('Foo') instance = described_class.new('Foo')
control_count = instance.whitelist[:transformers].size
3.times { instance.whitelist } 3.times { instance.whitelist }
expect(instance.whitelist[:transformers].size).to eq 5 expect(instance.whitelist[:transformers].size).to eq control_count
end end
it 'sanitizes `class` attribute from all elements' do it 'sanitizes `class` attribute from all elements' do
...@@ -101,16 +103,18 @@ describe Banzai::Filter::SanitizationFilter do ...@@ -101,16 +103,18 @@ describe Banzai::Filter::SanitizationFilter do
expect(filter(act).to_html).to eq exp expect(filter(act).to_html).to eq exp
end end
it 'disallows the `name` attribute globally' do it 'disallows the `name` attribute globally, allows on `a`' do
html = <<~HTML html = <<~HTML
<img name="getElementById" src=""> <img name="getElementById" src="">
<span name="foo" class="bar">Hi</span> <span name="foo" class="bar">Hi</span>
<a name="foo" class="bar">Bye</a>
HTML HTML
doc = filter(html) doc = filter(html)
expect(doc.at_css('img')).not_to have_attribute('name') expect(doc.at_css('img')).not_to have_attribute('name')
expect(doc.at_css('span')).not_to have_attribute('name') expect(doc.at_css('span')).not_to have_attribute('name')
expect(doc.at_css('a')).to have_attribute('name')
end end
it 'allows `summary` elements' do it 'allows `summary` elements' do
......
...@@ -181,7 +181,7 @@ describe Gitlab::Git::Commit, seed_helper: true do ...@@ -181,7 +181,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
end end
end end
describe '.where' do shared_examples '.where' do
context 'path is empty string' do context 'path is empty string' do
subject do subject do
commits = described_class.where( commits = described_class.where(
...@@ -279,6 +279,14 @@ describe Gitlab::Git::Commit, seed_helper: true do ...@@ -279,6 +279,14 @@ describe Gitlab::Git::Commit, seed_helper: true do
end end
end end
describe '.where with gitaly' do
it_should_behave_like '.where'
end
describe '.where without gitaly', skip_gitaly_mock: true do
it_should_behave_like '.where'
end
describe '.between' do describe '.between' do
subject do subject do
commits = described_class.between(repository, SeedRepo::Commit::PARENT_ID, SeedRepo::Commit::ID) commits = described_class.between(repository, SeedRepo::Commit::PARENT_ID, SeedRepo::Commit::ID)
......
require 'spec_helper'
describe SystemCheck::BaseCheck do
context 'helpers on instance level' do
it 'responds to SystemCheck::Helpers methods' do
expect(subject).to respond_to :fix_and_rerun, :for_more_information, :see_installation_guide_section,
:finished_checking, :start_checking, :try_fixing_it, :sanitized_message, :should_sanitize?, :omnibus_gitlab?,
:sudo_gitlab
end
it 'responds to Gitlab::TaskHelpers methods' do
expect(subject).to respond_to :ask_to_continue, :os_name, :prompt, :run_and_match, :run_command,
:run_command!, :uid_for, :gid_for, :gitlab_user, :gitlab_user?, :warn_user_is_not_gitlab, :all_repos,
:repository_storage_paths_args, :user_home, :checkout_or_clone_version, :clone_repo, :checkout_version
end
end
end
...@@ -183,75 +183,42 @@ describe Ci::Runner do ...@@ -183,75 +183,42 @@ describe Ci::Runner do
end end
end end
context 'when runner is locked' do context 'when runner is shared' do
before do before do
runner.locked = true runner.is_shared = true
build.project.runners = []
end end
shared_examples 'locked build picker' do it 'can handle builds' do
context 'when runner cannot pick untagged jobs' do expect(runner.can_pick?(build)).to be_truthy
before do end
runner.run_untagged = false
end
it 'cannot handle builds without tags' do context 'when runner is locked' do
expect(runner.can_pick?(build)).to be_falsey before do
end runner.locked = true
end end
context 'when having runner tags' do it 'can handle builds' do
before do expect(runner.can_pick?(build)).to be_truthy
runner.tag_list = %w(bb cc)
end
it 'cannot handle it for builds without matching tags' do
build.tag_list = ['aa']
expect(runner.can_pick?(build)).to be_falsey
end
end end
end end
end
context 'when serving the same project' do context 'when runner is not shared' do
it 'can handle it' do context 'when runner is assigned to a project' do
it 'can handle builds' do
expect(runner.can_pick?(build)).to be_truthy expect(runner.can_pick?(build)).to be_truthy
end end
it_behaves_like 'locked build picker'
context 'when having runner tags' do
before do
runner.tag_list = %w(bb cc)
build.tag_list = ['bb']
end
it 'can handle it for matching tags' do
expect(runner.can_pick?(build)).to be_truthy
end
end
end end
context 'serving a different project' do context 'when runner is not assigned to a project' do
before do before do
runner.runner_projects.destroy_all build.project.runners = []
end end
it 'cannot handle it' do it 'cannot handle builds' do
expect(runner.can_pick?(build)).to be_falsey expect(runner.can_pick?(build)).to be_falsey
end end
it_behaves_like 'locked build picker'
context 'when having runner tags' do
before do
runner.tag_list = %w(bb cc)
build.tag_list = ['bb']
end
it 'cannot handle it for matching tags' do
expect(runner.can_pick?(build)).to be_falsey
end
end
end end
end end
......
...@@ -156,10 +156,17 @@ describe ProjectPolicy do ...@@ -156,10 +156,17 @@ describe ProjectPolicy do
end end
end end
end end
<<<<<<< HEAD
context 'abilities for non-public projects' do context 'abilities for non-public projects' do
let(:project) { create(:project, namespace: owner.namespace) } let(:project) { create(:project, namespace: owner.namespace) }
=======
context 'abilities for non-public projects' do
let(:project) { create(:project, namespace: owner.namespace) }
>>>>>>> b187a3f2bb00a0a0a6e5f8369edf2d6430a7af6e
subject { described_class.new(nil, project) } subject { described_class.new(nil, project) }
it { is_expected.to be_banned } it { is_expected.to be_banned }
...@@ -232,11 +239,19 @@ describe ProjectPolicy do ...@@ -232,11 +239,19 @@ describe ProjectPolicy do
end end
end end
end end
<<<<<<< HEAD
shared_examples 'project policies as developer' do
context 'abilities for non-public projects' do
let(:project) { create(:project, namespace: owner.namespace) }
=======
shared_examples 'project policies as developer' do shared_examples 'project policies as developer' do
context 'abilities for non-public projects' do context 'abilities for non-public projects' do
let(:project) { create(:project, namespace: owner.namespace) } let(:project) { create(:project, namespace: owner.namespace) }
>>>>>>> b187a3f2bb00a0a0a6e5f8369edf2d6430a7af6e
subject { described_class.new(developer, project) } subject { described_class.new(developer, project) }
it do it do
...@@ -266,11 +281,19 @@ describe ProjectPolicy do ...@@ -266,11 +281,19 @@ describe ProjectPolicy do
end end
end end
end end
<<<<<<< HEAD
shared_examples 'project policies as owner' do
context 'abilities for non-public projects' do
let(:project) { create(:project, namespace: owner.namespace) }
=======
shared_examples 'project policies as owner' do shared_examples 'project policies as owner' do
context 'abilities for non-public projects' do context 'abilities for non-public projects' do
let(:project) { create(:project, namespace: owner.namespace) } let(:project) { create(:project, namespace: owner.namespace) }
>>>>>>> b187a3f2bb00a0a0a6e5f8369edf2d6430a7af6e
subject { described_class.new(owner, project) } subject { described_class.new(owner, project) }
it do it do
...@@ -308,6 +331,7 @@ describe ProjectPolicy do ...@@ -308,6 +331,7 @@ describe ProjectPolicy do
it_behaves_like 'project policies as master' it_behaves_like 'project policies as master'
it_behaves_like 'project policies as owner' it_behaves_like 'project policies as owner'
it_behaves_like 'project policies as admin' it_behaves_like 'project policies as admin'
<<<<<<< HEAD
context 'EE' do context 'EE' do
let(:additional_guest_permissions) { [:read_issue_link] } let(:additional_guest_permissions) { [:read_issue_link] }
...@@ -361,4 +385,6 @@ describe ProjectPolicy do ...@@ -361,4 +385,6 @@ describe ProjectPolicy do
end end
end end
end end
=======
>>>>>>> b187a3f2bb00a0a0a6e5f8369edf2d6430a7af6e
end end
...@@ -71,8 +71,16 @@ RSpec.configure do |config| ...@@ -71,8 +71,16 @@ RSpec.configure do |config|
config.infer_spec_type_from_file_location! config.infer_spec_type_from_file_location!
config.define_derived_metadata(file_path: %r{/spec/requests/(ci/)?api/}) do |metadata| config.define_derived_metadata(file_path: %r{/spec/}) do |metadata|
metadata[:api] = true location = metadata[:location]
metadata[:api] = true if location =~ %r{/spec/requests/api/}
# do not overwrite type if it's already set
next if metadata.key?(:type)
match = location.match(%r{/spec/([^/]+)/})
metadata[:type] = match[1].singularize.to_sym if match
end end
config.raise_errors_for_deprecations! config.raise_errors_for_deprecations!
......
...@@ -75,4 +75,24 @@ describe Gitlab::TaskHelpers do ...@@ -75,4 +75,24 @@ describe Gitlab::TaskHelpers do
subject.checkout_version(tag, clone_path) subject.checkout_version(tag, clone_path)
end end
end end
describe '#run_command' do
it 'runs command and return the output' do
expect(subject.run_command(%w(echo it works!))).to eq("it works!\n")
end
it 'returns empty string when command doesnt exist' do
expect(subject.run_command(%w(nonexistentcommand with arguments))).to eq('')
end
end
describe '#run_command!' do
it 'runs command and return the output' do
expect(subject.run_command!(%w(echo it works!))).to eq("it works!\n")
end
it 'returns and exception when command exit with non zero code' do
expect { subject.run_command!(['bash', '-c', 'exit 1']) }.to raise_error Gitlab::TaskFailedError
end
end
end end
require 'spec_helper'
describe 'dashboard/projects/_nav.html.haml' do
it 'highlights All tab by default' do
render
expect(rendered).to have_css('li.active a', text: 'All')
end
it 'highlights Personal tab personal param is present' do
controller.params[:personal] = true
render
expect(rendered).to have_css('li.active a', text: 'Personal')
end
end
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