Remove UJS actions from pipelines tables
This commit is contained in:
parent
bb1620aaf7
commit
b0f2cbceb3
48 changed files with 1781 additions and 1371 deletions
|
@ -1,8 +1,9 @@
|
|||
/* eslint-disable no-new, no-param-reassign */
|
||||
/* global Vue, CommitsPipelineStore, PipelinesService, Flash */
|
||||
/* eslint-disable no-param-reassign */
|
||||
import CommitPipelinesTable from './pipelines_table';
|
||||
|
||||
window.Vue = require('vue');
|
||||
require('./pipelines_table');
|
||||
window.Vue.use(require('vue-resource'));
|
||||
|
||||
/**
|
||||
* Commits View > Pipelines Tab > Pipelines Table.
|
||||
* Merge Request View > Pipelines Tab > Pipelines Table.
|
||||
|
@ -21,7 +22,7 @@ $(() => {
|
|||
}
|
||||
|
||||
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
|
||||
gl.commits.pipelines.PipelinesTableBundle = new gl.commits.pipelines.PipelinesTableView();
|
||||
gl.commits.pipelines.PipelinesTableBundle = new CommitPipelinesTable();
|
||||
|
||||
if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) {
|
||||
gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl);
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
/* globals Vue */
|
||||
/* eslint-disable no-unused-vars, no-param-reassign */
|
||||
|
||||
/**
|
||||
* Pipelines service.
|
||||
*
|
||||
* Used to fetch the data used to render the pipelines table.
|
||||
* Uses Vue.Resource
|
||||
*/
|
||||
class PipelinesService {
|
||||
|
||||
/**
|
||||
* FIXME: The url provided to request the pipelines in the new merge request
|
||||
* page already has `.json`.
|
||||
* This should be fixed when the endpoint is improved.
|
||||
*
|
||||
* @param {String} root
|
||||
*/
|
||||
constructor(root) {
|
||||
let endpoint;
|
||||
|
||||
if (root.indexOf('.json') === -1) {
|
||||
endpoint = `${root}.json`;
|
||||
} else {
|
||||
endpoint = root;
|
||||
}
|
||||
this.pipelines = Vue.resource(endpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the root param provided when the class is initialized, will
|
||||
* make a GET request.
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
all() {
|
||||
return this.pipelines.get();
|
||||
}
|
||||
}
|
||||
|
||||
window.gl = window.gl || {};
|
||||
gl.commits = gl.commits || {};
|
||||
gl.commits.pipelines = gl.commits.pipelines || {};
|
||||
gl.commits.pipelines.PipelinesService = PipelinesService;
|
|
@ -1,13 +1,12 @@
|
|||
/* eslint-disable no-new, no-param-reassign */
|
||||
/* global Vue, CommitsPipelineStore, PipelinesService, Flash */
|
||||
|
||||
window.Vue = require('vue');
|
||||
window.Vue.use(require('vue-resource'));
|
||||
require('../../lib/utils/common_utils');
|
||||
require('../../vue_shared/vue_resource_interceptor');
|
||||
require('../../vue_shared/components/pipelines_table');
|
||||
require('./pipelines_service');
|
||||
const PipelineStore = require('./pipelines_store');
|
||||
/* eslint-disable no-new*/
|
||||
/* global Flash */
|
||||
import Vue from 'vue';
|
||||
import PipelinesTableComponent from '../../vue_shared/components/pipelines_table';
|
||||
import PipelinesService from '../../vue_pipelines_index/services/pipelines_service';
|
||||
import PipelineStore from '../../vue_pipelines_index/stores/pipelines_store';
|
||||
import eventHub from '../../vue_pipelines_index/event_hub';
|
||||
import '../../lib/utils/common_utils';
|
||||
import '../../vue_shared/vue_resource_interceptor';
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -20,48 +19,59 @@ const PipelineStore = require('./pipelines_store');
|
|||
* as soon as we have Webpack and can load them directly into JS files.
|
||||
*/
|
||||
|
||||
(() => {
|
||||
window.gl = window.gl || {};
|
||||
gl.commits = gl.commits || {};
|
||||
gl.commits.pipelines = gl.commits.pipelines || {};
|
||||
export default Vue.component('pipelines-table', {
|
||||
components: {
|
||||
'pipelines-table-component': PipelinesTableComponent,
|
||||
},
|
||||
|
||||
gl.commits.pipelines.PipelinesTableView = Vue.component('pipelines-table', {
|
||||
/**
|
||||
* Accesses the DOM to provide the needed data.
|
||||
* Returns the necessary props to render `pipelines-table-component` component.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
data() {
|
||||
const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset;
|
||||
const store = new PipelineStore();
|
||||
|
||||
components: {
|
||||
'pipelines-table-component': gl.pipelines.PipelinesTableComponent,
|
||||
},
|
||||
return {
|
||||
endpoint: pipelinesTableData.endpoint,
|
||||
store,
|
||||
state: store.state,
|
||||
isLoading: false,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Accesses the DOM to provide the needed data.
|
||||
* Returns the necessary props to render `pipelines-table-component` component.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
data() {
|
||||
const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset;
|
||||
const store = new PipelineStore();
|
||||
/**
|
||||
* When the component is about to be mounted, tell the service to fetch the data
|
||||
*
|
||||
* A request to fetch the pipelines will be made.
|
||||
* In case of a successfull response we will store the data in the provided
|
||||
* store, in case of a failed response we need to warn the user.
|
||||
*
|
||||
*/
|
||||
beforeMount() {
|
||||
this.service = new PipelinesService(this.endpoint);
|
||||
|
||||
return {
|
||||
endpoint: pipelinesTableData.endpoint,
|
||||
store,
|
||||
state: store.state,
|
||||
isLoading: false,
|
||||
};
|
||||
},
|
||||
this.fetchPipelines();
|
||||
|
||||
/**
|
||||
* When the component is about to be mounted, tell the service to fetch the data
|
||||
*
|
||||
* A request to fetch the pipelines will be made.
|
||||
* In case of a successfull response we will store the data in the provided
|
||||
* store, in case of a failed response we need to warn the user.
|
||||
*
|
||||
*/
|
||||
beforeMount() {
|
||||
const pipelinesService = new gl.commits.pipelines.PipelinesService(this.endpoint);
|
||||
eventHub.$on('refreshPipelines', this.fetchPipelines);
|
||||
},
|
||||
|
||||
beforeUpdate() {
|
||||
if (this.state.pipelines.length && this.$children) {
|
||||
this.store.startTimeAgoLoops.call(this, Vue);
|
||||
}
|
||||
},
|
||||
|
||||
beforeDestroyed() {
|
||||
eventHub.$off('refreshPipelines');
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchPipelines() {
|
||||
this.isLoading = true;
|
||||
return pipelinesService.all()
|
||||
return this.service.getPipelines()
|
||||
.then(response => response.json())
|
||||
.then((json) => {
|
||||
// depending of the endpoint the response can either bring a `pipelines` key or not.
|
||||
|
@ -71,34 +81,30 @@ const PipelineStore = require('./pipelines_store');
|
|||
})
|
||||
.catch(() => {
|
||||
this.isLoading = false;
|
||||
new Flash('An error occurred while fetching the pipelines, please reload the page again.', 'alert');
|
||||
new Flash('An error occurred while fetching the pipelines, please reload the page again.');
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
beforeUpdate() {
|
||||
if (this.state.pipelines.length && this.$children) {
|
||||
PipelineStore.startTimeAgoLoops.call(this, Vue);
|
||||
}
|
||||
},
|
||||
|
||||
template: `
|
||||
<div class="pipelines">
|
||||
<div class="realtime-loading" v-if="isLoading">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
|
||||
<div class="blank-state blank-state-no-icon"
|
||||
v-if="!isLoading && state.pipelines.length === 0">
|
||||
<h2 class="blank-state-title js-blank-state-title">
|
||||
No pipelines to show
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="table-holder pipelines"
|
||||
v-if="!isLoading && state.pipelines.length > 0">
|
||||
<pipelines-table-component :pipelines="state.pipelines"/>
|
||||
</div>
|
||||
template: `
|
||||
<div class="pipelines">
|
||||
<div class="realtime-loading" v-if="isLoading">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
})();
|
||||
|
||||
<div class="blank-state blank-state-no-icon"
|
||||
v-if="!isLoading && state.pipelines.length === 0">
|
||||
<h2 class="blank-state-title js-blank-state-title">
|
||||
No pipelines to show
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="table-holder pipelines"
|
||||
v-if="!isLoading && state.pipelines.length > 0">
|
||||
<pipelines-table-component
|
||||
:pipelines="state.pipelines"
|
||||
:service="service" />
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
|
|
@ -1,21 +1,18 @@
|
|||
/* eslint-disable no-param-reassign, no-new */
|
||||
/* eslint-disable no-new */
|
||||
/* global Flash */
|
||||
import Vue from 'vue';
|
||||
import EnvironmentsService from '../services/environments_service';
|
||||
import EnvironmentTable from './environments_table';
|
||||
import EnvironmentsStore from '../stores/environments_store';
|
||||
import TablePaginationComponent from '../../vue_shared/components/table_pagination';
|
||||
import '../../lib/utils/common_utils';
|
||||
import eventHub from '../event_hub';
|
||||
|
||||
const Vue = window.Vue = require('vue');
|
||||
window.Vue.use(require('vue-resource'));
|
||||
require('../../vue_shared/components/table_pagination');
|
||||
require('../../lib/utils/common_utils');
|
||||
require('../../vue_shared/vue_resource_interceptor');
|
||||
|
||||
export default Vue.component('environment-component', {
|
||||
|
||||
components: {
|
||||
'environment-table': EnvironmentTable,
|
||||
'table-pagination': gl.VueGlPagination,
|
||||
'table-pagination': TablePaginationComponent,
|
||||
},
|
||||
|
||||
data() {
|
||||
|
@ -59,7 +56,6 @@ export default Vue.component('environment-component', {
|
|||
canCreateEnvironmentParsed() {
|
||||
return gl.utils.convertPermissionToBoolean(this.canCreateEnvironment);
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,24 +1,22 @@
|
|||
import Timeago from 'timeago.js';
|
||||
import '../../lib/utils/text_utility';
|
||||
import ActionsComponent from './environment_actions';
|
||||
import ExternalUrlComponent from './environment_external_url';
|
||||
import StopComponent from './environment_stop';
|
||||
import RollbackComponent from './environment_rollback';
|
||||
import TerminalButtonComponent from './environment_terminal_button';
|
||||
import '../../lib/utils/text_utility';
|
||||
import '../../vue_shared/components/commit';
|
||||
import CommitComponent from '../../vue_shared/components/commit';
|
||||
|
||||
/**
|
||||
* Envrionment Item Component
|
||||
*
|
||||
* Renders a table row for each environment.
|
||||
*/
|
||||
|
||||
const timeagoInstance = new Timeago();
|
||||
|
||||
export default {
|
||||
|
||||
components: {
|
||||
'commit-component': gl.CommitComponent,
|
||||
'commit-component': CommitComponent,
|
||||
'actions-component': ActionsComponent,
|
||||
'external-url-component': ExternalUrlComponent,
|
||||
'stop-component': StopComponent,
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
/**
|
||||
* Render environments table.
|
||||
*/
|
||||
import EnvironmentItem from './environment_item';
|
||||
import EnvironmentTableRowComponent from './environment_item';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'environment-item': EnvironmentItem,
|
||||
'environment-item': EnvironmentTableRowComponent,
|
||||
},
|
||||
|
||||
props: {
|
||||
|
|
|
@ -1,20 +1,17 @@
|
|||
/* eslint-disable no-param-reassign, no-new */
|
||||
/* eslint-disable no-new */
|
||||
/* global Flash */
|
||||
import Vue from 'vue';
|
||||
import EnvironmentsService from '../services/environments_service';
|
||||
import EnvironmentTable from '../components/environments_table';
|
||||
import EnvironmentsStore from '../stores/environments_store';
|
||||
|
||||
const Vue = window.Vue = require('vue');
|
||||
window.Vue.use(require('vue-resource'));
|
||||
require('../../vue_shared/components/table_pagination');
|
||||
require('../../lib/utils/common_utils');
|
||||
require('../../vue_shared/vue_resource_interceptor');
|
||||
import TablePaginationComponent from '../../vue_shared/components/table_pagination';
|
||||
import '../../lib/utils/common_utils';
|
||||
import '../../vue_shared/vue_resource_interceptor';
|
||||
|
||||
export default Vue.component('environment-folder-view', {
|
||||
|
||||
components: {
|
||||
'environment-table': EnvironmentTable,
|
||||
'table-pagination': gl.VueGlPagination,
|
||||
'table-pagination': TablePaginationComponent,
|
||||
},
|
||||
|
||||
data() {
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
import Vue from 'vue';
|
||||
import VueResource from 'vue-resource';
|
||||
|
||||
Vue.use(VueResource);
|
||||
|
||||
export default class EnvironmentsService {
|
||||
constructor(endpoint) {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import '~/lib/utils/common_utils';
|
||||
|
||||
/**
|
||||
* Environments Store.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
/* eslint-disable no-new, no-alert */
|
||||
/* global Flash */
|
||||
import '~/flash';
|
||||
import eventHub from '../event_hub';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
endpoint: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
service: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
cssClass: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
confirmActionMessage: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
iconClass() {
|
||||
return `fa fa-${this.icon}`;
|
||||
},
|
||||
|
||||
buttonClass() {
|
||||
return `btn has-tooltip ${this.cssClass}`;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClick() {
|
||||
if (this.confirmActionMessage && confirm(this.confirmActionMessage)) {
|
||||
this.makeRequest();
|
||||
} else if (!this.confirmActionMessage) {
|
||||
this.makeRequest();
|
||||
}
|
||||
},
|
||||
|
||||
makeRequest() {
|
||||
this.isLoading = true;
|
||||
|
||||
this.service.postAction(this.endpoint)
|
||||
.then(() => {
|
||||
this.isLoading = false;
|
||||
eventHub.$emit('refreshPipelines');
|
||||
})
|
||||
.catch(() => {
|
||||
this.isLoading = false;
|
||||
new Flash('An error occured while making the request.');
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
template: `
|
||||
<button
|
||||
type="button"
|
||||
@click="onClick"
|
||||
:class="buttonClass"
|
||||
:title="title"
|
||||
:aria-label="title"
|
||||
data-placement="top"
|
||||
:disabled="isLoading">
|
||||
<i :class="iconClass" aria-hidden="true"/>
|
||||
<i class="fa fa-spinner fa-spin" aria-hidden="true" v-if="isLoading" />
|
||||
</button>
|
||||
`,
|
||||
};
|
|
@ -0,0 +1,56 @@
|
|||
export default {
|
||||
props: [
|
||||
'pipeline',
|
||||
],
|
||||
computed: {
|
||||
user() {
|
||||
return !!this.pipeline.user;
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<td>
|
||||
<a
|
||||
:href="pipeline.path"
|
||||
class="js-pipeline-url-link">
|
||||
<span class="pipeline-id">#{{pipeline.id}}</span>
|
||||
</a>
|
||||
<span>by</span>
|
||||
<a
|
||||
class="js-pipeline-url-user"
|
||||
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="js-pipeline-url-api api monospace">
|
||||
API
|
||||
</span>
|
||||
<span
|
||||
v-if="pipeline.flags.latest"
|
||||
class="js-pipeline-url-lastest 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="js-pipeline-url-yaml 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="js-pipeline-url-stuck label label-warning">
|
||||
stuck
|
||||
</span>
|
||||
</td>
|
||||
`,
|
||||
};
|
|
@ -0,0 +1,71 @@
|
|||
/* eslint-disable no-new */
|
||||
/* global Flash */
|
||||
import '~/flash';
|
||||
import playIconSvg from 'icons/_icon_play.svg';
|
||||
import eventHub from '../event_hub';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
actions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
|
||||
service: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
playIconSvg,
|
||||
isLoading: false,
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClickAction(endpoint) {
|
||||
this.isLoading = true;
|
||||
|
||||
this.service.postAction(endpoint)
|
||||
.then(() => {
|
||||
this.isLoading = false;
|
||||
eventHub.$emit('refreshPipelines');
|
||||
})
|
||||
.catch(() => {
|
||||
this.isLoading = false;
|
||||
new Flash('An error occured while making the request.');
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
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"
|
||||
:disabled="isLoading">
|
||||
${playIconSvg}
|
||||
<i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||
<i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
|
||||
</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"
|
||||
@click="onClickAction(action.path)">
|
||||
${playIconSvg}
|
||||
<span>{{action.name}}</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
};
|
|
@ -0,0 +1,32 @@
|
|||
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"
|
||||
:href="artifact.path">
|
||||
<i class="fa fa-download" aria-hidden="true"></i>
|
||||
<span>Download {{artifact.name}} artifacts</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
};
|
116
app/assets/javascripts/vue_pipelines_index/components/stage.js
Normal file
116
app/assets/javascripts/vue_pipelines_index/components/stage.js
Normal file
|
@ -0,0 +1,116 @@
|
|||
/* global Flash */
|
||||
import canceledSvg from 'icons/_icon_status_canceled_borderless.svg';
|
||||
import createdSvg from 'icons/_icon_status_created_borderless.svg';
|
||||
import failedSvg from 'icons/_icon_status_failed_borderless.svg';
|
||||
import manualSvg from 'icons/_icon_status_manual_borderless.svg';
|
||||
import pendingSvg from 'icons/_icon_status_pending_borderless.svg';
|
||||
import runningSvg from 'icons/_icon_status_running_borderless.svg';
|
||||
import skippedSvg from 'icons/_icon_status_skipped_borderless.svg';
|
||||
import successSvg from 'icons/_icon_status_success_borderless.svg';
|
||||
import warningSvg from 'icons/_icon_status_warning_borderless.svg';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
const svgsDictionary = {
|
||||
icon_status_canceled: canceledSvg,
|
||||
icon_status_created: createdSvg,
|
||||
icon_status_failed: failedSvg,
|
||||
icon_status_manual: manualSvg,
|
||||
icon_status_pending: pendingSvg,
|
||||
icon_status_running: runningSvg,
|
||||
icon_status_skipped: skippedSvg,
|
||||
icon_status_success: successSvg,
|
||||
icon_status_warning: warningSvg,
|
||||
};
|
||||
|
||||
return {
|
||||
builds: '',
|
||||
spinner: '<span class="fa fa-spinner fa-spin"></span>',
|
||||
svg: svgsDictionary[this.stage.status.icon],
|
||||
};
|
||||
},
|
||||
|
||||
props: {
|
||||
stage: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
updated() {
|
||||
if (this.builds) {
|
||||
this.stopDropdownClickPropagation();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchBuilds(e) {
|
||||
const ariaExpanded = e.currentTarget.attributes['aria-expanded'];
|
||||
|
||||
if (ariaExpanded && (ariaExpanded.textContent === 'true')) return null;
|
||||
|
||||
return this.$http.get(this.stage.dropdown_path)
|
||||
.then((response) => {
|
||||
this.builds = JSON.parse(response.body).html;
|
||||
}, () => {
|
||||
const flash = new Flash('Something went wrong on our end.');
|
||||
return flash;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* When the user right clicks or cmd/ctrl + click in the job name
|
||||
* the dropdown should not be closed and the link should open in another tab,
|
||||
* so we stop propagation of the click event inside the dropdown.
|
||||
*
|
||||
* Since this component is rendered multiple times per page we need to guarantee we only
|
||||
* target the click event of this component.
|
||||
*/
|
||||
stopDropdownClickPropagation() {
|
||||
$(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')).on('click', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
buildsOrSpinner() {
|
||||
return this.builds ? this.builds : this.spinner;
|
||||
},
|
||||
dropdownClass() {
|
||||
if (this.builds) 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}`;
|
||||
},
|
||||
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($event)"
|
||||
:class="triggerButtonClass"
|
||||
:title="stage.title"
|
||||
data-placement="top"
|
||||
data-toggle="dropdown"
|
||||
type="button"
|
||||
:aria-label="stage.title">
|
||||
<span v-html="svg" aria-hidden="true"></span>
|
||||
<i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
|
||||
<div class="arrow-up" aria-hidden="true"></div>
|
||||
<div
|
||||
:class="dropdownClass"
|
||||
class="js-builds-dropdown-list scrollable-menu"
|
||||
v-html="buildsOrSpinner">
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
};
|
|
@ -0,0 +1,60 @@
|
|||
import canceledSvg from 'icons/_icon_status_canceled.svg';
|
||||
import createdSvg from 'icons/_icon_status_created.svg';
|
||||
import failedSvg from 'icons/_icon_status_failed.svg';
|
||||
import manualSvg from 'icons/_icon_status_manual.svg';
|
||||
import pendingSvg from 'icons/_icon_status_pending.svg';
|
||||
import runningSvg from 'icons/_icon_status_running.svg';
|
||||
import skippedSvg from 'icons/_icon_status_skipped.svg';
|
||||
import successSvg from 'icons/_icon_status_success.svg';
|
||||
import warningSvg from 'icons/_icon_status_warning.svg';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
pipeline: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
const svgsDictionary = {
|
||||
icon_status_canceled: canceledSvg,
|
||||
icon_status_created: createdSvg,
|
||||
icon_status_failed: failedSvg,
|
||||
icon_status_manual: manualSvg,
|
||||
icon_status_pending: pendingSvg,
|
||||
icon_status_running: runningSvg,
|
||||
icon_status_skipped: skippedSvg,
|
||||
icon_status_success: successSvg,
|
||||
icon_status_warning: warningSvg,
|
||||
};
|
||||
|
||||
return {
|
||||
svg: svgsDictionary[this.pipeline.details.status.icon],
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
cssClasses() {
|
||||
return `ci-status ci-${this.pipeline.details.status.group}`;
|
||||
},
|
||||
|
||||
detailsPath() {
|
||||
const { status } = this.pipeline.details;
|
||||
return status.has_details ? status.details_path : false;
|
||||
},
|
||||
|
||||
content() {
|
||||
return `${this.svg} ${this.pipeline.details.status.text}`;
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<td class="commit-link">
|
||||
<a
|
||||
:class="cssClasses"
|
||||
:href="detailsPath"
|
||||
v-html="content">
|
||||
</a>
|
||||
</td>
|
||||
`,
|
||||
};
|
|
@ -0,0 +1,71 @@
|
|||
import iconTimerSvg from 'icons/_icon_timer.svg';
|
||||
import '../../lib/utils/datetime_utility';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
currentTime: new Date(),
|
||||
iconTimerSvg,
|
||||
};
|
||||
},
|
||||
props: ['pipeline'],
|
||||
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 class="pipelines-time-ago">
|
||||
<p class="duration" v-if='duration'>
|
||||
<span v-html="iconTimerSvg"></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>
|
||||
`,
|
||||
};
|
3
app/assets/javascripts/vue_pipelines_index/event_hub.js
Normal file
3
app/assets/javascripts/vue_pipelines_index/event_hub.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
export default new Vue();
|
|
@ -1,29 +1,28 @@
|
|||
/* eslint-disable no-param-reassign */
|
||||
/* global Vue, VueResource, gl */
|
||||
window.Vue = require('vue');
|
||||
import PipelinesStore from './stores/pipelines_store';
|
||||
import PipelinesComponent from './pipelines';
|
||||
import '../vue_shared/vue_resource_interceptor';
|
||||
|
||||
const Vue = window.Vue = require('vue');
|
||||
window.Vue.use(require('vue-resource'));
|
||||
require('../lib/utils/common_utils');
|
||||
require('../vue_shared/vue_resource_interceptor');
|
||||
require('./pipelines');
|
||||
|
||||
$(() => new Vue({
|
||||
el: document.querySelector('.vue-pipelines-index'),
|
||||
|
||||
data() {
|
||||
const project = document.querySelector('.pipelines');
|
||||
const store = new PipelinesStore();
|
||||
|
||||
return {
|
||||
scope: project.dataset.url,
|
||||
store: new gl.PipelineStore(),
|
||||
store,
|
||||
endpoint: project.dataset.url,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
'vue-pipelines': gl.VuePipelines,
|
||||
'vue-pipelines': PipelinesComponent,
|
||||
},
|
||||
template: `
|
||||
<vue-pipelines
|
||||
:scope="scope"
|
||||
:store="store">
|
||||
</vue-pipelines>
|
||||
:endpoint="endpoint"
|
||||
:store="store" />
|
||||
`,
|
||||
}));
|
||||
|
|
|
@ -1,123 +0,0 @@
|
|||
/* global Vue, Flash, gl */
|
||||
/* eslint-disable no-param-reassign, no-alert */
|
||||
const playIconSvg = require('icons/_icon_play.svg');
|
||||
|
||||
((gl) => {
|
||||
gl.VuePipelineActions = Vue.extend({
|
||||
props: ['pipeline'],
|
||||
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`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows a dialog when the user clicks in the cancel button.
|
||||
* We need to prevent the default behavior and stop propagation because the
|
||||
* link relies on UJS.
|
||||
*
|
||||
* @param {Event} event
|
||||
*/
|
||||
confirmAction(event) {
|
||||
if (!confirm('Are you sure you want to cancel this pipeline?')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return { playIconSvg };
|
||||
},
|
||||
|
||||
template: `
|
||||
<td class="pipeline-actions">
|
||||
<div class="pull-right">
|
||||
<div class="btn-group">
|
||||
<div class="btn-group" v-if="actions">
|
||||
<button
|
||||
class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
|
||||
data-toggle="dropdown"
|
||||
title="Manual job"
|
||||
data-placement="top"
|
||||
data-container="body"
|
||||
aria-label="Manual job">
|
||||
<span v-html="playIconSvg" aria-hidden="true"></span>
|
||||
<i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
<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" >
|
||||
<span v-html="playIconSvg" aria-hidden="true"></span>
|
||||
<span>{{action.name}}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="btn-group" v-if="artifacts">
|
||||
<button
|
||||
class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
|
||||
title="Artifacts"
|
||||
data-placement="top"
|
||||
data-container="body"
|
||||
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 pipeline.details.artifacts'>
|
||||
<a
|
||||
rel="nofollow"
|
||||
:href="artifact.path">
|
||||
<i class="fa fa-download" aria-hidden="true"></i>
|
||||
<span>{{download(artifact.name)}}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="btn-group" v-if="pipeline.flags.retryable">
|
||||
<a
|
||||
class="btn btn-default btn-retry has-tooltip"
|
||||
title="Retry"
|
||||
rel="nofollow"
|
||||
data-method="post"
|
||||
data-placement="top"
|
||||
data-container="body"
|
||||
data-toggle="dropdown"
|
||||
:href='pipeline.retry_path'
|
||||
aria-label="Retry">
|
||||
<i class="fa fa-repeat" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="btn-group" v-if="pipeline.flags.cancelable">
|
||||
<a
|
||||
class="btn btn-remove has-tooltip"
|
||||
title="Cancel"
|
||||
rel="nofollow"
|
||||
data-method="post"
|
||||
data-placement="top"
|
||||
data-container="body"
|
||||
data-toggle="dropdown"
|
||||
:href='pipeline.cancel_path'
|
||||
aria-label="Cancel">
|
||||
<i class="fa fa-remove" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
`,
|
||||
});
|
||||
})(window.gl || (window.gl = {}));
|
|
@ -1,63 +0,0 @@
|
|||
/* 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 = {}));
|
|
@ -1,87 +1,121 @@
|
|||
/* global Vue, gl */
|
||||
/* eslint-disable no-param-reassign */
|
||||
/* global Flash */
|
||||
/* eslint-disable no-new */
|
||||
import '~/flash';
|
||||
import Vue from 'vue';
|
||||
import PipelinesService from './services/pipelines_service';
|
||||
import eventHub from './event_hub';
|
||||
import PipelinesTableComponent from '../vue_shared/components/pipelines_table';
|
||||
import TablePaginationComponent from '../vue_shared/components/table_pagination';
|
||||
|
||||
window.Vue = require('vue');
|
||||
require('../vue_shared/components/table_pagination');
|
||||
require('./store');
|
||||
require('../vue_shared/components/pipelines_table');
|
||||
const CommitPipelinesStoreWithTimeAgo = require('../commit/pipelines/pipelines_store');
|
||||
|
||||
((gl) => {
|
||||
gl.VuePipelines = Vue.extend({
|
||||
|
||||
components: {
|
||||
'gl-pagination': gl.VueGlPagination,
|
||||
'pipelines-table-component': gl.pipelines.PipelinesTableComponent,
|
||||
export default {
|
||||
props: {
|
||||
endpoint: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
pipelines: [],
|
||||
timeLoopInterval: '',
|
||||
intervalId: '',
|
||||
apiScope: 'all',
|
||||
pageInfo: {},
|
||||
pagenum: 1,
|
||||
count: {},
|
||||
pageRequest: false,
|
||||
};
|
||||
store: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
props: ['scope', 'store'],
|
||||
created() {
|
||||
const pagenum = gl.utils.getParameterByName('page');
|
||||
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);
|
||||
components: {
|
||||
'gl-pagination': TablePaginationComponent,
|
||||
'pipelines-table-component': PipelinesTableComponent,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
state: this.store.state,
|
||||
apiScope: 'all',
|
||||
pagenum: 1,
|
||||
pageRequest: false,
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.service = new PipelinesService(this.endpoint);
|
||||
|
||||
this.fetchPipelines();
|
||||
|
||||
eventHub.$on('refreshPipelines', this.fetchPipelines);
|
||||
},
|
||||
|
||||
beforeUpdate() {
|
||||
if (this.state.pipelines.length && this.$children) {
|
||||
this.store.startTimeAgoLoops.call(this, Vue);
|
||||
}
|
||||
},
|
||||
|
||||
beforeDestroyed() {
|
||||
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;
|
||||
},
|
||||
|
||||
beforeUpdate() {
|
||||
if (this.pipelines.length && this.$children) {
|
||||
CommitPipelinesStoreWithTimeAgo.startTimeAgoLoops.call(this, Vue);
|
||||
}
|
||||
fetchPipelines() {
|
||||
const pageNumber = gl.utils.getParameterByName('page') || this.pagenum;
|
||||
const scope = gl.utils.getParameterByName('scope') || this.apiScope;
|
||||
|
||||
this.pageRequest = true;
|
||||
return this.service.getPipelines(scope, pageNumber)
|
||||
.then(resp => ({
|
||||
headers: resp.headers,
|
||||
body: resp.json(),
|
||||
}))
|
||||
.then((response) => {
|
||||
this.store.storeCount(response.body.count);
|
||||
this.store.storePipelines(response.body.pipelines);
|
||||
this.store.storePagination(response.headers);
|
||||
})
|
||||
.then(() => {
|
||||
this.pageRequest = false;
|
||||
})
|
||||
.catch(() => {
|
||||
this.pageRequest = false;
|
||||
new Flash('An error occurred while fetching the pipelines, please reload the page again.');
|
||||
});
|
||||
},
|
||||
|
||||
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;
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<div class="pipelines realtime-loading" v-if='pageRequest'>
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
|
||||
<div class="blank-state blank-state-no-icon"
|
||||
v-if="!pageRequest && pipelines.length === 0">
|
||||
<h2 class="blank-state-title js-blank-state-title">
|
||||
No pipelines to show
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="table-holder" v-if='!pageRequest && pipelines.length'>
|
||||
<pipelines-table-component :pipelines='pipelines'/>
|
||||
</div>
|
||||
|
||||
<gl-pagination
|
||||
v-if='!pageRequest && pipelines.length && pageInfo.total > pageInfo.perPage'
|
||||
:pagenum='pagenum'
|
||||
:change='change'
|
||||
:count='count.all'
|
||||
:pageInfo='pageInfo'
|
||||
>
|
||||
</gl-pagination>
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<div class="pipelines realtime-loading" v-if="pageRequest">
|
||||
<i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
})(window.gl || (window.gl = {}));
|
||||
|
||||
<div class="blank-state blank-state-no-icon"
|
||||
v-if="!pageRequest && state.pipelines.length === 0">
|
||||
<h2 class="blank-state-title js-blank-state-title">
|
||||
No pipelines to show
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="table-holder" v-if="!pageRequest && state.pipelines.length">
|
||||
<pipelines-table-component
|
||||
:pipelines="state.pipelines"
|
||||
:service="service"/>
|
||||
</div>
|
||||
|
||||
<gl-pagination
|
||||
v-if="!pageRequest && state.pipelines.length && state.pageInfo.total > state.pageInfo.perPage"
|
||||
:pagenum="pagenum"
|
||||
:change="change"
|
||||
:count="state.count.all"
|
||||
:pageInfo="state.pageInfo"
|
||||
>
|
||||
</gl-pagination>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
import Vue from 'vue';
|
||||
import VueResource from 'vue-resource';
|
||||
|
||||
Vue.use(VueResource);
|
||||
|
||||
export default class PipelinesService {
|
||||
|
||||
/**
|
||||
* Commits and merge request endpoints need to be requested with `.json`.
|
||||
*
|
||||
* The url provided to request the pipelines in the new merge request
|
||||
* page already has `.json`.
|
||||
*
|
||||
* @param {String} root
|
||||
*/
|
||||
constructor(root) {
|
||||
let endpoint;
|
||||
|
||||
if (root.indexOf('.json') === -1) {
|
||||
endpoint = `${root}.json`;
|
||||
} else {
|
||||
endpoint = root;
|
||||
}
|
||||
|
||||
this.pipelines = Vue.resource(endpoint);
|
||||
}
|
||||
|
||||
getPipelines(scope, page) {
|
||||
return this.pipelines.get({ scope, page });
|
||||
}
|
||||
|
||||
/**
|
||||
* Post request for all pipelines actions.
|
||||
* Endpoint content type needs to be:
|
||||
* `Content-Type:application/x-www-form-urlencoded`
|
||||
*
|
||||
* @param {String} endpoint
|
||||
* @return {Promise}
|
||||
*/
|
||||
postAction(endpoint) {
|
||||
return Vue.http.post(endpoint, {}, { emulateJSON: true });
|
||||
}
|
||||
}
|
|
@ -1,119 +0,0 @@
|
|||
/* global Vue, Flash, gl */
|
||||
/* eslint-disable no-param-reassign */
|
||||
import canceledSvg from 'icons/_icon_status_canceled_borderless.svg';
|
||||
import createdSvg from 'icons/_icon_status_created_borderless.svg';
|
||||
import failedSvg from 'icons/_icon_status_failed_borderless.svg';
|
||||
import manualSvg from 'icons/_icon_status_manual_borderless.svg';
|
||||
import pendingSvg from 'icons/_icon_status_pending_borderless.svg';
|
||||
import runningSvg from 'icons/_icon_status_running_borderless.svg';
|
||||
import skippedSvg from 'icons/_icon_status_skipped_borderless.svg';
|
||||
import successSvg from 'icons/_icon_status_success_borderless.svg';
|
||||
import warningSvg from 'icons/_icon_status_warning_borderless.svg';
|
||||
|
||||
((gl) => {
|
||||
gl.VueStage = Vue.extend({
|
||||
data() {
|
||||
const svgsDictionary = {
|
||||
icon_status_canceled: canceledSvg,
|
||||
icon_status_created: createdSvg,
|
||||
icon_status_failed: failedSvg,
|
||||
icon_status_manual: manualSvg,
|
||||
icon_status_pending: pendingSvg,
|
||||
icon_status_running: runningSvg,
|
||||
icon_status_skipped: skippedSvg,
|
||||
icon_status_success: successSvg,
|
||||
icon_status_warning: warningSvg,
|
||||
};
|
||||
|
||||
return {
|
||||
builds: '',
|
||||
spinner: '<span class="fa fa-spinner fa-spin"></span>',
|
||||
svg: svgsDictionary[this.stage.status.icon],
|
||||
};
|
||||
},
|
||||
|
||||
props: {
|
||||
stage: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
updated() {
|
||||
if (this.builds) {
|
||||
this.stopDropdownClickPropagation();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchBuilds(e) {
|
||||
const areaExpanded = e.currentTarget.attributes['aria-expanded'];
|
||||
|
||||
if (areaExpanded && (areaExpanded.textContent === 'true')) return null;
|
||||
|
||||
return this.$http.get(this.stage.dropdown_path)
|
||||
.then((response) => {
|
||||
this.builds = JSON.parse(response.body).html;
|
||||
}, () => {
|
||||
const flash = new Flash('Something went wrong on our end.');
|
||||
return flash;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* When the user right clicks or cmd/ctrl + click in the job name
|
||||
* the dropdown should not be closed and the link should open in another tab,
|
||||
* so we stop propagation of the click event inside the dropdown.
|
||||
*
|
||||
* Since this component is rendered multiple times per page we need to guarantee we only
|
||||
* target the click event of this component.
|
||||
*/
|
||||
stopDropdownClickPropagation() {
|
||||
$(this.$el).on('click', '.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
buildsOrSpinner() {
|
||||
return this.builds ? this.builds : this.spinner;
|
||||
},
|
||||
dropdownClass() {
|
||||
if (this.builds) 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}`;
|
||||
},
|
||||
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($event)"
|
||||
:class="triggerButtonClass"
|
||||
:title="stage.title"
|
||||
data-placement="top"
|
||||
data-toggle="dropdown"
|
||||
type="button"
|
||||
:aria-label="stage.title">
|
||||
<span v-html="svg" aria-hidden="true"></span>
|
||||
<i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
|
||||
<div class="arrow-up" aria-hidden="true"></div>
|
||||
<div
|
||||
:class="dropdownClass"
|
||||
class="js-builds-dropdown-list scrollable-menu"
|
||||
v-html="buildsOrSpinner">
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
})(window.gl || (window.gl = {}));
|
|
@ -1,64 +0,0 @@
|
|||
/* global Vue, gl */
|
||||
/* eslint-disable no-param-reassign */
|
||||
|
||||
import canceledSvg from 'icons/_icon_status_canceled.svg';
|
||||
import createdSvg from 'icons/_icon_status_created.svg';
|
||||
import failedSvg from 'icons/_icon_status_failed.svg';
|
||||
import manualSvg from 'icons/_icon_status_manual.svg';
|
||||
import pendingSvg from 'icons/_icon_status_pending.svg';
|
||||
import runningSvg from 'icons/_icon_status_running.svg';
|
||||
import skippedSvg from 'icons/_icon_status_skipped.svg';
|
||||
import successSvg from 'icons/_icon_status_success.svg';
|
||||
import warningSvg from 'icons/_icon_status_warning.svg';
|
||||
|
||||
((gl) => {
|
||||
gl.VueStatusScope = Vue.extend({
|
||||
props: [
|
||||
'pipeline',
|
||||
],
|
||||
|
||||
data() {
|
||||
const svgsDictionary = {
|
||||
icon_status_canceled: canceledSvg,
|
||||
icon_status_created: createdSvg,
|
||||
icon_status_failed: failedSvg,
|
||||
icon_status_manual: manualSvg,
|
||||
icon_status_pending: pendingSvg,
|
||||
icon_status_running: runningSvg,
|
||||
icon_status_skipped: skippedSvg,
|
||||
icon_status_success: successSvg,
|
||||
icon_status_warning: warningSvg,
|
||||
};
|
||||
|
||||
return {
|
||||
svg: svgsDictionary[this.pipeline.details.status.icon],
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
cssClasses() {
|
||||
const cssObject = { 'ci-status': true };
|
||||
cssObject[`ci-${this.pipeline.details.status.group}`] = true;
|
||||
return cssObject;
|
||||
},
|
||||
|
||||
detailsPath() {
|
||||
const { status } = this.pipeline.details;
|
||||
return status.has_details ? status.details_path : false;
|
||||
},
|
||||
|
||||
content() {
|
||||
return `${this.svg} ${this.pipeline.details.status.text}`;
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<td class="commit-link">
|
||||
<a
|
||||
:class="cssClasses"
|
||||
:href="detailsPath"
|
||||
v-html="content">
|
||||
</a>
|
||||
</td>
|
||||
`,
|
||||
});
|
||||
})(window.gl || (window.gl = {}));
|
|
@ -1,31 +0,0 @@
|
|||
/* global gl, Flash */
|
||||
/* eslint-disable no-param-reassign */
|
||||
|
||||
((gl) => {
|
||||
const pageValues = (headers) => {
|
||||
const normalized = gl.utils.normalizeHeaders(headers);
|
||||
const paginationInfo = gl.utils.parseIntPagination(normalized);
|
||||
return paginationInfo;
|
||||
};
|
||||
|
||||
gl.PipelineStore = class {
|
||||
fetchDataLoop(Vue, pageNum, url, apiScope) {
|
||||
this.pageRequest = true;
|
||||
|
||||
return 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);
|
||||
|
||||
this.pageRequest = false;
|
||||
}, () => {
|
||||
this.pageRequest = false;
|
||||
return new Flash('An error occurred while fetching the pipelines, please reload the page again.');
|
||||
});
|
||||
}
|
||||
};
|
||||
})(window.gl || (window.gl = {}));
|
|
@ -1,31 +1,46 @@
|
|||
/* eslint-disable no-underscore-dangle*/
|
||||
/**
|
||||
* Pipelines' Store for commits view.
|
||||
*
|
||||
* Used to store the Pipelines rendered in the commit view in the pipelines table.
|
||||
*/
|
||||
require('../../vue_realtime_listener');
|
||||
import '../../vue_realtime_listener';
|
||||
|
||||
class PipelinesStore {
|
||||
export default class PipelinesStore {
|
||||
constructor() {
|
||||
this.state = {};
|
||||
|
||||
this.state.pipelines = [];
|
||||
this.state.count = {};
|
||||
this.state.pageInfo = {};
|
||||
}
|
||||
|
||||
storePipelines(pipelines = []) {
|
||||
this.state.pipelines = pipelines;
|
||||
}
|
||||
|
||||
return pipelines;
|
||||
storeCount(count = {}) {
|
||||
this.state.count = count;
|
||||
}
|
||||
|
||||
storePagination(pagination = {}) {
|
||||
let paginationInfo;
|
||||
|
||||
if (Object.keys(pagination).length) {
|
||||
const normalizedHeaders = gl.utils.normalizeHeaders(pagination);
|
||||
paginationInfo = gl.utils.parseIntPagination(normalizedHeaders);
|
||||
} else {
|
||||
paginationInfo = pagination;
|
||||
}
|
||||
|
||||
this.state.pageInfo = paginationInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME: Move this inside the component.
|
||||
*
|
||||
* Once the data is received we will start the time ago loops.
|
||||
*
|
||||
* Everytime a request is made like retry or cancel a pipeline, every 10 seconds we
|
||||
* update the time to show how long as passed.
|
||||
*
|
||||
*/
|
||||
static startTimeAgoLoops() {
|
||||
startTimeAgoLoops() {
|
||||
const startTimeLoops = () => {
|
||||
this.timeLoopInterval = setInterval(() => {
|
||||
this.$children[0].$children.reduce((acc, component) => {
|
||||
|
@ -44,5 +59,3 @@ class PipelinesStore {
|
|||
gl.VueRealtimeListener(removeIntervals, startIntervals);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PipelinesStore;
|
|
@ -1,78 +0,0 @@
|
|||
/* global Vue, gl */
|
||||
/* eslint-disable no-param-reassign */
|
||||
|
||||
window.Vue = require('vue');
|
||||
require('../lib/utils/datetime_utility');
|
||||
|
||||
const iconTimerSvg = require('../../../views/shared/icons/_icon_timer.svg');
|
||||
|
||||
((gl) => {
|
||||
gl.VueTimeAgo = Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
currentTime: new Date(),
|
||||
iconTimerSvg,
|
||||
};
|
||||
},
|
||||
props: ['pipeline'],
|
||||
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 class="pipelines-time-ago">
|
||||
<p class="duration" v-if='duration'>
|
||||
<span v-html="iconTimerSvg"></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 = {}));
|
|
@ -1,164 +1,157 @@
|
|||
/* global Vue */
|
||||
window.Vue = require('vue');
|
||||
const commitIconSvg = require('icons/_icon_commit.svg');
|
||||
import commitIconSvg from 'icons/_icon_commit.svg';
|
||||
|
||||
(() => {
|
||||
window.gl = window.gl || {};
|
||||
|
||||
window.gl.CommitComponent = Vue.component('commit-component', {
|
||||
|
||||
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: () => ({}),
|
||||
},
|
||||
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,
|
||||
},
|
||||
|
||||
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.web_url &&
|
||||
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;
|
||||
},
|
||||
/**
|
||||
* 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: () => ({}),
|
||||
},
|
||||
|
||||
data() {
|
||||
return { commitIconSvg };
|
||||
/**
|
||||
* Used to link to the commit sha.
|
||||
*/
|
||||
commitUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
|
||||
template: `
|
||||
<div class="branch-commit">
|
||||
/**
|
||||
* Used to show the commit short sha that links to the commit url.
|
||||
*/
|
||||
shortSha: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
|
||||
<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>
|
||||
/**
|
||||
* If provided shows the commit tile.
|
||||
*/
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
|
||||
<a v-if="hasCommitRef"
|
||||
class="monospace branch-name"
|
||||
:href="commitRef.ref_url">
|
||||
{{commitRef.name}}
|
||||
</a>
|
||||
/**
|
||||
* 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: () => ({}),
|
||||
},
|
||||
},
|
||||
|
||||
<div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div>
|
||||
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;
|
||||
},
|
||||
|
||||
<a class="commit-id monospace"
|
||||
:href="commitUrl">
|
||||
{{shortSha}}
|
||||
</a>
|
||||
/**
|
||||
* 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.web_url &&
|
||||
this.author.username;
|
||||
},
|
||||
|
||||
<p class="commit-title">
|
||||
<span v-if="title">
|
||||
<a v-if="hasAuthor"
|
||||
class="avatar-image-container"
|
||||
:href="author.web_url">
|
||||
<img
|
||||
class="avatar has-tooltip s20"
|
||||
:src="author.avatar_url"
|
||||
:alt="userImageAltDescription"
|
||||
:title="author.username" />
|
||||
</a>
|
||||
/**
|
||||
* 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;
|
||||
},
|
||||
},
|
||||
|
||||
<a class="commit-row-message"
|
||||
:href="commitUrl">
|
||||
{{title}}
|
||||
</a>
|
||||
</span>
|
||||
<span v-else>
|
||||
Cant find HEAD commit for this branch
|
||||
</span>
|
||||
</p>
|
||||
data() {
|
||||
return { commitIconSvg };
|
||||
},
|
||||
|
||||
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="monospace branch-name"
|
||||
:href="commitRef.ref_url">
|
||||
{{commitRef.name}}
|
||||
</a>
|
||||
|
||||
<div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div>
|
||||
|
||||
<a class="commit-id monospace"
|
||||
:href="commitUrl">
|
||||
{{shortSha}}
|
||||
</a>
|
||||
|
||||
<p class="commit-title">
|
||||
<span v-if="title">
|
||||
<a v-if="hasAuthor"
|
||||
class="avatar-image-container"
|
||||
:href="author.web_url">
|
||||
<img
|
||||
class="avatar has-tooltip s20"
|
||||
:src="author.avatar_url"
|
||||
:alt="userImageAltDescription"
|
||||
:title="author.username" />
|
||||
</a>
|
||||
|
||||
<a class="commit-row-message"
|
||||
:href="commitUrl">
|
||||
{{title}}
|
||||
</a>
|
||||
</span>
|
||||
<span v-else>
|
||||
Cant find HEAD commit for this branch
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
|
|
|
@ -1,52 +1,48 @@
|
|||
/* eslint-disable no-param-reassign */
|
||||
/* global Vue */
|
||||
import PipelinesTableRowComponent from './pipelines_table_row';
|
||||
|
||||
require('./pipelines_table_row');
|
||||
/**
|
||||
* Pipelines Table Component.
|
||||
*
|
||||
* Given an array of objects, renders a table.
|
||||
*/
|
||||
|
||||
(() => {
|
||||
window.gl = window.gl || {};
|
||||
gl.pipelines = gl.pipelines || {};
|
||||
|
||||
gl.pipelines.PipelinesTableComponent = Vue.component('pipelines-table-component', {
|
||||
|
||||
props: {
|
||||
pipelines: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => ([]),
|
||||
},
|
||||
|
||||
export default {
|
||||
props: {
|
||||
pipelines: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => ([]),
|
||||
},
|
||||
|
||||
components: {
|
||||
'pipelines-table-row-component': gl.pipelines.PipelinesTableRowComponent,
|
||||
service: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
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"></tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
`,
|
||||
});
|
||||
})();
|
||||
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"></tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
`,
|
||||
};
|
||||
|
|
|
@ -1,199 +1,228 @@
|
|||
/* eslint-disable no-param-reassign */
|
||||
/* global Vue */
|
||||
|
||||
require('../../vue_pipelines_index/status');
|
||||
require('../../vue_pipelines_index/pipeline_url');
|
||||
require('../../vue_pipelines_index/stage');
|
||||
require('../../vue_pipelines_index/pipeline_actions');
|
||||
require('../../vue_pipelines_index/time_ago');
|
||||
require('./commit');
|
||||
import AsyncButtonComponent from '../../vue_pipelines_index/components/async_button';
|
||||
import PipelinesActionsComponent from '../../vue_pipelines_index/components/pipelines_actions';
|
||||
import PipelinesArtifactsComponent from '../../vue_pipelines_index/components/pipelines_artifacts';
|
||||
import PipelinesStatusComponent from '../../vue_pipelines_index/components/status';
|
||||
import PipelinesStageComponent from '../../vue_pipelines_index/components/stage';
|
||||
import PipelinesUrlComponent from '../../vue_pipelines_index/components/pipeline_url';
|
||||
import PipelinesTimeagoComponent from '../../vue_pipelines_index/components/time_ago';
|
||||
import CommitComponent from './commit';
|
||||
|
||||
/**
|
||||
* Pipeline table row.
|
||||
*
|
||||
* Given the received object renders a table row in the pipelines' table.
|
||||
*/
|
||||
(() => {
|
||||
window.gl = window.gl || {};
|
||||
gl.pipelines = gl.pipelines || {};
|
||||
|
||||
gl.pipelines.PipelinesTableRowComponent = Vue.component('pipelines-table-row-component', {
|
||||
|
||||
props: {
|
||||
pipeline: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({}),
|
||||
},
|
||||
|
||||
export default {
|
||||
props: {
|
||||
pipeline: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
components: {
|
||||
'commit-component': gl.CommitComponent,
|
||||
'pipeline-actions': gl.VuePipelineActions,
|
||||
'dropdown-stage': gl.VueStage,
|
||||
'pipeline-url': gl.VuePipelineUrl,
|
||||
'status-scope': gl.VueStatusScope,
|
||||
'time-ago': gl.VueTimeAgo,
|
||||
service: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* If provided, returns the commit tag.
|
||||
* Needed to render the commit component column.
|
||||
*
|
||||
* This field needs a lot of verification, because of different possible cases:
|
||||
*
|
||||
* 1. person who is an author of a commit might be a GitLab user
|
||||
* 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar
|
||||
* 3. If GitLab user does not have avatar he/she might have a Gravatar
|
||||
* 4. If committer is not a GitLab User he/she can have a Gravatar
|
||||
* 5. We do not have consistent API object in this case
|
||||
* 6. We should improve API and the code
|
||||
*
|
||||
* @returns {Object|Undefined}
|
||||
*/
|
||||
commitAuthor() {
|
||||
let commitAuthorInformation;
|
||||
components: {
|
||||
'async-button-component': AsyncButtonComponent,
|
||||
'pipelines-actions-component': PipelinesActionsComponent,
|
||||
'pipelines-artifacts-component': PipelinesArtifactsComponent,
|
||||
'commit-component': CommitComponent,
|
||||
'dropdown-stage': PipelinesStageComponent,
|
||||
'pipeline-url': PipelinesUrlComponent,
|
||||
'status-scope': PipelinesStatusComponent,
|
||||
'time-ago': PipelinesTimeagoComponent,
|
||||
},
|
||||
|
||||
// 1. person who is an author of a commit might be a GitLab user
|
||||
if (this.pipeline &&
|
||||
this.pipeline.commit &&
|
||||
this.pipeline.commit.author) {
|
||||
// 2. if person who is an author of a commit is a GitLab user
|
||||
// he/she can have a GitLab avatar
|
||||
if (this.pipeline.commit.author.avatar_url) {
|
||||
commitAuthorInformation = this.pipeline.commit.author;
|
||||
computed: {
|
||||
/**
|
||||
* If provided, returns the commit tag.
|
||||
* Needed to render the commit component column.
|
||||
*
|
||||
* This field needs a lot of verification, because of different possible cases:
|
||||
*
|
||||
* 1. person who is an author of a commit might be a GitLab user
|
||||
* 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar
|
||||
* 3. If GitLab user does not have avatar he/she might have a Gravatar
|
||||
* 4. If committer is not a GitLab User he/she can have a Gravatar
|
||||
* 5. We do not have consistent API object in this case
|
||||
* 6. We should improve API and the code
|
||||
*
|
||||
* @returns {Object|Undefined}
|
||||
*/
|
||||
commitAuthor() {
|
||||
let commitAuthorInformation;
|
||||
|
||||
// 3. If GitLab user does not have avatar he/she might have a Gravatar
|
||||
} else if (this.pipeline.commit.author_gravatar_url) {
|
||||
commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, {
|
||||
avatar_url: this.pipeline.commit.author_gravatar_url,
|
||||
});
|
||||
}
|
||||
}
|
||||
// 1. person who is an author of a commit might be a GitLab user
|
||||
if (this.pipeline &&
|
||||
this.pipeline.commit &&
|
||||
this.pipeline.commit.author) {
|
||||
// 2. if person who is an author of a commit is a GitLab user
|
||||
// he/she can have a GitLab avatar
|
||||
if (this.pipeline.commit.author.avatar_url) {
|
||||
commitAuthorInformation = this.pipeline.commit.author;
|
||||
|
||||
// 4. If committer is not a GitLab User he/she can have a Gravatar
|
||||
if (this.pipeline &&
|
||||
this.pipeline.commit) {
|
||||
commitAuthorInformation = {
|
||||
// 3. If GitLab user does not have avatar he/she might have a Gravatar
|
||||
} else if (this.pipeline.commit.author_gravatar_url) {
|
||||
commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, {
|
||||
avatar_url: this.pipeline.commit.author_gravatar_url,
|
||||
web_url: `mailto:${this.pipeline.commit.author_email}`,
|
||||
username: this.pipeline.commit.author_name,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return commitAuthorInformation;
|
||||
},
|
||||
// 4. If committer is not a GitLab User he/she can have a Gravatar
|
||||
if (this.pipeline &&
|
||||
this.pipeline.commit) {
|
||||
commitAuthorInformation = {
|
||||
avatar_url: this.pipeline.commit.author_gravatar_url,
|
||||
web_url: `mailto:${this.pipeline.commit.author_email}`,
|
||||
username: this.pipeline.commit.author_name,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* If provided, returns the commit tag.
|
||||
* Needed to render the commit component column.
|
||||
*
|
||||
* @returns {String|Undefined}
|
||||
*/
|
||||
commitTag() {
|
||||
if (this.pipeline.ref &&
|
||||
this.pipeline.ref.tag) {
|
||||
return this.pipeline.ref.tag;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* If provided, returns the commit ref.
|
||||
* Needed to render the commit component column.
|
||||
*
|
||||
* Matches `path` prop sent in the API to `ref_url` prop needed
|
||||
* in the commit component.
|
||||
*
|
||||
* @returns {Object|Undefined}
|
||||
*/
|
||||
commitRef() {
|
||||
if (this.pipeline.ref) {
|
||||
return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
|
||||
if (prop === 'path') {
|
||||
accumulator.ref_url = this.pipeline.ref[prop];
|
||||
} else {
|
||||
accumulator[prop] = this.pipeline.ref[prop];
|
||||
}
|
||||
return accumulator;
|
||||
}, {});
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* If provided, returns the commit url.
|
||||
* Needed to render the commit component column.
|
||||
*
|
||||
* @returns {String|Undefined}
|
||||
*/
|
||||
commitUrl() {
|
||||
if (this.pipeline.commit &&
|
||||
this.pipeline.commit.commit_path) {
|
||||
return this.pipeline.commit.commit_path;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* If provided, returns the commit short sha.
|
||||
* Needed to render the commit component column.
|
||||
*
|
||||
* @returns {String|Undefined}
|
||||
*/
|
||||
commitShortSha() {
|
||||
if (this.pipeline.commit &&
|
||||
this.pipeline.commit.short_id) {
|
||||
return this.pipeline.commit.short_id;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* If provided, returns the commit title.
|
||||
* Needed to render the commit component column.
|
||||
*
|
||||
* @returns {String|Undefined}
|
||||
*/
|
||||
commitTitle() {
|
||||
if (this.pipeline.commit &&
|
||||
this.pipeline.commit.title) {
|
||||
return this.pipeline.commit.title;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
return commitAuthorInformation;
|
||||
},
|
||||
|
||||
template: `
|
||||
<tr class="commit">
|
||||
<status-scope :pipeline="pipeline"/>
|
||||
/**
|
||||
* If provided, returns the commit tag.
|
||||
* Needed to render the commit component column.
|
||||
*
|
||||
* @returns {String|Undefined}
|
||||
*/
|
||||
commitTag() {
|
||||
if (this.pipeline.ref &&
|
||||
this.pipeline.ref.tag) {
|
||||
return this.pipeline.ref.tag;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
<pipeline-url :pipeline="pipeline"></pipeline-url>
|
||||
/**
|
||||
* If provided, returns the commit ref.
|
||||
* Needed to render the commit component column.
|
||||
*
|
||||
* Matches `path` prop sent in the API to `ref_url` prop needed
|
||||
* in the commit component.
|
||||
*
|
||||
* @returns {Object|Undefined}
|
||||
*/
|
||||
commitRef() {
|
||||
if (this.pipeline.ref) {
|
||||
return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
|
||||
if (prop === 'path') {
|
||||
accumulator.ref_url = this.pipeline.ref[prop];
|
||||
} else {
|
||||
accumulator[prop] = this.pipeline.ref[prop];
|
||||
}
|
||||
return accumulator;
|
||||
}, {});
|
||||
}
|
||||
|
||||
<td>
|
||||
<commit-component
|
||||
:tag="commitTag"
|
||||
:commit-ref="commitRef"
|
||||
:commit-url="commitUrl"
|
||||
:short-sha="commitShortSha"
|
||||
:title="commitTitle"
|
||||
:author="commitAuthor"/>
|
||||
</td>
|
||||
return undefined;
|
||||
},
|
||||
|
||||
<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"/>
|
||||
</div>
|
||||
</td>
|
||||
/**
|
||||
* If provided, returns the commit url.
|
||||
* Needed to render the commit component column.
|
||||
*
|
||||
* @returns {String|Undefined}
|
||||
*/
|
||||
commitUrl() {
|
||||
if (this.pipeline.commit &&
|
||||
this.pipeline.commit.commit_path) {
|
||||
return this.pipeline.commit.commit_path;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
<time-ago :pipeline="pipeline"/>
|
||||
/**
|
||||
* If provided, returns the commit short sha.
|
||||
* Needed to render the commit component column.
|
||||
*
|
||||
* @returns {String|Undefined}
|
||||
*/
|
||||
commitShortSha() {
|
||||
if (this.pipeline.commit &&
|
||||
this.pipeline.commit.short_id) {
|
||||
return this.pipeline.commit.short_id;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
<pipeline-actions :pipeline="pipeline" />
|
||||
</tr>
|
||||
`,
|
||||
});
|
||||
})();
|
||||
/**
|
||||
* If provided, returns the commit title.
|
||||
* Needed to render the commit component column.
|
||||
*
|
||||
* @returns {String|Undefined}
|
||||
*/
|
||||
commitTitle() {
|
||||
if (this.pipeline.commit &&
|
||||
this.pipeline.commit.title) {
|
||||
return this.pipeline.commit.title;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
|
||||
template: `
|
||||
<tr class="commit">
|
||||
<status-scope :pipeline="pipeline"/>
|
||||
|
||||
<pipeline-url :pipeline="pipeline"></pipeline-url>
|
||||
|
||||
<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">
|
||||
<dropdown-stage :stage="stage"/>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<time-ago :pipeline="pipeline"/>
|
||||
|
||||
<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" />
|
||||
|
||||
<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>
|
||||
`,
|
||||
};
|
||||
|
|
|
@ -1,147 +1,135 @@
|
|||
/* global Vue, gl */
|
||||
/* eslint-disable no-param-reassign, no-plusplus */
|
||||
const PAGINATION_UI_BUTTON_LIMIT = 4;
|
||||
const UI_LIMIT = 6;
|
||||
const SPREAD = '...';
|
||||
const PREV = 'Prev';
|
||||
const NEXT = 'Next';
|
||||
const FIRST = '<< First';
|
||||
const LAST = 'Last >>';
|
||||
|
||||
window.Vue = require('vue');
|
||||
export default {
|
||||
props: {
|
||||
/**
|
||||
This function will take the information given by the pagination component
|
||||
|
||||
((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 >>';
|
||||
Here is an example `change` method:
|
||||
|
||||
gl.VueGlPagination = Vue.extend({
|
||||
props: {
|
||||
|
||||
// TODO: Consider refactoring in light of turbolinks removal.
|
||||
|
||||
/**
|
||||
This function will take the information given by the pagination component
|
||||
|
||||
Here is an example `change` method:
|
||||
|
||||
change(pagenum) {
|
||||
gl.utils.visitUrl(`?page=${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,
|
||||
change(pagenum) {
|
||||
gl.utils.visitUrl(`?page=${pagenum}`);
|
||||
},
|
||||
*/
|
||||
change: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
methods: {
|
||||
changePage(e) {
|
||||
const text = e.target.innerText;
|
||||
const { totalPages, nextPage, previousPage } = this.pageInfo;
|
||||
|
||||
switch (text) {
|
||||
case SPREAD:
|
||||
break;
|
||||
case LAST:
|
||||
this.change(totalPages);
|
||||
break;
|
||||
case NEXT:
|
||||
this.change(nextPage);
|
||||
break;
|
||||
case PREV:
|
||||
this.change(previousPage);
|
||||
break;
|
||||
case FIRST:
|
||||
this.change(1);
|
||||
break;
|
||||
default:
|
||||
this.change(+text);
|
||||
break;
|
||||
}
|
||||
},
|
||||
/**
|
||||
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,
|
||||
},
|
||||
computed: {
|
||||
prev() {
|
||||
return this.pageInfo.previousPage;
|
||||
},
|
||||
next() {
|
||||
return this.pageInfo.nextPage;
|
||||
},
|
||||
getItems() {
|
||||
const total = this.pageInfo.totalPages;
|
||||
const page = this.pageInfo.page;
|
||||
const items = [];
|
||||
},
|
||||
methods: {
|
||||
changePage(e) {
|
||||
const text = e.target.innerText;
|
||||
const { totalPages, nextPage, previousPage } = this.pageInfo;
|
||||
|
||||
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;
|
||||
},
|
||||
switch (text) {
|
||||
case SPREAD:
|
||||
break;
|
||||
case LAST:
|
||||
this.change(totalPages);
|
||||
break;
|
||||
case NEXT:
|
||||
this.change(nextPage);
|
||||
break;
|
||||
case PREV:
|
||||
this.change(previousPage);
|
||||
break;
|
||||
case FIRST:
|
||||
this.change(1);
|
||||
break;
|
||||
default:
|
||||
this.change(+text);
|
||||
break;
|
||||
}
|
||||
},
|
||||
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 = {}));
|
||||
},
|
||||
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 += 1) {
|
||||
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>
|
||||
`,
|
||||
};
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars,
|
||||
no-param-reassign, no-plusplus */
|
||||
/* global Vue */
|
||||
/* eslint-disable no-param-reassign, no-plusplus */
|
||||
import Vue from 'vue';
|
||||
import VueResource from 'vue-resource';
|
||||
|
||||
Vue.use(VueResource);
|
||||
|
||||
Vue.http.interceptors.push((request, next) => {
|
||||
Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
|
||||
|
||||
next((response) => {
|
||||
next(() => {
|
||||
Vue.activeResources--;
|
||||
});
|
||||
});
|
||||
|
|
|
@ -72,11 +72,6 @@
|
|||
color: $gl-text-color-secondary;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
svg,
|
||||
.fa {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
|
@ -921,3 +916,22 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Play button with icon in dropdowns
|
||||
*/
|
||||
.ci-table .no-btn {
|
||||
border: none;
|
||||
background: none;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
|
||||
.icon-play {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
margin-right: 5px;
|
||||
height: 13px;
|
||||
width: 12px;
|
||||
}
|
||||
}
|
||||
|
|
4
changelogs/unreleased/fl-remove-ujs-pipelines.yml
Normal file
4
changelogs/unreleased/fl-remove-ujs-pipelines.yml
Normal file
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: 'Removes UJS from pipelines tables'
|
||||
merge_request: 9929
|
||||
author:
|
|
@ -60,9 +60,6 @@ feature 'Merge request created from fork' do
|
|||
expect(page).to have_content pipeline.status
|
||||
expect(page).to have_content pipeline.id
|
||||
end
|
||||
|
||||
expect(page.find('a.btn-remove')[:href])
|
||||
.to include fork_project.path_with_namespace
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -99,15 +99,18 @@ describe 'Pipelines', :feature, :js do
|
|||
end
|
||||
|
||||
it 'indicates that pipeline can be canceled' do
|
||||
expect(page).to have_link('Cancel')
|
||||
expect(page).to have_selector('.js-pipelines-cancel-button')
|
||||
expect(page).to have_selector('.ci-running')
|
||||
end
|
||||
|
||||
context 'when canceling' do
|
||||
before { click_link('Cancel') }
|
||||
before do
|
||||
find('.js-pipelines-cancel-button').click
|
||||
wait_for_vue_resource
|
||||
end
|
||||
|
||||
it 'indicated that pipelines was canceled' do
|
||||
expect(page).not_to have_link('Cancel')
|
||||
expect(page).not_to have_selector('.js-pipelines-cancel-button')
|
||||
expect(page).to have_selector('.ci-canceled')
|
||||
end
|
||||
end
|
||||
|
@ -126,15 +129,18 @@ describe 'Pipelines', :feature, :js do
|
|||
end
|
||||
|
||||
it 'indicates that pipeline can be retried' do
|
||||
expect(page).to have_link('Retry')
|
||||
expect(page).to have_selector('.js-pipelines-retry-button')
|
||||
expect(page).to have_selector('.ci-failed')
|
||||
end
|
||||
|
||||
context 'when retrying' do
|
||||
before { click_link('Retry') }
|
||||
before do
|
||||
find('.js-pipelines-retry-button').click
|
||||
wait_for_vue_resource
|
||||
end
|
||||
|
||||
it 'shows running pipeline that is not retryable' do
|
||||
expect(page).not_to have_link('Retry')
|
||||
expect(page).not_to have_selector('.js-pipelines-retry-button')
|
||||
expect(page).to have_selector('.ci-running')
|
||||
end
|
||||
end
|
||||
|
@ -176,17 +182,17 @@ describe 'Pipelines', :feature, :js do
|
|||
it 'has link to the manual action' do
|
||||
find('.js-pipeline-dropdown-manual-actions').click
|
||||
|
||||
expect(page).to have_link('manual build')
|
||||
expect(page).to have_button('manual build')
|
||||
end
|
||||
|
||||
context 'when manual action was played' do
|
||||
before do
|
||||
find('.js-pipeline-dropdown-manual-actions').click
|
||||
click_link('manual build')
|
||||
click_button('manual build')
|
||||
end
|
||||
|
||||
it 'enqueues manual action job' do
|
||||
expect(manual.reload).to be_pending
|
||||
expect(page).to have_selector('.js-pipeline-dropdown-manual-actions:disabled')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -203,7 +209,7 @@ describe 'Pipelines', :feature, :js do
|
|||
before { visit_project_pipelines }
|
||||
|
||||
it 'is cancelable' do
|
||||
expect(page).to have_link('Cancel')
|
||||
expect(page).to have_selector('.js-pipelines-cancel-button')
|
||||
end
|
||||
|
||||
it 'has pipeline running' do
|
||||
|
@ -211,10 +217,10 @@ describe 'Pipelines', :feature, :js do
|
|||
end
|
||||
|
||||
context 'when canceling' do
|
||||
before { click_link('Cancel') }
|
||||
before { find('.js-pipelines-cancel-button').trigger('click') }
|
||||
|
||||
it 'indicates that pipeline was canceled' do
|
||||
expect(page).not_to have_link('Cancel')
|
||||
expect(page).not_to have_selector('.js-pipelines-cancel-button')
|
||||
expect(page).to have_selector('.ci-canceled')
|
||||
end
|
||||
end
|
||||
|
@ -233,7 +239,7 @@ describe 'Pipelines', :feature, :js do
|
|||
end
|
||||
|
||||
it 'is not retryable' do
|
||||
expect(page).not_to have_link('Retry')
|
||||
expect(page).not_to have_selector('.js-pipelines-retry-button')
|
||||
end
|
||||
|
||||
it 'has failed pipeline' do
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
/* eslint-disable no-unused-vars */
|
||||
const pipeline = {
|
||||
export default {
|
||||
id: 73,
|
||||
user: {
|
||||
name: 'Administrator',
|
||||
|
@ -88,5 +87,3 @@ const pipeline = {
|
|||
created_at: '2017-01-16T17:13:59.800Z',
|
||||
updated_at: '2017-01-25T00:00:17.132Z',
|
||||
};
|
||||
|
||||
module.exports = pipeline;
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
/* global pipeline, Vue */
|
||||
|
||||
require('~/flash');
|
||||
require('~/commit/pipelines/pipelines_store');
|
||||
require('~/commit/pipelines/pipelines_service');
|
||||
require('~/commit/pipelines/pipelines_table');
|
||||
require('~/vue_shared/vue_resource_interceptor');
|
||||
const pipeline = require('./mock_data');
|
||||
import Vue from 'vue';
|
||||
import PipelinesTable from '~/commit/pipelines/pipelines_table';
|
||||
import pipeline from './mock_data';
|
||||
|
||||
describe('Pipelines table in Commits and Merge requests', () => {
|
||||
preloadFixtures('static/pipelines_table.html.raw');
|
||||
|
@ -33,7 +28,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
|
|||
});
|
||||
|
||||
it('should render the empty state', (done) => {
|
||||
const component = new gl.commits.pipelines.PipelinesTableView({
|
||||
const component = new PipelinesTable({
|
||||
el: document.querySelector('#commit-pipeline-table-view'),
|
||||
});
|
||||
|
||||
|
@ -62,7 +57,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
|
|||
});
|
||||
|
||||
it('should render a table with the received pipelines', (done) => {
|
||||
const component = new gl.commits.pipelines.PipelinesTableView({
|
||||
const component = new PipelinesTable({
|
||||
el: document.querySelector('#commit-pipeline-table-view'),
|
||||
});
|
||||
|
||||
|
@ -92,7 +87,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
|
|||
});
|
||||
|
||||
it('should render empty state', (done) => {
|
||||
const component = new gl.commits.pipelines.PipelinesTableView({
|
||||
const component = new PipelinesTable({
|
||||
el: document.querySelector('#commit-pipeline-table-view'),
|
||||
});
|
||||
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
const PipelinesStore = require('~/commit/pipelines/pipelines_store');
|
||||
|
||||
describe('Store', () => {
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
store = new PipelinesStore();
|
||||
});
|
||||
|
||||
// unregister intervals and event handlers
|
||||
afterEach(() => gl.VueRealtimeListener.reset());
|
||||
|
||||
it('should start with a blank state', () => {
|
||||
expect(store.state.pipelines.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should store an array of pipelines', () => {
|
||||
const pipelines = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'pipeline',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'pipeline_2',
|
||||
},
|
||||
];
|
||||
|
||||
store.storePipelines(pipelines);
|
||||
|
||||
expect(store.state.pipelines.length).toBe(pipelines.length);
|
||||
});
|
||||
});
|
93
spec/javascripts/vue_pipelines_index/async_button_spec.js
Normal file
93
spec/javascripts/vue_pipelines_index/async_button_spec.js
Normal file
|
@ -0,0 +1,93 @@
|
|||
import Vue from 'vue';
|
||||
import asyncButtonComp from '~/vue_pipelines_index/components/async_button';
|
||||
|
||||
describe('Pipelines Async Button', () => {
|
||||
let component;
|
||||
let spy;
|
||||
let AsyncButtonComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
AsyncButtonComponent = Vue.extend(asyncButtonComp);
|
||||
|
||||
spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
|
||||
|
||||
component = new AsyncButtonComponent({
|
||||
propsData: {
|
||||
endpoint: '/foo',
|
||||
title: 'Foo',
|
||||
icon: 'fa fa-foo',
|
||||
cssClass: 'bar',
|
||||
service: {
|
||||
postAction: spy,
|
||||
},
|
||||
},
|
||||
}).$mount();
|
||||
});
|
||||
|
||||
it('should render a button', () => {
|
||||
expect(component.$el.tagName).toEqual('BUTTON');
|
||||
});
|
||||
|
||||
it('should render the provided icon', () => {
|
||||
expect(component.$el.querySelector('i').getAttribute('class')).toContain('fa fa-foo');
|
||||
});
|
||||
|
||||
it('should render the provided title', () => {
|
||||
expect(component.$el.getAttribute('title')).toContain('Foo');
|
||||
expect(component.$el.getAttribute('aria-label')).toContain('Foo');
|
||||
});
|
||||
|
||||
it('should render the provided cssClass', () => {
|
||||
expect(component.$el.getAttribute('class')).toContain('bar');
|
||||
});
|
||||
|
||||
it('should call the service when it is clicked with the provided endpoint', () => {
|
||||
component.$el.click();
|
||||
expect(spy).toHaveBeenCalledWith('/foo');
|
||||
});
|
||||
|
||||
it('should hide loading if request fails', () => {
|
||||
spy = jasmine.createSpy('spy').and.returnValue(Promise.reject());
|
||||
|
||||
component = new AsyncButtonComponent({
|
||||
propsData: {
|
||||
endpoint: '/foo',
|
||||
title: 'Foo',
|
||||
icon: 'fa fa-foo',
|
||||
cssClass: 'bar',
|
||||
dataAttributes: {
|
||||
'data-foo': 'foo',
|
||||
},
|
||||
service: {
|
||||
postAction: spy,
|
||||
},
|
||||
},
|
||||
}).$mount();
|
||||
|
||||
component.$el.click();
|
||||
expect(component.$el.querySelector('.fa-spinner')).toBe(null);
|
||||
});
|
||||
|
||||
describe('With confirm dialog', () => {
|
||||
it('should call the service when confimation is positive', () => {
|
||||
spyOn(window, 'confirm').and.returnValue(true);
|
||||
spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
|
||||
|
||||
component = new AsyncButtonComponent({
|
||||
propsData: {
|
||||
endpoint: '/foo',
|
||||
title: 'Foo',
|
||||
icon: 'fa fa-foo',
|
||||
cssClass: 'bar',
|
||||
service: {
|
||||
postAction: spy,
|
||||
},
|
||||
confirmActionMessage: 'bar',
|
||||
},
|
||||
}).$mount();
|
||||
|
||||
component.$el.click();
|
||||
expect(spy).toHaveBeenCalledWith('/foo');
|
||||
});
|
||||
});
|
||||
});
|
100
spec/javascripts/vue_pipelines_index/pipeline_url_spec.js
Normal file
100
spec/javascripts/vue_pipelines_index/pipeline_url_spec.js
Normal file
|
@ -0,0 +1,100 @@
|
|||
import Vue from 'vue';
|
||||
import pipelineUrlComp from '~/vue_pipelines_index/components/pipeline_url';
|
||||
|
||||
describe('Pipeline Url Component', () => {
|
||||
let PipelineUrlComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
PipelineUrlComponent = Vue.extend(pipelineUrlComp);
|
||||
});
|
||||
|
||||
it('should render a table cell', () => {
|
||||
const component = new PipelineUrlComponent({
|
||||
propsData: {
|
||||
pipeline: {
|
||||
id: 1,
|
||||
path: 'foo',
|
||||
flags: {},
|
||||
},
|
||||
},
|
||||
}).$mount();
|
||||
|
||||
expect(component.$el.tagName).toEqual('TD');
|
||||
});
|
||||
|
||||
it('should render a link the provided path and id', () => {
|
||||
const component = new PipelineUrlComponent({
|
||||
propsData: {
|
||||
pipeline: {
|
||||
id: 1,
|
||||
path: 'foo',
|
||||
flags: {},
|
||||
},
|
||||
},
|
||||
}).$mount();
|
||||
|
||||
expect(component.$el.querySelector('.js-pipeline-url-link').getAttribute('href')).toEqual('foo');
|
||||
expect(component.$el.querySelector('.js-pipeline-url-link span').textContent).toEqual('#1');
|
||||
});
|
||||
|
||||
it('should render user information when a user is provided', () => {
|
||||
const mockData = {
|
||||
pipeline: {
|
||||
id: 1,
|
||||
path: 'foo',
|
||||
flags: {},
|
||||
user: {
|
||||
web_url: '/',
|
||||
name: 'foo',
|
||||
avatar_url: '/',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const component = new PipelineUrlComponent({
|
||||
propsData: mockData,
|
||||
}).$mount();
|
||||
|
||||
const image = component.$el.querySelector('.js-pipeline-url-user img');
|
||||
|
||||
expect(
|
||||
component.$el.querySelector('.js-pipeline-url-user').getAttribute('href'),
|
||||
).toEqual(mockData.pipeline.user.web_url);
|
||||
expect(image.getAttribute('title')).toEqual(mockData.pipeline.user.name);
|
||||
expect(image.getAttribute('src')).toEqual(mockData.pipeline.user.avatar_url);
|
||||
});
|
||||
|
||||
it('should render "API" when no user is provided', () => {
|
||||
const component = new PipelineUrlComponent({
|
||||
propsData: {
|
||||
pipeline: {
|
||||
id: 1,
|
||||
path: 'foo',
|
||||
flags: {},
|
||||
},
|
||||
},
|
||||
}).$mount();
|
||||
|
||||
expect(component.$el.querySelector('.js-pipeline-url-api').textContent).toContain('API');
|
||||
});
|
||||
|
||||
it('should render latest, yaml invalid and stuck flags when provided', () => {
|
||||
const component = new PipelineUrlComponent({
|
||||
propsData: {
|
||||
pipeline: {
|
||||
id: 1,
|
||||
path: 'foo',
|
||||
flags: {
|
||||
latest: true,
|
||||
yaml_errors: true,
|
||||
stuck: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}).$mount();
|
||||
|
||||
expect(component.$el.querySelector('.js-pipeline-url-lastest').textContent).toContain('latest');
|
||||
expect(component.$el.querySelector('.js-pipeline-url-yaml').textContent).toContain('yaml invalid');
|
||||
expect(component.$el.querySelector('.js-pipeline-url-stuck').textContent).toContain('stuck');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,62 @@
|
|||
import Vue from 'vue';
|
||||
import pipelinesActionsComp from '~/vue_pipelines_index/components/pipelines_actions';
|
||||
|
||||
describe('Pipelines Actions dropdown', () => {
|
||||
let component;
|
||||
let spy;
|
||||
let actions;
|
||||
let ActionsComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
ActionsComponent = Vue.extend(pipelinesActionsComp);
|
||||
|
||||
actions = [
|
||||
{
|
||||
name: 'stop_review',
|
||||
path: '/root/review-app/builds/1893/play',
|
||||
},
|
||||
];
|
||||
|
||||
spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
|
||||
|
||||
component = new ActionsComponent({
|
||||
propsData: {
|
||||
actions,
|
||||
service: {
|
||||
postAction: spy,
|
||||
},
|
||||
},
|
||||
}).$mount();
|
||||
});
|
||||
|
||||
it('should render a dropdown with the provided actions', () => {
|
||||
expect(
|
||||
component.$el.querySelectorAll('.dropdown-menu li').length,
|
||||
).toEqual(actions.length);
|
||||
});
|
||||
|
||||
it('should call the service when an action is clicked', () => {
|
||||
component.$el.querySelector('.js-pipeline-dropdown-manual-actions').click();
|
||||
component.$el.querySelector('.js-pipeline-action-link').click();
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(actions[0].path);
|
||||
});
|
||||
|
||||
it('should hide loading if request fails', () => {
|
||||
spy = jasmine.createSpy('spy').and.returnValue(Promise.reject());
|
||||
|
||||
component = new ActionsComponent({
|
||||
propsData: {
|
||||
actions,
|
||||
service: {
|
||||
postAction: spy,
|
||||
},
|
||||
},
|
||||
}).$mount();
|
||||
|
||||
component.$el.querySelector('.js-pipeline-dropdown-manual-actions').click();
|
||||
component.$el.querySelector('.js-pipeline-action-link').click();
|
||||
|
||||
expect(component.$el.querySelector('.fa-spinner')).toEqual(null);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
import Vue from 'vue';
|
||||
import artifactsComp from '~/vue_pipelines_index/components/pipelines_artifacts';
|
||||
|
||||
describe('Pipelines Artifacts dropdown', () => {
|
||||
let component;
|
||||
let artifacts;
|
||||
|
||||
beforeEach(() => {
|
||||
const ArtifactsComponent = Vue.extend(artifactsComp);
|
||||
|
||||
artifacts = [
|
||||
{
|
||||
name: 'artifact',
|
||||
path: '/download/path',
|
||||
},
|
||||
];
|
||||
|
||||
component = new ArtifactsComponent({
|
||||
propsData: {
|
||||
artifacts,
|
||||
},
|
||||
}).$mount();
|
||||
});
|
||||
|
||||
it('should render a dropdown with the provided artifacts', () => {
|
||||
expect(
|
||||
component.$el.querySelectorAll('.dropdown-menu li').length,
|
||||
).toEqual(artifacts.length);
|
||||
});
|
||||
|
||||
it('should render a link with the provided path', () => {
|
||||
expect(
|
||||
component.$el.querySelector('.dropdown-menu li a').getAttribute('href'),
|
||||
).toEqual(artifacts[0].path);
|
||||
|
||||
expect(
|
||||
component.$el.querySelector('.dropdown-menu li a span').textContent,
|
||||
).toContain(artifacts[0].name);
|
||||
});
|
||||
});
|
72
spec/javascripts/vue_pipelines_index/pipelines_store_spec.js
Normal file
72
spec/javascripts/vue_pipelines_index/pipelines_store_spec.js
Normal file
|
@ -0,0 +1,72 @@
|
|||
import PipelineStore from '~/vue_pipelines_index/stores/pipelines_store';
|
||||
|
||||
describe('Pipelines Store', () => {
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
store = new PipelineStore();
|
||||
});
|
||||
|
||||
it('should be initialized with an empty state', () => {
|
||||
expect(store.state.pipelines).toEqual([]);
|
||||
expect(store.state.count).toEqual({});
|
||||
expect(store.state.pageInfo).toEqual({});
|
||||
});
|
||||
|
||||
describe('storePipelines', () => {
|
||||
it('should use the default parameter if none is provided', () => {
|
||||
store.storePipelines();
|
||||
expect(store.state.pipelines).toEqual([]);
|
||||
});
|
||||
|
||||
it('should store the provided array', () => {
|
||||
const array = [{ id: 1, status: 'running' }, { id: 2, status: 'success' }];
|
||||
store.storePipelines(array);
|
||||
expect(store.state.pipelines).toEqual(array);
|
||||
});
|
||||
});
|
||||
|
||||
describe('storeCount', () => {
|
||||
it('should use the default parameter if none is provided', () => {
|
||||
store.storeCount();
|
||||
expect(store.state.count).toEqual({});
|
||||
});
|
||||
|
||||
it('should store the provided count', () => {
|
||||
const count = { all: 20, finished: 10 };
|
||||
store.storeCount(count);
|
||||
|
||||
expect(store.state.count).toEqual(count);
|
||||
});
|
||||
});
|
||||
|
||||
describe('storePagination', () => {
|
||||
it('should use the default parameter if none is provided', () => {
|
||||
store.storePagination();
|
||||
expect(store.state.pageInfo).toEqual({});
|
||||
});
|
||||
|
||||
it('should store pagination information normalized and parsed', () => {
|
||||
const pagination = {
|
||||
'X-nExt-pAge': '2',
|
||||
'X-page': '1',
|
||||
'X-Per-Page': '1',
|
||||
'X-Prev-Page': '2',
|
||||
'X-TOTAL': '37',
|
||||
'X-Total-Pages': '2',
|
||||
};
|
||||
|
||||
const expectedResult = {
|
||||
perPage: 1,
|
||||
page: 1,
|
||||
total: 37,
|
||||
totalPages: 2,
|
||||
nextPage: 2,
|
||||
previousPage: 2,
|
||||
};
|
||||
|
||||
store.storePagination(pagination);
|
||||
expect(store.state.pageInfo).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,13 +1,17 @@
|
|||
require('~/vue_shared/components/commit');
|
||||
import Vue from 'vue';
|
||||
import commitComp from '~/vue_shared/components/commit';
|
||||
|
||||
describe('Commit component', () => {
|
||||
let props;
|
||||
let component;
|
||||
let CommitComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
CommitComponent = Vue.extend(commitComp);
|
||||
});
|
||||
|
||||
it('should render a code-fork icon if it does not represent a tag', () => {
|
||||
setFixtures('<div class="test-commit-container"></div>');
|
||||
component = new window.gl.CommitComponent({
|
||||
el: document.querySelector('.test-commit-container'),
|
||||
component = new CommitComponent({
|
||||
propsData: {
|
||||
tag: false,
|
||||
commitRef: {
|
||||
|
@ -23,15 +27,13 @@ describe('Commit component', () => {
|
|||
username: 'jschatz1',
|
||||
},
|
||||
},
|
||||
});
|
||||
}).$mount();
|
||||
|
||||
expect(component.$el.querySelector('.icon-container i').classList).toContain('fa-code-fork');
|
||||
});
|
||||
|
||||
describe('Given all the props', () => {
|
||||
beforeEach(() => {
|
||||
setFixtures('<div class="test-commit-container"></div>');
|
||||
|
||||
props = {
|
||||
tag: true,
|
||||
commitRef: {
|
||||
|
@ -49,10 +51,9 @@ describe('Commit component', () => {
|
|||
commitIconSvg: '<svg></svg>',
|
||||
};
|
||||
|
||||
component = new window.gl.CommitComponent({
|
||||
el: document.querySelector('.test-commit-container'),
|
||||
component = new CommitComponent({
|
||||
propsData: props,
|
||||
});
|
||||
}).$mount();
|
||||
});
|
||||
|
||||
it('should render a tag icon if it represents a tag', () => {
|
||||
|
@ -105,7 +106,6 @@ describe('Commit component', () => {
|
|||
|
||||
describe('When commit title is not provided', () => {
|
||||
it('should render default message', () => {
|
||||
setFixtures('<div class="test-commit-container"></div>');
|
||||
props = {
|
||||
tag: false,
|
||||
commitRef: {
|
||||
|
@ -118,10 +118,9 @@ describe('Commit component', () => {
|
|||
author: {},
|
||||
};
|
||||
|
||||
component = new window.gl.CommitComponent({
|
||||
el: document.querySelector('.test-commit-container'),
|
||||
component = new CommitComponent({
|
||||
propsData: props,
|
||||
});
|
||||
}).$mount();
|
||||
|
||||
expect(
|
||||
component.$el.querySelector('.commit-title span').textContent,
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
require('~/vue_shared/components/pipelines_table_row');
|
||||
const pipeline = require('../../commit/pipelines/mock_data');
|
||||
import Vue from 'vue';
|
||||
import tableRowComp from '~/vue_shared/components/pipelines_table_row';
|
||||
import pipeline from '../../commit/pipelines/mock_data';
|
||||
|
||||
describe('Pipelines Table Row', () => {
|
||||
let component;
|
||||
preloadFixtures('static/environments/element.html.raw');
|
||||
|
||||
beforeEach(() => {
|
||||
loadFixtures('static/environments/element.html.raw');
|
||||
const PipelinesTableRowComponent = Vue.extend(tableRowComp);
|
||||
|
||||
component = new gl.pipelines.PipelinesTableRowComponent({
|
||||
component = new PipelinesTableRowComponent({
|
||||
el: document.querySelector('.test-dom-element'),
|
||||
propsData: {
|
||||
pipeline,
|
||||
svgs: {},
|
||||
service: {},
|
||||
},
|
||||
});
|
||||
}).$mount();
|
||||
});
|
||||
|
||||
it('should render a table row', () => {
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
require('~/vue_shared/components/pipelines_table');
|
||||
require('~/lib/utils/datetime_utility');
|
||||
const pipeline = require('../../commit/pipelines/mock_data');
|
||||
import Vue from 'vue';
|
||||
import pipelinesTableComp from '~/vue_shared/components/pipelines_table';
|
||||
import '~/lib/utils/datetime_utility';
|
||||
import pipeline from '../../commit/pipelines/mock_data';
|
||||
|
||||
describe('Pipelines Table', () => {
|
||||
preloadFixtures('static/environments/element.html.raw');
|
||||
let PipelinesTableComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
loadFixtures('static/environments/element.html.raw');
|
||||
PipelinesTableComponent = Vue.extend(pipelinesTableComp);
|
||||
});
|
||||
|
||||
describe('table', () => {
|
||||
let component;
|
||||
beforeEach(() => {
|
||||
component = new gl.pipelines.PipelinesTableComponent({
|
||||
el: document.querySelector('.test-dom-element'),
|
||||
component = new PipelinesTableComponent({
|
||||
propsData: {
|
||||
pipelines: [],
|
||||
svgs: {},
|
||||
service: {},
|
||||
},
|
||||
});
|
||||
}).$mount();
|
||||
});
|
||||
|
||||
it('should render a table', () => {
|
||||
|
@ -37,26 +37,25 @@ describe('Pipelines Table', () => {
|
|||
|
||||
describe('without data', () => {
|
||||
it('should render an empty table', () => {
|
||||
const component = new gl.pipelines.PipelinesTableComponent({
|
||||
el: document.querySelector('.test-dom-element'),
|
||||
const component = new PipelinesTableComponent({
|
||||
propsData: {
|
||||
pipelines: [],
|
||||
svgs: {},
|
||||
service: {},
|
||||
},
|
||||
});
|
||||
}).$mount();
|
||||
expect(component.$el.querySelectorAll('tbody tr').length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with data', () => {
|
||||
it('should render rows', () => {
|
||||
const component = new gl.pipelines.PipelinesTableComponent({
|
||||
const component = new PipelinesTableComponent({
|
||||
el: document.querySelector('.test-dom-element'),
|
||||
propsData: {
|
||||
pipelines: [pipeline],
|
||||
svgs: {},
|
||||
service: {},
|
||||
},
|
||||
});
|
||||
}).$mount();
|
||||
|
||||
expect(component.$el.querySelectorAll('tbody tr').length).toEqual(1);
|
||||
});
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
require('~/lib/utils/common_utils');
|
||||
require('~/vue_shared/components/table_pagination');
|
||||
import Vue from 'vue';
|
||||
import paginationComp from '~/vue_shared/components/table_pagination';
|
||||
import '~/lib/utils/common_utils';
|
||||
|
||||
describe('Pagination component', () => {
|
||||
let component;
|
||||
let PaginationComponent;
|
||||
|
||||
const changeChanges = {
|
||||
one: '',
|
||||
|
@ -12,11 +14,12 @@ describe('Pagination component', () => {
|
|||
changeChanges.one = one;
|
||||
};
|
||||
|
||||
it('should render and start at page 1', () => {
|
||||
setFixtures('<div class="test-pagination-container"></div>');
|
||||
beforeEach(() => {
|
||||
PaginationComponent = Vue.extend(paginationComp);
|
||||
});
|
||||
|
||||
component = new window.gl.VueGlPagination({
|
||||
el: document.querySelector('.test-pagination-container'),
|
||||
it('should render and start at page 1', () => {
|
||||
component = new PaginationComponent({
|
||||
propsData: {
|
||||
pageInfo: {
|
||||
totalPages: 10,
|
||||
|
@ -25,7 +28,7 @@ describe('Pagination component', () => {
|
|||
},
|
||||
change,
|
||||
},
|
||||
});
|
||||
}).$mount();
|
||||
|
||||
expect(component.$el.classList).toContain('gl-pagination');
|
||||
|
||||
|
@ -35,10 +38,7 @@ describe('Pagination component', () => {
|
|||
});
|
||||
|
||||
it('should go to the previous page', () => {
|
||||
setFixtures('<div class="test-pagination-container"></div>');
|
||||
|
||||
component = new window.gl.VueGlPagination({
|
||||
el: document.querySelector('.test-pagination-container'),
|
||||
component = new PaginationComponent({
|
||||
propsData: {
|
||||
pageInfo: {
|
||||
totalPages: 10,
|
||||
|
@ -47,7 +47,7 @@ describe('Pagination component', () => {
|
|||
},
|
||||
change,
|
||||
},
|
||||
});
|
||||
}).$mount();
|
||||
|
||||
component.changePage({ target: { innerText: 'Prev' } });
|
||||
|
||||
|
@ -55,10 +55,7 @@ describe('Pagination component', () => {
|
|||
});
|
||||
|
||||
it('should go to the next page', () => {
|
||||
setFixtures('<div class="test-pagination-container"></div>');
|
||||
|
||||
component = new window.gl.VueGlPagination({
|
||||
el: document.querySelector('.test-pagination-container'),
|
||||
component = new PaginationComponent({
|
||||
propsData: {
|
||||
pageInfo: {
|
||||
totalPages: 10,
|
||||
|
@ -67,7 +64,7 @@ describe('Pagination component', () => {
|
|||
},
|
||||
change,
|
||||
},
|
||||
});
|
||||
}).$mount();
|
||||
|
||||
component.changePage({ target: { innerText: 'Next' } });
|
||||
|
||||
|
@ -75,10 +72,7 @@ describe('Pagination component', () => {
|
|||
});
|
||||
|
||||
it('should go to the last page', () => {
|
||||
setFixtures('<div class="test-pagination-container"></div>');
|
||||
|
||||
component = new window.gl.VueGlPagination({
|
||||
el: document.querySelector('.test-pagination-container'),
|
||||
component = new PaginationComponent({
|
||||
propsData: {
|
||||
pageInfo: {
|
||||
totalPages: 10,
|
||||
|
@ -87,7 +81,7 @@ describe('Pagination component', () => {
|
|||
},
|
||||
change,
|
||||
},
|
||||
});
|
||||
}).$mount();
|
||||
|
||||
component.changePage({ target: { innerText: 'Last >>' } });
|
||||
|
||||
|
@ -95,10 +89,7 @@ describe('Pagination component', () => {
|
|||
});
|
||||
|
||||
it('should go to the first page', () => {
|
||||
setFixtures('<div class="test-pagination-container"></div>');
|
||||
|
||||
component = new window.gl.VueGlPagination({
|
||||
el: document.querySelector('.test-pagination-container'),
|
||||
component = new PaginationComponent({
|
||||
propsData: {
|
||||
pageInfo: {
|
||||
totalPages: 10,
|
||||
|
@ -107,7 +98,7 @@ describe('Pagination component', () => {
|
|||
},
|
||||
change,
|
||||
},
|
||||
});
|
||||
}).$mount();
|
||||
|
||||
component.changePage({ target: { innerText: '<< First' } });
|
||||
|
||||
|
@ -115,10 +106,7 @@ describe('Pagination component', () => {
|
|||
});
|
||||
|
||||
it('should do nothing', () => {
|
||||
setFixtures('<div class="test-pagination-container"></div>');
|
||||
|
||||
component = new window.gl.VueGlPagination({
|
||||
el: document.querySelector('.test-pagination-container'),
|
||||
component = new PaginationComponent({
|
||||
propsData: {
|
||||
pageInfo: {
|
||||
totalPages: 10,
|
||||
|
@ -127,7 +115,7 @@ describe('Pagination component', () => {
|
|||
},
|
||||
change,
|
||||
},
|
||||
});
|
||||
}).$mount();
|
||||
|
||||
component.changePage({ target: { innerText: '...' } });
|
||||
|
||||
|
|
Loading…
Reference in a new issue