Commit db4a464f authored by Valery Sizov's avatar Valery Sizov

Merge branch 'ce_upstream' into 'master'

CE upstream

Closes gitlab-runner#2782 and gitaly#508

See merge request gitlab-org/gitlab-ee!3017
parents 7400ba31 bded707c
export default {
props: {
count: {
type: Number,
required: true,
},
},
template: `
<span v-if="count === 50" class="events-info pull-right">
<i class="fa fa-warning has-tooltip"
aria-hidden="true"
:title="n__('Limited to showing %d event at most', 'Limited to showing %d events at most', 50)"
data-placement="top"></i>
{{ n__('Showing %d event', 'Showing %d events', 50) }}
</span>
`,
};
<script>
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
count: {
type: Number,
required: true,
},
},
directives: {
tooltip,
},
};
</script>
<template>
<span v-if="count === 50" class="events-info pull-right">
<i
class="fa fa-warning"
v-tooltip
aria-hidden="true"
:title="n__('Limited to showing %d event at most', 'Limited to showing %d events at most', 50)"
data-placement="top"></i>
{{ n__('Showing %d event', 'Showing %d events', 50) }}
</span>
</template>
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageCodeComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item">
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="mergeRequest.author.avatarUrl"/>
<h5 class="item-title merge-merquest-title">
<a :href="mergeRequest.url">
{{ mergeRequest.title }}
</a>
</h5>
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
&middot;
<span>
{{ s__('OpenedNDaysAgo|Opened') }}
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
</span>
<span>
{{ s__('ByAuthor|by') }}
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
</span>
</div>
<div class="item-time">
<total-time :time="mergeRequest.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
<script>
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
export default {
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
};
</script>
<template>
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item">
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="mergeRequest.author.avatarUrl"/>
<h5 class="item-title merge-merquest-title">
<a :href="mergeRequest.url">
{{ mergeRequest.title }}
</a>
</h5>
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
&middot;
<span>
{{ s__('OpenedNDaysAgo|Opened') }}
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
</span>
<span>
{{ s__('ByAuthor|by') }}
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
</span>
</div>
<div class="item-time">
<total-time :time="mergeRequest.totalTime"></total-time>
</div>
</li>
</ul>
</div>
</template>
<script>
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
export default {
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
};
</script>
<template>
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li
v-for="(issue, i) in items"
:key="i"
class="stage-event-item">
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="issue.author.avatarUrl"/>
<h5 class="item-title issue-title">
<a class="issue-title" :href="issue.url">
{{ issue.title }}
</a>
</h5>
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
&middot;
<span>
{{ s__('OpenedNDaysAgo|Opened') }}
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
</span>
<span>
{{ s__('ByAuthor|by') }}
<a :href="issue.author.webUrl" class="issue-author-link">
{{ issue.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="issue.totalTime"/>
</div>
</li>
</ul>
</div>
</template>
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageIssueComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item">
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="issue.author.avatarUrl"/>
<h5 class="item-title issue-title">
<a class="issue-title" :href="issue.url">
{{ issue.title }}
</a>
</h5>
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
&middot;
<span>
{{ s__('OpenedNDaysAgo|Opened') }}
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
</span>
<span>
{{ s__('ByAuthor|by') }}
<a :href="issue.author.webUrl" class="issue-author-link">
{{ issue.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="issue.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import iconCommit from '../svg/icon_commit.svg';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StagePlanComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
data() {
return { iconCommit };
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="commit in items" class="stage-event-item">
<div class="item-details item-conmmit-component">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="commit.author.avatarUrl"/>
<h5 class="item-title commit-title">
<a :href="commit.commitUrl">
{{ commit.title }}
</a>
</h5>
<span>
{{ s__('FirstPushedBy|First') }}
<span class="commit-icon">${iconCommit}</span>
<a :href="commit.commitUrl" class="commit-hash-link commit-sha">{{ commit.shortSha }}</a>
{{ s__('FirstPushedBy|pushed by') }}
<a :href="commit.author.webUrl" class="commit-author-link">
{{ commit.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="commit.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
<script>
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import iconCommit from '../svg/icon_commit.svg';
export default {
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
computed: {
iconCommit() {
return iconCommit;
},
},
};
</script>
<template>
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li
v-for="(commit, i) in items"
:key="i"
class="stage-event-item">
<div class="item-details item-conmmit-component">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="commit.author.avatarUrl"/>
<h5 class="item-title commit-title">
<a :href="commit.commitUrl">
{{ commit.title }}
</a>
</h5>
<span>
{{ s__('FirstPushedBy|First') }}
<span class="commit-icon" v-html="iconCommit"></span>
<a :href="commit.commitUrl" class="commit-hash-link commit-sha">{{ commit.shortSha }}</a>
{{ s__('FirstPushedBy|pushed by') }}
<a :href="commit.author.webUrl" class="commit-author-link">
{{ commit.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="commit.totalTime" />
</div>
</li>
</ul>
</div>
</template>
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageProductionComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item">
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="issue.author.avatarUrl"/>
<h5 class="item-title issue-title">
<a class="issue-title" :href="issue.url">
{{ issue.title }}
</a>
</h5>
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
&middot;
<span>
{{ s__('OpenedNDaysAgo|Opened') }}
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
</span>
<span>
{{ s__('ByAuthor|by') }}
<a :href="issue.author.webUrl" class="issue-author-link">
{{ issue.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="issue.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageReviewComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item">
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="mergeRequest.author.avatarUrl"/>
<h5 class="item-title merge-merquest-title">
<a :href="mergeRequest.url">
{{ mergeRequest.title }}
</a>
</h5>
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
&middot;
<span>
{{ s__('OpenedNDaysAgo|Opened') }}
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
</span>
<span>
{{ s__('ByAuthor|by') }}
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
</span>
<template v-if="mergeRequest.state === 'closed'">
<span class="merge-request-state">
<i class="fa fa-ban"></i>
{{ mergeRequest.state.toUpperCase() }}
</span>
</template>
<template v-else>
<span class="merge-request-branch" v-if="mergeRequest.branch">
<i class= "fa fa-code-fork"></i>
<a :href="mergeRequest.branch.url">{{ mergeRequest.branch.name }}</a>
</span>
</template>
</div>
<div class="item-time">
<total-time :time="mergeRequest.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
<script>
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
export default {
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
};
</script>
<template>
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li
v-for="(mergeRequest, i) in items"
:key="i"
class="stage-event-item">
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="mergeRequest.author.avatarUrl"/>
<h5 class="item-title merge-merquest-title">
<a :href="mergeRequest.url">
{{ mergeRequest.title }}
</a>
</h5>
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
&middot;
<span>
{{ s__('OpenedNDaysAgo|Opened') }}
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
</span>
<span>
{{ s__('ByAuthor|by') }}
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
</span>
<template v-if="mergeRequest.state === 'closed'">
<span class="merge-request-state">
<i class="fa fa-ban"></i>
{{ mergeRequest.state.toUpperCase() }}
</span>
</template>
<template v-else>
<span class="merge-request-branch" v-if="mergeRequest.branch">
<i class= "fa fa-code-fork"></i>
<a :href="mergeRequest.branch.url">{{ mergeRequest.branch.name }}</a>
</span>
</template>
</div>
<div class="item-time">
<total-time :time="mergeRequest.totalTime"/>
</div>
</li>
</ul>
</div>
</template>
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import iconBranch from '../svg/icon_branch.svg';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageStagingComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
data() {
return { iconBranch };
},
components: {
userAvatarImage,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="build in items" class="stage-event-item item-build-component">
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="build.author.avatarUrl"/>
<h5 class="item-title">
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
<i class="fa fa-code-fork"></i>
<a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a>
<span class="icon-branch">${iconBranch}</span>
<a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a>
</h5>
<span>
<a :href="build.url" class="build-date">{{ build.date }}</a>
{{ s__('ByAuthor|by') }}
<a :href="build.author.webUrl" class="issue-author-link">
{{ build.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="build.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
<script>
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import iconBranch from '../svg/icon_branch.svg';
export default {
props: {
items: Array,
stage: Object,
},
components: {
userAvatarImage,
},
computed: {
iconBranch() {
return iconBranch;
},
},
};
</script>
<template>
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li
v-for="(build, i) in items"
class="stage-event-item item-build-component"
:key="i">
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="build.author.avatarUrl"/>
<h5 class="item-title">
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
<i class="fa fa-code-fork"></i>
<a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a>
<span class="icon-branch" v-html="iconBranch"></span>
<a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a>
</h5>
<span>
<a :href="build.url" class="build-date">{{ build.date }}</a>
{{ s__('ByAuthor|by') }}
<a :href="build.author.webUrl" class="issue-author-link">
{{ build.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="build.totalTime"/>
</div>
</li>
</ul>
</div>
</template>
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import iconBuildStatus from '../svg/icon_build_status.svg';
import iconBranch from '../svg/icon_branch.svg';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageTestComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
data() {
return { iconBuildStatus, iconBranch };
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="build in items" class="stage-event-item item-build-component">
<div class="item-details">
<h5 class="item-title">
<span class="icon-build-status">${iconBuildStatus}</span>
<a :href="build.url" class="item-build-name">{{ build.name }}</a>
&middot;
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
<i class="fa fa-code-fork"></i>
<a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a>
<span class="icon-branch">${iconBranch}</span>
<a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a>
</h5>
<span>
<a :href="build.url" class="issue-date">
{{ build.date }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="build.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
<script>
import iconBuildStatus from '../svg/icon_build_status.svg';
import iconBranch from '../svg/icon_branch.svg';
export default {
props: {
items: Array,
stage: Object,
},
computed: {
iconBuildStatus() {
return iconBuildStatus;
},
iconBranch() {
return iconBranch;
},
},
};
</script>
<template>
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li
v-for="(build, i) in items"
:key="i"
class="stage-event-item item-build-component">
<div class="item-details">
<h5 class="item-title">
<span class="icon-build-status" v-html="iconBuildStatus"></span>
<a :href="build.url" class="item-build-name">{{ build.name }}</a>
&middot;
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
<i class="fa fa-code-fork"></i>
<a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a>
<span class="icon-branch" v-html="iconBranch"></span>
<a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a>
</h5>
<span>
<a :href="build.url" class="issue-date">
{{ build.date }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="build.totalTime"/>
</div>
</li>
</ul>
</div>
</template>
/* eslint-disable no-param-reassign */
import Vue from 'vue';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.TotalTimeComponent = Vue.extend({
props: {
time: Object,
},
template: `
<span class="total-time">
<template v-if="Object.keys(time).length">
<template v-if="time.days">{{ time.days }} <span>{{ n__('day', 'days', time.days) }}</span></template>
<template v-if="time.hours">{{ time.hours }} <span>{{ n__('Time|hr', 'Time|hrs', time.hours) }}</span></template>
<template v-if="time.mins && !time.days">{{ time.mins }} <span>{{ n__('Time|min', 'Time|mins', time.mins) }}</span></template>
<template v-if="time.seconds && Object.keys(time).length === 1 || time.seconds === 0">{{ time.seconds }} <span>{{ s__('Time|s') }}</span></template>
</template>
<template v-else>
--
</template>
</span>
`,
});
<script>
export default {
props: {
time: {
type: Object,
required: false,
default: () => ({}),
},
},
computed: {
hasData() {
return Object.keys(this.time).length;
},
},
};
</script>
<template>
<span class="total-time">
<template v-if="hasData">
<template v-if="time.days">{{ time.days }} <span>{{ n__('day', 'days', time.days) }}</span></template>
<template v-if="time.hours">{{ time.hours }} <span>{{ n__('Time|hr', 'Time|hrs', time.hours) }}</span></template>
<template v-if="time.mins && !time.days">{{ time.mins }} <span>{{ n__('Time|min', 'Time|mins', time.mins) }}</span></template>
<template v-if="time.seconds && hasDa === 1 || time.seconds === 0">{{ time.seconds }} <span>{{ s__('Time|s') }}</span></template>
</template>
<template v-else>
--
</template>
</span>
</template>
......@@ -3,60 +3,63 @@
import Vue from 'vue';
import Cookies from 'js-cookie';
import Translate from '../vue_shared/translate';
import LimitWarningComponent from './components/limit_warning_component';
import './components/stage_code_component';
import './components/stage_issue_component';
import './components/stage_plan_component';
import './components/stage_production_component';
import './components/stage_review_component';
import './components/stage_staging_component';
import './components/stage_test_component';
import './components/total_time_component';
import './cycle_analytics_service';
import './cycle_analytics_store';
import limitWarningComponent from './components/limit_warning_component.vue';
import stageCodeComponent from './components/stage_code_component.vue';
import stagePlanComponent from './components/stage_plan_component.vue';
import stageComponent from './components/stage_component.vue';
import stageReviewComponent from './components/stage_review_component.vue';
import stageStagingComponent from './components/stage_staging_component.vue';
import stageTestComponent from './components/stage_test_component.vue';
import totalTime from './components/total_time_component.vue';
import CycleAnalyticsService from './cycle_analytics_service';
import CycleAnalyticsStore from './cycle_analytics_store';
Vue.use(Translate);
$(() => {
const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
const cycleAnalyticsEl = document.querySelector('#cycle-analytics');
const cycleAnalyticsStore = gl.cycleAnalytics.CycleAnalyticsStore;
const cycleAnalyticsService = new gl.cycleAnalytics.CycleAnalyticsService({
requestPath: cycleAnalyticsEl.dataset.requestPath,
});
gl.cycleAnalyticsApp = new Vue({
el: '#cycle-analytics',
name: 'CycleAnalytics',
data: {
state: cycleAnalyticsStore.state,
isLoading: false,
isLoadingStage: false,
isEmptyStage: false,
hasError: false,
startDate: 30,
isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE),
data() {
const cycleAnalyticsEl = document.querySelector('#cycle-analytics');
const cycleAnalyticsService = new CycleAnalyticsService({
requestPath: cycleAnalyticsEl.dataset.requestPath,
});
return {
store: CycleAnalyticsStore,
state: CycleAnalyticsStore.state,
isLoading: false,
isLoadingStage: false,
isEmptyStage: false,
hasError: false,
startDate: 30,
isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE),
service: cycleAnalyticsService,
};
},
computed: {
currentStage() {
return cycleAnalyticsStore.currentActiveStage();
return this.store.currentActiveStage();
},
},
components: {
'stage-issue-component': gl.cycleAnalytics.StageIssueComponent,
'stage-plan-component': gl.cycleAnalytics.StagePlanComponent,
'stage-code-component': gl.cycleAnalytics.StageCodeComponent,
'stage-test-component': gl.cycleAnalytics.StageTestComponent,
'stage-review-component': gl.cycleAnalytics.StageReviewComponent,
'stage-staging-component': gl.cycleAnalytics.StageStagingComponent,
'stage-production-component': gl.cycleAnalytics.StageProductionComponent,
'stage-issue-component': stageComponent,
'stage-plan-component': stagePlanComponent,
'stage-code-component': stageCodeComponent,
'stage-test-component': stageTestComponent,
'stage-review-component': stageReviewComponent,
'stage-staging-component': stageStagingComponent,
'stage-production-component': stageComponent,
},
created() {
this.fetchCycleAnalyticsData();
},
methods: {
handleError() {
cycleAnalyticsStore.setErrorState(true);
this.store.setErrorState(true);
return new Flash('There was an error while fetching cycle analytics data.');
},
initDropdown() {
......@@ -77,17 +80,17 @@ $(() => {
this.isLoading = true;
cycleAnalyticsService
this.service
.fetchCycleAnalyticsData(fetchOptions)
.done((response) => {
cycleAnalyticsStore.setCycleAnalyticsData(response);
.then(resp => resp.json())
.then((response) => {
this.store.setCycleAnalyticsData(response);
this.selectDefaultStage();
this.initDropdown();
this.isLoading = false;
})
.error(() => {
.catch(() => {
this.handleError();
})
.always(() => {
this.isLoading = false;
});
},
......@@ -100,27 +103,27 @@ $(() => {
if (this.currentStage === stage) return;
if (!stage.isUserAllowed) {
cycleAnalyticsStore.setActiveStage(stage);
this.store.setActiveStage(stage);
return;
}
this.isLoadingStage = true;
cycleAnalyticsStore.setStageEvents([], stage);
cycleAnalyticsStore.setActiveStage(stage);
this.store.setStageEvents([], stage);
this.store.setActiveStage(stage);
cycleAnalyticsService
this.service
.fetchStageData({
stage,
startDate: this.startDate,
})
.done((response) => {
.then(resp => resp.json())
.then((response) => {
this.isEmptyStage = !response.events.length;
cycleAnalyticsStore.setStageEvents(response.events, stage);
this.store.setStageEvents(response.events, stage);
this.isLoadingStage = false;
})
.error(() => {
.catch(() => {
this.isEmptyStage = true;
})
.always(() => {
this.isLoadingStage = false;
});
},
......@@ -132,6 +135,6 @@ $(() => {
});
// Register global components
Vue.component('limit-warning', LimitWarningComponent);
Vue.component('total-time', gl.cycleAnalytics.TotalTimeComponent);
Vue.component('limit-warning', limitWarningComponent);
Vue.component('total-time', totalTime);
});
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import VueResource from 'vue-resource';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
Vue.use(VueResource);
class CycleAnalyticsService {
export default class CycleAnalyticsService {
constructor(options) {
this.requestPath = options.requestPath;
this.cycleAnalytics = Vue.resource(this.requestPath);
}
fetchCycleAnalyticsData(options) {
options = options || { startDate: 30 };
return $.ajax({
url: this.requestPath,
method: 'GET',
dataType: 'json',
contentType: 'application/json',
data: {
cycle_analytics: {
start_date: options.startDate,
},
},
});
fetchCycleAnalyticsData(options = { startDate: 30 }) {
return this.cycleAnalytics.get({ cycle_analytics: { start_date: options.startDate } });
}
fetchStageData(options) {
......@@ -30,12 +19,12 @@ class CycleAnalyticsService {
startDate,
} = options;
return $.get(`${this.requestPath}/events/${stage.name}.json`, {
cycle_analytics: {
start_date: startDate,
return Vue.http.get(`${this.requestPath}/events/${stage.name}.json`, {
params: {
cycle_analytics: {
start_date: startDate,
},
},
});
}
}
global.cycleAnalytics.CycleAnalyticsService = CycleAnalyticsService;
......@@ -4,9 +4,6 @@ import { __ } from '../locale';
import '../lib/utils/text_utility';
import DEFAULT_EVENT_OBJECTS from './default_event_objects';
const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {};
const EMPTY_STAGE_TEXTS = {
issue: __('The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.'),
plan: __('The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.'),
......@@ -17,7 +14,7 @@ const EMPTY_STAGE_TEXTS = {
production: __('The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.'),
};
global.cycleAnalytics.CycleAnalyticsStore = {
export default {
state: {
summary: '',
stats: '',
......
......@@ -34,7 +34,7 @@ export const canShowActiveSubItems = (el) => {
export const canShowSubItems = () => bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md' || bp.getBreakpointSize() === 'lg';
export const getHideSubItemsInterval = () => {
if (!currentOpenMenu) return 0;
if (!currentOpenMenu || !mousePos.length) return 0;
const currentMousePos = mousePos[mousePos.length - 1];
const prevMousePos = mousePos[0];
......
export const isSticky = (el, scrollY, stickyTop) => {
export const createPlaceholder = () => {
const placeholder = document.createElement('div');
placeholder.classList.add('sticky-placeholder');
return placeholder;
};
export const isSticky = (el, scrollY, stickyTop, insertPlaceholder) => {
const top = Math.floor(el.offsetTop - scrollY);
if (top <= stickyTop) {
if (top <= stickyTop && !el.classList.contains('is-stuck')) {
const placeholder = insertPlaceholder ? createPlaceholder() : null;
const heightBefore = el.offsetHeight;
el.classList.add('is-stuck');
} else {
if (insertPlaceholder) {
el.parentNode.insertBefore(placeholder, el.nextElementSibling);
placeholder.style.height = `${heightBefore - el.offsetHeight}px`;
}
} else if (top > stickyTop && el.classList.contains('is-stuck')) {
el.classList.remove('is-stuck');
if (insertPlaceholder && el.nextElementSibling && el.nextElementSibling.classList.contains('sticky-placeholder')) {
el.nextElementSibling.remove();
}
}
};
export default (el) => {
export default (el, insertPlaceholder = true) => {
if (!el) return;
const computedStyle = window.getComputedStyle(el);
......@@ -17,7 +37,7 @@ export default (el) => {
const stickyTop = parseInt(computedStyle.top, 10);
document.addEventListener('scroll', () => isSticky(el, window.scrollY, stickyTop), {
document.addEventListener('scroll', () => isSticky(el, window.scrollY, stickyTop, insertPlaceholder), {
passive: true,
});
};
......@@ -178,8 +178,8 @@ const RepoHelper = {
setFile(data, file) {
const newFile = data;
newFile.url = file.url || Service.url; // Grab the URL from service, happens on page refresh.
newFile.url = file.url;
if (newFile.render_error === 'too_large' || newFile.render_error === 'collapsed') {
newFile.tooLarge = true;
}
......
......@@ -260,7 +260,7 @@
position: relative;
border: 1px solid $blue-300;
border-radius: $border-radius-default;
background-color: $blue-25;
background-color: $blue-50;
justify-content: center;
.dismiss-button {
......
......@@ -779,6 +779,14 @@
white-space: normal;
width: 100%;
&.dropdown-menu-user-link {
white-space: nowrap;
.dropdown-menu-user-username {
display: block;
}
}
// make sure the text color is not overriden
&.text-danger {
color: $brand-danger;
......
......@@ -6,7 +6,7 @@
// Header
header.navbar-gitlab-new {
background: linear-gradient(to right, $color-900, $color-800);
background-color: $color-900;
.navbar-collapse {
color: $color-200;
......@@ -201,7 +201,7 @@ body {
@include gitlab-theme($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-700, $theme-gray-700, $theme-gray-100, $theme-gray-700);
header.navbar-gitlab-new {
background: $theme-gray-100;
background-color: $theme-gray-100;
box-shadow: 0 2px 0 0 $border-color;
.logo-text svg {
......@@ -242,10 +242,10 @@ body {
&:hover {
background-color: $white-light;
box-shadow: inset 0 0 0 1px $blue-100;
box-shadow: inset 0 0 0 1px $blue-200;
.location-badge {
box-shadow: inset 0 0 0 1px $blue-100;
box-shadow: inset 0 0 0 1px $blue-200;
}
}
}
......
......@@ -142,7 +142,43 @@
}
@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) {
......
......@@ -137,7 +137,7 @@ $well-border: #eee;
//##
$code-color: $red-600;
$code-bg: lighten($red-50, 2%);
$code-bg: lighten($red-100, 2%);
$kbd-color: $white-light;
$kbd-bg: #333;
......
......@@ -29,46 +29,45 @@ $gray-dark: darken($gray-light, $darken-dark-factor);
$gray-darker: #eee;
$gray-darkest: #c4c4c4;
$green-25: #f6fcf8;
$green-50: #e4f5eb;
$green-100: #bae6cc;
$green-200: #8dd5aa;
$green-300: #5fc488;
$green-400: #3cb76f;
$green-50: #f1fdf6;
$green-100: #dcf5e7;
$green-200: #b3e6c8;
$green-300: #75d09b;
$green-400: #37b96d;
$green-500: #1aaa55;
$green-600: #168f48;
$green-700: #12753a;
$green-800: #0e5a2d;
$green-900: #0a4020;
$green-950: #072b15;
$blue-25: #f6fafd;
$blue-50: #e4eff9;
$blue-100: #bcd7f1;
$blue-200: #8fbce8;
$blue-300: #62a1df;
$blue-400: #418cd8;
$blue-50: #f6fafe;
$blue-100: #e4f0fb;
$blue-200: #b8d6f4;
$blue-300: #73afea;
$blue-400: #2e87e0;
$blue-500: #1f78d1;
$blue-600: #1b69b6;
$blue-700: #17599c;
$blue-800: #134a81;
$blue-900: #0f3b66;
$blue-950: #0a2744;
$orange-25: #fffcf8;
$orange-50: #fff2e1;
$orange-100: #fedfb3;
$orange-200: #feca81;
$orange-300: #fdb44f;
$orange-400: #fca429;
$orange-50: #fffaf4;
$orange-100: #fff1de;
$orange-200: #fed69f;
$orange-300: #fdbc60;
$orange-400: #fca121;
$orange-500: #fc9403;
$orange-600: #de7e00;
$orange-700: #c26700;
$orange-800: #a35100;
$orange-900: #853b00;
$orange-800: #a35200;
$orange-900: #853c00;
$orange-950: #592800;
$red-25: #fef7f6;
$red-50: #fbe7e4;
$red-100: #f4c4bc;
$red-200: #ed9d90;
$red-50: #fef6f5;
$red-100: #fbe5e1;
$red-200: #f2b4a9;
$red-300: #e67664;
$red-400: #e05842;
$red-500: #db3b21;
......@@ -76,6 +75,7 @@ $red-600: #c0341d;
$red-700: #a62d19;
$red-800: #8b2615;
$red-900: #711e11;
$red-950: #4b140b;
// GitLab themes
......@@ -186,8 +186,8 @@ $list-text-disabled-color: $gl-text-color-tertiary;
$list-border-light: #eee;
$list-border: rgba(0, 0, 0, 0.05);
$list-text-height: 42px;
$list-warning-row-bg: $orange-50;
$list-warning-row-border: $orange-100;
$list-warning-row-bg: $orange-100;
$list-warning-row-border: $orange-200;
$list-warning-row-color: $orange-700;
/*
......@@ -216,8 +216,8 @@ $gl-sidebar-padding: 22px;
/*
* Misc
*/
$row-hover: $blue-25;
$row-hover-border: $blue-100;
$row-hover: $blue-50;
$row-hover-border: $blue-200;
$progress-color: #c0392b;
$header-height: 50px;
$new-navbar-height: 40px;
......@@ -272,8 +272,8 @@ $time-color: #999;
$project-member-show-color: #aaa;
$gl-promo-color: #aaa;
$error-bg: $red-400;
$warning-message-bg: $orange-50;
$warning-message-border: $orange-100;
$warning-message-bg: $orange-100;
$warning-message-border: $orange-200;
$warning-message-color: $orange-700;
$control-group-descr-color: #666;
$table-permission-x-bg: #d9edf7;
......@@ -459,17 +459,17 @@ $builds-trace-bg: #111;
/*
* Callout
*/
$callout-danger-bg: $red-50;
$callout-danger-border: $red-100;
$callout-danger-bg: $red-100;
$callout-danger-border: $red-200;
$callout-danger-color: $red-700;
$callout-warning-bg: $orange-50;
$callout-warning-border: $orange-100;
$callout-warning-bg: $orange-100;
$callout-warning-border: $orange-200;
$callout-warning-color: $orange-700;
$callout-info-bg: $blue-50;
$callout-info-border: $blue-100;
$callout-info-bg: $blue-100;
$callout-info-border: $blue-200;
$callout-info-color: $blue-700;
$callout-success-bg: $green-50;
$callout-success-border: $green-100;
$callout-success-bg: $green-100;
$callout-success-border: $green-200;
$callout-success-color: $green-700;
/*
......
......@@ -83,7 +83,7 @@ $space-between-cards: 8px;
border-top-color: $color-low-score;
.card-score-big {
background-color: $red-25;
background-color: $red-50;
}
}
......@@ -91,7 +91,7 @@ $space-between-cards: 8px;
border-top-color: $color-average-score;
.card-score-big {
background-color: $orange-25;
background-color: $orange-50;
}
}
......@@ -99,7 +99,7 @@ $space-between-cards: 8px;
border-top-color: $color-high-score;
.card-score-big {
background-color: $green-25;
background-color: $green-50;
}
}
......
......@@ -451,7 +451,7 @@
}
.files {
margin-top: -1px;
margin-top: 1px;
.diff-file:last-child {
margin-bottom: 0;
......@@ -586,11 +586,6 @@
top: 76px;
}
+ .files,
+ .alert {
margin-top: 1px;
}
&:not(.is-stuck) .diff-stats-additions-deletions-collapsed {
display: none;
}
......@@ -605,11 +600,6 @@
.inline-parallel-buttons {
display: none;
}
+ .files,
+ .alert {
margin-top: 32px;
}
}
}
}
......
......@@ -7,7 +7,7 @@
.is-confidential {
color: $orange-600;
background-color: $orange-50;
background-color: $orange-100;
border-radius: $border-radius-default;
padding: 5px;
margin: 0 3px 0 -4px;
......
......@@ -255,7 +255,7 @@ $colors: (
&.saved {
.editor {
border-top: solid 2px $green-200;
border-top: solid 2px $green-300;
}
}
......
......@@ -103,7 +103,7 @@
.confidential-issue-warning {
color: $orange-600;
background-color: $orange-50;
background-color: $orange-100;
border-radius: $border-radius-default $border-radius-default 0 0;
border: 1px solid $border-gray-normal;
border-bottom: none;
......
......@@ -674,20 +674,20 @@ a.linked-pipeline-mini-item {
// Dropdown button animation in mini pipeline graph
&.ci-status-icon-success {
@include mini-pipeline-graph-color($green-50, $green-500, $green-600);
@include mini-pipeline-graph-color($green-100, $green-500, $green-600);
}
&.ci-status-icon-failed {
@include mini-pipeline-graph-color($red-50, $red-500, $red-600);
@include mini-pipeline-graph-color($red-100, $red-500, $red-600);
}
&.ci-status-icon-pending,
&.ci-status-icon-success_with_warnings {
@include mini-pipeline-graph-color($orange-50, $orange-500, $orange-600);
@include mini-pipeline-graph-color($orange-100, $orange-500, $orange-600);
}
&.ci-status-icon-running {
@include mini-pipeline-graph-color($blue-50, $blue-400, $blue-600);
@include mini-pipeline-graph-color($blue-100, $blue-400, $blue-600);
}
&.ci-status-icon-canceled,
......
......@@ -292,7 +292,7 @@ table.u2f-registrations {
padding: 32px;
border: 1px solid $blue-300;
border-radius: $border-radius-default;
background-color: $blue-25;
background-color: $blue-50;
position: relative;
display: flex;
justify-content: center;
......@@ -376,7 +376,7 @@ table.u2f-registrations {
.nav-wip {
border: 1px solid $blue-500;
background: $blue-25;
background: $blue-50;
padding: $gl-padding;
margin-bottom: $gl-padding;
......
......@@ -18,7 +18,7 @@
}
&.ci-failed {
@include status-color($red-50, $red-500, $red-600);
@include status-color($red-100, $red-500, $red-600);
}
&.ci-success {
......@@ -39,12 +39,12 @@
&.ci-pending,
&.ci-failed_with_warnings,
&.ci-success_with_warnings {
@include status-color($orange-50, $orange-500, $orange-700);
@include status-color($orange-100, $orange-500, $orange-700);
}
&.ci-info,
&.ci-running {
@include status-color($blue-50, $blue-500, $blue-600);
@include status-color($blue-100, $blue-500, $blue-600);
}
&.ci-created,
......
......@@ -15,3 +15,9 @@
-ms-animation: none !important;
animation: none !important;
}
// Disable sticky changes bar for tests
.diff-files-changed {
position: relative !important;
top: 0 !important;
}
......@@ -13,22 +13,29 @@ module AvatarsHelper
user_name = options[:user].try(:name) || options[:user_name]
avatar_url = options[:url] || avatar_icon(options[:user] || options[:user_email], avatar_size)
has_tooltip = options[:has_tooltip].nil? ? true : options[:has_tooltip]
data_attributes = {}
data_attributes = options[:data] || {}
css_class = %W[avatar s#{avatar_size}].push(*options[:css_class])
if has_tooltip
css_class.push('has-tooltip')
data_attributes = { container: 'body' }
data_attributes[:container] = 'body'
end
image_tag(
avatar_url,
if options[:lazy]
css_class << 'lazy'
data_attributes[:src] = avatar_url
avatar_url = LazyImageTagHelper.placeholder_image
end
image_options = {
alt: "#{user_name}'s avatar",
src: avatar_url,
data: data_attributes,
class: css_class,
alt: "#{user_name}'s avatar",
title: user_name,
data: data_attributes,
lazy: true
)
title: user_name
}
tag(:img, image_options)
end
def user_avatar(options = {})
......
......@@ -239,8 +239,8 @@ module ProjectsHelper
end
end
def has_projects_or_name?(projects, params)
!!(params[:name] || any_projects?(projects))
def show_projects?(projects, params)
!!(params[:personal] || params[:name] || any_projects?(projects))
end
private
......
......@@ -110,6 +110,10 @@ module SortingHelper
s_('SortOptions|Due date')
end
def sort_title_less_weight
s_('SortOptions|Less weight')
end
def sort_title_due_date_later
s_('SortOptions|Due later')
end
......@@ -126,6 +130,10 @@ module SortingHelper
s_('SortOptions|Largest group')
end
def sort_title_more_weight
s_('SortOptions|More weight')
end
def sort_title_largest_repo
s_('SortOptions|Largest repository')
end
......@@ -138,10 +146,6 @@ module SortingHelper
s_('SortOptions|Last updated')
end
def sort_title_less_weight
s_('SortOptions|Less weight')
end
def sort_title_milestone
s_('SortOptions|Milestone')
end
......@@ -154,10 +158,6 @@ module SortingHelper
s_('SortOptions|Milestone due soon')
end
def sort_title_more_weight
s_('SortOptions|More weight')
end
def sort_title_name
s_('SortOptions|Name')
end
......@@ -178,6 +178,10 @@ module SortingHelper
s_('SortOptions|Oldest created')
end
def sort_title_weight
s_('SortOptions|Weight')
end
def sort_title_oldest_joined
s_('SortOptions|Oldest joined')
end
......@@ -222,10 +226,6 @@ module SortingHelper
s_('SortOptions|Most popular')
end
def sort_title_weight
s_('SortOptions|Weight')
end
# Values.
def sort_value_access_level_asc
'access_level_asc'
......
......@@ -175,7 +175,7 @@ module Ci
end
def assignable_for?(project)
!locked? || projects.exists?(id: project.id)
is_shared? || projects.exists?(id: project.id)
end
def accepting_tags?(build)
......
.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 @@
= render "projects/last_push"
%div{ class: container_class }
- if has_projects_or_name?(@projects, params)
- if show_projects?(@projects, params)
= render 'dashboard/projects_head'
= render 'nav'
= render 'projects'
- else
= render "zero_authorized_projects"
- access = note_max_access_for_user(note)
- 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.") }
= 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)
- if note.resolvable?
......
......@@ -26,7 +26,8 @@
%strong Disable Auto DevOps
%br
%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
= form.label :enabled_nil do
= form.radio_button :enabled, ''
......
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 @@
%li.filter-dropdown-item{ class: ('js-current-user' if user == current_user) }
%button.btn.btn-link.dropdown-user{ type: :button }
.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
%span
= 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
SELECT true
FROM user_synced_attributes_metadata
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}
EOF
......
......@@ -33,7 +33,7 @@ class PostDeployMigrateUserExternalMailData < ActiveRecord::Migration
SELECT true
FROM user_synced_attributes_metadata
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}
EOF
......
......@@ -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
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
class ConsumeRemainingExtractServicesUrlJobs < ActiveRecord::Migration
disable_ddl_transaction!
class Service < ActiveRecord::Base
include ::EachBatch
self.table_name = 'services'
end
def up
# This must be included
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
def down
......@@ -230,6 +245,15 @@ class ConsumeRemainingExtractServicesUrlJobs < ActiveRecord::Migration
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
and continue once all jobs have been processed. Once done you can safely remove
the `services.properties` column.
......@@ -254,6 +278,9 @@ for more details.
1. Make sure that background migration jobs are idempotent.
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
[issue-rspec-hooks]: https://gitlab.com/gitlab-org/gitlab-ce/issues/35351
[reliable-sidekiq]: https://gitlab.com/gitlab-org/gitlab-ce/issues/36791
......@@ -162,6 +162,22 @@ should be done using `helm upgrade`:
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
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
To uninstall the GitLab Chart, run the following:
......
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
whitelist[:elements].push('abbr')
whitelist[:attributes]['abbr'] = %w(title)
# Disallow `name` attribute globally
# Disallow `name` attribute globally, allow on `a`
whitelist[:attributes][:all].delete('name')
whitelist[:attributes]['a'].push('name')
# Allow any protocol in `a` elements...
whitelist[:protocols].delete('a')
......
......@@ -386,7 +386,13 @@ module Gitlab
options[:limit] ||= 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
# Used in gitaly-ruby
......
......@@ -228,10 +228,18 @@ module Gitlab
path.read.chomp
end
def self.timestamp(t)
Google::Protobuf::Timestamp.new(seconds: t.to_i)
end
def self.encode(s)
s.dup.force_encoding(Encoding::ASCII_8BIT)
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
def self.count_stack
return unless RequestStore.active?
......
......@@ -230,6 +230,26 @@ module Gitlab
GitalyClient.call(@repository.storage, :commit_service, :commit_stats, request)
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
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
end
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
def check?
......
......@@ -9,7 +9,7 @@ module SystemCheck
end
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
def check?
......
......@@ -3,15 +3,19 @@
cd "$(dirname "$0")/.."
# 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 ]
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
fi
# 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)
echo 'Checking for CHANGELOG.md duplicate entries...'
if [ "${DUPLICATE_CHANGELOG_VERSIONS}" != "" ]
then
echo '✖ ERROR: Duplicate versions in CHANGELOG.md:' >&2
......@@ -19,5 +23,15 @@ then
exit 1
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"
exit 0
......@@ -50,6 +50,25 @@ feature 'Dashboard Projects' do
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
it 'shows only starred projects' do
user.toggle_star(project2)
......
......@@ -26,12 +26,13 @@ describe AvatarsHelper do
subject { helper.user_avatar_without_link(options) }
it 'displays user avatar' do
is_expected.to eq image_tag(
LazyImageTagHelper.placeholder_image,
class: 'avatar s16 has-tooltip lazy',
is_expected.to eq tag(
:img,
alt: "#{user.name}'s avatar",
title: user.name,
data: { container: 'body', src: avatar_icon(user, 16) }
src: avatar_icon(user, 16),
data: { container: 'body' },
class: 'avatar s16 has-tooltip',
title: user.name
)
end
......@@ -39,12 +40,13 @@ describe AvatarsHelper do
let(:options) { { user: user, css_class: '.cat-pics' } }
it 'uses provided css_class' do
is_expected.to eq image_tag(
LazyImageTagHelper.placeholder_image,
class: "avatar s16 #{options[:css_class]} has-tooltip lazy",
is_expected.to eq tag(
:img,
alt: "#{user.name}'s avatar",
title: user.name,
data: { container: 'body', src: avatar_icon(user, 16) }
src: avatar_icon(user, 16),
data: { container: 'body' },
class: "avatar s16 #{options[:css_class]} has-tooltip",
title: user.name
)
end
end
......@@ -53,12 +55,13 @@ describe AvatarsHelper do
let(:options) { { user: user, size: 99 } }
it 'uses provided size' do
is_expected.to eq image_tag(
LazyImageTagHelper.placeholder_image,
class: "avatar s#{options[:size]} has-tooltip lazy",
is_expected.to eq tag(
:img,
alt: "#{user.name}'s avatar",
title: user.name,
data: { container: 'body', src: avatar_icon(user, options[:size]) }
src: avatar_icon(user, options[:size]),
data: { container: 'body' },
class: "avatar s#{options[:size]} has-tooltip",
title: user.name
)
end
end
......@@ -67,12 +70,28 @@ describe AvatarsHelper do
let(:options) { { user: user, url: '/over/the/rainbow.png' } }
it 'uses provided url' do
is_expected.to eq image_tag(
LazyImageTagHelper.placeholder_image,
class: 'avatar s16 has-tooltip lazy',
is_expected.to eq tag(
:img,
alt: "#{user.name}'s avatar",
title: user.name,
data: { container: 'body', src: options[:url] }
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
......@@ -82,12 +101,13 @@ describe AvatarsHelper do
let(:options) { { user: user, has_tooltip: true } }
it 'adds has-tooltip' do
is_expected.to eq image_tag(
LazyImageTagHelper.placeholder_image,
class: 'avatar s16 has-tooltip lazy',
is_expected.to eq tag(
:img,
alt: "#{user.name}'s avatar",
title: user.name,
data: { container: 'body', src: avatar_icon(user, 16) }
src: avatar_icon(user, 16),
data: { container: 'body' },
class: "avatar s16 has-tooltip",
title: user.name
)
end
end
......@@ -96,12 +116,12 @@ describe AvatarsHelper do
let(:options) { { user: user, has_tooltip: false } }
it 'does not add has-tooltip or data container' do
is_expected.to eq image_tag(
LazyImageTagHelper.placeholder_image,
class: 'avatar s16 lazy',
is_expected.to eq tag(
:img,
alt: "#{user.name}'s avatar",
title: user.name,
data: { src: avatar_icon(user, 16) }
src: avatar_icon(user, 16),
class: "avatar s16",
title: user.name
)
end
end
......@@ -114,23 +134,25 @@ describe AvatarsHelper do
let(:options) { { user: user, user_name: 'Tinky Winky' } }
it 'prefers user parameter' do
is_expected.to eq image_tag(
LazyImageTagHelper.placeholder_image,
class: 'avatar s16 has-tooltip lazy',
is_expected.to eq tag(
:img,
alt: "#{user.name}'s avatar",
title: user.name,
data: { container: 'body', src: avatar_icon(user, 16) }
src: avatar_icon(user, 16),
data: { container: 'body' },
class: "avatar s16 has-tooltip",
title: user.name
)
end
end
it 'uses user_name and user_email parameter if user is not present' do
is_expected.to eq image_tag(
LazyImageTagHelper.placeholder_image,
class: 'avatar s16 has-tooltip lazy',
is_expected.to eq tag(
:img,
alt: "#{options[:user_name]}'s avatar",
title: options[:user_name],
data: { container: 'body', src: avatar_icon(options[:user_email], 16) }
src: avatar_icon(options[:user_email], 16),
data: { container: 'body' },
class: "avatar s16 has-tooltip",
title: options[:user_name]
)
end
end
......
......@@ -429,22 +429,26 @@ describe ProjectsHelper do
end
end
describe '#has_projects_or_name?' do
describe '#show_projects' do
let(:projects) do
create(:project)
Project.all
end
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
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
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
......
import Vue from 'vue';
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);
......
......@@ -73,6 +73,12 @@ describe('Fly out sidebar navigation', () => {
).toBe(0);
});
it('returns 0 if mousePos is empty', () => {
expect(
getHideSubItemsInterval(),
).toBe(0);
});
it('returns 0 when mouse above sub-items', () => {
showSubLevelItems(el);
documentMouseMove({
......
import { isSticky } from '~/lib/utils/sticky';
describe('sticky', () => {
const el = {
offsetTop: 0,
classList: {},
};
let el;
beforeEach(() => {
el.offsetTop = 0;
el.classList.add = jasmine.createSpy('spy');
el.classList.remove = jasmine.createSpy('spy');
document.body.innerHTML += `
<div class="parent">
<div id="js-sticky"></div>
</div>
`;
el = document.getElementById('js-sticky');
});
describe('classList.remove', () => {
it('does not call classList.remove when stuck', () => {
isSticky(el, 0, 0);
afterEach(() => {
el.parentNode.remove();
});
describe('when stuck', () => {
it('does not remove is-stuck class', () => {
isSticky(el, 0, el.offsetTop);
isSticky(el, 0, el.offsetTop);
expect(
el.classList.remove,
).not.toHaveBeenCalled();
el.classList.contains('is-stuck'),
).toBeTruthy();
});
it('calls classList.remove when not stuck', () => {
el.offsetTop = 10;
isSticky(el, 0, 0);
it('adds is-stuck class', () => {
isSticky(el, 0, el.offsetTop);
expect(
el.classList.remove,
).toHaveBeenCalledWith('is-stuck');
el.classList.contains('is-stuck'),
).toBeTruthy();
});
it('inserts placeholder element', () => {
isSticky(el, 0, el.offsetTop, true);
expect(
document.querySelector('.sticky-placeholder'),
).not.toBeNull();
});
});
describe('classList.add', () => {
it('calls classList.add when stuck', () => {
describe('when not stuck', () => {
it('removes is-stuck class', () => {
spyOn(el.classList, 'remove').and.callThrough();
isSticky(el, 0, el.offsetTop);
isSticky(el, 0, 0);
expect(
el.classList.add,
el.classList.remove,
).toHaveBeenCalledWith('is-stuck');
expect(
el.classList.contains('is-stuck'),
).toBeFalsy();
});
it('does not call classList.add when not stuck', () => {
el.offsetTop = 10;
it('does not add is-stuck class', () => {
isSticky(el, 0, 0);
expect(
el.classList.add,
).not.toHaveBeenCalled();
el.classList.contains('is-stuck'),
).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
describe 'custom whitelist' do
it 'customizes the whitelist only once' do
instance = described_class.new('Foo')
control_count = instance.whitelist[:transformers].size
3.times { instance.whitelist }
expect(instance.whitelist[:transformers].size).to eq 5
expect(instance.whitelist[:transformers].size).to eq control_count
end
it 'sanitizes `class` attribute from all elements' do
......@@ -101,16 +103,18 @@ describe Banzai::Filter::SanitizationFilter do
expect(filter(act).to_html).to eq exp
end
it 'disallows the `name` attribute globally' do
it 'disallows the `name` attribute globally, allows on `a`' do
html = <<~HTML
<img name="getElementById" src="">
<span name="foo" class="bar">Hi</span>
<a name="foo" class="bar">Bye</a>
HTML
doc = filter(html)
expect(doc.at_css('img')).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
it 'allows `summary` elements' do
......
......@@ -181,7 +181,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
end
describe '.where' do
shared_examples '.where' do
context 'path is empty string' do
subject do
commits = described_class.where(
......@@ -279,6 +279,14 @@ describe Gitlab::Git::Commit, seed_helper: true do
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
subject do
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
end
end
context 'when runner is locked' do
context 'when runner is shared' do
before do
runner.locked = true
runner.is_shared = true
build.project.runners = []
end
shared_examples 'locked build picker' do
context 'when runner cannot pick untagged jobs' do
before do
runner.run_untagged = false
end
it 'can handle builds' do
expect(runner.can_pick?(build)).to be_truthy
end
it 'cannot handle builds without tags' do
expect(runner.can_pick?(build)).to be_falsey
end
context 'when runner is locked' do
before do
runner.locked = true
end
context 'when having runner tags' do
before do
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
it 'can handle builds' do
expect(runner.can_pick?(build)).to be_truthy
end
end
end
context 'when serving the same project' do
it 'can handle it' do
context 'when runner is not shared' do
context 'when runner is assigned to a project' do
it 'can handle builds' do
expect(runner.can_pick?(build)).to be_truthy
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
context 'serving a different project' do
context 'when runner is not assigned to a project' do
before do
runner.runner_projects.destroy_all
build.project.runners = []
end
it 'cannot handle it' do
it 'cannot handle builds' do
expect(runner.can_pick?(build)).to be_falsey
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
......
......@@ -236,7 +236,6 @@ describe ProjectPolicy do
shared_examples 'project policies as developer' do
context 'abilities for non-public projects' do
let(:project) { create(:project, namespace: owner.namespace) }
subject { described_class.new(developer, project) }
it do
......
......@@ -71,8 +71,16 @@ RSpec.configure do |config|
config.infer_spec_type_from_file_location!
config.define_derived_metadata(file_path: %r{/spec/requests/(ci/)?api/}) do |metadata|
metadata[:api] = true
config.define_derived_metadata(file_path: %r{/spec/}) do |metadata|
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
config.raise_errors_for_deprecations!
......
......@@ -75,4 +75,24 @@ describe Gitlab::TaskHelpers do
subject.checkout_version(tag, clone_path)
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
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