Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Tatuya Kamada
gitlab-ce
Commits
2f1701c5
Commit
2f1701c5
authored
Jan 09, 2017
by
Fatih Acet
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'auto-pipelines-vue' into 'master'
Pipelines Vue See merge request !7196
parents
6524ec5d
1d02d5b1
Changes
43
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
43 changed files
with
2091 additions
and
242 deletions
+2091
-242
app/assets/javascripts/dispatcher.js.es6
app/assets/javascripts/dispatcher.js.es6
+0
-5
app/assets/javascripts/lib/utils/common_utils.js.es6
app/assets/javascripts/lib/utils/common_utils.js.es6
+15
-0
app/assets/javascripts/vue_pagination/index.js.es6
app/assets/javascripts/vue_pagination/index.js.es6
+148
-0
app/assets/javascripts/vue_pipelines_index/index.js.es6
app/assets/javascripts/vue_pipelines_index/index.js.es6
+41
-0
app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
...s/javascripts/vue_pipelines_index/pipeline_actions.js.es6
+99
-0
app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6
...ssets/javascripts/vue_pipelines_index/pipeline_url.js.es6
+63
-0
app/assets/javascripts/vue_pipelines_index/pipelines.js.es6
app/assets/javascripts/vue_pipelines_index/pipelines.js.es6
+131
-0
app/assets/javascripts/vue_pipelines_index/stage.js.es6
app/assets/javascripts/vue_pipelines_index/stage.js.es6
+76
-0
app/assets/javascripts/vue_pipelines_index/stages.js.es6
app/assets/javascripts/vue_pipelines_index/stages.js.es6
+21
-0
app/assets/javascripts/vue_pipelines_index/status.js.es6
app/assets/javascripts/vue_pipelines_index/status.js.es6
+34
-0
app/assets/javascripts/vue_pipelines_index/store.js.es6
app/assets/javascripts/vue_pipelines_index/store.js.es6
+59
-0
app/assets/javascripts/vue_pipelines_index/time_ago.js.es6
app/assets/javascripts/vue_pipelines_index/time_ago.js.es6
+73
-0
app/assets/javascripts/vue_realtime_listener/index.js.es6
app/assets/javascripts/vue_realtime_listener/index.js.es6
+18
-0
app/assets/stylesheets/pages/pipelines.scss
app/assets/stylesheets/pages/pipelines.scss
+10
-0
app/controllers/projects/pipelines_controller.rb
app/controllers/projects/pipelines_controller.rb
+26
-4
app/models/ci/pipeline.rb
app/models/ci/pipeline.rb
+10
-2
app/serializers/build_action_entity.rb
app/serializers/build_action_entity.rb
+14
-0
app/serializers/build_artifact_entity.rb
app/serializers/build_artifact_entity.rb
+14
-0
app/serializers/commit_entity.rb
app/serializers/commit_entity.rb
+4
-0
app/serializers/pipeline_entity.rb
app/serializers/pipeline_entity.rb
+83
-0
app/serializers/pipeline_serializer.rb
app/serializers/pipeline_serializer.rb
+40
-0
app/serializers/request_aware_entity.rb
app/serializers/request_aware_entity.rb
+3
-6
app/serializers/stage_entity.rb
app/serializers/stage_entity.rb
+38
-0
app/serializers/status_entity.rb
app/serializers/status_entity.rb
+8
-0
app/views/projects/pipelines/index.html.haml
app/views/projects/pipelines/index.html.haml
+27
-14
config/application.rb
config/application.rb
+2
-0
lib/api/helpers.rb
lib/api/helpers.rb
+1
-38
lib/api/helpers/pagination.rb
lib/api/helpers/pagination.rb
+45
-0
spec/controllers/projects/pipelines_controller_spec.rb
spec/controllers/projects/pipelines_controller_spec.rb
+21
-1
spec/factories/ci/pipelines.rb
spec/factories/ci/pipelines.rb
+8
-0
spec/factories/ci/runners.rb
spec/factories/ci/runners.rb
+4
-0
spec/features/projects/pipelines/pipelines_spec.rb
spec/features/projects/pipelines/pipelines_spec.rb
+269
-172
spec/javascripts/vue_pagination/pagination_spec.js.es6
spec/javascripts/vue_pagination/pagination_spec.js.es6
+168
-0
spec/lib/api/helpers/pagination_spec.rb
spec/lib/api/helpers/pagination_spec.rb
+94
-0
spec/models/ci/pipeline_spec.rb
spec/models/ci/pipeline_spec.rb
+42
-0
spec/serializers/build_action_entity_spec.rb
spec/serializers/build_action_entity_spec.rb
+21
-0
spec/serializers/build_artifact_entity_spec.rb
spec/serializers/build_artifact_entity_spec.rb
+22
-0
spec/serializers/commit_entity_spec.rb
spec/serializers/commit_entity_spec.rb
+4
-0
spec/serializers/pipeline_entity_spec.rb
spec/serializers/pipeline_entity_spec.rb
+138
-0
spec/serializers/pipeline_serializer_spec.rb
spec/serializers/pipeline_serializer_spec.rb
+101
-0
spec/serializers/request_aware_entity_spec.rb
spec/serializers/request_aware_entity_spec.rb
+22
-0
spec/serializers/stage_entity_spec.rb
spec/serializers/stage_entity_spec.rb
+51
-0
spec/serializers/status_entity_spec.rb
spec/serializers/status_entity_spec.rb
+23
-0
No files found.
app/assets/javascripts/dispatcher.js.es6
View file @
2f1701c5
...
...
@@ -184,11 +184,6 @@
new TreeView();
}
break;
case 'projects:pipelines:index':
new gl.MiniPipelineGraph({
container: '.js-pipeline-table',
});
break;
case 'projects:pipelines:builds':
case 'projects:pipelines:show':
const { controllerAction } = document.querySelector('.js-pipeline-container').dataset;
...
...
app/assets/javascripts/lib/utils/common_utils.js
→
app/assets/javascripts/lib/utils/common_utils.js
.es6
View file @
2f1701c5
...
...
@@ -139,6 +139,21 @@
}, 200);
};
/**
this will take in the `name` of the param you want to parse in the url
if the name does not exist this function will return `null`
otherwise it will return the value of the param key provided
*/
w.gl.utils.getParameterByName = (name) => {
const url = window.location.href;
name = name.replace(/[[\]]/g, '\\$&');
const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`);
const results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, ' '));
};
})(window);
}).call(this);
app/assets/javascripts/vue_pagination/index.js.es6
0 → 100644
View file @
2f1701c5
/* global Vue, gl */
/* eslint-disable no-param-reassign, no-plusplus */
((gl) => {
const PAGINATION_UI_BUTTON_LIMIT = 4;
const UI_LIMIT = 6;
const SPREAD = '...';
const PREV = 'Prev';
const NEXT = 'Next';
const FIRST = '<< First';
const LAST = 'Last >>';
gl.VueGlPagination = Vue.extend({
props: {
/**
This function will take the information given by the pagination component
And make a new Turbolinks call
Here is an example `change` method:
change(pagenum, apiScope) {
Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`);
},
*/
change: {
type: Function,
required: true,
},
/**
pageInfo will come from the headers of the API call
in the `.then` clause of the VueResource API call
there should be a function that contructs the pageInfo for this component
This is an example:
const pageInfo = headers => ({
perPage: +headers['X-Per-Page'],
page: +headers['X-Page'],
total: +headers['X-Total'],
totalPages: +headers['X-Total-Pages'],
nextPage: +headers['X-Next-Page'],
previousPage: +headers['X-Prev-Page'],
});
*/
pageInfo: {
type: Object,
required: true,
},
},
methods: {
changePage(e) {
let apiScope = gl.utils.getParameterByName('scope');
if (!apiScope) apiScope = 'all';
const text = e.target.innerText;
const { totalPages, nextPage, previousPage } = this.pageInfo;
switch (text) {
case SPREAD:
break;
case LAST:
this.change(totalPages, apiScope);
break;
case NEXT:
this.change(nextPage, apiScope);
break;
case PREV:
this.change(previousPage, apiScope);
break;
case FIRST:
this.change(1, apiScope);
break;
default:
this.change(+text, apiScope);
break;
}
},
},
computed: {
prev() {
return this.pageInfo.previousPage;
},
next() {
return this.pageInfo.nextPage;
},
getItems() {
const total = this.pageInfo.totalPages;
const page = this.pageInfo.page;
const items = [];
if (page > 1) items.push({ title: FIRST });
if (page > 1) {
items.push({ title: PREV, prev: true });
} else {
items.push({ title: PREV, disabled: true, prev: true });
}
if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true });
const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1);
const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total);
for (let i = start; i <= end; i++) {
const isActive = i === page;
items.push({ title: i, active: isActive, page: true });
}
if (total - page > PAGINATION_UI_BUTTON_LIMIT) {
items.push({ title: SPREAD, separator: true, page: true });
}
if (page === total) {
items.push({ title: NEXT, disabled: true, next: true });
} else if (total - page >= 1) {
items.push({ title: NEXT, next: true });
}
if (total - page >= 1) items.push({ title: LAST, last: true });
return items;
},
},
template: `
<div class="gl-pagination">
<ul class="pagination clearfix">
<li v-for='item in getItems'
:class='{
page: item.page,
prev: item.prev,
next: item.next,
separator: item.separator,
active: item.active,
disabled: item.disabled
}'
>
<a @click="changePage($event)">{{item.title}}</a>
</li>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
app/assets/javascripts/vue_pipelines_index/index.js.es6
0 → 100644
View file @
2f1701c5
/* global Vue, VueResource, gl */
/*= require vue_common_component/commit */
/*= require vue-resource
/*= require boards/vue_resource_interceptor */
/*= require ./status.js.es6 */
/*= require ./store.js.es6 */
/*= require ./pipeline_url.js.es6 */
/*= require ./stage.js.es6 */
/*= require ./stages.js.es6 */
/*= require ./pipeline_actions.js.es6 */
/*= require ./time_ago.js.es6 */
/*= require ./pipelines.js.es6 */
(() => {
const project = document.querySelector('.pipelines');
const entry = document.querySelector('.vue-pipelines-index');
const svgs = document.querySelector('.pipeline-svgs');
Vue.use(VueResource);
if (!entry) return null;
return new Vue({
el: entry,
data: {
scope: project.dataset.url,
store: new gl.PipelineStore(),
svgs: svgs.dataset,
},
components: {
'vue-pipelines': gl.VuePipelines,
},
template: `
<vue-pipelines
:scope='scope'
:store='store'
:svgs='svgs'
>
</vue-pipelines>
`,
});
})();
app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
0 → 100644
View file @
2f1701c5
/* global Vue, Flash, gl */
/* eslint-disable no-param-reassign */
((gl) => {
gl.VuePipelineActions = Vue.extend({
props: ['pipeline', 'svgs'],
computed: {
actions() {
return this.pipeline.details.manual_actions.length > 0;
},
artifacts() {
return this.pipeline.details.artifacts.length > 0;
},
},
methods: {
download(name) {
return `Download ${name} artifacts`;
},
},
template: `
<td class="pipeline-actions hidden-xs">
<div class="controls pull-right">
<div class="btn-group inline">
<div class="btn-group">
<a
v-if='actions'
class="dropdown-toggle btn btn-default js-pipeline-dropdown-manual-actions"
data-toggle="dropdown"
title="Manual build"
alt="Manual Build"
>
<span v-html='svgs.iconPlay'></span>
<i class="fa fa-caret-down"></i>
</a>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for='action in pipeline.details.manual_actions'>
<a
rel="nofollow"
data-method="post"
:href='action.path'
title="Manual build"
>
<span v-html='svgs.iconPlay'></span>
<span title="Manual build">{{action.name}}</span>
</a>
</li>
</ul>
</div>
<div class="btn-group">
<a
v-if='artifacts'
class="dropdown-toggle btn btn-default build-artifacts js-pipeline-dropdown-download"
data-toggle="dropdown"
type="button"
>
<i class="fa fa-download"></i>
<i class="fa fa-caret-down"></i>
</a>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for='artifact in pipeline.details.artifacts'>
<a
rel="nofollow"
:href='artifact.path'
>
<i class="fa fa-download"></i>
<span>{{download(artifact.name)}}</span>
</a>
</li>
</ul>
</div>
</div>
<div class="cancel-retry-btns inline">
<a
v-if='pipeline.flags.retryable'
class="btn has-tooltip"
title="Retry"
rel="nofollow"
data-method="post"
:href='pipeline.retry_path'
>
<i class="fa fa-repeat"></i>
</a>
<a
v-if='pipeline.flags.cancelable'
class="btn btn-remove has-tooltip"
title="Cancel"
rel="nofollow"
data-method="post"
:href='pipeline.cancel_path'
data-original-title="Cancel"
>
<i class="fa fa-remove"></i>
</a>
</div>
</div>
</td>
`,
});
})(window.gl || (window.gl = {}));
app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6
0 → 100644
View file @
2f1701c5
/* global Vue, gl */
/* eslint-disable no-param-reassign */
((gl) => {
gl.VuePipelineUrl = Vue.extend({
props: [
'pipeline',
],
computed: {
user() {
return !!this.pipeline.user;
},
},
template: `
<td>
<a :href='pipeline.path'>
<span class="pipeline-id">#{{pipeline.id}}</span>
</a>
<span>by</span>
<a
v-if='user'
:href='pipeline.user.web_url'
>
<img
v-if='user'
class="avatar has-tooltip s20 "
:title='pipeline.user.name'
data-container="body"
:src='pipeline.user.avatar_url'
>
</a>
<span
v-if='!user'
class="api monospace"
>
API
</span>
<span
v-if='pipeline.flags.latest'
class="label label-success has-tooltip"
title="Latest pipeline for this branch"
data-original-title="Latest pipeline for this branch"
>
latest
</span>
<span
v-if='pipeline.flags.yaml_errors'
class="label label-danger has-tooltip"
:title='pipeline.yaml_errors'
:data-original-title='pipeline.yaml_errors'
>
yaml invalid
</span>
<span
v-if='pipeline.flags.stuck'
class="label label-warning"
>
stuck
</span>
</td>
`,
});
})(window.gl || (window.gl = {}));
app/assets/javascripts/vue_pipelines_index/pipelines.js.es6
0 → 100644
View file @
2f1701c5
/* global Vue, Turbolinks, gl */
/* eslint-disable no-param-reassign */
((gl) => {
gl.VuePipelines = Vue.extend({
components: {
runningPipeline: gl.VueRunningPipeline,
pipelineActions: gl.VuePipelineActions,
stages: gl.VueStages,
commit: gl.CommitComponent,
pipelineUrl: gl.VuePipelineUrl,
pipelineHead: gl.VuePipelineHead,
glPagination: gl.VueGlPagination,
statusScope: gl.VueStatusScope,
timeAgo: gl.VueTimeAgo,
},
data() {
return {
pipelines: [],
timeLoopInterval: '',
intervalId: '',
apiScope: 'all',
pageInfo: {},
pagenum: 1,
count: { all: 0, running_or_pending: 0 },
pageRequest: false,
};
},
props: ['scope', 'store', 'svgs'],
created() {
const pagenum = gl.utils.getParameterByName('p');
const scope = gl.utils.getParameterByName('scope');
if (pagenum) this.pagenum = pagenum;
if (scope) this.apiScope = scope;
this.store.fetchDataLoop.call(this, Vue, this.pagenum, this.scope, this.apiScope);
},
methods: {
change(pagenum, apiScope) {
Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`);
},
author(pipeline) {
if (!pipeline.commit) return { avatar_url: '', web_url: '', username: '' };
if (pipeline.commit.author) return pipeline.commit.author;
return {
avatar_url: pipeline.commit.author_gravatar_url,
web_url: `mailto:${pipeline.commit.author_email}`,
username: pipeline.commit.author_name,
};
},
ref(pipeline) {
const { ref } = pipeline;
return { name: ref.name, tag: ref.tag, ref_url: ref.path };
},
commitTitle(pipeline) {
return pipeline.commit ? pipeline.commit.title : '';
},
commitSha(pipeline) {
return pipeline.commit ? pipeline.commit.short_id : '';
},
commitUrl(pipeline) {
return pipeline.commit ? pipeline.commit.commit_path : '';
},
match(string) {
return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase());
},
},
template: `
<div>
<div class="pipelines realtime-loading" v-if='pipelines.length < 1'>
<i class="fa fa-spinner fa-spin"></i>
</div>
<div class="table-holder" v-if='pipelines.length'>
<table class="table ci-table">
<thead>
<tr>
<th>Status</th>
<th>Pipeline</th>
<th>Commit</th>
<th>Stages</th>
<th></th>
<th class="hidden-xs"></th>
</tr>
</thead>
<tbody>
<tr class="commit" v-for='pipeline in pipelines'>
<status-scope
:pipeline='pipeline'
:match='match'
:svgs='svgs'
>
</status-scope>
<pipeline-url :pipeline='pipeline'></pipeline-url>
<td>
<commit
:commit-icon-svg='svgs.commitIconSvg'
:author='author(pipeline)'
:tag="pipeline.ref.tag"
:title='commitTitle(pipeline)'
:commit-ref='ref(pipeline)'
:short-sha='commitSha(pipeline)'
:commit-url='commitUrl(pipeline)'
>
</commit>
</td>
<stages
:pipeline='pipeline'
:svgs='svgs'
:match='match'
>
</stages>
<time-ago :pipeline='pipeline' :svgs='svgs'></time-ago>
<pipeline-actions :pipeline='pipeline' :svgs='svgs'></pipeline-actions>
</tr>
</tbody>
</table>
</div>
<div class="pipelines realtime-loading" v-if='pageRequest'>
<i class="fa fa-spinner fa-spin"></i>
</div>
<gl-pagination
v-if='pageInfo.total > pageInfo.perPage'
:pagenum='pagenum'
:change='change'
:count='count.all'
:pageInfo='pageInfo'
>
</gl-pagination>
</div>
`,
});
})(window.gl || (window.gl = {}));
app/assets/javascripts/vue_pipelines_index/stage.js.es6
0 → 100644
View file @
2f1701c5
/* global Vue, Flash, gl */
/* eslint-disable no-param-reassign */
((gl) => {
gl.VueStage = Vue.extend({
data() {
return {
request: false,
builds: '',
spinner: '<span class="fa fa-spinner fa-spin"></span>',
};
},
props: ['stage', 'svgs', 'match'],
methods: {
fetchBuilds() {
if (this.request) return this.clearBuilds();
return this.$http.get(this.stage.dropdown_path)
.then((response) => {
this.request = true;
this.builds = JSON.parse(response.body).html;
}, () => {
const flash = new Flash('Something went wrong on our end.');
this.request = false;
return flash;
});
},
clearBuilds() {
this.builds = '';
this.request = false;
},
},
computed: {
buildsOrSpinner() {
return this.request ? this.builds : this.spinner;
},
dropdownClass() {
if (this.request) return 'js-builds-dropdown-container';
return 'js-builds-dropdown-loading builds-dropdown-loading';
},
buildStatus() {
return `Build: ${this.stage.status.label}`;
},
tooltip() {
return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
},
svg() {
const icon = this.stage.status.icon;
const stageIcon = icon.replace(/icon/i, 'stage_icon');
return this.svgs[this.match(stageIcon)];
},
triggerButtonClass() {
return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`;
},
},
template: `
<div>
<button
@click='fetchBuilds'
@blur='fetchBuilds'
:class="triggerButtonClass"
:title='stage.title'
data-placement="top"
data-toggle="dropdown"
type="button">
<span v-html="svg"></span>
<i class="fa fa-caret-down "></i>
</button>
<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
<div class="arrow-up"></div>
<div :class="dropdownClass" class="js-builds-dropdown-list scrollable-menu" v-html="buildsOrSpinner"></div>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
app/assets/javascripts/vue_pipelines_index/stages.js.es6
0 → 100644
View file @
2f1701c5
/* global Vue, gl */
/* eslint-disable no-param-reassign */
((gl) => {
gl.VueStages = Vue.extend({
components: {
'vue-stage': gl.VueStage,
},
props: ['pipeline', 'svgs', 'match'],
template: `
<td class="stage-cell">
<div
class="stage-container dropdown js-mini-pipeline-graph"
v-for='stage in pipeline.details.stages'
>
<vue-stage :stage='stage' :svgs='svgs' :match='match'></vue-stage>
</div>
</td>
`,
});
})(window.gl || (window.gl = {}));
app/assets/javascripts/vue_pipelines_index/status.js.es6
0 → 100644
View file @
2f1701c5
/* global Vue, gl */
/* eslint-disable no-param-reassign */
((gl) => {
gl.VueStatusScope = Vue.extend({
props: [
'pipeline', 'svgs', 'match',
],
computed: {
cssClasses() {
const cssObject = { 'ci-status': true };
cssObject[`ci-${this.pipeline.details.status.group}`] = true;
return cssObject;
},
svg() {
return this.svgs[this.match(this.pipeline.details.status.icon)];
},
detailsPath() {
const { status } = this.pipeline.details;
return status.has_details ? status.details_path : false;
},
},
template: `
<td class="commit-link">
<a
:class='cssClasses'
:href='detailsPath'
v-html='svg + pipeline.details.status.text'
>
</a>
</td>
`,
});
})(window.gl || (window.gl = {}));
app/assets/javascripts/vue_pipelines_index/store.js.es6
0 → 100644
View file @
2f1701c5
/* global gl, Flash */
/* eslint-disable no-param-reassign, no-underscore-dangle */
/*= require vue_realtime_listener/index.js */
((gl) => {
const pageValues = headers => ({
perPage: +headers['X-Per-Page'],
page: +headers['X-Page'],
total: +headers['X-Total'],
totalPages: +headers['X-Total-Pages'],
nextPage: +headers['X-Next-Page'],
previousPage: +headers['X-Prev-Page'],
});
gl.PipelineStore = class {
fetchDataLoop(Vue, pageNum, url, apiScope) {
const updatePipelineNums = (count) => {
const { all } = count;
const running = count.running_or_pending;
document.querySelector('.js-totalbuilds-count').innerHTML = all;
document.querySelector('.js-running-count').innerHTML = running;
};
const goFetch = () =>
this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`)
.then((response) => {
const pageInfo = pageValues(response.headers);
this.pageInfo = Object.assign({}, this.pageInfo, pageInfo);
const res = JSON.parse(response.body);
this.count = Object.assign({}, this.count, res.count);
this.pipelines = Object.assign([], this.pipelines, res.pipelines);
updatePipelineNums(this.count);
this.pageRequest = false;
}, () => {
this.pageRequest = false;
return new Flash('Something went wrong on our end.');
});
goFetch();
const startTimeLoops = () => {
this.timeLoopInterval = setInterval(() => {
this.$children
.filter(e => e.$options._componentTag === 'time-ago')
.forEach(e => e.changeTime());
}, 10000);
};
startTimeLoops();
const removeIntervals = () => clearInterval(this.timeLoopInterval);
const startIntervals = () => startTimeLoops();
gl.VueRealtimeListener(removeIntervals, startIntervals);
}
};
})(window.gl || (window.gl = {}));
app/assets/javascripts/vue_pipelines_index/time_ago.js.es6
0 → 100644
View file @
2f1701c5
/* global Vue, gl */
/* eslint-disable no-param-reassign */
((gl) => {
gl.VueTimeAgo = Vue.extend({
data() {
return {
currentTime: new Date(),
};
},
props: ['pipeline', 'svgs'],
computed: {
timeAgo() {
return gl.utils.getTimeago();
},
localTimeFinished() {
return gl.utils.formatDate(this.pipeline.details.finished_at);
},
timeStopped() {
const changeTime = this.currentTime;
const options = {
weekday: 'long',
year: 'numeric',
month: 'short',
day: 'numeric',
};
options.timeZoneName = 'short';
const finished = this.pipeline.details.finished_at;
if (!finished && changeTime) return false;
return ({ words: this.timeAgo.format(finished) });
},
duration() {
const { duration } = this.pipeline.details;
const date = new Date(duration * 1000);
let hh = date.getUTCHours();
let mm = date.getUTCMinutes();
let ss = date.getSeconds();
if (hh < 10) hh = `0${hh}`;
if (mm < 10) mm = `0${mm}`;
if (ss < 10) ss = `0${ss}`;
if (duration !== null) return `${hh}:${mm}:${ss}`;
return false;
},
},
methods: {
changeTime() {
this.currentTime = new Date();
},
},
template: `
<td>
<p class="duration" v-if='duration'>
<span v-html='svgs.iconTimer'></span>
{{duration}}
</p>
<p class="finished-at" v-if='timeStopped'>
<i class="fa fa-calendar"></i>
<time
data-toggle="tooltip"
data-placement="top"
data-container="body"
:data-original-title='localTimeFinished'
>
{{timeStopped.words}}
</time>
</p>
</td>
`,
});
})(window.gl || (window.gl = {}));
app/assets/javascripts/vue_realtime_listener/index.js.es6
0 → 100644
View file @
2f1701c5
/* eslint-disable no-param-reassign */
((gl) => {
gl.VueRealtimeListener = (removeIntervals, startIntervals) => {
const removeAll = () => {
removeIntervals();
window.removeEventListener('beforeunload', removeIntervals);
window.removeEventListener('focus', startIntervals);
window.removeEventListener('blur', removeIntervals);
document.removeEventListener('page:fetch', removeAll);
};
window.addEventListener('beforeunload', removeIntervals);
window.addEventListener('focus', startIntervals);
window.addEventListener('blur', removeIntervals);
document.addEventListener('page:fetch', removeAll);
};
})(window.gl || (window.gl = {}));
app/assets/stylesheets/pages/pipelines.scss
View file @
2f1701c5
.pipelines
{
.realtime-loading
{
font-size
:
40px
;
text-align
:
center
;
}
.stage
{
max-width
:
90px
;
width
:
90px
;
...
...
@@ -24,6 +29,10 @@
min-width
:
1200px
;
table-layout
:
fixed
;
.label
{
margin-bottom
:
3px
;
}
.pipeline-id
{
color
:
$black
;
}
...
...
@@ -177,6 +186,7 @@
.stage-cell
{
font-size
:
0
;
>
.stage-container
>
div
>
button
>
span
>
svg
,
>
.stage-container
>
button
>
svg
{
height
:
22px
;
width
:
22px
;
...
...
app/controllers/projects/pipelines_controller.rb
View file @
2f1701c5
...
...
@@ -7,11 +7,33 @@ class Projects::PipelinesController < Projects::ApplicationController
def
index
@scope
=
params
[
:scope
]
@pipelines
=
PipelinesFinder
.
new
(
project
).
execute
(
scope:
@scope
).
page
(
params
[
:page
]).
per
(
30
)
@pipelines
=
@pipelines
.
includes
(
project: :namespace
)
@pipelines
=
PipelinesFinder
.
new
(
project
)
.
execute
(
scope:
@scope
)
.
page
(
params
[
:page
])
.
per
(
30
)
@running_or_pending_count
=
PipelinesFinder
.
new
(
project
).
execute
(
scope:
'running'
).
count
@pipelines_count
=
PipelinesFinder
.
new
(
project
).
execute
.
count
@running_or_pending_count
=
PipelinesFinder
.
new
(
project
).
execute
(
scope:
'running'
).
count
@pipelines_count
=
PipelinesFinder
.
new
(
project
).
execute
.
count
respond_to
do
|
format
|
format
.
html
format
.
json
do
render
json:
{
pipelines:
PipelineSerializer
.
new
(
project:
@project
,
user:
@current_user
)
.
with_pagination
(
request
,
response
)
.
represent
(
@pipelines
),
count:
{
all:
@pipelines_count
,
running_or_pending:
@running_or_pending_count
}
}
end
end
end
def
new
...
...
app/models/ci/pipeline.rb
View file @
2f1701c5
...
...
@@ -142,7 +142,7 @@ module Ci
end
def
artifacts
builds
.
latest
.
with_artifacts_not_expired
builds
.
latest
.
with_artifacts_not_expired
.
includes
(
project:
[
:namespace
])
end
def
project_id
...
...
@@ -191,7 +191,11 @@ module Ci
end
def
manual_actions
builds
.
latest
.
manual_actions
builds
.
latest
.
manual_actions
.
includes
(
project:
[
:namespace
])
end
def
stuck?
builds
.
pending
.
any?
(
&
:stuck?
)
end
def
retryable?
...
...
@@ -283,6 +287,10 @@ module Ci
end
end
def
has_yaml_errors?
yaml_errors
.
present?
end
def
environments
builds
.
where
.
not
(
environment:
nil
).
success
.
pluck
(
:environment
).
uniq
end
...
...
app/serializers/build_action_entity.rb
0 → 100644
View file @
2f1701c5
class
BuildActionEntity
<
Grape
::
Entity
include
RequestAwareEntity
expose
:name
do
|
build
|
build
.
name
.
humanize
end
expose
:path
do
|
build
|
play_namespace_project_build_path
(
build
.
project
.
namespace
,
build
.
project
,
build
)
end
end
app/serializers/build_artifact_entity.rb
0 → 100644
View file @
2f1701c5
class
BuildArtifactEntity
<
Grape
::
Entity
include
RequestAwareEntity
expose
:name
do
|
build
|
build
.
name
end
expose
:path
do
|
build
|
download_namespace_project_build_artifacts_path
(
build
.
project
.
namespace
,
build
.
project
,
build
)
end
end
app/serializers/commit_entity.rb
View file @
2f1701c5
...
...
@@ -3,6 +3,10 @@ class CommitEntity < API::Entities::RepoCommit
expose
:author
,
using:
UserEntity
expose
:author_gravatar_url
do
|
commit
|
GravatarService
.
new
.
execute
(
commit
.
author_email
)
end
expose
:commit_url
do
|
commit
|
namespace_project_tree_url
(
request
.
project
.
namespace
,
...
...
app/serializers/pipeline_entity.rb
0 → 100644
View file @
2f1701c5
class
PipelineEntity
<
Grape
::
Entity
include
RequestAwareEntity
expose
:id
expose
:user
,
using:
UserEntity
expose
:path
do
|
pipeline
|
namespace_project_pipeline_path
(
pipeline
.
project
.
namespace
,
pipeline
.
project
,
pipeline
)
end
expose
:details
do
expose
:status
do
|
pipeline
,
options
|
StatusEntity
.
represent
(
pipeline
.
detailed_status
(
request
.
user
),
options
)
end
expose
:duration
expose
:finished_at
expose
:stages
,
using:
StageEntity
expose
:artifacts
,
using:
BuildArtifactEntity
expose
:manual_actions
,
using:
BuildActionEntity
end
expose
:flags
do
expose
:latest?
,
as: :latest
expose
:triggered?
,
as: :triggered
expose
:stuck?
,
as: :stuck
expose
:has_yaml_errors?
,
as: :yaml_errors
expose
:can_retry?
,
as: :retryable
expose
:can_cancel?
,
as: :cancelable
end
expose
:ref
do
expose
:name
do
|
pipeline
|
pipeline
.
ref
end
expose
:path
do
|
pipeline
|
namespace_project_tree_path
(
pipeline
.
project
.
namespace
,
pipeline
.
project
,
id:
pipeline
.
ref
)
end
expose
:tag?
,
as: :tag
expose
:branch?
,
as: :branch
end
expose
:commit
,
using:
CommitEntity
expose
:yaml_errors
,
if:
->
(
pipeline
,
_
)
{
pipeline
.
has_yaml_errors?
}
expose
:retry_path
,
if:
proc
{
can_retry?
}
do
|
pipeline
|
retry_namespace_project_pipeline_path
(
pipeline
.
project
.
namespace
,
pipeline
.
project
,
pipeline
.
id
)
end
expose
:cancel_path
,
if:
proc
{
can_cancel?
}
do
|
pipeline
|
cancel_namespace_project_pipeline_path
(
pipeline
.
project
.
namespace
,
pipeline
.
project
,
pipeline
.
id
)
end
expose
:created_at
,
:updated_at
private
alias_method
:pipeline
,
:object
def
can_retry?
pipeline
.
retryable?
&&
can?
(
request
.
user
,
:update_pipeline
,
pipeline
)
end
def
can_cancel?
pipeline
.
cancelable?
&&
can?
(
request
.
user
,
:update_pipeline
,
pipeline
)
end
end
app/serializers/pipeline_serializer.rb
0 → 100644
View file @
2f1701c5
class
PipelineSerializer
<
BaseSerializer
entity
PipelineEntity
class
InvalidResourceError
<
StandardError
;
end
include
API
::
Helpers
::
Pagination
Struct
.
new
(
'Pagination'
,
:request
,
:response
)
def
represent
(
resource
,
opts
=
{})
if
paginated?
raise
InvalidResourceError
unless
resource
.
respond_to?
(
:page
)
super
(
paginate
(
resource
.
includes
(
project: :namespace
)),
opts
)
else
super
(
resource
,
opts
)
end
end
def
paginated?
defined?
(
@pagination
)
end
def
with_pagination
(
request
,
response
)
tap
{
@pagination
=
Struct
::
Pagination
.
new
(
request
,
response
)
}
end
private
# Methods needed by `API::Helpers::Pagination`
#
def
params
@pagination
.
request
.
query_parameters
end
def
request
@pagination
.
request
end
def
header
(
header
,
value
)
@pagination
.
response
.
headers
[
header
]
=
value
end
end
app/serializers/request_aware_entity.rb
View file @
2f1701c5
...
...
@@ -2,14 +2,11 @@ module RequestAwareEntity
extend
ActiveSupport
::
Concern
included
do
include
Gitlab
::
Routing
.
url_helpers
include
Gitlab
::
Routing
include
Gitlab
::
Allowable
end
def
request
@options
.
fetch
(
:request
)
end
def
can?
(
object
,
action
,
subject
)
Ability
.
allowed?
(
object
,
action
,
subject
)
options
.
fetch
(
:request
)
end
end
app/serializers/stage_entity.rb
0 → 100644
View file @
2f1701c5
class
StageEntity
<
Grape
::
Entity
include
RequestAwareEntity
expose
:name
expose
:title
do
|
stage
|
"
#{
stage
.
name
}
:
#{
detailed_status
.
label
}
"
end
expose
:detailed_status
,
as: :status
,
with:
StatusEntity
expose
:path
do
|
stage
|
namespace_project_pipeline_path
(
stage
.
pipeline
.
project
.
namespace
,
stage
.
pipeline
.
project
,
stage
.
pipeline
,
anchor:
stage
.
name
)
end
expose
:dropdown_path
do
|
stage
|
stage_namespace_project_pipeline_path
(
stage
.
pipeline
.
project
.
namespace
,
stage
.
pipeline
.
project
,
stage
.
pipeline
,
stage:
stage
.
name
,
format: :json
)
end
private
alias_method
:stage
,
:object
def
detailed_status
stage
.
detailed_status
(
request
.
user
)
end
end
app/serializers/status_entity.rb
0 → 100644
View file @
2f1701c5
class
StatusEntity
<
Grape
::
Entity
include
RequestAwareEntity
expose
:icon
,
:text
,
:label
,
:group
expose
:has_details?
,
as: :has_details
expose
:details_path
end
app/views/projects/pipelines/index.html.haml
View file @
2f1701c5
...
...
@@ -35,21 +35,34 @@
=
link_to
ci_lint_path
,
class:
'btn btn-default'
do
%span
CI Lint
.content-list.pipelines
.content-list.pipelines
{
data:
{
url:
namespace_project_pipelines_path
(
@project
.
namespace
,
@project
,
format: :json
)
}
}
-
if
@pipelines
.
blank?
%div
.nothing-here-block
No pipelines to show
-
else
.table-holder
%table
.table.ci-table.js-pipeline-table
%thead
%th
.pipeline-status
Status
%th
.pipeline-info
Pipeline
%th
.pipeline-commit
Commit
%th
.pipeline-stages
Stages
%th
.pipeline-date
%th
.pipeline-actions.hidden-xs
=
render
@pipelines
,
commit_sha:
true
,
stage:
true
,
allow_retry:
true
=
paginate
@pipelines
,
theme:
'gitlab'
.pipeline-svgs
{
"data"
=>
{
"commit_icon_svg"
=>
custom_icon
(
"icon_commit"
),
"icon_status_canceled"
=>
custom_icon
(
"icon_status_canceled"
),
"icon_status_running"
=>
custom_icon
(
"icon_status_running"
),
"icon_status_skipped"
=>
custom_icon
(
"icon_status_skipped"
),
"icon_status_created"
=>
custom_icon
(
"icon_status_created"
),
"icon_status_pending"
=>
custom_icon
(
"icon_status_pending"
),
"icon_status_success"
=>
custom_icon
(
"icon_status_success"
),
"icon_status_failed"
=>
custom_icon
(
"icon_status_failed"
),
"icon_status_warning"
=>
custom_icon
(
"icon_status_warning"
),
"stage_icon_status_canceled"
=>
custom_icon
(
"icon_status_canceled_borderless"
),
"stage_icon_status_running"
=>
custom_icon
(
"icon_status_running_borderless"
),
"stage_icon_status_skipped"
=>
custom_icon
(
"icon_status_skipped_borderless"
),
"stage_icon_status_created"
=>
custom_icon
(
"icon_status_created_borderless"
),
"stage_icon_status_pending"
=>
custom_icon
(
"icon_status_pending_borderless"
),
"stage_icon_status_success"
=>
custom_icon
(
"icon_status_success_borderless"
),
"stage_icon_status_failed"
=>
custom_icon
(
"icon_status_failed_borderless"
),
"stage_icon_status_warning"
=>
custom_icon
(
"icon_status_warning_borderless"
),
"icon_play"
=>
custom_icon
(
"icon_play"
),
"icon_timer"
=>
custom_icon
(
"icon_timer"
),
"icon_status_manual"
=>
custom_icon
(
"icon_status_manual"
),
}
}
.vue-pipelines-index
=
page_specific_javascript_tag
(
'vue_pagination/index.js'
)
=
page_specific_javascript_tag
(
'vue_pipelines_index/index.js'
)
config/application.rb
View file @
2f1701c5
...
...
@@ -109,6 +109,8 @@ module Gitlab
config
.
assets
.
precompile
<<
"lib/utils/*.js"
config
.
assets
.
precompile
<<
"lib/*.js"
config
.
assets
.
precompile
<<
"u2f.js"
config
.
assets
.
precompile
<<
"vue_pipelines_index/index.js"
config
.
assets
.
precompile
<<
"vue_pagination/index.js"
config
.
assets
.
precompile
<<
"vendor/assets/fonts/*"
# Version of your assets, change this if you want to expire all your assets
...
...
lib/api/helpers.rb
View file @
2f1701c5
module
API
module
Helpers
include
Gitlab
::
Utils
include
Helpers
::
Pagination
SUDO_HEADER
=
"HTTP_SUDO"
SUDO_PARAM
=
:sudo
...
...
@@ -85,12 +86,6 @@ module API
IssuesFinder
.
new
(
current_user
,
project_id:
user_project
.
id
).
find
(
id
)
end
def
paginate
(
relation
)
relation
.
page
(
params
[
:page
]).
per
(
params
[
:per_page
].
to_i
).
tap
do
|
data
|
add_pagination_headers
(
data
)
end
end
def
authenticate!
unauthorized!
unless
current_user
end
...
...
@@ -361,38 +356,6 @@ module API
@sudo_identifier
||=
params
[
SUDO_PARAM
]
||
env
[
SUDO_HEADER
]
end
def
add_pagination_headers
(
paginated_data
)
header
'X-Total'
,
paginated_data
.
total_count
.
to_s
header
'X-Total-Pages'
,
paginated_data
.
total_pages
.
to_s
header
'X-Per-Page'
,
paginated_data
.
limit_value
.
to_s
header
'X-Page'
,
paginated_data
.
current_page
.
to_s
header
'X-Next-Page'
,
paginated_data
.
next_page
.
to_s
header
'X-Prev-Page'
,
paginated_data
.
prev_page
.
to_s
header
'Link'
,
pagination_links
(
paginated_data
)
end
def
pagination_links
(
paginated_data
)
request_url
=
request
.
url
.
split
(
'?'
).
first
request_params
=
params
.
clone
request_params
[
:per_page
]
=
paginated_data
.
limit_value
links
=
[]
request_params
[
:page
]
=
paginated_data
.
current_page
-
1
links
<<
%(<#{request_url}?#{request_params.to_query}>; rel="prev")
unless
paginated_data
.
first_page?
request_params
[
:page
]
=
paginated_data
.
current_page
+
1
links
<<
%(<#{request_url}?#{request_params.to_query}>; rel="next")
unless
paginated_data
.
last_page?
request_params
[
:page
]
=
1
links
<<
%(<#{request_url}?#{request_params.to_query}>; rel="first")
request_params
[
:page
]
=
paginated_data
.
total_pages
links
<<
%(<#{request_url}?#{request_params.to_query}>; rel="last")
links
.
join
(
', '
)
end
def
secret_token
Gitlab
::
Shell
.
secret_token
end
...
...
lib/api/helpers/pagination.rb
0 → 100644
View file @
2f1701c5
module
API
module
Helpers
module
Pagination
def
paginate
(
relation
)
relation
.
page
(
params
[
:page
]).
per
(
params
[
:per_page
].
to_i
).
tap
do
|
data
|
add_pagination_headers
(
data
)
end
end
private
def
add_pagination_headers
(
paginated_data
)
header
'X-Total'
,
paginated_data
.
total_count
.
to_s
header
'X-Total-Pages'
,
paginated_data
.
total_pages
.
to_s
header
'X-Per-Page'
,
paginated_data
.
limit_value
.
to_s
header
'X-Page'
,
paginated_data
.
current_page
.
to_s
header
'X-Next-Page'
,
paginated_data
.
next_page
.
to_s
header
'X-Prev-Page'
,
paginated_data
.
prev_page
.
to_s
header
'Link'
,
pagination_links
(
paginated_data
)
end
def
pagination_links
(
paginated_data
)
request_url
=
request
.
url
.
split
(
'?'
).
first
request_params
=
params
.
clone
request_params
[
:per_page
]
=
paginated_data
.
limit_value
links
=
[]
request_params
[
:page
]
=
paginated_data
.
current_page
-
1
links
<<
%(<#{request_url}?#{request_params.to_query}>; rel="prev")
unless
paginated_data
.
first_page?
request_params
[
:page
]
=
paginated_data
.
current_page
+
1
links
<<
%(<#{request_url}?#{request_params.to_query}>; rel="next")
unless
paginated_data
.
last_page?
request_params
[
:page
]
=
1
links
<<
%(<#{request_url}?#{request_params.to_query}>; rel="first")
request_params
[
:page
]
=
paginated_data
.
total_pages
links
<<
%(<#{request_url}?#{request_params.to_query}>; rel="last")
links
.
join
(
', '
)
end
end
end
end
spec/controllers/projects/pipelines_controller_spec.rb
View file @
2f1701c5
...
...
@@ -5,13 +5,33 @@ describe Projects::PipelinesController do
let
(
:user
)
{
create
(
:user
)
}
let
(
:project
)
{
create
(
:empty_project
,
:public
)
}
let
(
:pipeline
)
{
create
(
:ci_pipeline
,
project:
project
)
}
before
do
sign_in
(
user
)
end
describe
'GET index.json'
do
before
do
create_list
(
:ci_empty_pipeline
,
2
,
project:
project
)
get
:index
,
namespace_id:
project
.
namespace
.
path
,
project_id:
project
.
path
,
format: :json
end
it
'returns JSON with serialized pipelines'
do
expect
(
response
).
to
have_http_status
(
:ok
)
expect
(
json_response
).
to
include
(
'pipelines'
)
expect
(
json_response
[
'pipelines'
].
count
).
to
eq
2
expect
(
json_response
[
'count'
][
'all'
]).
to
eq
2
expect
(
json_response
[
'count'
][
'running_or_pending'
]).
to
eq
2
end
end
describe
'GET stages.json'
do
let
(
:pipeline
)
{
create
(
:ci_pipeline
,
project:
project
)
}
context
'when accessing existing stage'
do
before
do
create
(
:ci_build
,
pipeline:
pipeline
,
stage:
'build'
)
...
...
spec/factories/ci/pipelines.rb
View file @
2f1701c5
...
...
@@ -31,6 +31,14 @@ FactoryGirl.define do
File
.
read
(
Rails
.
root
.
join
(
'spec/support/gitlab_stubs/gitlab_ci.yml'
))
end
end
# Populates pipeline with errors
#
pipeline
.
config_processor
if
evaluator
.
config
end
trait
:invalid
do
config
(
rspec:
nil
)
end
end
end
...
...
spec/factories/ci/runners.rb
View file @
2f1701c5
...
...
@@ -8,6 +8,10 @@ FactoryGirl.define do
is_shared
false
active
true
trait
:online
do
contacted_at
Time
.
now
end
trait
:shared
do
is_shared
true
end
...
...
spec/features/projects/pipelines/pipelines_spec.rb
View file @
2f1701c5
This diff is collapsed.
Click to expand it.
spec/javascripts/vue_pagination/pagination_spec.js.es6
0 → 100644
View file @
2f1701c5
//= require vue
//= require lib/utils/common_utils
//= require vue_pagination/index
/* global fixture, gl */
describe('Pagination component', () => {
let component;
const changeChanges = {
one: '',
two: '',
};
const change = (one, two) => {
changeChanges.one = one;
changeChanges.two = two;
};
it('should render and start at page 1', () => {
fixture.set('<div class="test-pagination-container"></div>');
component = new window.gl.VueGlPagination({
el: document.querySelector('.test-pagination-container'),
propsData: {
pageInfo: {
totalPages: 10,
nextPage: 2,
previousPage: '',
},
change,
},
});
expect(component.$el.classList).toContain('gl-pagination');
component.changePage({ target: { innerText: '1' } });
expect(changeChanges.one).toEqual(1);
expect(changeChanges.two).toEqual('all');
});
it('should go to the previous page', () => {
fixture.set('<div class="test-pagination-container"></div>');
component = new window.gl.VueGlPagination({
el: document.querySelector('.test-pagination-container'),
propsData: {
pageInfo: {
totalPages: 10,
nextPage: 3,
previousPage: 1,
},
change,
},
});
component.changePage({ target: { innerText: 'Prev' } });
expect(changeChanges.one).toEqual(1);
expect(changeChanges.two).toEqual('all');
});
it('should go to the next page', () => {
fixture.set('<div class="test-pagination-container"></div>');
component = new window.gl.VueGlPagination({
el: document.querySelector('.test-pagination-container'),
propsData: {
pageInfo: {
totalPages: 10,
nextPage: 5,
previousPage: 3,
},
change,
},
});
component.changePage({ target: { innerText: 'Next' } });
expect(changeChanges.one).toEqual(5);
expect(changeChanges.two).toEqual('all');
});
it('should go to the last page', () => {
fixture.set('<div class="test-pagination-container"></div>');
component = new window.gl.VueGlPagination({
el: document.querySelector('.test-pagination-container'),
propsData: {
pageInfo: {
totalPages: 10,
nextPage: 5,
previousPage: 3,
},
change,
},
});
component.changePage({ target: { innerText: 'Last >>' } });
expect(changeChanges.one).toEqual(10);
expect(changeChanges.two).toEqual('all');
});
it('should go to the first page', () => {
fixture.set('<div class="test-pagination-container"></div>');
component = new window.gl.VueGlPagination({
el: document.querySelector('.test-pagination-container'),
propsData: {
pageInfo: {
totalPages: 10,
nextPage: 5,
previousPage: 3,
},
change,
},
});
component.changePage({ target: { innerText: '<< First' } });
expect(changeChanges.one).toEqual(1);
expect(changeChanges.two).toEqual('all');
});
it('should do nothing', () => {
fixture.set('<div class="test-pagination-container"></div>');
component = new window.gl.VueGlPagination({
el: document.querySelector('.test-pagination-container'),
propsData: {
pageInfo: {
totalPages: 10,
nextPage: 2,
previousPage: '',
},
change,
},
});
component.changePage({ target: { innerText: '...' } });
expect(changeChanges.one).toEqual(1);
expect(changeChanges.two).toEqual('all');
});
});
describe('paramHelper', () => {
it('can parse url parameters correctly', () => {
window.history.pushState({}, null, '?scope=all&p=2');
const scope = gl.utils.getParameterByName('scope');
const p = gl.utils.getParameterByName('p');
expect(scope).toEqual('all');
expect(p).toEqual('2');
});
it('returns null if param not in url', () => {
window.history.pushState({}, null, '?p=2');
const scope = gl.utils.getParameterByName('scope');
const p = gl.utils.getParameterByName('p');
expect(scope).toEqual(null);
expect(p).toEqual('2');
});
});
spec/lib/api/helpers/pagination_spec.rb
0 → 100644
View file @
2f1701c5
require
'spec_helper'
describe
API
::
Helpers
::
Pagination
do
let
(
:resource
)
{
Project
.
all
}
subject
do
Class
.
new
.
include
(
described_class
).
new
end
describe
'#paginate'
do
let
(
:value
)
{
spy
(
'return value'
)
}
before
do
allow
(
value
).
to
receive
(
:to_query
).
and_return
(
value
)
allow
(
subject
).
to
receive
(
:header
).
and_return
(
value
)
allow
(
subject
).
to
receive
(
:params
).
and_return
(
value
)
allow
(
subject
).
to
receive
(
:request
).
and_return
(
value
)
end
describe
'required instance methods'
do
let
(
:return_spy
)
{
spy
}
it
'requires some instance methods'
do
expect_message
(
:header
)
expect_message
(
:params
)
expect_message
(
:request
)
subject
.
paginate
(
resource
)
end
end
context
'when resource can be paginated'
do
before
do
create_list
(
:empty_project
,
3
)
end
describe
'first page'
do
before
do
allow
(
subject
).
to
receive
(
:params
)
.
and_return
({
page:
1
,
per_page:
2
})
end
it
'returns appropriate amount of resources'
do
expect
(
subject
.
paginate
(
resource
).
count
).
to
eq
2
end
it
'adds appropriate headers'
do
expect_header
(
'X-Total'
,
'3'
)
expect_header
(
'X-Total-Pages'
,
'2'
)
expect_header
(
'X-Per-Page'
,
'2'
)
expect_header
(
'X-Page'
,
'1'
)
expect_header
(
'X-Next-Page'
,
'2'
)
expect_header
(
'X-Prev-Page'
,
''
)
expect_header
(
'Link'
,
any_args
)
subject
.
paginate
(
resource
)
end
end
describe
'second page'
do
before
do
allow
(
subject
).
to
receive
(
:params
)
.
and_return
({
page:
2
,
per_page:
2
})
end
it
'returns appropriate amount of resources'
do
expect
(
subject
.
paginate
(
resource
).
count
).
to
eq
1
end
it
'adds appropriate headers'
do
expect_header
(
'X-Total'
,
'3'
)
expect_header
(
'X-Total-Pages'
,
'2'
)
expect_header
(
'X-Per-Page'
,
'2'
)
expect_header
(
'X-Page'
,
'2'
)
expect_header
(
'X-Next-Page'
,
''
)
expect_header
(
'X-Prev-Page'
,
'1'
)
expect_header
(
'Link'
,
any_args
)
subject
.
paginate
(
resource
)
end
end
end
def
expect_header
(
name
,
value
)
expect
(
subject
).
to
receive
(
:header
).
with
(
name
,
value
)
end
def
expect_message
(
method
)
expect
(
subject
).
to
receive
(
method
)
.
at_least
(
:once
).
and_return
(
value
)
end
end
end
spec/models/ci/pipeline_spec.rb
View file @
2f1701c5
...
...
@@ -888,6 +888,48 @@ describe Ci::Pipeline, models: true do
end
end
describe
'#stuck?'
do
before
do
create
(
:ci_build
,
:pending
,
pipeline:
pipeline
)
end
context
'when pipeline is stuck'
do
it
'is stuck'
do
expect
(
pipeline
).
to
be_stuck
end
end
context
'when pipeline is not stuck'
do
before
{
create
(
:ci_runner
,
:shared
,
:online
)
}
it
'is not stuck'
do
expect
(
pipeline
).
not_to
be_stuck
end
end
end
describe
'#has_yaml_errors?'
do
context
'when pipeline has errors'
do
let
(
:pipeline
)
do
create
(
:ci_pipeline
,
config:
{
rspec:
nil
})
end
it
'contains yaml errors'
do
expect
(
pipeline
).
to
have_yaml_errors
end
end
context
'when pipeline does not have errors'
do
let
(
:pipeline
)
do
create
(
:ci_pipeline
,
config:
{
rspec:
{
script:
'rake test'
}
})
end
it
'does not containyaml errors'
do
expect
(
pipeline
).
not_to
have_yaml_errors
end
end
end
describe
'notifications when pipeline success or failed'
do
let
(
:project
)
{
create
(
:project
)
}
...
...
spec/serializers/build_action_entity_spec.rb
0 → 100644
View file @
2f1701c5
require
'spec_helper'
describe
BuildActionEntity
do
let
(
:build
)
{
create
(
:ci_build
,
name:
'test_build'
)
}
let
(
:entity
)
do
described_class
.
new
(
build
,
request:
double
)
end
describe
'#as_json'
do
subject
{
entity
.
as_json
}
it
'contains humanized build name'
do
expect
(
subject
[
:name
]).
to
eq
'Test build'
end
it
'contains path to the action play'
do
expect
(
subject
[
:path
]).
to
include
"builds/
#{
build
.
id
}
/play"
end
end
end
spec/serializers/build_artifact_entity_spec.rb
0 → 100644
View file @
2f1701c5
require
'spec_helper'
describe
BuildArtifactEntity
do
let
(
:build
)
{
create
(
:ci_build
,
name:
'test:build'
)
}
let
(
:entity
)
do
described_class
.
new
(
build
,
request:
double
)
end
describe
'#as_json'
do
subject
{
entity
.
as_json
}
it
'contains build name'
do
expect
(
subject
[
:name
]).
to
eq
'test:build'
end
it
'contains path to the artifacts'
do
expect
(
subject
[
:path
])
.
to
include
"builds/
#{
build
.
id
}
/artifacts/download"
end
end
end
spec/serializers/commit_entity_spec.rb
View file @
2f1701c5
...
...
@@ -45,4 +45,8 @@ describe CommitEntity do
subject
end
it
'exposes gravatar url that belongs to author'
do
expect
(
subject
.
fetch
(
:author_gravatar_url
)).
to
match
/gravatar/
end
end
spec/serializers/pipeline_entity_spec.rb
0 → 100644
View file @
2f1701c5
require
'spec_helper'
describe
PipelineEntity
do
let
(
:user
)
{
create
(
:user
)
}
let
(
:request
)
{
double
(
'request'
)
}
before
do
allow
(
request
).
to
receive
(
:user
).
and_return
(
user
)
end
let
(
:entity
)
do
described_class
.
represent
(
pipeline
,
request:
request
)
end
describe
'#as_json'
do
subject
{
entity
.
as_json
}
context
'when pipeline is empty'
do
let
(
:pipeline
)
{
create
(
:ci_empty_pipeline
)
}
it
'contains required fields'
do
expect
(
subject
).
to
include
:id
,
:user
,
:path
expect
(
subject
).
to
include
:ref
,
:commit
expect
(
subject
).
to
include
:updated_at
,
:created_at
end
it
'contains details'
do
expect
(
subject
).
to
include
:details
expect
(
subject
[
:details
])
.
to
include
:duration
,
:finished_at
expect
(
subject
[
:details
])
.
to
include
:stages
,
:artifacts
,
:manual_actions
expect
(
subject
[
:details
][
:status
]).
to
include
:icon
,
:text
,
:label
end
it
'contains flags'
do
expect
(
subject
).
to
include
:flags
expect
(
subject
[
:flags
])
.
to
include
:latest
,
:triggered
,
:stuck
,
:yaml_errors
,
:retryable
,
:cancelable
end
end
context
'when pipeline is retryable'
do
let
(
:project
)
{
create
(
:empty_project
)
}
let
(
:pipeline
)
do
create
(
:ci_pipeline
,
status: :success
,
project:
project
)
end
before
do
create
(
:ci_build
,
:failed
,
pipeline:
pipeline
)
end
context
'user has ability to retry pipeline'
do
before
{
project
.
team
<<
[
user
,
:developer
]
}
it
'retryable flag is true'
do
expect
(
subject
[
:flags
][
:retryable
]).
to
eq
true
end
it
'contains retry path'
do
expect
(
subject
[
:retry_path
]).
to
be_present
end
end
context
'user does not have ability to retry pipeline'
do
it
'retryable flag is false'
do
expect
(
subject
[
:flags
][
:retryable
]).
to
eq
false
end
it
'does not contain retry path'
do
expect
(
subject
).
not_to
have_key
(
:retry_path
)
end
end
end
context
'when pipeline is cancelable'
do
let
(
:project
)
{
create
(
:empty_project
)
}
let
(
:pipeline
)
do
create
(
:ci_pipeline
,
status: :running
,
project:
project
)
end
before
do
create
(
:ci_build
,
:pending
,
pipeline:
pipeline
)
end
context
'user has ability to cancel pipeline'
do
before
{
project
.
team
<<
[
user
,
:developer
]
}
it
'cancelable flag is true'
do
expect
(
subject
[
:flags
][
:cancelable
]).
to
eq
true
end
it
'contains cancel path'
do
expect
(
subject
[
:cancel_path
]).
to
be_present
end
end
context
'user does not have ability to cancel pipeline'
do
it
'cancelable flag is false'
do
expect
(
subject
[
:flags
][
:cancelable
]).
to
eq
false
end
it
'does not contain cancel path'
do
expect
(
subject
).
not_to
have_key
(
:cancel_path
)
end
end
end
context
'when pipeline has YAML errors'
do
let
(
:pipeline
)
do
create
(
:ci_pipeline
,
config:
{
rspec:
{
invalid: :value
}
})
end
it
'contains flag that indicates there are errors'
do
expect
(
subject
[
:flags
][
:yaml_errors
]).
to
be
true
end
it
'contains information about error'
do
expect
(
subject
[
:yaml_errors
]).
to
be_present
end
end
context
'when pipeline does not have YAML errors'
do
let
(
:pipeline
)
{
create
(
:ci_empty_pipeline
)
}
it
'contains flag that indicates there are no errors'
do
expect
(
subject
[
:flags
][
:yaml_errors
]).
to
be
false
end
it
'does not contain field that normally holds an error'
do
expect
(
subject
).
not_to
have_key
(
:yaml_errors
)
end
end
end
end
spec/serializers/pipeline_serializer_spec.rb
0 → 100644
View file @
2f1701c5
require
'spec_helper'
describe
PipelineSerializer
do
let
(
:user
)
{
create
(
:user
)
}
let
(
:serializer
)
do
described_class
.
new
(
user:
user
)
end
let
(
:entity
)
do
serializer
.
represent
(
resource
)
end
subject
{
entity
.
as_json
}
describe
'#represent'
do
context
'when used without pagination'
do
it
'created a not paginated serializer'
do
expect
(
serializer
).
not_to
be_paginated
end
context
'when a single object is being serialized'
do
let
(
:resource
)
{
create
(
:ci_empty_pipeline
)
}
it
'serializers the pipeline object'
do
expect
(
subject
[
:id
]).
to
eq
resource
.
id
end
end
context
'when multiple objects are being serialized'
do
let
(
:resource
)
{
create_list
(
:ci_pipeline
,
2
)
}
it
'serializers the array of pipelines'
do
expect
(
subject
).
not_to
be_empty
end
end
end
context
'when used with pagination'
do
let
(
:request
)
{
spy
(
'request'
)
}
let
(
:response
)
{
spy
(
'response'
)
}
let
(
:pagination
)
{
{}
}
before
do
allow
(
request
)
.
to
receive
(
:query_parameters
)
.
and_return
(
pagination
)
end
let
(
:serializer
)
do
described_class
.
new
(
user:
user
)
.
with_pagination
(
request
,
response
)
end
it
'created a paginated serializer'
do
expect
(
serializer
).
to
be_paginated
end
context
'when resource does is not paginatable'
do
context
'when a single pipeline object is being serialized'
do
let
(
:resource
)
{
create
(
:ci_empty_pipeline
)
}
let
(
:pagination
)
{
{
page:
1
,
per_page:
1
}
}
it
'raises error'
do
expect
{
subject
}
.
to
raise_error
(
PipelineSerializer
::
InvalidResourceError
)
end
end
end
context
'when resource is paginatable relation'
do
let
(
:resource
)
{
Ci
::
Pipeline
.
all
}
let
(
:pagination
)
{
{
page:
1
,
per_page:
2
}
}
context
'when a single pipeline object is present in relation'
do
before
{
create
(
:ci_empty_pipeline
)
}
it
'serializes pipeline relation'
do
expect
(
subject
.
first
).
to
have_key
:id
end
end
context
'when a multiple pipeline objects are being serialized'
do
before
{
create_list
(
:ci_empty_pipeline
,
3
)
}
it
'serializes appropriate number of objects'
do
expect
(
subject
.
count
).
to
be
2
end
it
'appends relevant headers'
do
expect
(
response
).
to
receive
(
:[]=
).
with
(
'X-Total'
,
'3'
)
expect
(
response
).
to
receive
(
:[]=
).
with
(
'X-Total-Pages'
,
'2'
)
expect
(
response
).
to
receive
(
:[]=
).
with
(
'X-Per-Page'
,
'2'
)
subject
end
end
end
end
end
end
spec/serializers/request_aware_entity_spec.rb
0 → 100644
View file @
2f1701c5
require
'spec_helper'
describe
RequestAwareEntity
do
subject
do
Class
.
new
.
include
(
described_class
).
new
end
it
'includes URL helpers'
do
expect
(
subject
).
to
respond_to
(
:namespace_project_path
)
end
it
'includes method for checking abilities'
do
expect
(
subject
).
to
respond_to
(
:can?
)
end
it
'fetches request from options'
do
expect
(
subject
).
to
receive
(
:options
)
.
and_return
({
request:
'some value'
})
expect
(
subject
.
request
).
to
eq
'some value'
end
end
spec/serializers/stage_entity_spec.rb
0 → 100644
View file @
2f1701c5
require
'spec_helper'
describe
StageEntity
do
let
(
:pipeline
)
{
create
(
:ci_pipeline
)
}
let
(
:request
)
{
double
(
'request'
)
}
let
(
:user
)
{
create
(
:user
)
}
let
(
:entity
)
do
described_class
.
new
(
stage
,
request:
request
)
end
let
(
:stage
)
do
build
(
:ci_stage
,
pipeline:
pipeline
,
name:
'test'
)
end
before
do
allow
(
request
).
to
receive
(
:user
).
and_return
(
user
)
create
(
:ci_build
,
:success
,
pipeline:
pipeline
)
end
describe
'#as_json'
do
subject
{
entity
.
as_json
}
it
'contains relevant fields'
do
expect
(
subject
).
to
include
:name
,
:status
,
:path
end
it
'contains detailed status'
do
expect
(
subject
[
:status
]).
to
include
:text
,
:label
,
:group
,
:icon
expect
(
subject
[
:status
][
:label
]).
to
eq
'passed'
end
it
'contains valid name'
do
expect
(
subject
[
:name
]).
to
eq
'test'
end
it
'contains path to the stage'
do
expect
(
subject
[
:path
])
.
to
include
"pipelines/
#{
pipeline
.
id
}
#
#{
stage
.
name
}
"
end
it
'contains path to the stage dropdown'
do
expect
(
subject
[
:dropdown_path
])
.
to
include
"pipelines/
#{
pipeline
.
id
}
/stage.json?stage=test"
end
it
'contains stage title'
do
expect
(
subject
[
:title
]).
to
eq
'test: passed'
end
end
end
spec/serializers/status_entity_spec.rb
0 → 100644
View file @
2f1701c5
require
'spec_helper'
describe
StatusEntity
do
let
(
:entity
)
{
described_class
.
new
(
status
)
}
let
(
:status
)
do
Gitlab
::
Ci
::
Status
::
Success
.
new
(
double
(
'object'
),
double
(
'user'
))
end
before
do
allow
(
status
).
to
receive
(
:has_details?
).
and_return
(
true
)
allow
(
status
).
to
receive
(
:details_path
).
and_return
(
'some/path'
)
end
describe
'#as_json'
do
subject
{
entity
.
as_json
}
it
'contains status details'
do
expect
(
subject
).
to
include
:text
,
:icon
,
:label
,
:group
expect
(
subject
).
to
include
:has_details
,
:details_path
end
end
end
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment