Commit b6feafe4 authored by Nick Thomas's avatar Nick Thomas

Merge remote-tracking branch 'ce/master' into ce-to-ee-2017-06-15

parents 43a570dc 75d425e4
...@@ -10,10 +10,10 @@ engines: ...@@ -10,10 +10,10 @@ engines:
languages: languages:
- ruby - ruby
- javascript - javascript
exclude_paths:
- "lib/api/v3/*"
eslint: eslint:
enabled: true enabled: true
fixme:
enabled: true
rubocop: rubocop:
enabled: true enabled: true
ratings: ratings:
...@@ -35,4 +35,13 @@ exclude_paths: ...@@ -35,4 +35,13 @@ exclude_paths:
- node_modules/ - node_modules/
- spec/ - spec/
- vendor/ - vendor/
- lib/api/v3/ - .yarn-cache/
- tmp/
- builds/
- coverage/
- public/
- shared/
- webpack-report/
- log/
- backups/
- coverage-javascript/
...@@ -442,6 +442,21 @@ karma: ...@@ -442,6 +442,21 @@ karma:
paths: paths:
- coverage-javascript/ - coverage-javascript/
codeclimate:
before_script: []
image: docker:latest
stage: test
variables:
SETUP_DB: "false"
DOCKER_DRIVER: overlay
services:
- docker:dind
script:
- docker pull codeclimate/codeclimate
- docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > codeclimate.json
artifacts:
paths: [codeclimate.json]
coverage: coverage:
stage: post-test stage: post-test
services: [] services: []
......
...@@ -399,7 +399,7 @@ Style/ParenthesesAroundCondition: ...@@ -399,7 +399,7 @@ Style/ParenthesesAroundCondition:
# Configuration parameters: EnforcedStyle, SupportedStyles. # Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: short, verbose # SupportedStyles: short, verbose
Style/PreferredHashMethods: Style/PreferredHashMethods:
Enabled: true Enabled: false
# Checks for an obsolete RuntimeException argument in raise/fail. # Checks for an obsolete RuntimeException argument in raise/fail.
Style/RedundantException: Style/RedundantException:
......
...@@ -149,27 +149,34 @@ window.Build = (function () { ...@@ -149,27 +149,34 @@ window.Build = (function () {
Build.prototype.verifyTopPosition = function () { Build.prototype.verifyTopPosition = function () {
const $buildPage = $('.build-page'); const $buildPage = $('.build-page');
const $flashError = $('.alert-wrapper');
const $header = $('.build-header', $buildPage); const $header = $('.build-header', $buildPage);
const $runnersStuck = $('.js-build-stuck', $buildPage); const $runnersStuck = $('.js-build-stuck', $buildPage);
const $startsEnvironment = $('.js-environment-container', $buildPage); const $startsEnvironment = $('.js-environment-container', $buildPage);
const $erased = $('.js-build-erased', $buildPage); const $erased = $('.js-build-erased', $buildPage);
const prependTopDefault = 20;
// header + navigation + margin
let topPostion = 168; let topPostion = 168;
if ($header) { if ($header.length) {
topPostion += $header.outerHeight(); topPostion += $header.outerHeight();
} }
if ($runnersStuck) { if ($runnersStuck.length) {
topPostion += $runnersStuck.outerHeight(); topPostion += $runnersStuck.outerHeight();
} }
if ($startsEnvironment) { if ($startsEnvironment.length) {
topPostion += $startsEnvironment.outerHeight(); topPostion += $startsEnvironment.outerHeight() + prependTopDefault;
} }
if ($erased) { if ($erased.length) {
topPostion += $erased.outerHeight() + 10; topPostion += $erased.outerHeight() + prependTopDefault;
}
if ($flashError.length) {
topPostion += $flashError.outerHeight();
} }
this.$buildTrace.css({ this.$buildTrace.css({
...@@ -245,6 +252,7 @@ window.Build = (function () { ...@@ -245,6 +252,7 @@ window.Build = (function () {
Build.prototype.toggleSidebar = function (shouldHide) { Build.prototype.toggleSidebar = function (shouldHide) {
const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined; const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
const $toggleButton = $('.js-sidebar-build-toggle-header');
this.$buildTrace this.$buildTrace
.toggleClass('sidebar-expanded', shouldShow) .toggleClass('sidebar-expanded', shouldShow)
...@@ -252,6 +260,16 @@ window.Build = (function () { ...@@ -252,6 +260,16 @@ window.Build = (function () {
this.$sidebar this.$sidebar
.toggleClass('right-sidebar-expanded', shouldShow) .toggleClass('right-sidebar-expanded', shouldShow)
.toggleClass('right-sidebar-collapsed', shouldHide); .toggleClass('right-sidebar-collapsed', shouldHide);
$('.js-build-page')
.toggleClass('sidebar-expanded', shouldShow)
.toggleClass('sidebar-collapsed', shouldHide);
if (this.$sidebar.hasClass('right-sidebar-expanded')) {
$toggleButton.addClass('hidden');
} else {
$toggleButton.removeClass('hidden');
}
}; };
Build.prototype.sidebarOnResize = function () { Build.prototype.sidebarOnResize = function () {
...@@ -266,6 +284,7 @@ window.Build = (function () { ...@@ -266,6 +284,7 @@ window.Build = (function () {
Build.prototype.sidebarOnClick = function () { Build.prototype.sidebarOnClick = function () {
if (this.shouldHideSidebarForViewport()) this.toggleSidebar(); if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
this.verifyTopPosition();
}; };
Build.prototype.updateArtifactRemoveDate = function () { Build.prototype.updateArtifactRemoveDate = function () {
......
import Vue from 'vue'; import Vue from 'vue';
import Visibility from 'visibilityjs'; import Visibility from 'visibilityjs';
import pipelinesTableComponent from '../../vue_shared/components/pipelines_table'; import pipelinesTableComponent from '../../vue_shared/components/pipelines_table.vue';
import PipelinesService from '../../pipelines/services/pipelines_service'; import PipelinesService from '../../pipelines/services/pipelines_service';
import PipelineStore from '../../pipelines/stores/pipelines_store'; import PipelineStore from '../../pipelines/stores/pipelines_store';
import eventHub from '../../pipelines/event_hub'; import eventHub from '../../pipelines/event_hub';
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
/* global UsernameValidator */ /* global UsernameValidator */
/* global ActiveTabMemoizer */ /* global ActiveTabMemoizer */
/* global ShortcutsNavigation */ /* global ShortcutsNavigation */
/* global Build */
/* global IssuableIndex */ /* global IssuableIndex */
/* global ShortcutsIssuable */ /* global ShortcutsIssuable */
/* global ZenMode */ /* global ZenMode */
...@@ -126,9 +125,6 @@ import AuditLogs from './audit_logs'; ...@@ -126,9 +125,6 @@ import AuditLogs from './audit_logs';
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new UsersSelect(); new UsersSelect();
break; break;
case 'projects:jobs:show':
new Build();
break;
case 'projects:merge_requests:index': case 'projects:merge_requests:index':
case 'projects:issues:index': case 'projects:issues:index':
if (gl.FilteredSearchManager && document.querySelector('.filtered-search')) { if (gl.FilteredSearchManager && document.querySelector('.filtered-search')) {
......
...@@ -9,7 +9,7 @@ import StopComponent from './environment_stop.vue'; ...@@ -9,7 +9,7 @@ import StopComponent from './environment_stop.vue';
import RollbackComponent from './environment_rollback.vue'; import RollbackComponent from './environment_rollback.vue';
import TerminalButtonComponent from './environment_terminal_button.vue'; import TerminalButtonComponent from './environment_terminal_button.vue';
import MonitoringButtonComponent from './environment_monitoring.vue'; import MonitoringButtonComponent from './environment_monitoring.vue';
import CommitComponent from '../../vue_shared/components/commit'; import CommitComponent from '../../vue_shared/components/commit.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
const timeagoInstance = new Timeago(); const timeagoInstance = new Timeago();
......
<script>
import ciHeader from '../../vue_shared/components/header_ci_component.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
name: 'jobHeaderSection',
props: {
job: {
type: Object,
required: true,
},
isLoading: {
type: Boolean,
required: true,
},
},
components: {
ciHeader,
loadingIcon,
},
data() {
return {
actions: this.getActions(),
};
},
computed: {
status() {
return this.job && this.job.status;
},
shouldRenderContent() {
return !this.isLoading && Object.keys(this.job).length;
},
},
methods: {
getActions() {
const actions = [];
if (this.job.new_issue_path) {
actions.push({
label: 'New issue',
path: this.job.new_issue_path,
cssClass: 'js-new-issue btn btn-new btn-inverted visible-md-block visible-lg-block',
type: 'ujs-link',
});
}
if (this.job.retry_path) {
actions.push({
label: 'Retry',
path: this.job.retry_path,
cssClass: 'js-retry-button btn btn-inverted-secondary visible-md-block visible-lg-block',
type: 'ujs-link',
});
}
return actions;
},
},
watch: {
job() {
this.actions = this.getActions();
},
},
};
</script>
<template>
<div class="js-build-header build-header top-area">
<ci-header
v-if="shouldRenderContent"
:status="status"
item-name="Job"
:item-id="job.id"
:time="job.created_at"
:user="job.user"
:actions="actions"
:hasSidebarButton="true"
/>
<loading-icon
v-if="isLoading"
size="2"
/>
</div>
</template>
<script>
export default {
name: 'SidebarDetailRow',
props: {
title: {
type: String,
required: false,
default: '',
},
value: {
type: String,
required: true,
},
},
computed: {
hasTitle() {
return this.title.length > 0;
},
},
};
</script>
<template>
<p class="build-detail-row">
<span
v-if="hasTitle"
class="build-light-text">
{{title}}:
</span>
{{value}}
</p>
</template>
<script>
import detailRow from './sidebar_detail_row.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import timeagoMixin from '../../vue_shared/mixins/timeago';
import { timeIntervalInWords } from '../../lib/utils/datetime_utility';
export default {
name: 'SidebarDetailsBlock',
props: {
job: {
type: Object,
required: true,
},
isLoading: {
type: Boolean,
required: true,
},
},
mixins: [
timeagoMixin,
],
components: {
detailRow,
loadingIcon,
},
computed: {
shouldRenderContent() {
return !this.isLoading && Object.keys(this.job).length > 0;
},
coverage() {
return `${this.job.coverage}%`;
},
duration() {
return timeIntervalInWords(this.job.duration);
},
queued() {
return timeIntervalInWords(this.job.queued);
},
runnerId() {
return `#${this.job.runner.id}`;
},
},
};
</script>
<template>
<div>
<template v-if="shouldRenderContent">
<div
class="block retry-link"
v-if="job.retry_path || job.new_issue_path">
<a
v-if="job.new_issue_path"
class="js-new-issue btn btn-new btn-inverted"
:href="job.new_issue_path">
New issue
</a>
<a
v-if="job.retry_path"
class="js-retry-job btn btn-inverted-secondary"
:href="job.retry_path"
data-method="post"
rel="nofollow">
Retry
</a>
</div>
<div class="block">
<p
class="build-detail-row js-job-mr"
v-if="job.merge_request">
<span
class="build-light-text">
Merge Request:
</span>
<a :href="job.merge_request.path">
!{{job.merge_request.iid}}
</a>
</p>
<detail-row
class="js-job-duration"
v-if="job.duration"
title="Duration"
:value="duration"
/>
<detail-row
class="js-job-finished"
v-if="job.finished_at"
title="Finished"
:value="timeFormated(job.finished_at)"
/>
<detail-row
class="js-job-erased"
v-if="job.erased_at"
title="Erased"
:value="timeFormated(job.erased_at)"
/>
<detail-row
class="js-job-queued"
v-if="job.queued"
title="Queued"
:value="queued"
/>
<detail-row
class="js-job-runner"
v-if="job.runner"
title="Runner"
:value="runnerId"
/>
<detail-row
class="js-job-coverage"
v-if="job.coverage"
title="Coverage"
:value="coverage"
/>
<p
class="build-detail-row js-job-tags"
v-if="job.tags.length">
<span
class="build-light-text">
Tags:
</span>
<span
v-for="tag in job.tags"
key="tag"
class="label label-primary">
{{tag}}
</span>
</p>
<div
v-if="job.cancel_path"
class="btn-group prepend-top-5"
role="group">
<a
class="js-cancel-job btn btn-sm btn-default"
:href="job.cancel_path"
data-method="post"
rel="nofollow">
Cancel
</a>
</div>
</div>
</template>
<loading-icon
class="prepend-top-10"
v-if="isLoading"
size="2"
/>
</div>
</template>
/* global Flash */
import Vue from 'vue';
import JobMediator from './job_details_mediator';
import jobHeader from './components/header.vue';
import detailsBlock from './components/sidebar_details_block.vue';
document.addEventListener('DOMContentLoaded', () => {
const dataset = document.getElementById('js-job-details-vue').dataset;
const mediator = new JobMediator({ endpoint: dataset.endpoint });
mediator.fetchJob();
// Header
// eslint-disable-next-line no-new
new Vue({
el: '#js-build-header-vue',
data() {
return {
mediator,
};
},
components: {
jobHeader,
},
mounted() {
this.mediator.initBuildClass();
},
updated() {
// Wait for flash message to be appended
Vue.nextTick(() => {
if (this.mediator.build) {
this.mediator.build.verifyTopPosition();
}
});
},
render(createElement) {
return createElement('job-header', {
props: {
isLoading: this.mediator.state.isLoading,
job: this.mediator.store.state.job,
},
});
},
});
// Sidebar information block
// eslint-disable-next-line
new Vue({
el: '#js-details-block-vue',
data() {
return {
mediator,
};
},
components: {
detailsBlock,
},
render(createElement) {
return createElement('details-block', {
props: {
isLoading: this.mediator.state.isLoading,
job: this.mediator.store.state.job,
},
});
},
});
});
/* global Flash */
/* global Build */
import Visibility from 'visibilityjs';
import Poll from '../lib/utils/poll';
import JobStore from './stores/job_store';
import JobService from './services/job_service';
import '../build';
export default class JobMediator {
constructor(options = {}) {
this.options = options;
this.store = new JobStore();
this.service = new JobService(options.endpoint);
this.state = {
isLoading: false,
};
}
initBuildClass() {
this.build = new Build();
}
fetchJob() {
this.poll = new Poll({
resource: this.service,
method: 'getJob',
successCallback: this.successCallback.bind(this),
errorCallback: this.errorCallback.bind(this),
});
if (!Visibility.hidden()) {
this.state.isLoading = true;
this.poll.makeRequest();
} else {
this.getJob();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
}
getJob() {
return this.service.getJob()
.then(response => this.successCallback(response))
.catch(() => this.errorCallback());
}
successCallback(response) {
const data = response.json();
this.state.isLoading = false;
this.store.storeJob(data);
}
errorCallback() {
this.state.isLoading = false;
return new Flash('An error occurred while fetching the job.');
}
}
import Vue from 'vue';
import VueResource from 'vue-resource';
Vue.use(VueResource);
export default class JobService {
constructor(endpoint) {
this.job = Vue.resource(endpoint);
}
getJob() {
return this.job.get();
}
}
export default class JobStore {
constructor() {
this.state = {
job: {},
};
}
storeJob(job = {}) {
this.state.job = job;
}
}
...@@ -146,3 +146,24 @@ window.dateFormat = dateFormat; ...@@ -146,3 +146,24 @@ window.dateFormat = dateFormat;
}; };
})(window); })(window);
}).call(window); }).call(window);
/**
* Port of ruby helper time_interval_in_words.
*
* @param {Number} seconds
* @return {String}
*/
// eslint-disable-next-line import/prefer-default-export
export function timeIntervalInWords(intervalInSeconds) {
const secondsInteger = parseInt(intervalInSeconds, 10);
const minutes = Math.floor(secondsInteger / 60);
const seconds = secondsInteger - (minutes * 60);
let text = '';
if (minutes >= 1) {
text = `${minutes} ${gl.text.pluralize('minute', minutes)} ${seconds} ${gl.text.pluralize('second', seconds)}`;
} else {
text = `${seconds} ${gl.text.pluralize('second', seconds)}`;
}
return text;
}
...@@ -65,14 +65,18 @@ ...@@ -65,14 +65,18 @@
}; };
Milestone.successCallback = function(data, element) { Milestone.successCallback = function(data, element) {
var img_tag; const $avatarContainer = $(element).find('.assignee-icon');
if (data.assignee) { $avatarContainer.empty();
img_tag = $('<img/>');
img_tag.attr('src', data.assignee.avatar_url); if (data.assignees && data.assignees.length > 0) {
img_tag.addClass('avatar s16'); const $avatars = data.assignees.map((assignee) => {
$(element).find('.assignee-icon img').replaceWith(img_tag); const img_tag = $('<img/>');
} else { img_tag.attr('src', assignee.avatar_url);
$(element).find('.assignee-icon').empty(); img_tag.addClass('avatar s16');
return img_tag;
});
$avatarContainer.append($avatars);
} }
}; };
...@@ -161,9 +165,9 @@ ...@@ -161,9 +165,9 @@
data = (function() { data = (function() {
switch (newState) { switch (newState) {
case 'ongoing': case 'ongoing':
return opts.fieldName + '[assignee_id]=' + gon.current_user_id; return `${opts.fieldName}[assignee_ids][]=${gon.current_user_id}`;
case 'unassigned': case 'unassigned':
return opts.fieldName + '[assignee_id]='; return `${opts.fieldName}[assignee_ids][]=0`;
case 'closed': case 'closed':
return opts.fieldName + '[state_event]=close'; return opts.fieldName + '[state_event]=close';
} }
......
...@@ -56,6 +56,7 @@ const normalizeNewlines = function(str) { ...@@ -56,6 +56,7 @@ const normalizeNewlines = function(str) {
this.toggleCommitList = this.toggleCommitList.bind(this); this.toggleCommitList = this.toggleCommitList.bind(this);
this.postComment = this.postComment.bind(this); this.postComment = this.postComment.bind(this);
this.clearFlashWrapper = this.clearFlash.bind(this); this.clearFlashWrapper = this.clearFlash.bind(this);
this.onHashChange = this.onHashChange.bind(this);
this.notes_url = notes_url; this.notes_url = notes_url;
this.note_ids = note_ids; this.note_ids = note_ids;
...@@ -127,7 +128,9 @@ const normalizeNewlines = function(str) { ...@@ -127,7 +128,9 @@ const normalizeNewlines = function(str) {
$(document).on('ajax:success', '.js-main-target-form', this.resetMainTargetForm); $(document).on('ajax:success', '.js-main-target-form', this.resetMainTargetForm);
$(document).on('ajax:complete', '.js-main-target-form', this.reenableTargetFormSubmitButton); $(document).on('ajax:complete', '.js-main-target-form', this.reenableTargetFormSubmitButton);
// when a key is clicked on the notes // when a key is clicked on the notes
return $(document).on('keydown', '.js-note-text', this.keydownNoteText); $(document).on('keydown', '.js-note-text', this.keydownNoteText);
// When the URL fragment/hash has changed, `#note_xxx`
return $(window).on('hashchange', this.onHashChange);
}; };
Notes.prototype.cleanBinding = function() { Notes.prototype.cleanBinding = function() {
...@@ -148,6 +151,7 @@ const normalizeNewlines = function(str) { ...@@ -148,6 +151,7 @@ const normalizeNewlines = function(str) {
$(document).off('ajax:success', '.js-main-target-form'); $(document).off('ajax:success', '.js-main-target-form');
$(document).off('ajax:success', '.js-discussion-note-form'); $(document).off('ajax:success', '.js-discussion-note-form');
$(document).off('ajax:complete', '.js-main-target-form'); $(document).off('ajax:complete', '.js-main-target-form');
$(window).off('hashchange', this.onHashChange);
}; };
Notes.initCommentTypeToggle = function (form) { Notes.initCommentTypeToggle = function (form) {
...@@ -298,8 +302,27 @@ const normalizeNewlines = function(str) { ...@@ -298,8 +302,27 @@ const normalizeNewlines = function(str) {
Notes.prototype.setupNewNote = function($note) { Notes.prototype.setupNewNote = function($note) {
// Update datetime format on the recent note // Update datetime format on the recent note
gl.utils.localTimeAgo($note.find('.js-timeago'), false); gl.utils.localTimeAgo($note.find('.js-timeago'), false);
this.collapseLongCommitList(); this.collapseLongCommitList();
this.taskList.init(); this.taskList.init();
// This stops the note highlight, #note_xxx`, from being removed after real time update
// The `:target` selector does not re-evaluate after we replace element in the DOM
Notes.updateNoteTargetSelector($note);
this.$noteToCleanHighlight = $note;
};
Notes.prototype.onHashChange = function() {
if (this.$noteToCleanHighlight) {
Notes.updateNoteTargetSelector(this.$noteToCleanHighlight);
}
this.$noteToCleanHighlight = null;
};
Notes.updateNoteTargetSelector = function($note) {
const hash = gl.utils.getLocationHash();
$note.toggleClass('target', hash && $note.filter(`#${hash}`).length > 0);
}; };
/* /*
...@@ -597,13 +620,12 @@ const normalizeNewlines = function(str) { ...@@ -597,13 +620,12 @@ const normalizeNewlines = function(str) {
$noteEntityEl = $(noteEntity.html); $noteEntityEl = $(noteEntity.html);
$noteEntityEl.addClass('fade-in-full'); $noteEntityEl.addClass('fade-in-full');
this.revertNoteEditForm($targetNote); this.revertNoteEditForm($targetNote);
gl.utils.localTimeAgo($('.js-timeago', $noteEntityEl));
$noteEntityEl.renderGFM(); $noteEntityEl.renderGFM();
$noteEntityEl.find('.js-task-list-container').taskList('enable');
// Find the note's `li` element by ID and replace it with the updated HTML // Find the note's `li` element by ID and replace it with the updated HTML
$note_li = $('.note-row-' + noteEntity.id); $note_li = $('.note-row-' + noteEntity.id);
$note_li.replaceWith($noteEntityEl); $note_li.replaceWith($noteEntityEl);
this.setupNewNote($noteEntityEl);
if (typeof gl.diffNotesCompileComponents !== 'undefined') { if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents(); gl.diffNotesCompileComponents();
...@@ -1060,7 +1082,7 @@ const normalizeNewlines = function(str) { ...@@ -1060,7 +1082,7 @@ const normalizeNewlines = function(str) {
var targetId = $originalContentEl.data('target-id'); var targetId = $originalContentEl.data('target-id');
var targetType = $originalContentEl.data('target-type'); var targetType = $originalContentEl.data('target-type');
new gl.GLForm($editForm.find('form')); new gl.GLForm($editForm.find('form'), this.enableGFM);
$editForm.find('form') $editForm.find('form')
.attr('action', postUrl) .attr('action', postUrl)
......
...@@ -91,7 +91,7 @@ export default { ...@@ -91,7 +91,7 @@ export default {
@actionClicked="postAction" @actionClicked="postAction"
/> />
<loading-icon <loading-icon
v-else v-if="isLoading"
size="2"/> size="2"/>
</div> </div>
</template> </template>
<script>
export default { export default {
name: 'PipelineNavControls',
props: { props: {
newPipelinePath: { newPipelinePath: {
type: String, type: String,
...@@ -25,28 +27,28 @@ export default { ...@@ -25,28 +27,28 @@ export default {
required: true, required: true,
}, },
}, },
};
</script>
<template>
<div class="nav-controls">
<a
v-if="canCreatePipeline"
:href="newPipelinePath"
class="btn btn-create">
Run Pipeline
</a>
template: ` <a
<div class="nav-controls"> v-if="!hasCiEnabled"
<a :href="helpPagePath"
v-if="canCreatePipeline" class="btn btn-info">
:href="newPipelinePath" Get started with Pipelines
class="btn btn-create"> </a>
Run Pipeline
</a>
<a
v-if="!hasCiEnabled"
:href="helpPagePath"
class="btn btn-info">
Get started with Pipelines
</a>
<a <a
:href="ciLintPath" :href="ciLintPath"
class="btn btn-default"> class="btn btn-default">
CI Lint CI Lint
</a> </a>
</div> </div>
`, </template>
};
export default {
props: {
scope: {
type: String,
required: true,
},
count: {
type: Object,
required: true,
},
paths: {
type: Object,
required: true,
},
},
mounted() {
$(document).trigger('init.scrolling-tabs');
},
template: `
<ul class="nav-links scrolling-tabs">
<li
class="js-pipelines-tab-all"
:class="{ 'active': scope === 'all'}">
<a :href="paths.allPath">
All
<span class="badge js-totalbuilds-count">
{{count.all}}
</span>
</a>
</li>
<li class="js-pipelines-tab-pending"
:class="{ 'active': scope === 'pending'}">
<a :href="paths.pendingPath">
Pending
<span class="badge">
{{count.pending}}
</span>
</a>
</li>
<li class="js-pipelines-tab-running"
:class="{ 'active': scope === 'running'}">
<a :href="paths.runningPath">
Running
<span class="badge">
{{count.running}}
</span>
</a>
</li>
<li class="js-pipelines-tab-finished"
:class="{ 'active': scope === 'finished'}">
<a :href="paths.finishedPath">
Finished
<span class="badge">
{{count.finished}}
</span>
</a>
</li>
<li class="js-pipelines-tab-branches"
:class="{ 'active': scope === 'branches'}">
<a :href="paths.branchesPath">Branches</a>
</li>
<li class="js-pipelines-tab-tags"
:class="{ 'active': scope === 'tags'}">
<a :href="paths.tagsPath">Tags</a>
</li>
</ul>
`,
};
<script>
export default {
name: 'PipelineNavigationTabs',
props: {
scope: {
type: String,
required: true,
},
count: {
type: Object,
required: true,
},
paths: {
type: Object,
required: true,
},
},
mounted() {
$(document).trigger('init.scrolling-tabs');
},
};
</script>
<template>
<ul class="nav-links scrolling-tabs">
<li
class="js-pipelines-tab-all"
:class="{ active: scope === 'all'}">
<a :href="paths.allPath">
All
<span class="badge js-totalbuilds-count">
{{count.all}}
</span>
</a>
</li>
<li
class="js-pipelines-tab-pending"
:class="{ active: scope === 'pending'}">
<a :href="paths.pendingPath">
Pending
<span class="badge">
{{count.pending}}
</span>
</a>
</li>
<li
class="js-pipelines-tab-running"
:class="{ active: scope === 'running'}">
<a :href="paths.runningPath">
Running
<span class="badge">
{{count.running}}
</span>
</a>
</li>
<li
class="js-pipelines-tab-finished"
:class="{ active: scope === 'finished'}">
<a :href="paths.finishedPath">
Finished
<span class="badge">
{{count.finished}}
</span>
</a>
</li>
<li
class="js-pipelines-tab-branches"
:class="{ active: scope === 'branches'}">
<a :href="paths.branchesPath">Branches</a>
</li>
<li
class="js-pipelines-tab-tags"
:class="{ active: scope === 'tags'}">
<a :href="paths.tagsPath">Tags</a>
</li>
</ul>
</template>
<script>
import Visibility from 'visibilityjs';
import PipelinesService from '../services/pipelines_service';
import eventHub from '../event_hub';
import pipelinesTableComponent from '../../vue_shared/components/pipelines_table.vue';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
import emptyState from './empty_state.vue';
import errorState from './error_state.vue';
import navigationTabs from './navigation_tabs.vue';
import navigationControls from './nav_controls.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import Poll from '../../lib/utils/poll';
export default {
props: {
store: {
type: Object,
required: true,
},
},
components: {
tablePagination,
pipelinesTableComponent,
emptyState,
errorState,
navigationTabs,
navigationControls,
loadingIcon,
},
data() {
const pipelinesData = document.querySelector('#pipelines-list-vue').dataset;
return {
endpoint: pipelinesData.endpoint,
cssClass: pipelinesData.cssClass,
helpPagePath: pipelinesData.helpPagePath,
newPipelinePath: pipelinesData.newPipelinePath,
canCreatePipeline: pipelinesData.canCreatePipeline,
allPath: pipelinesData.allPath,
pendingPath: pipelinesData.pendingPath,
runningPath: pipelinesData.runningPath,
finishedPath: pipelinesData.finishedPath,
branchesPath: pipelinesData.branchesPath,
tagsPath: pipelinesData.tagsPath,
hasCi: pipelinesData.hasCi,
ciLintPath: pipelinesData.ciLintPath,
state: this.store.state,
apiScope: 'all',
pagenum: 1,
isLoading: false,
hasError: false,
isMakingRequest: false,
updateGraphDropdown: false,
hasMadeRequest: false,
};
},
computed: {
canCreatePipelineParsed() {
return gl.utils.convertPermissionToBoolean(this.canCreatePipeline);
},
scope() {
const scope = gl.utils.getParameterByName('scope');
return scope === null ? 'all' : scope;
},
shouldRenderErrorState() {
return this.hasError && !this.isLoading;
},
/**
* The empty state should only be rendered when the request is made to fetch all pipelines
* and none is returned.
*
* @return {Boolean}
*/
shouldRenderEmptyState() {
return !this.isLoading &&
!this.hasError &&
this.hasMadeRequest &&
!this.state.pipelines.length &&
(this.scope === 'all' || this.scope === null);
},
/**
* When a specific scope does not have pipelines we render a message.
*
* @return {Boolean}
*/
shouldRenderNoPipelinesMessage() {
return !this.isLoading &&
!this.hasError &&
!this.state.pipelines.length &&
this.scope !== 'all' &&
this.scope !== null;
},
shouldRenderTable() {
return !this.hasError &&
!this.isLoading && this.state.pipelines.length;
},
/**
* Pagination should only be rendered when there is more than one page.
*
* @return {Boolean}
*/
shouldRenderPagination() {
return !this.isLoading &&
this.state.pipelines.length &&
this.state.pageInfo.total > this.state.pageInfo.perPage;
},
hasCiEnabled() {
return this.hasCi !== undefined;
},
paths() {
return {
allPath: this.allPath,
pendingPath: this.pendingPath,
finishedPath: this.finishedPath,
runningPath: this.runningPath,
branchesPath: this.branchesPath,
tagsPath: this.tagsPath,
};
},
pageParameter() {
return gl.utils.getParameterByName('page') || this.pagenum;
},
scopeParameter() {
return gl.utils.getParameterByName('scope') || this.apiScope;
},
},
created() {
this.service = new PipelinesService(this.endpoint);
const poll = new Poll({
resource: this.service,
method: 'getPipelines',
data: { page: this.pageParameter, scope: this.scopeParameter },
successCallback: this.successCallback,
errorCallback: this.errorCallback,
notificationCallback: this.setIsMakingRequest,
});
if (!Visibility.hidden()) {
this.isLoading = true;
poll.makeRequest();
} else {
// If tab is not visible we need to make the first request so we don't show the empty
// state without knowing if there are any pipelines
this.fetchPipelines();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
poll.restart();
} else {
poll.stop();
}
});
eventHub.$on('refreshPipelines', this.fetchPipelines);
},
beforeDestroy() {
eventHub.$off('refreshPipelines');
},
methods: {
/**
* Will change the page number and update the URL.
*
* @param {Number} pageNumber desired page to go to.
*/
change(pageNumber) {
const param = gl.utils.setParamInURL('page', pageNumber);
gl.utils.visitUrl(param);
return param;
},
fetchPipelines() {
if (!this.isMakingRequest) {
this.isLoading = true;
this.service.getPipelines({ scope: this.scopeParameter, page: this.pageParameter })
.then(response => this.successCallback(response))
.catch(() => this.errorCallback());
}
},
successCallback(resp) {
const response = {
headers: resp.headers,
body: resp.json(),
};
this.store.storeCount(response.body.count);
this.store.storePipelines(response.body.pipelines);
this.store.storePagination(response.headers);
this.isLoading = false;
this.updateGraphDropdown = true;
this.hasMadeRequest = true;
},
errorCallback() {
this.hasError = true;
this.isLoading = false;
this.updateGraphDropdown = false;
},
setIsMakingRequest(isMakingRequest) {
this.isMakingRequest = isMakingRequest;
if (isMakingRequest) {
this.updateGraphDropdown = false;
}
},
},
};
</script>
<template>
<div :class="cssClass">
<div
class="top-area scrolling-tabs-container inner-page-scroll-tabs"
v-if="!isLoading && !shouldRenderEmptyState">
<div class="fade-left">
<i
class="fa fa-angle-left"
aria-hidden="true">
</i>
</div>
<div class="fade-right">
<i
class="fa fa-angle-right"
aria-hidden="true">
</i>
</div>
<navigation-tabs
:scope="scope"
:count="state.count"
:paths="paths"
/>
<navigation-controls
:new-pipeline-path="newPipelinePath"
:has-ci-enabled="hasCiEnabled"
:help-page-path="helpPagePath"
:ciLintPath="ciLintPath"
:can-create-pipeline="canCreatePipelineParsed "
/>
</div>
<div class="content-list pipelines">
<loading-icon
label="Loading Pipelines"
size="3"
v-if="isLoading"
/>
<empty-state
v-if="shouldRenderEmptyState"
:help-page-path="helpPagePath"
/>
<error-state v-if="shouldRenderErrorState" />
<div
class="blank-state blank-state-no-icon"
v-if="shouldRenderNoPipelinesMessage">
<h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2>
</div>
<div
class="table-holder"
v-if="shouldRenderTable">
<pipelines-table-component
:pipelines="state.pipelines"
:service="service"
:update-graph-dropdown="updateGraphDropdown"
/>
</div>
<table-pagination
v-if="shouldRenderPagination"
:change="change"
:pageInfo="state.pageInfo"
/>
</div>
</div>
</template>
/* eslint-disable no-new */
/* global Flash */
import '~/flash';
import playIconSvg from 'icons/_icon_play.svg';
import eventHub from '../event_hub';
import loadingIconComponent from '../../vue_shared/components/loading_icon.vue';
export default {
props: {
actions: {
type: Array,
required: true,
},
service: {
type: Object,
required: true,
},
},
components: {
loadingIconComponent,
},
data() {
return {
playIconSvg,
isLoading: false,
};
},
methods: {
onClickAction(endpoint) {
this.isLoading = true;
$(this.$refs.tooltip).tooltip('destroy');
this.service.postAction(endpoint)
.then(() => {
this.isLoading = false;
eventHub.$emit('refreshPipelines');
})
.catch(() => {
this.isLoading = false;
new Flash('An error occured while making the request.');
});
},
isActionDisabled(action) {
if (action.playable === undefined) {
return false;
}
return !action.playable;
},
},
template: `
<div class="btn-group" v-if="actions">
<button
type="button"
class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
title="Manual job"
data-toggle="dropdown"
data-placement="top"
aria-label="Manual job"
ref="tooltip"
:disabled="isLoading">
${playIconSvg}
<i
class="fa fa-caret-down"
aria-hidden="true" />
<loading-icon v-if="isLoading" />
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="action in actions">
<button
type="button"
class="js-pipeline-action-link no-btn btn"
@click="onClickAction(action.path)"
:class="{ 'disabled': isActionDisabled(action) }"
:disabled="isActionDisabled(action)">
${playIconSvg}
<span>{{action.name}}</span>
</button>
</li>
</ul>
</div>
`,
};
<script>
/* global Flash */
import '~/flash';
import playIconSvg from 'icons/_icon_play.svg';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
props: {
actions: {
type: Array,
required: true,
},
service: {
type: Object,
required: true,
},
},
components: {
loadingIcon,
},
data() {
return {
playIconSvg,
isLoading: false,
};
},
methods: {
onClickAction(endpoint) {
this.isLoading = true;
$(this.$refs.tooltip).tooltip('destroy');
this.service.postAction(endpoint)
.then(() => {
this.isLoading = false;
eventHub.$emit('refreshPipelines');
})
.catch(() => {
this.isLoading = false;
// eslint-disable-next-line no-new
new Flash('An error occured while making the request.');
});
},
isActionDisabled(action) {
if (action.playable === undefined) {
return false;
}
return !action.playable;
},
},
};
</script>
<template>
<div class="btn-group">
<button
type="button"
class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
title="Manual job"
data-toggle="dropdown"
data-placement="top"
aria-label="Manual job"
ref="tooltip"
:disabled="isLoading">
<span v-html="playIconSvg"></span>
<i
class="fa fa-caret-down"
aria-hidden="true">
</i>
<loading-icon v-if="isLoading" />
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="action in actions">
<button
type="button"
class="js-pipeline-action-link no-btn btn"
@click="onClickAction(action.path)"
:class="{ disabled: isActionDisabled(action) }"
:disabled="isActionDisabled(action)">
<span v-html="playIconSvg"></span>
<span>{{action.name}}</span>
</button>
</li>
</ul>
</div>
</template>
export default {
props: {
artifacts: {
type: Array,
required: true,
},
},
template: `
<div class="btn-group" role="group">
<button
class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
title="Artifacts"
data-placement="top"
data-toggle="dropdown"
aria-label="Artifacts">
<i class="fa fa-download" aria-hidden="true"></i>
<i class="fa fa-caret-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="artifact in artifacts">
<a
rel="nofollow"
download
:href="artifact.path">
<i class="fa fa-download" aria-hidden="true"></i>
<span>Download {{artifact.name}} artifacts</span>
</a>
</li>
</ul>
</div>
`,
};
<script>
import tooltipMixin from '../../vue_shared/mixins/tooltip';
export default {
props: {
artifacts: {
type: Array,
required: true,
},
},
mixins: [
tooltipMixin,
],
};
</script>
<template>
<div
class="btn-group"
role="group">
<button
class="dropdown-toggle btn btn-default build-artifacts js-pipeline-dropdown-download"
title="Artifacts"
data-placement="top"
data-toggle="dropdown"
aria-label="Artifacts"
ref="tooltip">
<i
class="fa fa-download"
aria-hidden="true">
</i>
<i
class="fa fa-caret-down"
aria-hidden="true">
</i>
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="artifact in artifacts">
<a
rel="nofollow"
download
:href="artifact.path">
<i
class="fa fa-download"
aria-hidden="true">
</i>
<span>Download {{artifact.name}} artifacts</span>
</a>
</li>
</ul>
</div>
</template>
import iconTimerSvg from 'icons/_icon_timer.svg';
import '../../lib/utils/datetime_utility';
export default {
props: {
finishedTime: {
type: String,
required: true,
},
duration: {
type: Number,
required: true,
},
},
data() {
return {
iconTimerSvg,
};
},
updated() {
$(this.$refs.tooltip).tooltip('fixTitle');
},
computed: {
hasDuration() {
return this.duration > 0;
},
hasFinishedTime() {
return this.finishedTime !== '';
},
localTimeFinished() {
return gl.utils.formatDate(this.finishedTime);
},
durationFormated() {
const date = new Date(this.duration * 1000);
let hh = date.getUTCHours();
let mm = date.getUTCMinutes();
let ss = date.getSeconds();
// left pad
if (hh < 10) {
hh = `0${hh}`;
}
if (mm < 10) {
mm = `0${mm}`;
}
if (ss < 10) {
ss = `0${ss}`;
}
return `${hh}:${mm}:${ss}`;
},
finishedTimeFormated() {
const timeAgo = gl.utils.getTimeago();
return timeAgo.format(this.finishedTime);
},
},
template: `
<td class="pipelines-time-ago">
<p
class="duration"
v-if="hasDuration">
<span
v-html="iconTimerSvg">
</span>
{{durationFormated}}
</p>
<p
class="finished-at"
v-if="hasFinishedTime">
<i
class="fa fa-calendar"
aria-hidden="true" />
<time
ref="tooltip"
data-toggle="tooltip"
data-placement="top"
data-container="body"
:title="localTimeFinished">
{{finishedTimeFormated}}
</time>
</p>
</td>
`,
};
<script>
import iconTimerSvg from 'icons/_icon_timer.svg';
import '../../lib/utils/datetime_utility';
import tooltipMixin from '../../vue_shared/mixins/tooltip';
import timeagoMixin from '../../vue_shared/mixins/timeago';
export default {
props: {
finishedTime: {
type: String,
required: true,
},
duration: {
type: Number,
required: true,
},
},
mixins: [
tooltipMixin,
timeagoMixin,
],
data() {
return {
iconTimerSvg,
};
},
computed: {
hasDuration() {
return this.duration > 0;
},
hasFinishedTime() {
return this.finishedTime !== '';
},
durationFormated() {
const date = new Date(this.duration * 1000);
let hh = date.getUTCHours();
let mm = date.getUTCMinutes();
let ss = date.getSeconds();
// left pad
if (hh < 10) {
hh = `0${hh}`;
}
if (mm < 10) {
mm = `0${mm}`;
}
if (ss < 10) {
ss = `0${ss}`;
}
return `${hh}:${mm}:${ss}`;
},
},
};
</script>
<template>
<td class="pipelines-time-ago">
<p
class="duration"
v-if="hasDuration">
<span v-html="iconTimerSvg">
</span>
{{durationFormated}}
</p>
<p
class="finished-at"
v-if="hasFinishedTime">
<i
class="fa fa-calendar"
aria-hidden="true">
</i>
<time
ref="tooltip"
data-placement="top"
data-container="body"
:title="tooltipTitle(finishedTime)">
{{timeFormated(finishedTime)}}
</time>
</p>
</td>
</script>
import Visibility from 'visibilityjs';
import PipelinesService from './services/pipelines_service';
import eventHub from './event_hub';
import pipelinesTableComponent from '../vue_shared/components/pipelines_table';
import tablePagination from '../vue_shared/components/table_pagination.vue';
import emptyState from './components/empty_state.vue';
import errorState from './components/error_state.vue';
import navigationTabs from './components/navigation_tabs';
import navigationControls from './components/nav_controls';
import loadingIcon from '../vue_shared/components/loading_icon.vue';
import Poll from '../lib/utils/poll';
export default {
props: {
store: {
type: Object,
required: true,
},
},
components: {
tablePagination,
pipelinesTableComponent,
emptyState,
errorState,
navigationTabs,
navigationControls,
loadingIcon,
},
data() {
const pipelinesData = document.querySelector('#pipelines-list-vue').dataset;
return {
endpoint: pipelinesData.endpoint,
cssClass: pipelinesData.cssClass,
helpPagePath: pipelinesData.helpPagePath,
newPipelinePath: pipelinesData.newPipelinePath,
canCreatePipeline: pipelinesData.canCreatePipeline,
allPath: pipelinesData.allPath,
pendingPath: pipelinesData.pendingPath,
runningPath: pipelinesData.runningPath,
finishedPath: pipelinesData.finishedPath,
branchesPath: pipelinesData.branchesPath,
tagsPath: pipelinesData.tagsPath,
hasCi: pipelinesData.hasCi,
ciLintPath: pipelinesData.ciLintPath,
state: this.store.state,
apiScope: 'all',
pagenum: 1,
isLoading: false,
hasError: false,
isMakingRequest: false,
updateGraphDropdown: false,
hasMadeRequest: false,
};
},
computed: {
canCreatePipelineParsed() {
return gl.utils.convertPermissionToBoolean(this.canCreatePipeline);
},
scope() {
const scope = gl.utils.getParameterByName('scope');
return scope === null ? 'all' : scope;
},
shouldRenderErrorState() {
return this.hasError && !this.isLoading;
},
/**
* The empty state should only be rendered when the request is made to fetch all pipelines
* and none is returned.
*
* @return {Boolean}
*/
shouldRenderEmptyState() {
return !this.isLoading &&
!this.hasError &&
this.hasMadeRequest &&
!this.state.pipelines.length &&
(this.scope === 'all' || this.scope === null);
},
/**
* When a specific scope does not have pipelines we render a message.
*
* @return {Boolean}
*/
shouldRenderNoPipelinesMessage() {
return !this.isLoading &&
!this.hasError &&
!this.state.pipelines.length &&
this.scope !== 'all' &&
this.scope !== null;
},
shouldRenderTable() {
return !this.hasError &&
!this.isLoading && this.state.pipelines.length;
},
/**
* Pagination should only be rendered when there is more than one page.
*
* @return {Boolean}
*/
shouldRenderPagination() {
return !this.isLoading &&
this.state.pipelines.length &&
this.state.pageInfo.total > this.state.pageInfo.perPage;
},
hasCiEnabled() {
return this.hasCi !== undefined;
},
paths() {
return {
allPath: this.allPath,
pendingPath: this.pendingPath,
finishedPath: this.finishedPath,
runningPath: this.runningPath,
branchesPath: this.branchesPath,
tagsPath: this.tagsPath,
};
},
pageParameter() {
return gl.utils.getParameterByName('page') || this.pagenum;
},
scopeParameter() {
return gl.utils.getParameterByName('scope') || this.apiScope;
},
},
created() {
this.service = new PipelinesService(this.endpoint);
const poll = new Poll({
resource: this.service,
method: 'getPipelines',
data: { page: this.pageParameter, scope: this.scopeParameter },
successCallback: this.successCallback,
errorCallback: this.errorCallback,
notificationCallback: this.setIsMakingRequest,
});
if (!Visibility.hidden()) {
this.isLoading = true;
poll.makeRequest();
} else {
// If tab is not visible we need to make the first request so we don't show the empty
// state without knowing if there are any pipelines
this.fetchPipelines();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
poll.restart();
} else {
poll.stop();
}
});
eventHub.$on('refreshPipelines', this.fetchPipelines);
},
beforeDestroy() {
eventHub.$off('refreshPipelines');
},
methods: {
/**
* Will change the page number and update the URL.
*
* @param {Number} pageNumber desired page to go to.
*/
change(pageNumber) {
const param = gl.utils.setParamInURL('page', pageNumber);
gl.utils.visitUrl(param);
return param;
},
fetchPipelines() {
if (!this.isMakingRequest) {
this.isLoading = true;
this.service.getPipelines({ scope: this.scopeParameter, page: this.pageParameter })
.then(response => this.successCallback(response))
.catch(() => this.errorCallback());
}
},
successCallback(resp) {
const response = {
headers: resp.headers,
body: resp.json(),
};
this.store.storeCount(response.body.count);
this.store.storePipelines(response.body.pipelines);
this.store.storePagination(response.headers);
this.isLoading = false;
this.updateGraphDropdown = true;
this.hasMadeRequest = true;
},
errorCallback() {
this.hasError = true;
this.isLoading = false;
this.updateGraphDropdown = false;
},
setIsMakingRequest(isMakingRequest) {
this.isMakingRequest = isMakingRequest;
if (isMakingRequest) {
this.updateGraphDropdown = false;
}
},
},
template: `
<div :class="cssClass">
<div
class="top-area scrolling-tabs-container inner-page-scroll-tabs"
v-if="!isLoading && !shouldRenderEmptyState">
<div class="fade-left">
<i class="fa fa-angle-left" aria-hidden="true"></i>
</div>
<div class="fade-right">
<i class="fa fa-angle-right" aria-hidden="true"></i>
</div>
<navigation-tabs
:scope="scope"
:count="state.count"
:paths="paths" />
<navigation-controls
:new-pipeline-path="newPipelinePath"
:has-ci-enabled="hasCiEnabled"
:help-page-path="helpPagePath"
:ciLintPath="ciLintPath"
:can-create-pipeline="canCreatePipelineParsed " />
</div>
<div class="content-list pipelines">
<loading-icon
label="Loading Pipelines"
size="3"
v-if="isLoading"
/>
<empty-state
v-if="shouldRenderEmptyState"
:help-page-path="helpPagePath" />
<error-state v-if="shouldRenderErrorState" />
<div
class="blank-state blank-state-no-icon"
v-if="shouldRenderNoPipelinesMessage">
<h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2>
</div>
<div
class="table-holder"
v-if="shouldRenderTable">
<pipelines-table-component
:pipelines="state.pipelines"
:service="service"
:update-graph-dropdown="updateGraphDropdown"
/>
</div>
<table-pagination
v-if="shouldRenderPagination"
:change="change"
:pageInfo="state.pageInfo"
/>
</div>
</div>
`,
};
import Vue from 'vue'; import Vue from 'vue';
import PipelinesStore from './stores/pipelines_store'; import PipelinesStore from './stores/pipelines_store';
import PipelinesComponent from './pipelines'; import pipelinesComponent from './components/pipelines.vue';
import '../vue_shared/vue_resource_interceptor';
$(() => new Vue({
el: document.querySelector('#pipelines-list-vue'),
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#pipelines-list-vue',
data() { data() {
const store = new PipelinesStore(); const store = new PipelinesStore();
...@@ -14,9 +12,13 @@ $(() => new Vue({ ...@@ -14,9 +12,13 @@ $(() => new Vue({
}; };
}, },
components: { components: {
'vue-pipelines': PipelinesComponent, pipelinesComponent,
},
render(createElement) {
return createElement('pipelines-component', {
props: {
store: this.store,
},
});
}, },
template: `
<vue-pipelines :store="store" />
`,
})); }));
import commitIconSvg from 'icons/_icon_commit.svg';
import userAvatarLink from './user_avatar/user_avatar_link.vue';
export default {
props: {
/**
* Indicates the existance of a tag.
* Used to render the correct icon, if true will render `fa-tag` icon,
* if false will render `fa-code-fork` icon.
*/
tag: {
type: Boolean,
required: false,
default: false,
},
/**
* If provided is used to render the branch name and url.
* Should contain the following properties:
* name
* ref_url
*/
commitRef: {
type: Object,
required: false,
default: () => ({}),
},
/**
* Used to link to the commit sha.
*/
commitUrl: {
type: String,
required: false,
default: '',
},
/**
* Used to show the commit short sha that links to the commit url.
*/
shortSha: {
type: String,
required: false,
default: '',
},
/**
* If provided shows the commit tile.
*/
title: {
type: String,
required: false,
default: '',
},
/**
* If provided renders information about the author of the commit.
* When provided should include:
* `avatar_url` to render the avatar icon
* `web_url` to link to user profile
* `username` to render alt and title tags
*/
author: {
type: Object,
required: false,
default: () => ({}),
},
},
computed: {
/**
* Used to verify if all the properties needed to render the commit
* ref section were provided.
*
* TODO: Improve this! Use lodash _.has when we have it.
*
* @returns {Boolean}
*/
hasCommitRef() {
return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
},
/**
* Used to verify if all the properties needed to render the commit
* author section were provided.
*
* TODO: Improve this! Use lodash _.has when we have it.
*
* @returns {Boolean}
*/
hasAuthor() {
return this.author &&
this.author.avatar_url &&
this.author.path &&
this.author.username;
},
/**
* If information about the author is provided will return a string
* to be rendered as the alt attribute of the img tag.
*
* @returns {String}
*/
userImageAltDescription() {
return this.author &&
this.author.username ? `${this.author.username}'s avatar` : null;
},
},
data() {
return { commitIconSvg };
},
components: {
userAvatarLink,
},
template: `
<div class="branch-commit">
<div v-if="hasCommitRef" class="icon-container">
<i v-if="tag" class="fa fa-tag"></i>
<i v-if="!tag" class="fa fa-code-fork"></i>
</div>
<a v-if="hasCommitRef"
class="ref-name"
:href="commitRef.ref_url">
{{commitRef.name}}
</a>
<div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div>
<a class="commit-sha"
:href="commitUrl">
{{shortSha}}
</a>
<div class="commit-title flex-truncate-parent">
<span v-if="title" class="flex-truncate-child">
<user-avatar-link
v-if="hasAuthor"
class="avatar-image-container"
:link-href="author.path"
:img-src="author.avatar_url"
:img-alt="userImageAltDescription"
:tooltip-text="author.username"
/>
<a class="commit-row-message"
:href="commitUrl">
{{title}}
</a>
</span>
<span v-else>
Cant find HEAD commit for this branch
</span>
</div>
</div>
`,
};
<script>
import commitIconSvg from 'icons/_icon_commit.svg';
import userAvatarLink from './user_avatar/user_avatar_link.vue';
export default {
props: {
/**
* Indicates the existance of a tag.
* Used to render the correct icon, if true will render `fa-tag` icon,
* if false will render `fa-code-fork` icon.
*/
tag: {
type: Boolean,
required: false,
default: false,
},
/**
* If provided is used to render the branch name and url.
* Should contain the following properties:
* name
* ref_url
*/
commitRef: {
type: Object,
required: false,
default: () => ({}),
},
/**
* Used to link to the commit sha.
*/
commitUrl: {
type: String,
required: false,
default: '',
},
/**
* Used to show the commit short sha that links to the commit url.
*/
shortSha: {
type: String,
required: false,
default: '',
},
/**
* If provided shows the commit tile.
*/
title: {
type: String,
required: false,
default: '',
},
/**
* If provided renders information about the author of the commit.
* When provided should include:
* `avatar_url` to render the avatar icon
* `web_url` to link to user profile
* `username` to render alt and title tags
*/
author: {
type: Object,
required: false,
default: () => ({}),
},
},
computed: {
/**
* Used to verify if all the properties needed to render the commit
* ref section were provided.
*
* TODO: Improve this! Use lodash _.has when we have it.
*
* @returns {Boolean}
*/
hasCommitRef() {
return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
},
/**
* Used to verify if all the properties needed to render the commit
* author section were provided.
*
* TODO: Improve this! Use lodash _.has when we have it.
*
* @returns {Boolean}
*/
hasAuthor() {
return this.author &&
this.author.avatar_url &&
this.author.path &&
this.author.username;
},
/**
* If information about the author is provided will return a string
* to be rendered as the alt attribute of the img tag.
*
* @returns {String}
*/
userImageAltDescription() {
return this.author &&
this.author.username ? `${this.author.username}'s avatar` : null;
},
},
data() {
return { commitIconSvg };
},
components: {
userAvatarLink,
},
};
</script>
<template>
<div class="branch-commit">
<div v-if="hasCommitRef" class="icon-container">
<i
v-if="tag"
class="fa fa-tag"
aria-hidden="true">
</i>
<i
v-if="!tag"
class="fa fa-code-fork"
aria-hidden="true">
</i>
</div>
<a
v-if="hasCommitRef"
class="ref-name"
:href="commitRef.ref_url">
{{commitRef.name}}
</a>
<div
v-html="commitIconSvg"
class="commit-icon js-commit-icon">
</div>
<a
class="commit-sha"
:href="commitUrl">
{{shortSha}}
</a>
<div class="commit-title flex-truncate-parent">
<span
v-if="title"
class="flex-truncate-child">
<user-avatar-link
v-if="hasAuthor"
class="avatar-image-container"
:link-href="author.path"
:img-src="author.avatar_url"
:img-alt="userImageAltDescription"
:tooltip-text="author.username"
/>
<a class="commit-row-message"
:href="commitUrl">
{{title}}
</a>
</span>
<span v-else>
Cant find HEAD commit for this branch
</span>
</div>
</div>
</template>
...@@ -40,6 +40,11 @@ export default { ...@@ -40,6 +40,11 @@ export default {
required: false, required: false,
default: () => [], default: () => [],
}, },
hasSidebarButton: {
type: Boolean,
required: false,
default: false,
},
}, },
mixins: [ mixins: [
...@@ -66,8 +71,9 @@ export default { ...@@ -66,8 +71,9 @@ export default {
}, },
}; };
</script> </script>
<template> <template>
<header class="page-content-header"> <header class="page-content-header ci-header-container">
<section class="header-main-content"> <section class="header-main-content">
<ci-icon-badge :status="status" /> <ci-icon-badge :status="status" />
...@@ -102,7 +108,7 @@ export default { ...@@ -102,7 +108,7 @@ export default {
</section> </section>
<section <section
class="header-action-button nav-controls" class="header-action-buttons"
v-if="actions.length"> v-if="actions.length">
<template <template
v-for="action in actions"> v-for="action in actions">
...@@ -113,6 +119,15 @@ export default { ...@@ -113,6 +119,15 @@ export default {
{{action.label}} {{action.label}}
</a> </a>
<a
v-if="action.type === 'ujs-link'"
:href="action.path"
data-method="post"
rel="nofollow"
:class="action.cssClass">
{{action.label}}
</a>
<button <button
v-else="action.type === 'button'" v-else="action.type === 'button'"
@click="onClickAction(action)" @click="onClickAction(action)"
...@@ -120,7 +135,6 @@ export default { ...@@ -120,7 +135,6 @@ export default {
:class="action.cssClass" :class="action.cssClass"
type="button"> type="button">
{{action.label}} {{action.label}}
<i <i
v-show="action.isLoading" v-show="action.isLoading"
class="fa fa-spin fa-spinner" class="fa fa-spin fa-spinner"
...@@ -128,6 +142,18 @@ export default { ...@@ -128,6 +142,18 @@ export default {
</i> </i>
</button> </button>
</template> </template>
<button
v-if="hasSidebarButton"
type="button"
class="btn btn-default visible-xs-block visible-sm-block sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header"
aria-label="Toggle Sidebar"
id="toggleSidebar">
<i
class="fa fa-angle-double-left"
aria-hidden="true"
aria-labelledby="toggleSidebar">
</i>
</button>
</section> </section>
</header> </header>
</template> </template>
import PipelinesTableRowComponent from './pipelines_table_row';
/**
* Pipelines Table Component.
*
* Given an array of objects, renders a table.
*/
export default {
props: {
pipelines: {
type: Array,
required: true,
},
service: {
type: Object,
required: true,
},
updateGraphDropdown: {
type: Boolean,
required: false,
default: false,
},
},
components: {
'pipelines-table-row-component': PipelinesTableRowComponent,
},
template: `
<table class="table ci-table">
<thead>
<tr>
<th class="js-pipeline-status pipeline-status">Status</th>
<th class="js-pipeline-info pipeline-info">Pipeline</th>
<th class="js-pipeline-commit pipeline-commit">Commit</th>
<th class="js-pipeline-stages pipeline-stages">Stages</th>
<th class="js-pipeline-date pipeline-date"></th>
<th class="js-pipeline-actions pipeline-actions"></th>
</tr>
</thead>
<tbody>
<template v-for="model in pipelines"
v-bind:model="model">
<tr is="pipelines-table-row-component"
:pipeline="model"
:service="service"
:update-graph-dropdown="updateGraphDropdown"
/>
</template>
</tbody>
</table>
`,
};
<script>
import pipelinesTableRowComponent from './pipelines_table_row.vue';
/**
* Pipelines Table Component.
*
* Given an array of objects, renders a table.
*/
export default {
props: {
pipelines: {
type: Array,
required: true,
},
service: {
type: Object,
required: true,
},
updateGraphDropdown: {
type: Boolean,
required: false,
default: false,
},
},
components: {
pipelinesTableRowComponent,
},
};
</script>
<template>
<table class="table ci-table">
<thead>
<tr>
<th class="js-pipeline-status pipeline-status">Status</th>
<th class="js-pipeline-info pipeline-info">Pipeline</th>
<th class="js-pipeline-commit pipeline-commit">Commit</th>
<th class="js-pipeline-stages pipeline-stages">Stages</th>
<th class="js-pipeline-date pipeline-date"></th>
<th class="js-pipeline-actions pipeline-actions"></th>
</tr>
</thead>
<tbody>
<template
v-for="model in pipelines"
:model="model">
<tr
is="pipelines-table-row-component"
:pipeline="model"
:service="service"
:update-graph-dropdown="updateGraphDropdown"
/>
</template>
</tbody>
</table>
</template>
<script>
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import AsyncButtonComponent from '../../pipelines/components/async_button.vue'; import asyncButtonComponent from '../../pipelines/components/async_button.vue';
import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions'; import pipelinesActionsComponent from '../../pipelines/components/pipelines_actions.vue';
import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts'; import pipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts.vue';
import ciBadge from './ci_badge_link.vue'; import ciBadge from './ci_badge_link.vue';
import PipelinesStageComponent from '../../pipelines/components/stage.vue'; import pipelineStage from '../../pipelines/components/stage.vue';
import PipelinesUrlComponent from '../../pipelines/components/pipeline_url.vue'; import pipelineUrl from '../../pipelines/components/pipeline_url.vue';
import PipelinesTimeagoComponent from '../../pipelines/components/time_ago'; import pipelinesTimeago from '../../pipelines/components/time_ago.vue';
import CommitComponent from './commit'; import commitComponent from './commit.vue';
/** /**
* Pipeline table row. * Pipeline table row.
...@@ -19,30 +20,26 @@ export default { ...@@ -19,30 +20,26 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
service: { service: {
type: Object, type: Object,
required: true, required: true,
}, },
updateGraphDropdown: { updateGraphDropdown: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false, default: false,
}, },
}, },
components: { components: {
'async-button-component': AsyncButtonComponent, asyncButtonComponent,
'pipelines-actions-component': PipelinesActionsComponent, pipelinesActionsComponent,
'pipelines-artifacts-component': PipelinesArtifactsComponent, pipelinesArtifactsComponent,
'commit-component': CommitComponent, commitComponent,
'dropdown-stage': PipelinesStageComponent, pipelineStage,
'pipeline-url': PipelinesUrlComponent, pipelineUrl,
ciBadge, ciBadge,
'time-ago': PipelinesTimeagoComponent, pipelinesTimeago,
}, },
computed: { computed: {
/** /**
* If provided, returns the commit tag. * If provided, returns the commit tag.
...@@ -204,69 +201,76 @@ export default { ...@@ -204,69 +201,76 @@ export default {
return {}; return {};
}, },
}, },
};
</script>
<template>
<tr class="commit">
<td class="commit-link">
<ci-badge :status="pipelineStatus" />
</td>
template: ` <pipeline-url :pipeline="pipeline" />
<tr class="commit">
<td class="commit-link">
<ci-badge :status="pipelineStatus"/>
</td>
<pipeline-url :pipeline="pipeline"></pipeline-url>
<td> <td>
<commit-component <commit-component
:tag="commitTag" :tag="commitTag"
:commit-ref="commitRef" :commit-ref="commitRef"
:commit-url="commitUrl" :commit-url="commitUrl"
:short-sha="commitShortSha" :short-sha="commitShortSha"
:title="commitTitle" :title="commitTitle"
:author="commitAuthor"/> :author="commitAuthor"
</td> />
</td>
<td class="stage-cell"> <td class="stage-cell">
<div class="stage-container dropdown js-mini-pipeline-graph" <div class="stage-container dropdown js-mini-pipeline-graph"
v-if="pipeline.details.stages.length > 0" v-if="pipeline.details.stages.length > 0"
v-for="stage in pipeline.details.stages"> v-for="stage in pipeline.details.stages">
<dropdown-stage <pipeline-stage
:stage="stage" :stage="stage"
:update-dropdown="updateGraphDropdown"/> :update-dropdown="updateGraphDropdown"
</div> />
</td> </div>
</td>
<time-ago <pipelines-timeago
:duration="pipelineDuration" :duration="pipelineDuration"
:finished-time="pipelineFinishedAt" /> :finished-time="pipelineFinishedAt"
/>
<td class="pipeline-actions"> <td class="pipeline-actions">
<div class="pull-right btn-group"> <div class="pull-right btn-group">
<pipelines-actions-component <pipelines-actions-component
v-if="pipeline.details.manual_actions.length" v-if="pipeline.details.manual_actions.length"
:actions="pipeline.details.manual_actions" :actions="pipeline.details.manual_actions"
:service="service" /> :service="service"
/>
<pipelines-artifacts-component <pipelines-artifacts-component
v-if="pipeline.details.artifacts.length" v-if="pipeline.details.artifacts.length"
:artifacts="pipeline.details.artifacts" /> :artifacts="pipeline.details.artifacts"
/>
<async-button-component <async-button-component
v-if="pipeline.flags.retryable" v-if="pipeline.flags.retryable"
:service="service" :service="service"
:endpoint="pipeline.retry_path" :endpoint="pipeline.retry_path"
css-class="js-pipelines-retry-button btn-default btn-retry" css-class="js-pipelines-retry-button btn-default btn-retry"
title="Retry" title="Retry"
icon="repeat" /> icon="repeat"
/>
<async-button-component <async-button-component
v-if="pipeline.flags.cancelable" v-if="pipeline.flags.cancelable"
:service="service" :service="service"
:endpoint="pipeline.cancel_path" :endpoint="pipeline.cancel_path"
css-class="js-pipelines-cancel-button btn-remove" css-class="js-pipelines-cancel-button btn-remove"
title="Cancel" title="Cancel"
icon="remove" icon="remove"
confirm-action-message="Are you sure you want to cancel this pipeline?" /> confirm-action-message="Are you sure you want to cancel this pipeline?"
</div> />
</td> </div>
</tr> </td>
`, </tr>
}; </template>
...@@ -20,12 +20,6 @@ export default { ...@@ -20,12 +20,6 @@ export default {
default: 'top', default: 'top',
}, },
shortFormat: {
type: Boolean,
required: false,
default: false,
},
cssClass: { cssClass: {
type: String, type: String,
required: false, required: false,
...@@ -37,18 +31,12 @@ export default { ...@@ -37,18 +31,12 @@ export default {
tooltipMixin, tooltipMixin,
timeagoMixin, timeagoMixin,
], ],
computed: {
timeagoCssClass() {
return this.shortFormat ? 'js-short-timeago' : 'js-timeago';
},
},
}; };
</script> </script>
<template> <template>
<time <time
:class="[timeagoCssClass, cssClass]" :class="cssClass"
class="js-timeago js-timeago-render" class="js-vue-timeago"
:title="tooltipTitle(time)" :title="tooltipTitle(time)"
:data-placement="tooltipPlacement" :data-placement="tooltipPlacement"
data-container="body" data-container="body"
......
...@@ -59,6 +59,43 @@ ...@@ -59,6 +59,43 @@
} }
} }
.file-blame-legend {
background-color: $gray-light;
text-align: right;
padding: 8px $gl-padding;
@media (max-width: $screen-xs-max) {
text-align: left;
}
.left-label {
padding-right: 5px;
}
.right-label {
padding-left: 5px;
}
.legend-box {
display: inline-block;
width: 10px;
height: 10px;
padding: 0 2px;
}
@for $i from 0 through 5 {
.legend-box-#{$i} {
background-color: mix($blame-cyan, $blame-blue, $i / 5.0 * 100%);
}
}
@for $i from 1 through 4 {
.legend-box-#{$i + 5} {
background-color: mix($blame-gray, $blame-cyan, $i / 4.0 * 100%);
}
}
}
.file-content { .file-content {
background: $white-light; background: $white-light;
...@@ -118,6 +155,19 @@ ...@@ -118,6 +155,19 @@
padding: 5px 10px; padding: 5px 10px;
min-width: 400px; min-width: 400px;
background: $gray-light; background: $gray-light;
border-left: 3px solid;
}
@for $i from 0 through 5 {
td.blame-commit-age-#{$i} {
border-left-color: mix($blame-cyan, $blame-blue, $i / 5.0 * 100%);
}
}
@for $i from 1 through 4 {
td.blame-commit-age-#{$i + 5} {
border-left-color: mix($blame-gray, $blame-cyan, $i / 4.0 * 100%);
}
} }
td.line-numbers { td.line-numbers {
......
...@@ -148,7 +148,8 @@ label { ...@@ -148,7 +148,8 @@ label {
margin-top: 35px; margin-top: 35px;
} }
.form-group .control-label { .form-group .control-label,
.form-group .control-label-full-width {
font-weight: normal; font-weight: normal;
} }
......
...@@ -51,6 +51,10 @@ body { ...@@ -51,6 +51,10 @@ body {
&.limit-container-width { &.limit-container-width {
max-width: $limited-layout-width; max-width: $limited-layout-width;
} }
&.limit-container-width-sm {
max-width: 790px;
}
} }
.alert-wrapper { .alert-wrapper {
......
...@@ -26,10 +26,19 @@ ...@@ -26,10 +26,19 @@
margin-left: 5px; margin-left: 5px;
} }
<<<<<<< HEAD
&.split { &.split {
display: flex; display: flex;
align-items: center; align-items: center;
} }
=======
.panel-empty-heading {
border-bottom: 0;
}
.panel-body {
padding: $gl-padding;
>>>>>>> ce/master
.left { .left {
flex: 1 1 auto; flex: 1 1 auto;
......
...@@ -110,10 +110,12 @@ ...@@ -110,10 +110,12 @@
line-height: 15px; line-height: 15px;
background-color: $gray-light; background-color: $gray-light;
background-image: none; background-image: none;
padding: 3px 18px 3px 5px;
.select2-search-choice-close { .select2-search-choice-close {
top: 4px; top: 5px;
left: 3px; left: initial;
right: 3px;
} }
&.select2-search-choice-focus { &.select2-search-choice-focus {
......
...@@ -74,9 +74,9 @@ $pagination-hover-color: $gl-text-color; ...@@ -74,9 +74,9 @@ $pagination-hover-color: $gl-text-color;
$pagination-hover-bg: $row-hover; $pagination-hover-bg: $row-hover;
$pagination-hover-border: $border-color; $pagination-hover-border: $border-color;
$pagination-active-color: $blue-600; $pagination-active-color: $white-light;
$pagination-active-bg: $white-light; $pagination-active-bg: $gl-link-color;
$pagination-active-border: $border-color; $pagination-active-border: $gl-link-color;
$pagination-disabled-color: #cdcdcd; $pagination-disabled-color: #cdcdcd;
$pagination-disabled-bg: $gray-light; $pagination-disabled-bg: $gray-light;
......
...@@ -369,6 +369,13 @@ $avatar_radius: 50%; ...@@ -369,6 +369,13 @@ $avatar_radius: 50%;
$avatar-border: rgba(0, 0, 0, .1); $avatar-border: rgba(0, 0, 0, .1);
$gl-avatar-size: 40px; $gl-avatar-size: 40px;
/*
* Blame
*/
$blame-gray: #ededed;
$blame-cyan: #acd5f2;
$blame-blue: #254e77;
/* /*
* Builds * Builds
*/ */
......
...@@ -164,7 +164,7 @@ ...@@ -164,7 +164,7 @@
} }
.board-list-component, .board-list-component,
.board-issue-count-holder { .issue-count-badge {
display: none; display: none;
} }
} }
...@@ -459,6 +459,7 @@ ...@@ -459,6 +459,7 @@
margin: 5px; margin: 5px;
} }
<<<<<<< HEAD
.boards-title-holder { .boards-title-holder {
padding: 25px 13px $gl-padding; padding: 25px 13px $gl-padding;
...@@ -479,6 +480,8 @@ ...@@ -479,6 +480,8 @@
border-top: 1px solid $border-color; border-top: 1px solid $border-color;
} }
=======
>>>>>>> ce/master
.page-with-layout-nav.page-with-sub-nav .issue-boards-sidebar { .page-with-layout-nav.page-with-sub-nav .issue-boards-sidebar {
&.right-sidebar { &.right-sidebar {
top: 0; top: 0;
......
...@@ -72,7 +72,7 @@ ...@@ -72,7 +72,7 @@
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
background: $gray-light; background: $gray-light;
border: 1px solid $gray-normal; border: 1px solid $border-color;
color: $gl-text-color; color: $gl-text-color;
.truncated-info { .truncated-info {
...@@ -150,18 +150,20 @@ ...@@ -150,18 +150,20 @@
overflow-y: scroll; overflow-y: scroll;
overflow-x: hidden; overflow-x: hidden;
padding: 10px 20px 20px 5px; padding: 10px 20px 20px 5px;
white-space: pre;
} }
.environment-information { .environment-information {
background-color: $gray-light;
border: 1px solid $border-color; border: 1px solid $border-color;
padding: 12px $gl-padding; padding: 8px $gl-padding 12px;
border-radius: $border-radius-default; border-radius: $border-radius-default;
svg { svg {
position: relative; position: relative;
top: 1px; top: 5px;
margin-right: 5px; margin-right: 5px;
width: 22px;
height: 22px;
} }
} }
...@@ -175,54 +177,31 @@ ...@@ -175,54 +177,31 @@
} }
} }
.status-message { .build-header {
display: inline-block; .ci-header-container,
color: $white-light; .header-action-buttons {
display: flex;
.status-icon {
display: inline-block;
width: 16px;
height: 33px;
} }
.status-text { .ci-header-container {
float: left; min-height: 54px;
opacity: 0;
margin-right: 10px;
font-weight: normal;
line-height: 1.8;
transition: opacity 1s ease-out;
&.animate {
animation: fade-out-status 2s ease;
}
} }
&:hover .status-text { .page-content-header {
opacity: 1; padding: 10px 0 9px;
} }
}
.build-header {
position: relative;
padding: 0;
display: flex;
min-height: 58px;
align-items: center;
@media (max-width: $screen-sm-max) {
padding-right: 40px;
margin-top: 6px;
.btn-inverted { .header-action-buttons {
display: none; @media (max-width: $screen-xs-max) {
.sidebar-toggle-btn {
margin-top: 0;
margin-left: 10px;
max-height: 34px;
}
} }
} }
.header-content { .header-content {
flex: 1;
line-height: 1.8;
a { a {
color: $gl-text-color; color: $gl-text-color;
...@@ -245,7 +224,7 @@ ...@@ -245,7 +224,7 @@
} }
.right-sidebar.build-sidebar { .right-sidebar.build-sidebar {
padding: $gl-padding 0; padding: 0;
&.right-sidebar-collapsed { &.right-sidebar-collapsed {
display: none; display: none;
...@@ -258,6 +237,10 @@ ...@@ -258,6 +237,10 @@
.block { .block {
width: 100%; width: 100%;
&:last-child {
border-bottom: 1px solid $border-gray-normal;
}
&.coverage { &.coverage {
padding: 0 16px 11px; padding: 0 16px 11px;
} }
...@@ -267,34 +250,39 @@ ...@@ -267,34 +250,39 @@
} }
} }
.js-build-variable { .trigger-build-variable {
color: $code-color; color: $code-color;
} }
.js-build-value { .trigger-build-value {
padding: 2px 4px; padding: 2px 4px;
color: $black; color: $black;
background-color: $white-light; background-color: $white-light;
} }
.build-sidebar-header { .label {
padding: 0 $gl-padding $gl-padding; margin-left: 2px;
.gutter-toggle {
margin-top: 0;
}
} }
.retry-link { .retry-link {
color: $gl-link-color;
display: none; display: none;
&:hover { .btn-inverted-secondary {
text-decoration: underline; color: $blue-500;
&:hover {
color: $white-light;
}
} }
@media (max-width: $screen-sm-max) { @media (max-width: $screen-sm-max) {
display: block; display: block;
.btn {
i {
margin-left: 5px;
}
}
} }
} }
...@@ -318,6 +306,12 @@ ...@@ -318,6 +306,12 @@
left: $gl-padding; left: $gl-padding;
width: auto; width: auto;
} }
svg {
position: relative;
top: 2px;
margin-right: 3px;
}
} }
.builds-container { .builds-container {
...@@ -379,6 +373,10 @@ ...@@ -379,6 +373,10 @@
} }
} }
} }
.link-commit {
color: $blue-600;
}
} }
.build-sidebar { .build-sidebar {
......
...@@ -89,7 +89,6 @@ ...@@ -89,7 +89,6 @@
background: $gray-light; background: $gray-light;
border-radius: 0; border-radius: 0;
color: $events-pre-color; color: $events-pre-color;
margin: 0 20px;
overflow: hidden; overflow: hidden;
} }
......
@import "./issues/issue_count_badge"; @import "./issues/issue_count_badge";
<<<<<<< HEAD
@import "./issues/related_issues"; @import "./issues/related_issues";
=======
>>>>>>> ce/master
.issues-list { .issues-list {
.issue { .issue {
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
.interval-pattern-form-group { .interval-pattern-form-group {
label { label {
margin-right: 10px; margin-right: 10px;
font-size: 12px; font-weight: normal;
&[for='custom'] { &[for='custom'] {
margin-right: 0; margin-right: 0;
......
...@@ -564,7 +564,23 @@ ...@@ -564,7 +564,23 @@
} }
.build-content { .build-content {
<<<<<<< HEAD
@include build-content(); @include build-content();
=======
display: inline-block;
padding: 8px 10px 9px;
width: 100%;
border: 1px solid $border-color;
border-radius: 30px;
background-color: $white-light;
}
a.build-content:hover,
button.build-content:hover {
background-color: $stage-hover-bg;
border: 1px solid $stage-hover-border;
color: $gl-text-color;
>>>>>>> ce/master
} }
// Connect first build in each stage with right horizontal line // Connect first build in each stage with right horizontal line
...@@ -942,7 +958,8 @@ ...@@ -942,7 +958,8 @@
border-color: transparent; border-color: transparent;
border-style: solid; border-style: solid;
top: -6px; top: -6px;
left: 2px; left: 50%;
transform: translate(-50%, 0);
border-width: 0 5px 6px; border-width: 0 5px 6px;
} }
...@@ -957,6 +974,14 @@ ...@@ -957,6 +974,14 @@
} }
} }
/**
* Center dropdown menu in mini graph
*/
.mini-pipeline-graph-dropdown-menu.dropdown-menu {
right: auto;
left: 50%;
transform: translate(-50%, 0);
}
/** /**
* Terminal * Terminal
*/ */
...@@ -999,6 +1024,7 @@ ...@@ -999,6 +1024,7 @@
} }
} }
<<<<<<< HEAD
.linked-pipeline-mini-list { .linked-pipeline-mini-list {
display: inline-block; display: inline-block;
...@@ -1199,10 +1225,20 @@ ...@@ -1199,10 +1225,20 @@
} }
.pipeline-header-container { .pipeline-header-container {
=======
.ci-header-container {
>>>>>>> ce/master
min-height: 55px; min-height: 55px;
.text-center { .text-center {
padding-top: 12px; padding-top: 12px;
} }
.header-action-buttons {
.btn,
a {
margin-left: 10px;
}
}
} }
...@@ -19,8 +19,7 @@ ...@@ -19,8 +19,7 @@
overflow: visible; overflow: visible;
} }
&.ci-failed, &.ci-failed {
&.ci-failed_with_warnings {
color: $red-500; color: $red-500;
border-color: $red-500; border-color: $red-500;
...@@ -39,8 +38,7 @@ ...@@ -39,8 +38,7 @@
} }
} }
&.ci-success, &.ci-success {
&.ci-success_with_warnings {
color: $green-600; color: $green-600;
border-color: $green-500; border-color: $green-500;
...@@ -73,7 +71,9 @@ ...@@ -73,7 +71,9 @@
} }
} }
&.ci-pending { &.ci-pending,
&.ci-success_with_warnings,
&.ci-failed_with_warnings {
color: $orange-600; color: $orange-600;
border-color: $orange-500; border-color: $orange-500;
......
...@@ -42,9 +42,7 @@ ...@@ -42,9 +42,7 @@
} }
.git-access-header { .git-access-header {
padding: 16px 40px 11px 0; padding: $gl-padding 0 $gl-padding-top;
line-height: 28px;
font-size: 18px;
} }
.git-clone-holder { .git-clone-holder {
...@@ -66,6 +64,7 @@ ...@@ -66,6 +64,7 @@
.git-clone-holder { .git-clone-holder {
width: 480px; width: 480px;
padding-bottom: $gl-padding;
} }
.nav-controls { .nav-controls {
...@@ -89,9 +88,9 @@ ...@@ -89,9 +88,9 @@
margin: $gl-padding 0; margin: $gl-padding 0;
h3 { h3 {
font-size: 22px; font-size: 19px;
font-weight: normal; font-weight: normal;
margin-top: 1.4em; margin: $gl-padding 0;
} }
} }
......
...@@ -101,6 +101,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -101,6 +101,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:enabled_git_access_protocol, :enabled_git_access_protocol,
:gravatar_enabled, :gravatar_enabled,
:help_page_text, :help_page_text,
:help_page_hide_commercial_content,
:help_page_support_url,
:home_page_url, :home_page_url,
:housekeeping_bitmaps_enabled, :housekeeping_bitmaps_enabled,
:housekeeping_enabled, :housekeeping_enabled,
......
...@@ -14,7 +14,7 @@ module IssuesAction ...@@ -14,7 +14,7 @@ module IssuesAction
respond_to do |format| respond_to do |format|
format.html format.html
format.atom { render layout: false } format.atom { render layout: 'xml.atom' }
end end
end end
end end
...@@ -17,10 +17,18 @@ module SpammableActions ...@@ -17,10 +17,18 @@ module SpammableActions
private private
def ensure_spam_config_loaded!
return @spam_config_loaded if defined?(@spam_config_loaded)
@spam_config_loaded = Gitlab::Recaptcha.load_configurations!
end
def recaptcha_check_with_fallback(&fallback) def recaptcha_check_with_fallback(&fallback)
if spammable.valid? if spammable.valid?
redirect_to spammable redirect_to spammable
elsif render_recaptcha? elsif render_recaptcha?
ensure_spam_config_loaded!
if params[:recaptcha_verification] if params[:recaptcha_verification]
flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.' flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
end end
...@@ -35,7 +43,7 @@ module SpammableActions ...@@ -35,7 +43,7 @@ module SpammableActions
default_params = { request: request } default_params = { request: request }
recaptcha_check = params[:recaptcha_verification] && recaptcha_check = params[:recaptcha_verification] &&
Gitlab::Recaptcha.load_configurations! && ensure_spam_config_loaded! &&
verify_recaptcha verify_recaptcha
return default_params unless recaptcha_check return default_params unless recaptcha_check
......
...@@ -11,7 +11,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -11,7 +11,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
format.html format.html
format.atom do format.atom do
load_events load_events
render layout: false render layout: 'xml.atom'
end end
format.json do format.json do
render json: { render json: {
......
...@@ -47,11 +47,6 @@ class Dashboard::TodosController < Dashboard::ApplicationController ...@@ -47,11 +47,6 @@ class Dashboard::TodosController < Dashboard::ApplicationController
render json: todos_counts render json: todos_counts
end end
# Used in TodosHelper also
def self.todos_count_format(count)
count >= 100 ? '99+' : count
end
private private
def find_todos def find_todos
......
...@@ -58,7 +58,7 @@ class GroupsController < Groups::ApplicationController ...@@ -58,7 +58,7 @@ class GroupsController < Groups::ApplicationController
format.atom do format.atom do
load_events load_events
render layout: false render layout: 'xml.atom'
end end
end end
end end
......
...@@ -80,10 +80,6 @@ class Projects::ApplicationController < ApplicationController ...@@ -80,10 +80,6 @@ class Projects::ApplicationController < ApplicationController
cookies.permanent[:diff_view] = params.delete(:view) if params[:view].present? cookies.permanent[:diff_view] = params.delete(:view) if params[:view].present?
end end
def builds_enabled
return render_404 unless @project.feature_available?(:builds, current_user)
end
def require_pages_enabled! def require_pages_enabled!
not_found unless Gitlab.config.pages.enabled not_found unless Gitlab.config.pages.enabled
end end
......
...@@ -26,7 +26,7 @@ class Projects::CommitsController < Projects::ApplicationController ...@@ -26,7 +26,7 @@ class Projects::CommitsController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html format.html
format.atom { render layout: false } format.atom { render layout: 'xml.atom' }
format.json do format.json do
pager_json( pager_json(
......
...@@ -5,7 +5,6 @@ class Projects::GraphsController < Projects::ApplicationController ...@@ -5,7 +5,6 @@ class Projects::GraphsController < Projects::ApplicationController
before_action :require_non_empty_project before_action :require_non_empty_project
before_action :assign_ref_vars before_action :assign_ref_vars
before_action :authorize_download_code! before_action :authorize_download_code!
before_action :builds_enabled, only: :ci
def show def show
respond_to do |format| respond_to do |format|
......
...@@ -10,11 +10,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -10,11 +10,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :redirect_to_external_issue_tracker, only: [:index, :new] before_action :redirect_to_external_issue_tracker, only: [:index, :new]
before_action :module_enabled before_action :module_enabled
before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests, before_action :issue, except: [:index, :new, :create, :bulk_update]
:related_branches, :can_create_branch, :realtime_changes, :create_merge_request]
# Allow read any issue
before_action :authorize_read_issue!, only: [:show, :realtime_changes]
# Allow write(create) issue # Allow write(create) issue
before_action :authorize_create_issue!, only: [:new, :create] before_action :authorize_create_issue!, only: [:new, :create]
...@@ -56,7 +52,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -56,7 +52,7 @@ class Projects::IssuesController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html format.html
format.atom { render layout: false } format.atom { render layout: 'xml.atom' }
format.json do format.json do
render json: { render json: {
html: view_to_html_string("projects/issues/_issues"), html: view_to_html_string("projects/issues/_issues"),
...@@ -237,18 +233,19 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -237,18 +233,19 @@ class Projects::IssuesController < Projects::ApplicationController
protected protected
def issue def issue
return @issue if defined?(@issue)
# The Sortable default scope causes performance issues when used with find_by # The Sortable default scope causes performance issues when used with find_by
@noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take! @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take!
return render_404 unless can?(current_user, :read_issue, @issue)
@issue
end end
alias_method :subscribable_resource, :issue alias_method :subscribable_resource, :issue
alias_method :issuable, :issue alias_method :issuable, :issue
alias_method :awardable, :issue alias_method :awardable, :issue
alias_method :spammable, :issue alias_method :spammable, :issue
def authorize_read_issue!
return render_404 unless can?(current_user, :read_issue, @issue)
end
def authorize_update_issue! def authorize_update_issue!
return render_404 unless can?(current_user, :update_issue, @issue) return render_404 unless can?(current_user, :update_issue, @issue)
end end
......
...@@ -4,7 +4,6 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -4,7 +4,6 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action :authorize_read_pipeline! before_action :authorize_read_pipeline!
before_action :authorize_create_pipeline!, only: [:new, :create] before_action :authorize_create_pipeline!, only: [:new, :create]
before_action :authorize_update_pipeline!, only: [:retry, :cancel] before_action :authorize_update_pipeline!, only: [:retry, :cancel]
before_action :builds_enabled, only: :charts
wrap_parameters Ci::Pipeline wrap_parameters Ci::Pipeline
......
...@@ -109,7 +109,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -109,7 +109,7 @@ class ProjectsController < Projects::ApplicationController
format.atom do format.atom do
load_events load_events
render layout: false render layout: 'xml.atom'
end end
end end
end end
......
...@@ -10,7 +10,7 @@ class UsersController < ApplicationController ...@@ -10,7 +10,7 @@ class UsersController < ApplicationController
format.atom do format.atom do
load_events load_events
render layout: false render layout: 'xml.atom'
end end
format.json do format.json do
......
...@@ -207,6 +207,10 @@ module ApplicationHelper ...@@ -207,6 +207,10 @@ module ApplicationHelper
'https://' + promo_host 'https://' + promo_host
end end
def support_url
current_application_settings.help_page_support_url.presence || promo_url + '/getting-help/'
end
def page_filter_path(options = {}) def page_filter_path(options = {})
without = options.delete(:without) without = options.delete(:without)
add_label = options.delete(:label) add_label = options.delete(:label)
......
module BlameHelper
def age_map_duration(blame_groups, project)
now = Time.zone.now
start_date = blame_groups.map { |blame_group| blame_group[:commit].committed_date }
.append(project.created_at).min
{
now: now,
started_days_ago: (now - start_date).to_i / 1.day
}
end
def age_map_class(commit_date, duration)
commit_date_days_ago = (duration[:now] - commit_date).to_i / 1.day
# Numbers 0 to 10 come from this calculation, but only commits on the oldest
# day get number 10 (all other numbers can be multiple days), so the range
# is normalized to 0-9
age_group = [(10 * commit_date_days_ago) / duration[:started_days_ago], 9].min
"blame-commit-age-#{age_group}"
end
end
module BroadcastMessagesHelper module BroadcastMessagesHelper
def broadcast_message(message = BroadcastMessage.current) def broadcast_message(message)
return unless message.present? return unless message.present?
content_tag :div, class: 'broadcast-message', style: broadcast_message_style(message) do content_tag :div, class: 'broadcast-message', style: broadcast_message_style(message) do
......
...@@ -124,6 +124,30 @@ module DiffHelper ...@@ -124,6 +124,30 @@ module DiffHelper
!diff_file.deleted_file? && @merge_request && @merge_request.source_project !diff_file.deleted_file? && @merge_request && @merge_request.source_project
end end
def diff_render_error_reason(viewer)
case viewer.render_error
when :too_large
"it is too large"
when :server_side_but_stored_externally
case viewer.diff_file.external_storage
when :lfs
'it is stored in LFS'
else
'it is stored externally'
end
end
end
def diff_render_error_options(viewer)
diff_file = viewer.diff_file
options = []
blob_url = namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.content_sha, diff_file.file_path))
options << link_to('view the blob', blob_url)
options
end
private private
def diff_btn(title, name, selected) def diff_btn(title, name, selected)
......
...@@ -29,7 +29,7 @@ module FormHelper ...@@ -29,7 +29,7 @@ module FormHelper
current_user: true, current_user: true,
project_id: issuable.project.try(:id), project_id: issuable.project.try(:id),
field_name: "#{issuable.class.model_name.param_key}[assignee_ids][]", field_name: "#{issuable.class.model_name.param_key}[assignee_ids][]",
default_label: 'Assignee', default_label: 'Unassigned',
'max-select': 1, 'max-select': 1,
'dropdown-header': 'Assignee', 'dropdown-header': 'Assignee',
multi_select: true, multi_select: true,
......
...@@ -218,6 +218,10 @@ module ProjectsHelper ...@@ -218,6 +218,10 @@ module ProjectsHelper
nav_tabs << :container_registry nav_tabs << :container_registry
end end
if project.builds_enabled? && can?(current_user, :read_pipeline, project)
nav_tabs << :pipelines
end
tab_ability_map.each do |tab, ability| tab_ability_map.each do |tab, ability|
if can?(current_user, ability, project) if can?(current_user, ability, project)
nav_tabs << tab nav_tabs << tab
...@@ -231,7 +235,6 @@ module ProjectsHelper ...@@ -231,7 +235,6 @@ module ProjectsHelper
{ {
environments: :read_environment, environments: :read_environment,
milestones: :read_milestone, milestones: :read_milestone,
pipelines: :read_pipeline,
snippets: :read_project_snippet, snippets: :read_project_snippet,
settings: :admin_project, settings: :admin_project,
builds: :read_build, builds: :read_build,
......
...@@ -4,7 +4,7 @@ module TodosHelper ...@@ -4,7 +4,7 @@ module TodosHelper
end end
def todos_count_format(count) def todos_count_format(count)
count > 99 ? '99+' : count count > 99 ? '99+' : count.to_s
end end
def todos_done_count def todos_done_count
......
module U2fHelper module U2fHelper
def inject_u2f_api? def inject_u2f_api?
browser.chrome? && browser.version.to_i >= 41 && !browser.device.mobile? ((browser.chrome? && browser.version.to_i >= 41) || (browser.opera? && browser.version.to_i >= 40)) && !browser.device.mobile?
end end
end end
...@@ -38,7 +38,12 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -38,7 +38,12 @@ class ApplicationSetting < ActiveRecord::Base
validates :home_page_url, validates :home_page_url,
allow_blank: true, allow_blank: true,
url: true, url: true,
if: :home_page_url_column_exist if: :home_page_url_column_exists?
validates :help_page_support_url,
allow_blank: true,
url: true,
if: :help_page_support_url_column_exists?
validates :after_sign_out_path, validates :after_sign_out_path,
allow_blank: true, allow_blank: true,
...@@ -228,6 +233,7 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -228,6 +233,7 @@ class ApplicationSetting < ActiveRecord::Base
domain_whitelist: Settings.gitlab['domain_whitelist'], domain_whitelist: Settings.gitlab['domain_whitelist'],
gravatar_enabled: Settings.gravatar['enabled'], gravatar_enabled: Settings.gravatar['enabled'],
help_page_text: nil, help_page_text: nil,
help_page_hide_commercial_content: false,
unique_ips_limit_per_user: 10, unique_ips_limit_per_user: 10,
unique_ips_limit_time_window: 3600, unique_ips_limit_time_window: 3600,
unique_ips_limit_enabled: false, unique_ips_limit_enabled: false,
...@@ -276,6 +282,7 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -276,6 +282,7 @@ class ApplicationSetting < ActiveRecord::Base
end end
end end
<<<<<<< HEAD
def elasticsearch_indexing def elasticsearch_indexing
License.feature_available?(:elastic_search) && super License.feature_available?(:elastic_search) && super
end end
...@@ -307,9 +314,16 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -307,9 +314,16 @@ class ApplicationSetting < ActiveRecord::Base
end end
def home_page_url_column_exist def home_page_url_column_exist
=======
def home_page_url_column_exists?
>>>>>>> ce/master
ActiveRecord::Base.connection.column_exists?(:application_settings, :home_page_url) ActiveRecord::Base.connection.column_exists?(:application_settings, :home_page_url)
end end
def help_page_support_url_column_exists?
ActiveRecord::Base.connection.column_exists?(:application_settings, :help_page_support_url)
end
def sidekiq_throttling_column_exists? def sidekiq_throttling_column_exists?
ActiveRecord::Base.connection.column_exists?(:application_settings, :sidekiq_throttling_enabled) ActiveRecord::Base.connection.column_exists?(:application_settings, :sidekiq_throttling_enabled)
end end
......
...@@ -13,14 +13,12 @@ module BlobViewer ...@@ -13,14 +13,12 @@ module BlobViewer
end end
def render_error def render_error
if blob.stored_externally? # Files that are not stored in the repository, like LFS files and
# Files that are not stored in the repository, like LFS files and # build artifacts, can only be rendered using a client-side viewer,
# build artifacts, can only be rendered using a client-side viewer, # since we do not want to read large amounts of data into memory on the
# since we do not want to read large amounts of data into memory on the # server side. Client-side viewers use JS and can fetch the file from
# server side. Client-side viewers use JS and can fetch the file from # `blob_raw_url` using AJAX.
# `blob_raw_url` using AJAX. return :server_side_but_stored_externally if blob.stored_externally?
return :server_side_but_stored_externally
end
super super
end end
......
...@@ -16,7 +16,7 @@ class BroadcastMessage < ActiveRecord::Base ...@@ -16,7 +16,7 @@ class BroadcastMessage < ActiveRecord::Base
def self.current def self.current
Rails.cache.fetch("broadcast_message_current", expires_in: 1.minute) do Rails.cache.fetch("broadcast_message_current", expires_in: 1.minute) do
where("ends_at > :now AND starts_at <= :now", now: Time.zone.now).last where('ends_at > :now AND starts_at <= :now', now: Time.zone.now).order([:created_at, :id]).to_a
end end
end end
......
...@@ -36,8 +36,12 @@ module Ci ...@@ -36,8 +36,12 @@ module Ci
scope :with_artifacts_not_expired, ->() { with_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) } scope :with_artifacts_not_expired, ->() { with_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) }
scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) } scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) }
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
<<<<<<< HEAD
scope :manual_actions, ->() { where(when: :manual).relevant } scope :manual_actions, ->() { where(when: :manual).relevant }
scope :codeclimate, ->() { where(name: 'codeclimate') } scope :codeclimate, ->() { where(name: 'codeclimate') }
=======
scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) }
>>>>>>> ce/master
mount_uploader :artifacts_file, ArtifactUploader mount_uploader :artifacts_file, ArtifactUploader
mount_uploader :artifacts_metadata, ArtifactUploader mount_uploader :artifacts_metadata, ArtifactUploader
...@@ -113,7 +117,7 @@ module Ci ...@@ -113,7 +117,7 @@ module Ci
end end
def playable? def playable?
action? && manual? action? && (manual? || complete?)
end end
def action? def action?
......
...@@ -15,7 +15,7 @@ class CommitStatus < ActiveRecord::Base ...@@ -15,7 +15,7 @@ class CommitStatus < ActiveRecord::Base
validates :pipeline, presence: true, unless: :importing? validates :pipeline, presence: true, unless: :importing?
validates :name, presence: true validates :name, presence: true, unless: :importing?
alias_attribute :author, :user alias_attribute :author, :user
...@@ -112,7 +112,7 @@ class CommitStatus < ActiveRecord::Base ...@@ -112,7 +112,7 @@ class CommitStatus < ActiveRecord::Base
end end
def group_name def group_name
name.gsub(/\d+[\s:\/\\]+\d+\s*/, '').strip name.to_s.gsub(/\d+[\s:\/\\]+\d+\s*/, '').strip
end end
def failed_but_allowed? def failed_but_allowed?
...@@ -132,6 +132,11 @@ class CommitStatus < ActiveRecord::Base ...@@ -132,6 +132,11 @@ class CommitStatus < ActiveRecord::Base
false false
end end
# To be overriden when inherrited from
def cancelable?
false
end
def stuck? def stuck?
false false
end end
...@@ -151,7 +156,7 @@ class CommitStatus < ActiveRecord::Base ...@@ -151,7 +156,7 @@ class CommitStatus < ActiveRecord::Base
end end
def sortable_name def sortable_name
name.split(/(\d+)/).map do |v| name.to_s.split(/(\d+)/).map do |v|
v =~ /\d+/ ? v.to_i : v v =~ /\d+/ ? v.to_i : v
end end
end end
......
module DiffViewer
class Added < Base
include Simple
include Static
self.partial_name = 'added'
end
end
module DiffViewer
class Base
PARTIAL_PATH_PREFIX = 'projects/diffs/viewers'.freeze
class_attribute :partial_name, :type, :extensions, :file_types, :binary, :switcher_icon, :switcher_title
# These limits relate to the sum of the old and new blob sizes.
# Limits related to the actual size of the diff are enforced in Gitlab::Diff::File.
class_attribute :collapse_limit, :size_limit
delegate :partial_path, :loading_partial_path, :rich?, :simple?, :text?, :binary?, to: :class
attr_reader :diff_file
delegate :project, to: :diff_file
def initialize(diff_file)
@diff_file = diff_file
@initially_binary = diff_file.binary?
end
def self.partial_path
File.join(PARTIAL_PATH_PREFIX, partial_name)
end
def self.rich?
type == :rich
end
def self.simple?
type == :simple
end
def self.binary?
binary
end
def self.text?
!binary?
end
def self.can_render?(diff_file, verify_binary: true)
can_render_blob?(diff_file.old_blob, verify_binary: verify_binary) &&
can_render_blob?(diff_file.new_blob, verify_binary: verify_binary)
end
def self.can_render_blob?(blob, verify_binary: true)
return true if blob.nil?
return false if verify_binary && binary? != blob.binary?
return true if extensions&.include?(blob.extension)
return true if file_types&.include?(blob.file_type)
false
end
def collapsed?
return @collapsed if defined?(@collapsed)
return @collapsed = true if diff_file.collapsed?
@collapsed = !diff_file.expanded? && collapse_limit && diff_file.raw_size > collapse_limit
end
def too_large?
return @too_large if defined?(@too_large)
return @too_large = true if diff_file.too_large?
@too_large = size_limit && diff_file.raw_size > size_limit
end
def binary_detected_after_load?
!@initially_binary && diff_file.binary?
end
# This method is used on the server side to check whether we can attempt to
# render the diff_file at all. Human-readable error messages are found in the
# `BlobHelper#diff_render_error_reason` helper.
def render_error
if too_large?
:too_large
end
end
def prepare!
# To be overridden by subclasses
end
end
end
module DiffViewer
module ClientSide
extend ActiveSupport::Concern
included do
self.collapse_limit = 1.megabyte
self.size_limit = 10.megabytes
end
end
end
module DiffViewer
class Deleted < Base
include Simple
include Static
self.partial_name = 'deleted'
end
end
module DiffViewer
class Image < Base
include Rich
include ClientSide
self.partial_name = 'image'
self.extensions = UploaderHelper::IMAGE_EXT
self.binary = true
self.switcher_icon = 'picture-o'
self.switcher_title = 'image diff'
end
end
module DiffViewer
class ModeChanged < Base
include Simple
include Static
self.partial_name = 'mode_changed'
end
end
module DiffViewer
class NoPreview < Base
include Simple
include Static
self.partial_name = 'no_preview'
self.binary = true
end
end
module DiffViewer
class NotDiffable < Base
include Simple
include Static
self.partial_name = 'not_diffable'
self.binary = true
end
end
module DiffViewer
class Renamed < Base
include Simple
include Static
self.partial_name = 'renamed'
end
end
module DiffViewer
module Rich
extend ActiveSupport::Concern
included do
self.type = :rich
self.switcher_icon = 'file-text-o'
self.switcher_title = 'rendered diff'
end
end
end
module DiffViewer
module ServerSide
extend ActiveSupport::Concern
included do
self.collapse_limit = 1.megabyte
self.size_limit = 5.megabytes
end
def prepare!
diff_file.old_blob&.load_all_data!
diff_file.new_blob&.load_all_data!
end
def render_error
# Files that are not stored in the repository, like LFS files and
# build artifacts, can only be rendered using a client-side viewer,
# since we do not want to read large amounts of data into memory on the
# server side. Client-side viewers use JS and can fetch the file from
# `diff_file_blob_raw_path` and `diff_file_old_blob_raw_path` using AJAX.
return :server_side_but_stored_externally if diff_file.stored_externally?
super
end
end
end
module DiffViewer
module Simple
extend ActiveSupport::Concern
included do
self.type = :simple
self.switcher_icon = 'code'
self.switcher_title = 'source diff'
end
end
end
module DiffViewer
module Static
extend ActiveSupport::Concern
# We can always render a static viewer, even if the diff is too large.
def render_error
nil
end
end
end
module DiffViewer
class Text < Base
include Simple
include ServerSide
self.partial_name = 'text'
self.binary = false
# Since the text diff viewer doesn't render the old and new blobs in full,
# we only need the limits related to the actual size of the diff which are
# already enforced in Gitlab::Diff::File.
self.collapse_limit = nil
self.size_limit = nil
end
end
...@@ -213,7 +213,8 @@ class Environment < ActiveRecord::Base ...@@ -213,7 +213,8 @@ class Environment < ActiveRecord::Base
def etag_cache_key def etag_cache_key
Gitlab::Routing.url_helpers.namespace_project_environments_path( Gitlab::Routing.url_helpers.namespace_project_environments_path(
project.namespace, project.namespace,
project) project,
format: :json)
end end
private private
......
...@@ -11,6 +11,7 @@ class GenericCommitStatus < CommitStatus ...@@ -11,6 +11,7 @@ class GenericCommitStatus < CommitStatus
def set_default_values def set_default_values
self.context ||= 'default' self.context ||= 'default'
self.stage ||= 'external' self.stage ||= 'external'
self.stage_idx ||= 1000000
end end
def tags def tags
......
...@@ -122,6 +122,7 @@ class KubernetesService < DeploymentService ...@@ -122,6 +122,7 @@ class KubernetesService < DeploymentService
end end
end end
<<<<<<< HEAD
def rollout_status(environment) def rollout_status(environment)
with_reactive_cache do |data| with_reactive_cache do |data|
specs = filter_by_label(data[:deployments], app: environment.slug) specs = filter_by_label(data[:deployments], app: environment.slug)
...@@ -132,11 +133,19 @@ class KubernetesService < DeploymentService ...@@ -132,11 +133,19 @@ class KubernetesService < DeploymentService
# Caches all pods & deployments in the namespace so other calls don't need to # Caches all pods & deployments in the namespace so other calls don't need to
# block on network access. # block on network access.
=======
# Caches resources in the namespace so other calls don't need to block on
# network access
>>>>>>> ce/master
def calculate_reactive_cache def calculate_reactive_cache
return unless active? && project && !project.pending_delete? return unless active? && project && !project.pending_delete?
# We may want to cache extra things in the future # We may want to cache extra things in the future
<<<<<<< HEAD
{ pods: read_pods, deployments: read_deployments } { pods: read_pods, deployments: read_deployments }
=======
{ pods: read_pods }
>>>>>>> ce/master
end end
TEMPLATE_PLACEHOLDER = 'Kubernetes namespace'.freeze TEMPLATE_PLACEHOLDER = 'Kubernetes namespace'.freeze
...@@ -173,6 +182,7 @@ class KubernetesService < DeploymentService ...@@ -173,6 +182,7 @@ class KubernetesService < DeploymentService
[] []
end end
<<<<<<< HEAD
def read_deployments def read_deployments
kubeclient = build_kubeclient!(api_path: 'apis/extensions', api_version: 'v1beta1') kubeclient = build_kubeclient!(api_path: 'apis/extensions', api_version: 'v1beta1')
...@@ -182,6 +192,8 @@ class KubernetesService < DeploymentService ...@@ -182,6 +192,8 @@ class KubernetesService < DeploymentService
[] []
end end
=======
>>>>>>> ce/master
def kubeclient_ssl_options def kubeclient_ssl_options
opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER } opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER }
......
...@@ -236,7 +236,7 @@ class ProjectPolicy < BasePolicy ...@@ -236,7 +236,7 @@ class ProjectPolicy < BasePolicy
unless project.feature_available?(:builds, user) && repository_enabled unless project.feature_available?(:builds, user) && repository_enabled
cannot!(*named_abilities(:build)) cannot!(*named_abilities(:build))
cannot!(*named_abilities(:pipeline)) cannot!(*named_abilities(:pipeline) - [:read_pipeline])
cannot!(*named_abilities(:pipeline_schedule)) cannot!(*named_abilities(:pipeline_schedule))
cannot!(*named_abilities(:environment)) cannot!(*named_abilities(:environment))
cannot!(*named_abilities(:deployment)) cannot!(*named_abilities(:deployment))
......
class BuildDetailsEntity < BuildEntity class BuildDetailsEntity < JobEntity
expose :coverage, :erased_at, :duration expose :coverage, :erased_at, :duration
expose :tag_list, as: :tags expose :tag_list, as: :tags
expose :user, using: UserEntity expose :user, using: UserEntity
expose :runner, using: RunnerEntity
expose :pipeline, using: PipelineEntity
expose :erased_by, if: -> (*) { build.erased? }, using: UserEntity expose :erased_by, if: -> (*) { build.erased? }, using: UserEntity
expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :update_build, project) } do |build| expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :update_build, project) } do |build|
erase_namespace_project_job_path(project.namespace, project, build) erase_namespace_project_job_path(project.namespace, project, build)
end end
expose :artifacts, using: BuildArtifactEntity
expose :runner, using: RunnerEntity
expose :pipeline, using: PipelineEntity
expose :merge_request, if: -> (*) { can?(current_user, :read_merge_request, build.merge_request) } do expose :merge_request, if: -> (*) { can?(current_user, :read_merge_request, build.merge_request) } do
expose :iid do |build| expose :iid do |build|
build.merge_request.iid build.merge_request.iid
...@@ -28,16 +25,14 @@ class BuildDetailsEntity < BuildEntity ...@@ -28,16 +25,14 @@ class BuildDetailsEntity < BuildEntity
end end
expose :raw_path do |build| expose :raw_path do |build|
raw_namespace_project_build_path(project.namespace, project, build) raw_namespace_project_job_path(project.namespace, project, build)
end end
private private
def build_failed_issue_options def build_failed_issue_options
{ { title: "Build Failed ##{build.id}",
title: "Build Failed ##{build.id}", description: namespace_project_job_path(project.namespace, project, build) }
description: namespace_project_job_url(project.namespace, project, build)
}
end end
def current_user def current_user
......
class BuildSerializer < BaseSerializer class BuildSerializer < BaseSerializer
entity BuildEntity entity JobEntity
def represent_status(resource) def represent_status(resource)
data = represent(resource, { only: [:status] }) data = represent(resource, { only: [:status] })
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment