Commit aaf5e6b8 authored by Robert Speicher's avatar Robert Speicher

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

CE upstream: Thursday

Closes gitlab-ce#31782, gitlab-ce#33529, gitlab-ce#32861, gitlab-ce#33483, #2525, gitlab-ce#31263, gitlab-ce#33403, and gitlab-ce#33106

See merge request !2137
parents 5b23c49f 21856c85
......@@ -10,10 +10,10 @@ engines:
languages:
- ruby
- javascript
exclude_paths:
- "lib/api/v3/*"
eslint:
enabled: true
fixme:
enabled: true
rubocop:
enabled: true
ratings:
......@@ -35,4 +35,13 @@ exclude_paths:
- node_modules/
- spec/
- vendor/
- lib/api/v3/
- .yarn-cache/
- tmp/
- builds/
- coverage/
- public/
- shared/
- webpack-report/
- log/
- backups/
- coverage-javascript/
......@@ -442,6 +442,21 @@ karma:
paths:
- 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:
stage: post-test
services: []
......
......@@ -399,7 +399,7 @@ Style/ParenthesesAroundCondition:
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: short, verbose
Style/PreferredHashMethods:
Enabled: true
Enabled: false
# Checks for an obsolete RuntimeException argument in raise/fail.
Style/RedundantException:
......
......@@ -149,27 +149,34 @@ window.Build = (function () {
Build.prototype.verifyTopPosition = function () {
const $buildPage = $('.build-page');
const $flashError = $('.alert-wrapper');
const $header = $('.build-header', $buildPage);
const $runnersStuck = $('.js-build-stuck', $buildPage);
const $startsEnvironment = $('.js-environment-container', $buildPage);
const $erased = $('.js-build-erased', $buildPage);
const prependTopDefault = 20;
// header + navigation + margin
let topPostion = 168;
if ($header) {
if ($header.length) {
topPostion += $header.outerHeight();
}
if ($runnersStuck) {
if ($runnersStuck.length) {
topPostion += $runnersStuck.outerHeight();
}
if ($startsEnvironment) {
topPostion += $startsEnvironment.outerHeight();
if ($startsEnvironment.length) {
topPostion += $startsEnvironment.outerHeight() + prependTopDefault;
}
if ($erased) {
topPostion += $erased.outerHeight() + 10;
if ($erased.length) {
topPostion += $erased.outerHeight() + prependTopDefault;
}
if ($flashError.length) {
topPostion += $flashError.outerHeight();
}
this.$buildTrace.css({
......@@ -245,6 +252,7 @@ window.Build = (function () {
Build.prototype.toggleSidebar = function (shouldHide) {
const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
const $toggleButton = $('.js-sidebar-build-toggle-header');
this.$buildTrace
.toggleClass('sidebar-expanded', shouldShow)
......@@ -252,6 +260,16 @@ window.Build = (function () {
this.$sidebar
.toggleClass('right-sidebar-expanded', shouldShow)
.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 () {
......@@ -266,6 +284,7 @@ window.Build = (function () {
Build.prototype.sidebarOnClick = function () {
if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
this.verifyTopPosition();
};
Build.prototype.updateArtifactRemoveDate = function () {
......
import Vue from 'vue';
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 PipelineStore from '../../pipelines/stores/pipelines_store';
import eventHub from '../../pipelines/event_hub';
......
......@@ -2,7 +2,6 @@
/* global UsernameValidator */
/* global ActiveTabMemoizer */
/* global ShortcutsNavigation */
/* global Build */
/* global IssuableIndex */
/* global ShortcutsIssuable */
/* global ZenMode */
......@@ -137,9 +136,6 @@ import AuditLogs from './audit_logs';
shortcut_handler = new ShortcutsNavigation();
new UsersSelect();
break;
case 'projects:jobs:show':
new Build();
break;
case 'projects:merge_requests:index':
case 'projects:issues:index':
if (gl.FilteredSearchManager && document.querySelector('.filtered-search')) {
......
......@@ -9,7 +9,7 @@ import StopComponent from './environment_stop.vue';
import RollbackComponent from './environment_rollback.vue';
import TerminalButtonComponent from './environment_terminal_button.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';
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;
};
})(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 @@
};
Milestone.successCallback = function(data, element) {
var img_tag;
if (data.assignee) {
img_tag = $('<img/>');
img_tag.attr('src', data.assignee.avatar_url);
img_tag.addClass('avatar s16');
$(element).find('.assignee-icon img').replaceWith(img_tag);
} else {
$(element).find('.assignee-icon').empty();
const $avatarContainer = $(element).find('.assignee-icon');
$avatarContainer.empty();
if (data.assignees && data.assignees.length > 0) {
const $avatars = data.assignees.map((assignee) => {
const img_tag = $('<img/>');
img_tag.attr('src', assignee.avatar_url);
img_tag.addClass('avatar s16');
return img_tag;
});
$avatarContainer.append($avatars);
}
};
......@@ -161,9 +165,9 @@
data = (function() {
switch (newState) {
case 'ongoing':
return opts.fieldName + '[assignee_id]=' + gon.current_user_id;
return `${opts.fieldName}[assignee_ids][]=${gon.current_user_id}`;
case 'unassigned':
return opts.fieldName + '[assignee_id]=';
return `${opts.fieldName}[assignee_ids][]=0`;
case 'closed':
return opts.fieldName + '[state_event]=close';
}
......
......@@ -56,6 +56,7 @@ const normalizeNewlines = function(str) {
this.toggleCommitList = this.toggleCommitList.bind(this);
this.postComment = this.postComment.bind(this);
this.clearFlashWrapper = this.clearFlash.bind(this);
this.onHashChange = this.onHashChange.bind(this);
this.notes_url = notes_url;
this.note_ids = note_ids;
......@@ -127,7 +128,9 @@ const normalizeNewlines = function(str) {
$(document).on('ajax:success', '.js-main-target-form', this.resetMainTargetForm);
$(document).on('ajax:complete', '.js-main-target-form', this.reenableTargetFormSubmitButton);
// 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() {
......@@ -148,6 +151,7 @@ const normalizeNewlines = function(str) {
$(document).off('ajax:success', '.js-main-target-form');
$(document).off('ajax:success', '.js-discussion-note-form');
$(document).off('ajax:complete', '.js-main-target-form');
$(window).off('hashchange', this.onHashChange);
};
Notes.initCommentTypeToggle = function (form) {
......@@ -298,8 +302,27 @@ const normalizeNewlines = function(str) {
Notes.prototype.setupNewNote = function($note) {
// Update datetime format on the recent note
gl.utils.localTimeAgo($note.find('.js-timeago'), false);
this.collapseLongCommitList();
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) {
$noteEntityEl = $(noteEntity.html);
$noteEntityEl.addClass('fade-in-full');
this.revertNoteEditForm($targetNote);
gl.utils.localTimeAgo($('.js-timeago', $noteEntityEl));
$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
$note_li = $('.note-row-' + noteEntity.id);
$note_li.replaceWith($noteEntityEl);
this.setupNewNote($noteEntityEl);
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents();
......@@ -1060,7 +1082,7 @@ const normalizeNewlines = function(str) {
var targetId = $originalContentEl.data('target-id');
var targetType = $originalContentEl.data('target-type');
new gl.GLForm($editForm.find('form'));
new gl.GLForm($editForm.find('form'), this.enableGFM);
$editForm.find('form')
.attr('action', postUrl)
......
......@@ -91,7 +91,7 @@ export default {
@actionClicked="postAction"
/>
<loading-icon
v-else
v-if="isLoading"
size="2"/>
</div>
</template>
<script>
export default {
name: 'PipelineNavControls',
props: {
newPipelinePath: {
type: String,
......@@ -25,28 +27,28 @@ export default {
required: true,
},
},
};
</script>
<template>
<div class="nav-controls">
<a
v-if="canCreatePipeline"
:href="newPipelinePath"
class="btn btn-create">
Run Pipeline
</a>
template: `
<div class="nav-controls">
<a
v-if="canCreatePipeline"
:href="newPipelinePath"
class="btn btn-create">
Run Pipeline
</a>
<a
v-if="!hasCiEnabled"
:href="helpPagePath"
class="btn btn-info">
Get started with Pipelines
</a>
<a
v-if="!hasCiEnabled"
:href="helpPagePath"
class="btn btn-info">
Get started with Pipelines
</a>
<a
:href="ciLintPath"
class="btn btn-default">
CI Lint
</a>
</div>
`,
};
<a
:href="ciLintPath"
class="btn btn-default">
CI Lint
</a>
</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 PipelinesStore from './stores/pipelines_store';
import PipelinesComponent from './pipelines';
import '../vue_shared/vue_resource_interceptor';
$(() => new Vue({
el: document.querySelector('#pipelines-list-vue'),
import pipelinesComponent from './components/pipelines.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#pipelines-list-vue',
data() {
const store = new PipelinesStore();
......@@ -14,9 +12,13 @@ $(() => new Vue({
};
},
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 {
required: false,
default: () => [],
},
hasSidebarButton: {
type: Boolean,
required: false,
default: false,
},
},
mixins: [
......@@ -66,8 +71,9 @@ export default {
},
};
</script>
<template>
<header class="page-content-header">
<header class="page-content-header ci-header-container">
<section class="header-main-content">
<ci-icon-badge :status="status" />
......@@ -102,7 +108,7 @@ export default {
</section>
<section
class="header-action-button nav-controls"
class="header-action-buttons"
v-if="actions.length">
<template
v-for="action in actions">
......@@ -113,6 +119,15 @@ export default {
{{action.label}}
</a>
<a
v-if="action.type === 'ujs-link'"
:href="action.path"
data-method="post"
rel="nofollow"
:class="action.cssClass">
{{action.label}}
</a>
<button
v-else="action.type === 'button'"
@click="onClickAction(action)"
......@@ -120,7 +135,6 @@ export default {
:class="action.cssClass"
type="button">
{{action.label}}
<i
v-show="action.isLoading"
class="fa fa-spin fa-spinner"
......@@ -128,6 +142,18 @@ export default {
</i>
</button>
</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>
</header>
</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 */
import AsyncButtonComponent from '../../pipelines/components/async_button.vue';
import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions';
import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts';
import asyncButtonComponent from '../../pipelines/components/async_button.vue';
import pipelinesActionsComponent from '../../pipelines/components/pipelines_actions.vue';
import pipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts.vue';
import ciBadge from './ci_badge_link.vue';
import PipelinesStageComponent from '../../pipelines/components/stage.vue';
import PipelinesUrlComponent from '../../pipelines/components/pipeline_url.vue';
import PipelinesTimeagoComponent from '../../pipelines/components/time_ago';
import CommitComponent from './commit';
import pipelineStage from '../../pipelines/components/stage.vue';
import pipelineUrl from '../../pipelines/components/pipeline_url.vue';
import pipelinesTimeago from '../../pipelines/components/time_ago.vue';
import commitComponent from './commit.vue';
/**
* Pipeline table row.
......@@ -19,30 +20,26 @@ export default {
type: Object,
required: true,
},
service: {
type: Object,
required: true,
},
updateGraphDropdown: {
type: Boolean,
required: false,
default: false,
},
},
components: {
'async-button-component': AsyncButtonComponent,
'pipelines-actions-component': PipelinesActionsComponent,
'pipelines-artifacts-component': PipelinesArtifactsComponent,
'commit-component': CommitComponent,
'dropdown-stage': PipelinesStageComponent,
'pipeline-url': PipelinesUrlComponent,
asyncButtonComponent,
pipelinesActionsComponent,
pipelinesArtifactsComponent,
commitComponent,
pipelineStage,
pipelineUrl,
ciBadge,
'time-ago': PipelinesTimeagoComponent,
pipelinesTimeago,
},
computed: {
/**
* If provided, returns the commit tag.
......@@ -204,69 +201,76 @@ export default {
return {};
},
},
};
</script>
<template>
<tr class="commit">
<td class="commit-link">
<ci-badge :status="pipelineStatus" />
</td>
template: `
<tr class="commit">
<td class="commit-link">
<ci-badge :status="pipelineStatus"/>
</td>
<pipeline-url :pipeline="pipeline"></pipeline-url>
<pipeline-url :pipeline="pipeline" />
<td>
<commit-component
:tag="commitTag"
:commit-ref="commitRef"
:commit-url="commitUrl"
:short-sha="commitShortSha"
:title="commitTitle"
:author="commitAuthor"/>
</td>
<td>
<commit-component
:tag="commitTag"
:commit-ref="commitRef"
:commit-url="commitUrl"
:short-sha="commitShortSha"
:title="commitTitle"
:author="commitAuthor"
/>
</td>
<td class="stage-cell">
<div class="stage-container dropdown js-mini-pipeline-graph"
v-if="pipeline.details.stages.length > 0"
v-for="stage in pipeline.details.stages">
<td class="stage-cell">
<div class="stage-container dropdown js-mini-pipeline-graph"
v-if="pipeline.details.stages.length > 0"
v-for="stage in pipeline.details.stages">
<dropdown-stage
:stage="stage"
:update-dropdown="updateGraphDropdown"/>
</div>
</td>
<pipeline-stage
:stage="stage"
:update-dropdown="updateGraphDropdown"
/>
</div>
</td>
<time-ago
:duration="pipelineDuration"
:finished-time="pipelineFinishedAt" />
<pipelines-timeago
:duration="pipelineDuration"
:finished-time="pipelineFinishedAt"
/>
<td class="pipeline-actions">
<div class="pull-right btn-group">
<pipelines-actions-component
v-if="pipeline.details.manual_actions.length"
:actions="pipeline.details.manual_actions"
:service="service" />
<td class="pipeline-actions">
<div class="pull-right btn-group">
<pipelines-actions-component
v-if="pipeline.details.manual_actions.length"
:actions="pipeline.details.manual_actions"
:service="service"
/>
<pipelines-artifacts-component
v-if="pipeline.details.artifacts.length"
:artifacts="pipeline.details.artifacts" />
<pipelines-artifacts-component
v-if="pipeline.details.artifacts.length"
:artifacts="pipeline.details.artifacts"
/>
<async-button-component
v-if="pipeline.flags.retryable"
:service="service"
:endpoint="pipeline.retry_path"
css-class="js-pipelines-retry-button btn-default btn-retry"
title="Retry"
icon="repeat" />
<async-button-component
v-if="pipeline.flags.retryable"
:service="service"
:endpoint="pipeline.retry_path"
css-class="js-pipelines-retry-button btn-default btn-retry"
title="Retry"
icon="repeat"
/>
<async-button-component
v-if="pipeline.flags.cancelable"
:service="service"
:endpoint="pipeline.cancel_path"
css-class="js-pipelines-cancel-button btn-remove"
title="Cancel"
icon="remove"
confirm-action-message="Are you sure you want to cancel this pipeline?" />
</div>
</td>
</tr>
`,
};
<async-button-component
v-if="pipeline.flags.cancelable"
:service="service"
:endpoint="pipeline.cancel_path"
css-class="js-pipelines-cancel-button btn-remove"
title="Cancel"
icon="remove"
confirm-action-message="Are you sure you want to cancel this pipeline?"
/>
</div>
</td>
</tr>
</template>
......@@ -20,12 +20,6 @@ export default {
default: 'top',
},
shortFormat: {
type: Boolean,
required: false,
default: false,
},
cssClass: {
type: String,
required: false,
......@@ -37,18 +31,12 @@ export default {
tooltipMixin,
timeagoMixin,
],
computed: {
timeagoCssClass() {
return this.shortFormat ? 'js-short-timeago' : 'js-timeago';
},
},
};
</script>
<template>
<time
:class="[timeagoCssClass, cssClass]"
class="js-timeago js-timeago-render"
:class="cssClass"
class="js-vue-timeago"
:title="tooltipTitle(time)"
:data-placement="tooltipPlacement"
data-container="body"
......
......@@ -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 {
background: $white-light;
......@@ -118,6 +155,19 @@
padding: 5px 10px;
min-width: 400px;
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 {
......
......@@ -148,7 +148,8 @@ label {
margin-top: 35px;
}
.form-group .control-label {
.form-group .control-label,
.form-group .control-label-full-width {
font-weight: normal;
}
......
......@@ -51,6 +51,10 @@ body {
&.limit-container-width {
max-width: $limited-layout-width;
}
&.limit-container-width-sm {
max-width: 790px;
}
}
.alert-wrapper {
......
......@@ -31,6 +31,14 @@
align-items: center;
}
.panel-empty-heading {
border-bottom: 0;
}
.panel-body {
padding: $gl-padding;
}
.left {
flex: 1 1 auto;
}
......
......@@ -110,10 +110,12 @@
line-height: 15px;
background-color: $gray-light;
background-image: none;
padding: 3px 18px 3px 5px;
.select2-search-choice-close {
top: 4px;
left: 3px;
top: 5px;
left: initial;
right: 3px;
}
&.select2-search-choice-focus {
......
......@@ -74,9 +74,9 @@ $pagination-hover-color: $gl-text-color;
$pagination-hover-bg: $row-hover;
$pagination-hover-border: $border-color;
$pagination-active-color: $blue-600;
$pagination-active-bg: $white-light;
$pagination-active-border: $border-color;
$pagination-active-color: $white-light;
$pagination-active-bg: $gl-link-color;
$pagination-active-border: $gl-link-color;
$pagination-disabled-color: #cdcdcd;
$pagination-disabled-bg: $gray-light;
......
......@@ -369,6 +369,13 @@ $avatar_radius: 50%;
$avatar-border: rgba(0, 0, 0, .1);
$gl-avatar-size: 40px;
/*
* Blame
*/
$blame-gray: #ededed;
$blame-cyan: #acd5f2;
$blame-blue: #254e77;
/*
* Builds
*/
......
......@@ -164,7 +164,7 @@
}
.board-list-component,
.board-issue-count-holder {
.issue-count-badge {
display: none;
}
}
......
......@@ -72,7 +72,7 @@
display: flex;
justify-content: flex-end;
background: $gray-light;
border: 1px solid $gray-normal;
border: 1px solid $border-color;
color: $gl-text-color;
.truncated-info {
......@@ -150,18 +150,20 @@
overflow-y: scroll;
overflow-x: hidden;
padding: 10px 20px 20px 5px;
white-space: pre;
}
.environment-information {
background-color: $gray-light;
border: 1px solid $border-color;
padding: 12px $gl-padding;
padding: 8px $gl-padding 12px;
border-radius: $border-radius-default;
svg {
position: relative;
top: 1px;
top: 5px;
margin-right: 5px;
width: 22px;
height: 22px;
}
}
......@@ -175,54 +177,31 @@
}
}
.status-message {
display: inline-block;
color: $white-light;
.status-icon {
display: inline-block;
width: 16px;
height: 33px;
.build-header {
.ci-header-container,
.header-action-buttons {
display: flex;
}
.status-text {
float: left;
opacity: 0;
margin-right: 10px;
font-weight: normal;
line-height: 1.8;
transition: opacity 1s ease-out;
&.animate {
animation: fade-out-status 2s ease;
}
.ci-header-container {
min-height: 54px;
}
&:hover .status-text {
opacity: 1;
.page-content-header {
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 {
display: none;
.header-action-buttons {
@media (max-width: $screen-xs-max) {
.sidebar-toggle-btn {
margin-top: 0;
margin-left: 10px;
max-height: 34px;
}
}
}
.header-content {
flex: 1;
line-height: 1.8;
a {
color: $gl-text-color;
......@@ -245,7 +224,7 @@
}
.right-sidebar.build-sidebar {
padding: $gl-padding 0;
padding: 0;
&.right-sidebar-collapsed {
display: none;
......@@ -258,6 +237,10 @@
.block {
width: 100%;
&:last-child {
border-bottom: 1px solid $border-gray-normal;
}
&.coverage {
padding: 0 16px 11px;
}
......@@ -267,34 +250,39 @@
}
}
.js-build-variable {
.trigger-build-variable {
color: $code-color;
}
.js-build-value {
.trigger-build-value {
padding: 2px 4px;
color: $black;
background-color: $white-light;
}
.build-sidebar-header {
padding: 0 $gl-padding $gl-padding;
.gutter-toggle {
margin-top: 0;
}
.label {
margin-left: 2px;
}
.retry-link {
color: $gl-link-color;
display: none;
&:hover {
text-decoration: underline;
.btn-inverted-secondary {
color: $blue-500;
&:hover {
color: $white-light;
}
}
@media (max-width: $screen-sm-max) {
display: block;
.btn {
i {
margin-left: 5px;
}
}
}
}
......@@ -318,6 +306,12 @@
left: $gl-padding;
width: auto;
}
svg {
position: relative;
top: 2px;
margin-right: 3px;
}
}
.builds-container {
......@@ -379,6 +373,10 @@
}
}
}
.link-commit {
color: $blue-600;
}
}
.build-sidebar {
......
......@@ -89,7 +89,6 @@
background: $gray-light;
border-radius: 0;
color: $events-pre-color;
margin: 0 20px;
overflow: hidden;
}
......
......@@ -12,7 +12,7 @@
.interval-pattern-form-group {
label {
margin-right: 10px;
font-size: 12px;
font-weight: normal;
&[for='custom'] {
margin-right: 0;
......
......@@ -565,6 +565,19 @@
.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;
}
// Connect first build in each stage with right horizontal line
......@@ -942,7 +955,8 @@
border-color: transparent;
border-style: solid;
top: -6px;
left: 2px;
left: 50%;
transform: translate(-50%, 0);
border-width: 0 5px 6px;
}
......@@ -957,6 +971,14 @@
}
}
/**
* Center dropdown menu in mini graph
*/
.mini-pipeline-graph-dropdown-menu.dropdown-menu {
right: auto;
left: 50%;
transform: translate(-50%, 0);
}
/**
* Terminal
*/
......@@ -1198,11 +1220,18 @@
vertical-align: top;
}
.pipeline-header-container {
.ci-header-container {
min-height: 55px;
.text-center {
padding-top: 12px;
}
.header-action-buttons {
.btn,
a {
margin-left: 10px;
}
}
}
......@@ -19,8 +19,7 @@
overflow: visible;
}
&.ci-failed,
&.ci-failed_with_warnings {
&.ci-failed {
color: $red-500;
border-color: $red-500;
......@@ -39,8 +38,7 @@
}
}
&.ci-success,
&.ci-success_with_warnings {
&.ci-success {
color: $green-600;
border-color: $green-500;
......@@ -73,7 +71,9 @@
}
}
&.ci-pending {
&.ci-pending,
&.ci-success_with_warnings,
&.ci-failed_with_warnings {
color: $orange-600;
border-color: $orange-500;
......
......@@ -42,9 +42,7 @@
}
.git-access-header {
padding: 16px 40px 11px 0;
line-height: 28px;
font-size: 18px;
padding: $gl-padding 0 $gl-padding-top;
}
.git-clone-holder {
......@@ -66,6 +64,7 @@
.git-clone-holder {
width: 480px;
padding-bottom: $gl-padding;
}
.nav-controls {
......@@ -89,9 +88,9 @@
margin: $gl-padding 0;
h3 {
font-size: 22px;
font-size: 19px;
font-weight: normal;
margin-top: 1.4em;
margin: $gl-padding 0;
}
}
......
......@@ -101,6 +101,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:enabled_git_access_protocol,
:gravatar_enabled,
:help_page_text,
:help_page_hide_commercial_content,
:help_page_support_url,
:home_page_url,
:housekeeping_bitmaps_enabled,
:housekeeping_enabled,
......
......@@ -14,7 +14,7 @@ module IssuesAction
respond_to do |format|
format.html
format.atom { render layout: false }
format.atom { render layout: 'xml.atom' }
end
end
end
......@@ -17,10 +17,18 @@ module SpammableActions
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)
if spammable.valid?
redirect_to spammable
elsif render_recaptcha?
ensure_spam_config_loaded!
if params[:recaptcha_verification]
flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
end
......@@ -35,7 +43,7 @@ module SpammableActions
default_params = { request: request }
recaptcha_check = params[:recaptcha_verification] &&
Gitlab::Recaptcha.load_configurations! &&
ensure_spam_config_loaded! &&
verify_recaptcha
return default_params unless recaptcha_check
......
......@@ -11,7 +11,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
format.html
format.atom do
load_events
render layout: false
render layout: 'xml.atom'
end
format.json do
render json: {
......
......@@ -47,11 +47,6 @@ class Dashboard::TodosController < Dashboard::ApplicationController
render json: todos_counts
end
# Used in TodosHelper also
def self.todos_count_format(count)
count >= 100 ? '99+' : count
end
private
def find_todos
......
......@@ -58,7 +58,7 @@ class GroupsController < Groups::ApplicationController
format.atom do
load_events
render layout: false
render layout: 'xml.atom'
end
end
end
......
......@@ -80,10 +80,6 @@ class Projects::ApplicationController < ApplicationController
cookies.permanent[:diff_view] = params.delete(:view) if params[:view].present?
end
def builds_enabled
return render_404 unless @project.feature_available?(:builds, current_user)
end
def require_pages_enabled!
not_found unless Gitlab.config.pages.enabled
end
......
......@@ -26,7 +26,7 @@ class Projects::CommitsController < Projects::ApplicationController
respond_to do |format|
format.html
format.atom { render layout: false }
format.atom { render layout: 'xml.atom' }
format.json do
pager_json(
......
......@@ -5,7 +5,6 @@ class Projects::GraphsController < Projects::ApplicationController
before_action :require_non_empty_project
before_action :assign_ref_vars
before_action :authorize_download_code!
before_action :builds_enabled, only: :ci
def show
respond_to do |format|
......
......@@ -10,11 +10,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :redirect_to_external_issue_tracker, only: [:index, :new]
before_action :module_enabled
before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests,
:related_branches, :can_create_branch, :realtime_changes, :create_merge_request]
# Allow read any issue
before_action :authorize_read_issue!, only: [:show, :realtime_changes]
before_action :issue, except: [:index, :new, :create, :bulk_update, :export_csv]
# Allow write(create) issue
before_action :authorize_create_issue!, only: [:new, :create]
......@@ -56,7 +52,7 @@ class Projects::IssuesController < Projects::ApplicationController
respond_to do |format|
format.html
format.atom { render layout: false }
format.atom { render layout: 'xml.atom' }
format.json do
render json: {
html: view_to_html_string("projects/issues/_issues"),
......@@ -237,18 +233,19 @@ class Projects::IssuesController < Projects::ApplicationController
protected
def issue
return @issue if defined?(@issue)
# The Sortable default scope causes performance issues when used with find_by
@noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take!
return render_404 unless can?(current_user, :read_issue, @issue)
@issue
end
alias_method :subscribable_resource, :issue
alias_method :issuable, :issue
alias_method :awardable, :issue
alias_method :spammable, :issue
def authorize_read_issue!
return render_404 unless can?(current_user, :read_issue, @issue)
end
def authorize_update_issue!
return render_404 unless can?(current_user, :update_issue, @issue)
end
......
......@@ -4,7 +4,6 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action :authorize_read_pipeline!
before_action :authorize_create_pipeline!, only: [:new, :create]
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
before_action :builds_enabled, only: :charts
wrap_parameters Ci::Pipeline
......
......@@ -109,7 +109,7 @@ class ProjectsController < Projects::ApplicationController
format.atom do
load_events
render layout: false
render layout: 'xml.atom'
end
end
end
......
......@@ -10,7 +10,7 @@ class UsersController < ApplicationController
format.atom do
load_events
render layout: false
render layout: 'xml.atom'
end
format.json do
......
......@@ -207,6 +207,10 @@ module ApplicationHelper
'https://' + promo_host
end
def support_url
current_application_settings.help_page_support_url.presence || promo_url + '/getting-help/'
end
def page_filter_path(options = {})
without = options.delete(:without)
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
def broadcast_message(message = BroadcastMessage.current)
def broadcast_message(message)
return unless message.present?
content_tag :div, class: 'broadcast-message', style: broadcast_message_style(message) do
......
......@@ -124,6 +124,30 @@ module DiffHelper
!diff_file.deleted_file? && @merge_request && @merge_request.source_project
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
def diff_btn(title, name, selected)
......
......@@ -29,7 +29,7 @@ module FormHelper
current_user: true,
project_id: issuable.project.try(:id),
field_name: "#{issuable.class.model_name.param_key}[assignee_ids][]",
default_label: 'Assignee',
default_label: 'Unassigned',
'max-select': 1,
'dropdown-header': 'Assignee',
multi_select: true,
......
......@@ -218,6 +218,10 @@ module ProjectsHelper
nav_tabs << :container_registry
end
if project.builds_enabled? && can?(current_user, :read_pipeline, project)
nav_tabs << :pipelines
end
tab_ability_map.each do |tab, ability|
if can?(current_user, ability, project)
nav_tabs << tab
......@@ -231,7 +235,6 @@ module ProjectsHelper
{
environments: :read_environment,
milestones: :read_milestone,
pipelines: :read_pipeline,
snippets: :read_project_snippet,
settings: :admin_project,
builds: :read_build,
......
......@@ -4,7 +4,7 @@ module TodosHelper
end
def todos_count_format(count)
count > 99 ? '99+' : count
count > 99 ? '99+' : count.to_s
end
def todos_done_count
......
module U2fHelper
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
......@@ -38,7 +38,12 @@ class ApplicationSetting < ActiveRecord::Base
validates :home_page_url,
allow_blank: 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,
allow_blank: true,
......@@ -228,6 +233,7 @@ class ApplicationSetting < ActiveRecord::Base
domain_whitelist: Settings.gitlab['domain_whitelist'],
gravatar_enabled: Settings.gravatar['enabled'],
help_page_text: nil,
help_page_hide_commercial_content: false,
unique_ips_limit_per_user: 10,
unique_ips_limit_time_window: 3600,
unique_ips_limit_enabled: false,
......@@ -306,10 +312,14 @@ class ApplicationSetting < ActiveRecord::Base
}
end
def home_page_url_column_exist
def home_page_url_column_exists?
ActiveRecord::Base.connection.column_exists?(:application_settings, :home_page_url)
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?
ActiveRecord::Base.connection.column_exists?(:application_settings, :sidekiq_throttling_enabled)
end
......
......@@ -13,14 +13,12 @@ module BlobViewer
end
def render_error
if blob.stored_externally?
# 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
# `blob_raw_url` using AJAX.
return :server_side_but_stored_externally
end
# 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
# `blob_raw_url` using AJAX.
return :server_side_but_stored_externally if blob.stored_externally?
super
end
......
......@@ -16,7 +16,7 @@ class BroadcastMessage < ActiveRecord::Base
def self.current
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
......
......@@ -36,7 +36,7 @@ module Ci
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 :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
scope :manual_actions, ->() { where(when: :manual).relevant }
scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) }
scope :codeclimate, ->() { where(name: 'codeclimate') }
mount_uploader :artifacts_file, ArtifactUploader
......@@ -113,7 +113,7 @@ module Ci
end
def playable?
action? && manual?
action? && (manual? || complete?)
end
def action?
......
......@@ -15,7 +15,7 @@ class CommitStatus < ActiveRecord::Base
validates :pipeline, presence: true, unless: :importing?
validates :name, presence: true
validates :name, presence: true, unless: :importing?
alias_attribute :author, :user
......@@ -112,7 +112,7 @@ class CommitStatus < ActiveRecord::Base
end
def group_name
name.gsub(/\d+[\s:\/\\]+\d+\s*/, '').strip
name.to_s.gsub(/\d+[\s:\/\\]+\d+\s*/, '').strip
end
def failed_but_allowed?
......@@ -132,6 +132,11 @@ class CommitStatus < ActiveRecord::Base
false
end
# To be overriden when inherrited from
def cancelable?
false
end
def stuck?
false
end
......@@ -151,7 +156,7 @@ class CommitStatus < ActiveRecord::Base
end
def sortable_name
name.split(/(\d+)/).map do |v|
name.to_s.split(/(\d+)/).map do |v|
v =~ /\d+/ ? v.to_i : v
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
def etag_cache_key
Gitlab::Routing.url_helpers.namespace_project_environments_path(
project.namespace,
project)
project,
format: :json)
end
private
......
......@@ -11,6 +11,7 @@ class GenericCommitStatus < CommitStatus
def set_default_values
self.context ||= 'default'
self.stage ||= 'external'
self.stage_idx ||= 1000000
end
def tags
......
......@@ -130,8 +130,8 @@ class KubernetesService < DeploymentService
end
end
# Caches all pods & deployments in the namespace so other calls don't need to
# block on network access.
# Caches resources in the namespace so other calls don't need to block on
# network access
def calculate_reactive_cache
return unless active? && project && !project.pending_delete?
......
......@@ -236,7 +236,7 @@ class ProjectPolicy < BasePolicy
unless project.feature_available?(:builds, user) && repository_enabled
cannot!(*named_abilities(:build))
cannot!(*named_abilities(:pipeline))
cannot!(*named_abilities(:pipeline) - [:read_pipeline])
cannot!(*named_abilities(:pipeline_schedule))
cannot!(*named_abilities(:environment))
cannot!(*named_abilities(:deployment))
......
class BuildDetailsEntity < BuildEntity
class BuildDetailsEntity < JobEntity
expose :coverage, :erased_at, :duration
expose :tag_list, as: :tags
expose :user, using: UserEntity
expose :runner, using: RunnerEntity
expose :pipeline, using: PipelineEntity
expose :erased_by, if: -> (*) { build.erased? }, using: UserEntity
expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :update_build, project) } do |build|
erase_namespace_project_job_path(project.namespace, project, build)
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 :iid do |build|
build.merge_request.iid
......@@ -34,10 +31,8 @@ class BuildDetailsEntity < BuildEntity
private
def build_failed_issue_options
{
title: "Build Failed ##{build.id}",
description: namespace_project_job_url(project.namespace, project, build)
}
{ title: "Build Failed ##{build.id}",
description: namespace_project_job_path(project.namespace, project, build) }
end
def current_user
......
class BuildSerializer < BaseSerializer
entity BuildEntity
entity JobEntity
def represent_status(resource)
data = represent(resource, { only: [:status] })
......
......@@ -24,6 +24,6 @@ class DeploymentEntity < Grape::Entity
expose :user, using: UserEntity
expose :commit, using: CommitEntity
expose :deployable, using: BuildEntity
expose :manual_actions, using: BuildEntity
expose :deployable, using: JobEntity
expose :manual_actions, using: JobEntity
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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