Single commit squash of all changes for https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10878
It's needed due to https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10777 being merged with squash.
This commit is contained in:
parent
c17e6a6c68
commit
aa440eb1c0
|
@ -1,34 +0,0 @@
|
|||
import CANCELED_SVG from 'icons/_icon_status_canceled_borderless.svg';
|
||||
import CREATED_SVG from 'icons/_icon_status_created_borderless.svg';
|
||||
import FAILED_SVG from 'icons/_icon_status_failed_borderless.svg';
|
||||
import MANUAL_SVG from 'icons/_icon_status_manual_borderless.svg';
|
||||
import PENDING_SVG from 'icons/_icon_status_pending_borderless.svg';
|
||||
import RUNNING_SVG from 'icons/_icon_status_running_borderless.svg';
|
||||
import SKIPPED_SVG from 'icons/_icon_status_skipped_borderless.svg';
|
||||
import SUCCESS_SVG from 'icons/_icon_status_success_borderless.svg';
|
||||
import WARNING_SVG from 'icons/_icon_status_warning_borderless.svg';
|
||||
|
||||
const StatusIconEntityMap = {
|
||||
icon_status_canceled: CANCELED_SVG,
|
||||
icon_status_created: CREATED_SVG,
|
||||
icon_status_failed: FAILED_SVG,
|
||||
icon_status_manual: MANUAL_SVG,
|
||||
icon_status_pending: PENDING_SVG,
|
||||
icon_status_running: RUNNING_SVG,
|
||||
icon_status_skipped: SKIPPED_SVG,
|
||||
icon_status_success: SUCCESS_SVG,
|
||||
icon_status_warning: WARNING_SVG,
|
||||
};
|
||||
|
||||
export {
|
||||
CANCELED_SVG,
|
||||
CREATED_SVG,
|
||||
FAILED_SVG,
|
||||
MANUAL_SVG,
|
||||
PENDING_SVG,
|
||||
RUNNING_SVG,
|
||||
SKIPPED_SVG,
|
||||
SUCCESS_SVG,
|
||||
WARNING_SVG,
|
||||
StatusIconEntityMap as default,
|
||||
};
|
|
@ -49,6 +49,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
|
|||
import UserCallout from './user_callout';
|
||||
import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags';
|
||||
import ShortcutsWiki from './shortcuts_wiki';
|
||||
import Pipelines from './pipelines';
|
||||
import BlobViewer from './blob/viewer/index';
|
||||
import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select';
|
||||
|
||||
|
@ -257,7 +258,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
|
|||
const { controllerAction } = document.querySelector('.js-pipeline-container').dataset;
|
||||
const pipelineStatusUrl = `${document.querySelector('.js-pipeline-tab-link a').getAttribute('href')}/status.json`;
|
||||
|
||||
new gl.Pipelines({
|
||||
new Pipelines({
|
||||
initTabs: true,
|
||||
pipelineStatusUrl,
|
||||
tabsOptions: {
|
||||
|
|
|
@ -31,82 +31,78 @@
|
|||
*
|
||||
* ### How to use
|
||||
*
|
||||
* new window.gl.LinkedTabs({
|
||||
* new LinkedTabs({
|
||||
* action: "#{controller.action_name}",
|
||||
* defaultAction: 'tab1',
|
||||
* parentEl: '.tab-links'
|
||||
* });
|
||||
*/
|
||||
|
||||
(() => {
|
||||
window.gl = window.gl || {};
|
||||
export default class LinkedTabs {
|
||||
/**
|
||||
* Binds the events and activates de default tab.
|
||||
*
|
||||
* @param {Object} options
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
this.options = options;
|
||||
|
||||
window.gl.LinkedTabs = class LinkedTabs {
|
||||
/**
|
||||
* Binds the events and activates de default tab.
|
||||
*
|
||||
* @param {Object} options
|
||||
*/
|
||||
constructor(options) {
|
||||
this.options = options || {};
|
||||
this.defaultAction = this.options.defaultAction;
|
||||
this.action = this.options.action || this.defaultAction;
|
||||
|
||||
this.defaultAction = this.options.defaultAction;
|
||||
this.action = this.options.action || this.defaultAction;
|
||||
|
||||
if (this.action === 'show') {
|
||||
this.action = this.defaultAction;
|
||||
}
|
||||
|
||||
this.currentLocation = window.location;
|
||||
|
||||
const tabSelector = `${this.options.parentEl} a[data-toggle="tab"]`;
|
||||
|
||||
// since this is a custom event we need jQuery :(
|
||||
$(document)
|
||||
.off('shown.bs.tab', tabSelector)
|
||||
.on('shown.bs.tab', tabSelector, e => this.tabShown(e));
|
||||
|
||||
this.activateTab(this.action);
|
||||
if (this.action === 'show') {
|
||||
this.action = this.defaultAction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the `shown.bs.tab` event to set the currect url action.
|
||||
*
|
||||
* @param {type} evt
|
||||
* @return {Function}
|
||||
*/
|
||||
tabShown(evt) {
|
||||
const source = evt.target.getAttribute('href');
|
||||
this.currentLocation = window.location;
|
||||
|
||||
return this.setCurrentAction(source);
|
||||
}
|
||||
const tabSelector = `${this.options.parentEl} a[data-toggle="tab"]`;
|
||||
|
||||
/**
|
||||
* Updates the URL with the path that matched the given action.
|
||||
*
|
||||
* @param {String} source
|
||||
* @return {String}
|
||||
*/
|
||||
setCurrentAction(source) {
|
||||
const copySource = source;
|
||||
// since this is a custom event we need jQuery :(
|
||||
$(document)
|
||||
.off('shown.bs.tab', tabSelector)
|
||||
.on('shown.bs.tab', tabSelector, e => this.tabShown(e));
|
||||
|
||||
copySource.replace(/\/+$/, '');
|
||||
this.activateTab(this.action);
|
||||
}
|
||||
|
||||
const newState = `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`;
|
||||
/**
|
||||
* Handles the `shown.bs.tab` event to set the currect url action.
|
||||
*
|
||||
* @param {type} evt
|
||||
* @return {Function}
|
||||
*/
|
||||
tabShown(evt) {
|
||||
const source = evt.target.getAttribute('href');
|
||||
|
||||
history.replaceState({
|
||||
url: newState,
|
||||
}, document.title, newState);
|
||||
return newState;
|
||||
}
|
||||
return this.setCurrentAction(source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the current action activates the correct tab.
|
||||
* http://getbootstrap.com/javascript/#tab-show
|
||||
* Note: Will trigger `shown.bs.tab`
|
||||
*/
|
||||
activateTab() {
|
||||
return $(`${this.options.parentEl} a[data-action='${this.action}']`).tab('show');
|
||||
}
|
||||
};
|
||||
})();
|
||||
/**
|
||||
* Updates the URL with the path that matched the given action.
|
||||
*
|
||||
* @param {String} source
|
||||
* @return {String}
|
||||
*/
|
||||
setCurrentAction(source) {
|
||||
const copySource = source;
|
||||
|
||||
copySource.replace(/\/+$/, '');
|
||||
|
||||
const newState = `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`;
|
||||
|
||||
history.replaceState({
|
||||
url: newState,
|
||||
}, document.title, newState);
|
||||
return newState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the current action activates the correct tab.
|
||||
* http://getbootstrap.com/javascript/#tab-show
|
||||
* Note: Will trigger `shown.bs.tab`
|
||||
*/
|
||||
activateTab() {
|
||||
return $(`${this.options.parentEl} a[data-action='${this.action}']`).tab('show');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,42 +1,14 @@
|
|||
/* eslint-disable no-new, guard-for-in, no-restricted-syntax, no-continue, no-param-reassign, max-len */
|
||||
import LinkedTabs from './lib/utils/bootstrap_linked_tabs';
|
||||
|
||||
require('./lib/utils/bootstrap_linked_tabs');
|
||||
|
||||
((global) => {
|
||||
class Pipelines {
|
||||
constructor(options = {}) {
|
||||
if (options.initTabs && options.tabsOptions) {
|
||||
new global.LinkedTabs(options.tabsOptions);
|
||||
}
|
||||
|
||||
if (options.pipelineStatusUrl) {
|
||||
gl.utils.setCiStatusFavicon(options.pipelineStatusUrl);
|
||||
}
|
||||
|
||||
this.addMarginToBuildColumns();
|
||||
export default class Pipelines {
|
||||
constructor(options = {}) {
|
||||
if (options.initTabs && options.tabsOptions) {
|
||||
// eslint-disable-next-line no-new
|
||||
new LinkedTabs(options.tabsOptions);
|
||||
}
|
||||
|
||||
addMarginToBuildColumns() {
|
||||
this.pipelineGraph = document.querySelector('.js-pipeline-graph');
|
||||
|
||||
const secondChildBuildNodes = this.pipelineGraph.querySelectorAll('.build:nth-child(2)');
|
||||
|
||||
for (const buildNodeIndex in secondChildBuildNodes) {
|
||||
const buildNode = secondChildBuildNodes[buildNodeIndex];
|
||||
const firstChildBuildNode = buildNode.previousElementSibling;
|
||||
if (!firstChildBuildNode || !firstChildBuildNode.matches('.build')) continue;
|
||||
const multiBuildColumn = buildNode.closest('.stage-column');
|
||||
const previousColumn = multiBuildColumn.previousElementSibling;
|
||||
if (!previousColumn || !previousColumn.matches('.stage-column')) continue;
|
||||
multiBuildColumn.classList.add('left-margin');
|
||||
firstChildBuildNode.classList.add('left-connector');
|
||||
const columnBuilds = previousColumn.querySelectorAll('.build');
|
||||
if (columnBuilds.length === 1) previousColumn.classList.add('no-margin');
|
||||
}
|
||||
|
||||
this.pipelineGraph.classList.remove('hidden');
|
||||
if (options.pipelineStatusUrl) {
|
||||
gl.utils.setCiStatusFavicon(options.pipelineStatusUrl);
|
||||
}
|
||||
}
|
||||
|
||||
global.Pipelines = Pipelines;
|
||||
})(window.gl || (window.gl = {}));
|
||||
}
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
<script>
|
||||
import getActionIcon from '../../../vue_shared/ci_action_icons';
|
||||
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
|
||||
|
||||
/**
|
||||
* Renders either a cancel, retry or play icon pointing to the given path.
|
||||
* TODO: Remove UJS from here and use an async request instead.
|
||||
*/
|
||||
export default {
|
||||
props: {
|
||||
tooltipText: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
link: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
actionMethod: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
actionIcon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
mixins: [
|
||||
tooltipMixin,
|
||||
],
|
||||
|
||||
computed: {
|
||||
actionIconSvg() {
|
||||
return getActionIcon(this.actionIcon);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<a
|
||||
:data-method="actionMethod"
|
||||
:title="tooltipText"
|
||||
:href="link"
|
||||
ref="tooltip"
|
||||
class="ci-action-icon-container"
|
||||
data-toggle="tooltip"
|
||||
data-container="body">
|
||||
|
||||
<i
|
||||
class="ci-action-icon-wrapper"
|
||||
v-html="actionIconSvg"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</a>
|
||||
</template>
|
|
@ -0,0 +1,56 @@
|
|||
<script>
|
||||
import getActionIcon from '../../../vue_shared/ci_action_icons';
|
||||
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
|
||||
|
||||
/**
|
||||
* Renders either a cancel, retry or play icon pointing to the given path.
|
||||
* TODO: Remove UJS from here and use an async request instead.
|
||||
*/
|
||||
export default {
|
||||
props: {
|
||||
tooltipText: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
link: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
actionMethod: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
actionIcon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
mixins: [
|
||||
tooltipMixin,
|
||||
],
|
||||
|
||||
computed: {
|
||||
actionIconSvg() {
|
||||
return getActionIcon(this.actionIcon);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<a
|
||||
:data-method="actionMethod"
|
||||
:title="tooltipText"
|
||||
:href="link"
|
||||
ref="tooltip"
|
||||
rel="nofollow"
|
||||
class="ci-action-icon-wrapper js-ci-status-icon"
|
||||
data-toggle="tooltip"
|
||||
data-container="body"
|
||||
v-html="actionIconSvg"
|
||||
aria-label="Job's action">
|
||||
</a>
|
||||
</template>
|
|
@ -0,0 +1,86 @@
|
|||
<script>
|
||||
import jobNameComponent from './job_name_component.vue';
|
||||
import jobComponent from './job_component.vue';
|
||||
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
|
||||
|
||||
/**
|
||||
* Renders the dropdown for the pipeline graph.
|
||||
*
|
||||
* The following object should be provided as `job`:
|
||||
*
|
||||
* {
|
||||
* "id": 4256,
|
||||
* "name": "test",
|
||||
* "status": {
|
||||
* "icon": "icon_status_success",
|
||||
* "text": "passed",
|
||||
* "label": "passed",
|
||||
* "group": "success",
|
||||
* "details_path": "/root/ci-mock/builds/4256",
|
||||
* "action": {
|
||||
* "icon": "icon_action_retry",
|
||||
* "title": "Retry",
|
||||
* "path": "/root/ci-mock/builds/4256/retry",
|
||||
* "method": "post"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export default {
|
||||
props: {
|
||||
job: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
mixins: [
|
||||
tooltipMixin,
|
||||
],
|
||||
|
||||
components: {
|
||||
jobComponent,
|
||||
jobNameComponent,
|
||||
},
|
||||
|
||||
computed: {
|
||||
tooltipText() {
|
||||
return `${this.job.name} - ${this.job.status.label}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
data-toggle="dropdown"
|
||||
data-container="body"
|
||||
class="dropdown-menu-toggle build-content"
|
||||
:title="tooltipText"
|
||||
ref="tooltip">
|
||||
|
||||
<job-name-component
|
||||
:name="job.name"
|
||||
:status="job.status" />
|
||||
|
||||
<span class="dropdown-counter-badge">
|
||||
{{job.size}}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<ul class="dropdown-menu big-pipeline-graph-dropdown-menu js-grouped-pipeline-dropdown">
|
||||
<li class="scrollable-menu">
|
||||
<ul>
|
||||
<li v-for="item in job.jobs">
|
||||
<job-component
|
||||
:job="item"
|
||||
:is-dropdown="true"
|
||||
css-class-job-name="mini-pipeline-graph-dropdown-item"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,92 @@
|
|||
<script>
|
||||
/* global Flash */
|
||||
import Visibility from 'visibilityjs';
|
||||
import Poll from '../../../lib/utils/poll';
|
||||
import PipelineService from '../../services/pipeline_service';
|
||||
import PipelineStore from '../../stores/pipeline_store';
|
||||
import stageColumnComponent from './stage_column_component.vue';
|
||||
import '../../../flash';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
stageColumnComponent,
|
||||
},
|
||||
|
||||
data() {
|
||||
const DOMdata = document.getElementById('js-pipeline-graph-vue').dataset;
|
||||
const store = new PipelineStore();
|
||||
|
||||
return {
|
||||
isLoading: false,
|
||||
endpoint: DOMdata.endpoint,
|
||||
store,
|
||||
state: store.state,
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.service = new PipelineService(this.endpoint);
|
||||
|
||||
const poll = new Poll({
|
||||
resource: this.service,
|
||||
method: 'getPipeline',
|
||||
successCallback: this.successCallback,
|
||||
errorCallback: this.errorCallback,
|
||||
});
|
||||
|
||||
if (!Visibility.hidden()) {
|
||||
this.isLoading = true;
|
||||
poll.makeRequest();
|
||||
}
|
||||
|
||||
Visibility.change(() => {
|
||||
if (!Visibility.hidden()) {
|
||||
poll.restart();
|
||||
} else {
|
||||
poll.stop();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
successCallback(response) {
|
||||
const data = response.json();
|
||||
|
||||
this.isLoading = false;
|
||||
this.store.storeGraph(data.details.stages);
|
||||
},
|
||||
|
||||
errorCallback() {
|
||||
this.isLoading = false;
|
||||
return new Flash('An error occurred while fetching the pipeline.');
|
||||
},
|
||||
|
||||
capitalizeStageName(name) {
|
||||
return name.charAt(0).toUpperCase() + name.slice(1);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="build-content middle-block js-pipeline-graph">
|
||||
<div class="pipeline-visualization pipeline-graph">
|
||||
<div class="text-center">
|
||||
<i
|
||||
v-if="isLoading"
|
||||
class="loading-icon fa fa-spin fa-spinner fa-3x"
|
||||
aria-label="Loading"
|
||||
aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
<ul
|
||||
v-if="!isLoading"
|
||||
class="stage-column-list">
|
||||
<stage-column-component
|
||||
v-for="stage in state.graph"
|
||||
:title="capitalizeStageName(stage.name)"
|
||||
:jobs="stage.groups"
|
||||
:key="stage.name"/>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,124 @@
|
|||
<script>
|
||||
import actionComponent from './action_component.vue';
|
||||
import dropdownActionComponent from './dropdown_action_component.vue';
|
||||
import jobNameComponent from './job_name_component.vue';
|
||||
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
|
||||
|
||||
/**
|
||||
* Renders the badge for the pipeline graph and the job's dropdown.
|
||||
*
|
||||
* The following object should be provided as `job`:
|
||||
*
|
||||
* {
|
||||
* "id": 4256,
|
||||
* "name": "test",
|
||||
* "status": {
|
||||
* "icon": "icon_status_success",
|
||||
* "text": "passed",
|
||||
* "label": "passed",
|
||||
* "group": "success",
|
||||
* "details_path": "/root/ci-mock/builds/4256",
|
||||
* "action": {
|
||||
* "icon": "icon_action_retry",
|
||||
* "title": "Retry",
|
||||
* "path": "/root/ci-mock/builds/4256/retry",
|
||||
* "method": "post"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
export default {
|
||||
props: {
|
||||
job: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
cssClassJobName: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
|
||||
isDropdown: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
components: {
|
||||
actionComponent,
|
||||
dropdownActionComponent,
|
||||
jobNameComponent,
|
||||
},
|
||||
|
||||
mixins: [
|
||||
tooltipMixin,
|
||||
],
|
||||
|
||||
computed: {
|
||||
tooltipText() {
|
||||
return `${this.job.name} - ${this.job.status.label}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Verifies if the provided job has an action path
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
hasAction() {
|
||||
return this.job.status && this.job.status.action && this.job.status.action.path;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<a
|
||||
v-if="job.status.details_path"
|
||||
:href="job.status.details_path"
|
||||
:title="tooltipText"
|
||||
:class="cssClassJobName"
|
||||
ref="tooltip"
|
||||
data-toggle="tooltip"
|
||||
data-container="body">
|
||||
|
||||
<job-name-component
|
||||
:name="job.name"
|
||||
:status="job.status"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<div
|
||||
v-else
|
||||
:title="tooltipText"
|
||||
:class="cssClassJobName"
|
||||
ref="tooltip"
|
||||
data-toggle="tooltip"
|
||||
data-container="body">
|
||||
|
||||
<job-name-component
|
||||
:name="job.name"
|
||||
:status="job.status"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<action-component
|
||||
v-if="hasAction && !isDropdown"
|
||||
:tooltip-text="job.status.action.title"
|
||||
:link="job.status.action.path"
|
||||
:action-icon="job.status.action.icon"
|
||||
:action-method="job.status.action.method"
|
||||
/>
|
||||
|
||||
<dropdown-action-component
|
||||
v-if="hasAction && isDropdown"
|
||||
:tooltip-text="job.status.action.title"
|
||||
:link="job.status.action.path"
|
||||
:action-icon="job.status.action.icon"
|
||||
:action-method="job.status.action.method"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,37 @@
|
|||
<script>
|
||||
import ciIcon from '../../../vue_shared/components/ci_icon.vue';
|
||||
|
||||
/**
|
||||
* Component that renders both the CI icon status and the job name.
|
||||
* Used in
|
||||
* - Badge component
|
||||
* - Dropdown badge components
|
||||
*/
|
||||
export default {
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
status: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
components: {
|
||||
ciIcon,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<span>
|
||||
<ci-icon
|
||||
:status="status" />
|
||||
|
||||
<span class="ci-status-text">
|
||||
{{name}}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
|
@ -0,0 +1,64 @@
|
|||
<script>
|
||||
import jobComponent from './job_component.vue';
|
||||
import dropdownJobComponent from './dropdown_job_component.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
jobs: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
components: {
|
||||
jobComponent,
|
||||
dropdownJobComponent,
|
||||
},
|
||||
|
||||
methods: {
|
||||
firstJob(list) {
|
||||
return list[0];
|
||||
},
|
||||
|
||||
jobId(job) {
|
||||
return `ci-badge-${job.name}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<li class="stage-column">
|
||||
<div class="stage-name">
|
||||
{{title}}
|
||||
</div>
|
||||
<div class="builds-container">
|
||||
<ul>
|
||||
<li
|
||||
v-for="job in jobs"
|
||||
:key="job.id"
|
||||
class="build"
|
||||
:id="jobId(job)">
|
||||
|
||||
<div class="curve"></div>
|
||||
|
||||
<job-component
|
||||
v-if="job.size === 1"
|
||||
:job="job"
|
||||
css-class-job-name="build-content"
|
||||
/>
|
||||
|
||||
<dropdown-job-component
|
||||
v-if="job.size > 1"
|
||||
:job="job"
|
||||
/>
|
||||
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
|
@ -14,7 +14,7 @@
|
|||
*/
|
||||
|
||||
/* global Flash */
|
||||
import StatusIconEntityMap from '../../ci_status_icons';
|
||||
import { statusCssClasses, borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
|
@ -109,11 +109,11 @@ export default {
|
|||
},
|
||||
|
||||
triggerButtonClass() {
|
||||
return `ci-status-icon-${this.stage.status.group}`;
|
||||
return `ci-status-icon-${statusCssClasses[this.stage.status.icon]}`;
|
||||
},
|
||||
|
||||
svgIcon() {
|
||||
return StatusIconEntityMap[this.stage.status.icon];
|
||||
return borderlessStatusIconEntityMap[this.stage.status.icon];
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import Vue from 'vue';
|
||||
import pipelineGraph from './components/graph/graph_component.vue';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => new Vue({
|
||||
el: '#js-pipeline-graph-vue',
|
||||
components: {
|
||||
pipelineGraph,
|
||||
},
|
||||
render: createElement => createElement('pipeline-graph'),
|
||||
}));
|
|
@ -0,0 +1,14 @@
|
|||
import Vue from 'vue';
|
||||
import VueResource from 'vue-resource';
|
||||
|
||||
Vue.use(VueResource);
|
||||
|
||||
export default class PipelineService {
|
||||
constructor(endpoint) {
|
||||
this.pipeline = Vue.resource(endpoint);
|
||||
}
|
||||
|
||||
getPipeline() {
|
||||
return this.pipeline.get();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
export default class PipelineStore {
|
||||
constructor() {
|
||||
this.state = {};
|
||||
|
||||
this.state.graph = [];
|
||||
}
|
||||
|
||||
storeGraph(graph = []) {
|
||||
this.state.graph = graph;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import cancelSVG from 'icons/_icon_action_cancel.svg';
|
||||
import retrySVG from 'icons/_icon_action_retry.svg';
|
||||
import playSVG from 'icons/_icon_action_play.svg';
|
||||
|
||||
export default function getActionIcon(action) {
|
||||
let icon;
|
||||
switch (action) {
|
||||
case 'icon_action_cancel':
|
||||
icon = cancelSVG;
|
||||
break;
|
||||
case 'icon_action_retry':
|
||||
icon = retrySVG;
|
||||
break;
|
||||
case 'icon_action_play':
|
||||
icon = playSVG;
|
||||
break;
|
||||
default:
|
||||
icon = '';
|
||||
}
|
||||
|
||||
return icon;
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import BORDERLESS_CANCELED_SVG from 'icons/_icon_status_canceled_borderless.svg';
|
||||
import BORDERLESS_CREATED_SVG from 'icons/_icon_status_created_borderless.svg';
|
||||
import BORDERLESS_FAILED_SVG from 'icons/_icon_status_failed_borderless.svg';
|
||||
import BORDERLESS_MANUAL_SVG from 'icons/_icon_status_manual_borderless.svg';
|
||||
import BORDERLESS_PENDING_SVG from 'icons/_icon_status_pending_borderless.svg';
|
||||
import BORDERLESS_RUNNING_SVG from 'icons/_icon_status_running_borderless.svg';
|
||||
import BORDERLESS_SKIPPED_SVG from 'icons/_icon_status_skipped_borderless.svg';
|
||||
import BORDERLESS_SUCCESS_SVG from 'icons/_icon_status_success_borderless.svg';
|
||||
import BORDERLESS_WARNING_SVG from 'icons/_icon_status_warning_borderless.svg';
|
||||
|
||||
import CANCELED_SVG from 'icons/_icon_status_canceled.svg';
|
||||
import CREATED_SVG from 'icons/_icon_status_created.svg';
|
||||
import FAILED_SVG from 'icons/_icon_status_failed.svg';
|
||||
import MANUAL_SVG from 'icons/_icon_status_manual.svg';
|
||||
import PENDING_SVG from 'icons/_icon_status_pending.svg';
|
||||
import RUNNING_SVG from 'icons/_icon_status_running.svg';
|
||||
import SKIPPED_SVG from 'icons/_icon_status_skipped.svg';
|
||||
import SUCCESS_SVG from 'icons/_icon_status_success.svg';
|
||||
import WARNING_SVG from 'icons/_icon_status_warning.svg';
|
||||
|
||||
export const borderlessStatusIconEntityMap = {
|
||||
icon_status_canceled: BORDERLESS_CANCELED_SVG,
|
||||
icon_status_created: BORDERLESS_CREATED_SVG,
|
||||
icon_status_failed: BORDERLESS_FAILED_SVG,
|
||||
icon_status_manual: BORDERLESS_MANUAL_SVG,
|
||||
icon_status_pending: BORDERLESS_PENDING_SVG,
|
||||
icon_status_running: BORDERLESS_RUNNING_SVG,
|
||||
icon_status_skipped: BORDERLESS_SKIPPED_SVG,
|
||||
icon_status_success: BORDERLESS_SUCCESS_SVG,
|
||||
icon_status_warning: BORDERLESS_WARNING_SVG,
|
||||
};
|
||||
|
||||
export const statusIconEntityMap = {
|
||||
icon_status_canceled: CANCELED_SVG,
|
||||
icon_status_created: CREATED_SVG,
|
||||
icon_status_failed: FAILED_SVG,
|
||||
icon_status_manual: MANUAL_SVG,
|
||||
icon_status_pending: PENDING_SVG,
|
||||
icon_status_running: RUNNING_SVG,
|
||||
icon_status_skipped: SKIPPED_SVG,
|
||||
icon_status_success: SUCCESS_SVG,
|
||||
icon_status_warning: WARNING_SVG,
|
||||
};
|
||||
|
||||
export const statusCssClasses = {
|
||||
icon_status_canceled: 'canceled',
|
||||
icon_status_created: 'created',
|
||||
icon_status_failed: 'failed',
|
||||
icon_status_manual: 'manual',
|
||||
icon_status_pending: 'pending',
|
||||
icon_status_running: 'running',
|
||||
icon_status_skipped: 'skipped',
|
||||
icon_status_success: 'success',
|
||||
icon_status_warning: 'warning',
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
<script>
|
||||
import { statusIconEntityMap, statusCssClasses } from '../../vue_shared/ci_status_icons';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
status: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
statusIconSvg() {
|
||||
return statusIconEntityMap[this.status.icon];
|
||||
},
|
||||
|
||||
cssClass() {
|
||||
const status = statusCssClasses[this.status.icon];
|
||||
return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<span
|
||||
:class="cssClass"
|
||||
v-html="statusIconSvg">
|
||||
</span>
|
||||
</template>
|
|
@ -0,0 +1,9 @@
|
|||
export default {
|
||||
mounted() {
|
||||
$(this.$refs.tooltip).tooltip();
|
||||
},
|
||||
|
||||
updated() {
|
||||
$(this.$refs.tooltip).tooltip('fixTitle');
|
||||
},
|
||||
};
|
|
@ -486,7 +486,7 @@
|
|||
color: $gl-text-color-secondary;
|
||||
|
||||
// Action Icons in big pipeline-graph nodes
|
||||
> .ci-action-icon-container .ci-action-icon-wrapper {
|
||||
> div > .ci-action-icon-container .ci-action-icon-wrapper {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
background: $white-light;
|
||||
|
@ -511,7 +511,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
> .ci-action-icon-container {
|
||||
> div > .ci-action-icon-container {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
|
@ -541,7 +541,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
> .build-content {
|
||||
> div > .build-content {
|
||||
display: inline-block;
|
||||
padding: 8px 10px 9px;
|
||||
width: 100%;
|
||||
|
@ -557,34 +557,6 @@
|
|||
}
|
||||
|
||||
|
||||
.arrow {
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-color: transparent;
|
||||
border-style: solid;
|
||||
top: 18px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
left: -5px;
|
||||
margin-top: -6px;
|
||||
border-width: 7px 5px 7px 0;
|
||||
border-right-color: $border-color;
|
||||
}
|
||||
|
||||
&::after {
|
||||
left: -4px;
|
||||
margin-top: -9px;
|
||||
border-width: 10px 7px 10px 0;
|
||||
border-right-color: $white-light;
|
||||
}
|
||||
}
|
||||
|
||||
// Connect first build in each stage with right horizontal line
|
||||
&:first-child {
|
||||
&::after {
|
||||
|
@ -859,7 +831,8 @@
|
|||
border-radius: 3px;
|
||||
|
||||
// build name
|
||||
.ci-build-text {
|
||||
.ci-build-text,
|
||||
.ci-status-text {
|
||||
font-weight: 200;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
@ -911,6 +884,38 @@
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Top arrow in the dropdown in the big pipeline graph
|
||||
*/
|
||||
.big-pipeline-graph-dropdown-menu {
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-color: transparent;
|
||||
border-style: solid;
|
||||
top: 18px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
left: -5px;
|
||||
margin-top: -6px;
|
||||
border-width: 7px 5px 7px 0;
|
||||
border-right-color: $border-color;
|
||||
}
|
||||
|
||||
&::after {
|
||||
left: -4px;
|
||||
margin-top: -9px;
|
||||
border-width: 10px 7px 10px 0;
|
||||
border-right-color: $white-light;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Top arrow in the dropdown in the mini pipeline graph
|
||||
*/
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
-# Renders the graph node with both the status icon, status name and action icon
|
||||
|
||||
- subject = local_assigns.fetch(:subject)
|
||||
- status = subject.detailed_status(current_user)
|
||||
- klass = "ci-status-icon ci-status-icon-#{status.group} js-ci-status-icon-#{status.group}"
|
||||
- tooltip = "#{subject.name} - #{status.label}"
|
||||
|
||||
- if status.has_details?
|
||||
= link_to status.details_path, class: 'build-content has-tooltip', data: { toggle: 'tooltip', title: tooltip, container: 'body' } do
|
||||
%span{ class: klass }= custom_icon(status.icon)
|
||||
.ci-status-text= subject.name
|
||||
- else
|
||||
.build-content.has-tooltip{ data: { toggle: 'tooltip', title: tooltip } }
|
||||
%span{ class: klass }= custom_icon(status.icon)
|
||||
.ci-status-text= subject.name
|
||||
|
||||
- if status.has_action?
|
||||
= link_to status.action_path, class: 'ci-action-icon-container has-tooltip', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' } do
|
||||
%i.ci-action-icon-wrapper{ class: "js-#{status.action_icon.dasherize}" }
|
||||
= custom_icon(status.action_icon)
|
|
@ -1,4 +0,0 @@
|
|||
- pipeline = local_assigns.fetch(:pipeline)
|
||||
.pipeline-visualization.pipeline-graph
|
||||
%ul.stage-column-list
|
||||
= render partial: "projects/stage/graph", collection: pipeline.stages, as: :stage
|
|
@ -17,8 +17,11 @@
|
|||
|
||||
.tab-content
|
||||
#js-tab-pipeline.tab-pane
|
||||
.build-content.middle-block.js-pipeline-graph
|
||||
= render "projects/pipelines/graph", pipeline: pipeline
|
||||
#js-pipeline-graph-vue{ data: { endpoint: namespace_project_pipeline_path(@project.namespace, @project, @pipeline, format: :json) } }
|
||||
|
||||
- content_for :page_specific_javascripts do
|
||||
= page_specific_javascript_bundle_tag('common_vue')
|
||||
= page_specific_javascript_bundle_tag('pipelines_graph')
|
||||
|
||||
#js-tab-builds.tab-pane
|
||||
- if pipeline.yaml_errors.present?
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
- stage = local_assigns.fetch(:stage)
|
||||
- statuses = stage.statuses.latest
|
||||
- status_groups = statuses.sort_by(&:sortable_name).group_by(&:group_name)
|
||||
%li.stage-column
|
||||
.stage-name
|
||||
%a{ name: stage.name }
|
||||
= stage.name.titleize
|
||||
.builds-container
|
||||
%ul
|
||||
- status_groups.each do |group_name, grouped_statuses|
|
||||
- if grouped_statuses.one?
|
||||
- status = grouped_statuses.first
|
||||
%li.build{ 'id' => "ci-badge-#{group_name}" }
|
||||
.curve
|
||||
= render 'ci/status/graph_badge', subject: status
|
||||
- else
|
||||
%li.build{ 'id' => "ci-badge-#{group_name}" }
|
||||
.curve
|
||||
= render 'projects/stage/in_stage_group', name: group_name, subject: grouped_statuses
|
|
@ -1,14 +0,0 @@
|
|||
- group_status = CommitStatus.where(id: subject).status
|
||||
%button.dropdown-menu-toggle.build-content.has-tooltip{ type: 'button', data: { toggle: 'dropdown', title: "#{name} - #{group_status}", container: 'body' } }
|
||||
%span{ class: "ci-status-icon ci-status-icon-#{group_status}" }
|
||||
= ci_icon_for_status(group_status)
|
||||
%span.ci-status-text
|
||||
= name
|
||||
%span.dropdown-counter-badge= subject.size
|
||||
|
||||
%ul.dropdown-menu.big-pipeline-graph-dropdown-menu.js-grouped-pipeline-dropdown
|
||||
.arrow
|
||||
.scrollable-menu
|
||||
- subject.each do |status|
|
||||
%li
|
||||
= render 'ci/status/dropdown_graph_badge', subject: status
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Re-rewrites pipeline graph in vue to support realtime data updates
|
||||
merge_request:
|
||||
author:
|
|
@ -49,6 +49,7 @@ var config = {
|
|||
pdf_viewer: './blob/pdf_viewer.js',
|
||||
pipelines: './pipelines/index.js',
|
||||
balsamiq_viewer: './blob/balsamiq_viewer.js',
|
||||
pipelines_graph: './pipelines/graph_bundle.js',
|
||||
profile: './profile/profile_bundle.js',
|
||||
protected_branches: './protected_branches/protected_branches_bundle.js',
|
||||
protected_tags: './protected_tags',
|
||||
|
@ -145,6 +146,7 @@ var config = {
|
|||
'pdf_viewer',
|
||||
'pipelines',
|
||||
'balsamiq_viewer',
|
||||
'pipelines_graph',
|
||||
],
|
||||
minChunks: function(module, count) {
|
||||
return module.resource && (/vue_shared/).test(module.resource);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
require('~/lib/utils/bootstrap_linked_tabs');
|
||||
import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs';
|
||||
|
||||
(() => {
|
||||
// TODO: remove this hack!
|
||||
|
@ -25,7 +25,7 @@ require('~/lib/utils/bootstrap_linked_tabs');
|
|||
});
|
||||
|
||||
it('should activate the tab correspondent to the given action', () => {
|
||||
const linkedTabs = new window.gl.LinkedTabs({ // eslint-disable-line
|
||||
const linkedTabs = new LinkedTabs({ // eslint-disable-line
|
||||
action: 'tab1',
|
||||
defaultAction: 'tab1',
|
||||
parentEl: '.linked-tabs',
|
||||
|
@ -35,7 +35,7 @@ require('~/lib/utils/bootstrap_linked_tabs');
|
|||
});
|
||||
|
||||
it('should active the default tab action when the action is show', () => {
|
||||
const linkedTabs = new window.gl.LinkedTabs({ // eslint-disable-line
|
||||
const linkedTabs = new LinkedTabs({ // eslint-disable-line
|
||||
action: 'show',
|
||||
defaultAction: 'tab1',
|
||||
parentEl: '.linked-tabs',
|
||||
|
@ -49,7 +49,7 @@ require('~/lib/utils/bootstrap_linked_tabs');
|
|||
it('should change the url according to the clicked tab', () => {
|
||||
const historySpy = !phantomjs && spyOn(history, 'replaceState').and.callFake(() => {});
|
||||
|
||||
const linkedTabs = new window.gl.LinkedTabs({ // eslint-disable-line
|
||||
const linkedTabs = new LinkedTabs({
|
||||
action: 'show',
|
||||
defaultAction: 'tab1',
|
||||
parentEl: '.linked-tabs',
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
import * as icons from '~/ci_status_icons';
|
||||
|
||||
describe('CI status icons', () => {
|
||||
const statuses = [
|
||||
'canceled',
|
||||
'created',
|
||||
'failed',
|
||||
'manual',
|
||||
'pending',
|
||||
'running',
|
||||
'skipped',
|
||||
'success',
|
||||
'warning',
|
||||
];
|
||||
|
||||
statuses.forEach((status) => {
|
||||
it(`should export a ${status} svg`, () => {
|
||||
const key = `${status.toUpperCase()}_SVG`;
|
||||
|
||||
expect(Object.hasOwnProperty.call(icons, key)).toBe(true);
|
||||
expect(icons[key]).toMatch(/^<svg/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('default export map', () => {
|
||||
const entityIconNames = [
|
||||
'icon_status_canceled',
|
||||
'icon_status_created',
|
||||
'icon_status_failed',
|
||||
'icon_status_manual',
|
||||
'icon_status_pending',
|
||||
'icon_status_running',
|
||||
'icon_status_skipped',
|
||||
'icon_status_success',
|
||||
'icon_status_warning',
|
||||
];
|
||||
|
||||
entityIconNames.forEach((iconName) => {
|
||||
it(`should have a '${iconName}' key`, () => {
|
||||
expect(Object.hasOwnProperty.call(icons.default, iconName)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1 @@
|
|||
#js-pipeline-graph-vue{ data: { endpoint: "foo" } }
|
|
@ -0,0 +1,40 @@
|
|||
import Vue from 'vue';
|
||||
import actionComponent from '~/pipelines/components/graph/action_component.vue';
|
||||
|
||||
describe('pipeline graph action component', () => {
|
||||
let component;
|
||||
|
||||
beforeEach(() => {
|
||||
const ActionComponent = Vue.extend(actionComponent);
|
||||
component = new ActionComponent({
|
||||
propsData: {
|
||||
tooltipText: 'bar',
|
||||
link: 'foo',
|
||||
actionMethod: 'post',
|
||||
actionIcon: 'icon_action_cancel',
|
||||
},
|
||||
}).$mount();
|
||||
});
|
||||
|
||||
it('should render a link', () => {
|
||||
expect(component.$el.getAttribute('href')).toEqual('foo');
|
||||
});
|
||||
|
||||
it('should render the provided title as a bootstrap tooltip', () => {
|
||||
expect(component.$el.getAttribute('data-original-title')).toEqual('bar');
|
||||
});
|
||||
|
||||
it('should update bootstrap tooltip when title changes', (done) => {
|
||||
component.tooltipText = 'changed';
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(component.$el.getAttribute('data-original-title')).toBe('changed');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render an svg', () => {
|
||||
expect(component.$el.querySelector('.ci-action-icon-wrapper')).toBeDefined();
|
||||
expect(component.$el.querySelector('svg')).toBeDefined();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
import Vue from 'vue';
|
||||
import dropdownActionComponent from '~/pipelines/components/graph/dropdown_action_component.vue';
|
||||
|
||||
describe('action component', () => {
|
||||
let component;
|
||||
|
||||
beforeEach(() => {
|
||||
const DropdownActionComponent = Vue.extend(dropdownActionComponent);
|
||||
component = new DropdownActionComponent({
|
||||
propsData: {
|
||||
tooltipText: 'bar',
|
||||
link: 'foo',
|
||||
actionMethod: 'post',
|
||||
actionIcon: 'icon_action_cancel',
|
||||
},
|
||||
}).$mount();
|
||||
});
|
||||
|
||||
it('should render a link', () => {
|
||||
expect(component.$el.getAttribute('href')).toEqual('foo');
|
||||
});
|
||||
|
||||
it('should render the provided title as a bootstrap tooltip', () => {
|
||||
expect(component.$el.getAttribute('data-original-title')).toEqual('bar');
|
||||
});
|
||||
|
||||
it('should render an svg', () => {
|
||||
expect(component.$el.querySelector('svg')).toBeDefined();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,83 @@
|
|||
import Vue from 'vue';
|
||||
import graphComponent from '~/pipelines/components/graph/graph_component.vue';
|
||||
|
||||
describe('graph component', () => {
|
||||
preloadFixtures('static/graph.html.raw');
|
||||
|
||||
let GraphComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
loadFixtures('static/graph.html.raw');
|
||||
GraphComponent = Vue.extend(graphComponent);
|
||||
});
|
||||
|
||||
describe('while is loading', () => {
|
||||
it('should render a loading icon', () => {
|
||||
const component = new GraphComponent().$mount('#js-pipeline-graph-vue');
|
||||
expect(component.$el.querySelector('.loading-icon')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a successfull response', () => {
|
||||
const interceptor = (request, next) => {
|
||||
next(request.respondWith(JSON.stringify({
|
||||
details: {
|
||||
stages: [{
|
||||
name: 'test',
|
||||
title: 'test: passed',
|
||||
status: {
|
||||
icon: 'icon_status_success',
|
||||
text: 'passed',
|
||||
label: 'passed',
|
||||
details_path: '/root/ci-mock/pipelines/123#test',
|
||||
},
|
||||
path: '/root/ci-mock/pipelines/123#test',
|
||||
groups: [{
|
||||
name: 'test',
|
||||
size: 1,
|
||||
jobs: [{
|
||||
id: 4153,
|
||||
name: 'test',
|
||||
status: {
|
||||
icon: 'icon_status_success',
|
||||
text: 'passed',
|
||||
label: 'passed',
|
||||
details_path: '/root/ci-mock/builds/4153',
|
||||
action: {
|
||||
icon: 'icon_action_retry',
|
||||
title: 'Retry',
|
||||
path: '/root/ci-mock/builds/4153/retry',
|
||||
method: 'post',
|
||||
},
|
||||
},
|
||||
}],
|
||||
}],
|
||||
}],
|
||||
},
|
||||
}), {
|
||||
status: 200,
|
||||
}));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
Vue.http.interceptors.push(interceptor);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
|
||||
});
|
||||
|
||||
it('should render the graph', (done) => {
|
||||
const component = new GraphComponent().$mount('#js-pipeline-graph-vue');
|
||||
|
||||
setTimeout(() => {
|
||||
expect(component.$el.classList.contains('js-pipeline-graph')).toEqual(true);
|
||||
|
||||
expect(component.$el.querySelector('loading-icon')).toBe(null);
|
||||
|
||||
expect(component.$el.querySelector('.stage-column-list')).toBeDefined();
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,117 @@
|
|||
import Vue from 'vue';
|
||||
import jobComponent from '~/pipelines/components/graph/job_component.vue';
|
||||
|
||||
describe('pipeline graph job component', () => {
|
||||
let JobComponent;
|
||||
|
||||
const mockJob = {
|
||||
id: 4256,
|
||||
name: 'test',
|
||||
status: {
|
||||
icon: 'icon_status_success',
|
||||
text: 'passed',
|
||||
label: 'passed',
|
||||
group: 'success',
|
||||
details_path: '/root/ci-mock/builds/4256',
|
||||
action: {
|
||||
icon: 'icon_action_retry',
|
||||
title: 'Retry',
|
||||
path: '/root/ci-mock/builds/4256/retry',
|
||||
method: 'post',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
JobComponent = Vue.extend(jobComponent);
|
||||
});
|
||||
|
||||
describe('name with link', () => {
|
||||
it('should render the job name and status with a link', () => {
|
||||
const component = new JobComponent({
|
||||
propsData: {
|
||||
job: mockJob,
|
||||
},
|
||||
}).$mount();
|
||||
|
||||
const link = component.$el.querySelector('a');
|
||||
|
||||
expect(link.getAttribute('href')).toEqual(mockJob.status.details_path);
|
||||
|
||||
expect(
|
||||
link.getAttribute('data-original-title'),
|
||||
).toEqual(`${mockJob.name} - ${mockJob.status.label}`);
|
||||
|
||||
expect(component.$el.querySelector('.js-status-icon-success')).toBeDefined();
|
||||
|
||||
expect(
|
||||
component.$el.querySelector('.ci-status-text').textContent.trim(),
|
||||
).toEqual(mockJob.name);
|
||||
});
|
||||
});
|
||||
|
||||
describe('name without link', () => {
|
||||
it('it should render status and name', () => {
|
||||
const component = new JobComponent({
|
||||
propsData: {
|
||||
job: {
|
||||
id: 4256,
|
||||
name: 'test',
|
||||
status: {
|
||||
icon: 'icon_status_success',
|
||||
text: 'passed',
|
||||
label: 'passed',
|
||||
group: 'success',
|
||||
details_path: '/root/ci-mock/builds/4256',
|
||||
},
|
||||
},
|
||||
},
|
||||
}).$mount();
|
||||
|
||||
expect(component.$el.querySelector('.js-status-icon-success')).toBeDefined();
|
||||
|
||||
expect(
|
||||
component.$el.querySelector('.ci-status-text').textContent.trim(),
|
||||
).toEqual(mockJob.name);
|
||||
});
|
||||
});
|
||||
|
||||
describe('action icon', () => {
|
||||
it('it should render the action icon', () => {
|
||||
const component = new JobComponent({
|
||||
propsData: {
|
||||
job: mockJob,
|
||||
},
|
||||
}).$mount();
|
||||
|
||||
expect(component.$el.querySelector('a.ci-action-icon-container')).toBeDefined();
|
||||
expect(component.$el.querySelector('i.ci-action-icon-wrapper')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('dropdown', () => {
|
||||
it('should render the dropdown action icon', () => {
|
||||
const component = new JobComponent({
|
||||
propsData: {
|
||||
job: mockJob,
|
||||
isDropdown: true,
|
||||
},
|
||||
}).$mount();
|
||||
|
||||
expect(component.$el.querySelector('a.ci-action-icon-wrapper')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render provided class name', () => {
|
||||
const component = new JobComponent({
|
||||
propsData: {
|
||||
job: mockJob,
|
||||
cssClassJobName: 'css-class-job-name',
|
||||
},
|
||||
}).$mount();
|
||||
|
||||
expect(
|
||||
component.$el.querySelector('a').classList.contains('css-class-job-name'),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
import Vue from 'vue';
|
||||
import jobNameComponent from '~/pipelines/components/graph/job_name_component.vue';
|
||||
|
||||
describe('job name component', () => {
|
||||
let component;
|
||||
|
||||
beforeEach(() => {
|
||||
const JobNameComponent = Vue.extend(jobNameComponent);
|
||||
component = new JobNameComponent({
|
||||
propsData: {
|
||||
name: 'foo',
|
||||
status: {
|
||||
icon: 'icon_status_success',
|
||||
},
|
||||
},
|
||||
}).$mount();
|
||||
});
|
||||
|
||||
it('should render the provided name', () => {
|
||||
expect(component.$el.querySelector('.ci-status-text').textContent.trim()).toEqual('foo');
|
||||
});
|
||||
|
||||
it('should render an icon with the provided status', () => {
|
||||
expect(component.$el.querySelector('.ci-status-icon-success')).toBeDefined();
|
||||
expect(component.$el.querySelector('.ci-status-icon-success svg')).toBeDefined();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,42 @@
|
|||
import Vue from 'vue';
|
||||
import stageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
|
||||
|
||||
describe('stage column component', () => {
|
||||
let component;
|
||||
const mockJob = {
|
||||
id: 4256,
|
||||
name: 'test',
|
||||
status: {
|
||||
icon: 'icon_status_success',
|
||||
text: 'passed',
|
||||
label: 'passed',
|
||||
group: 'success',
|
||||
details_path: '/root/ci-mock/builds/4256',
|
||||
action: {
|
||||
icon: 'icon_action_retry',
|
||||
title: 'Retry',
|
||||
path: '/root/ci-mock/builds/4256/retry',
|
||||
method: 'post',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
const StageColumnComponent = Vue.extend(stageColumnComponent);
|
||||
|
||||
component = new StageColumnComponent({
|
||||
propsData: {
|
||||
title: 'foo',
|
||||
jobs: [mockJob, mockJob, mockJob],
|
||||
},
|
||||
}).$mount();
|
||||
});
|
||||
|
||||
it('should render provided title', () => {
|
||||
expect(component.$el.querySelector('.stage-name').textContent.trim()).toEqual('foo');
|
||||
});
|
||||
|
||||
it('should render the provided jobs', () => {
|
||||
expect(component.$el.querySelectorAll('.builds-container > ul > li').length).toEqual(3);
|
||||
});
|
||||
});
|
|
@ -1,30 +1,22 @@
|
|||
require('~/pipelines');
|
||||
import Pipelines from '~/pipelines';
|
||||
|
||||
// Fix for phantomJS
|
||||
if (!Element.prototype.matches && Element.prototype.webkitMatchesSelector) {
|
||||
Element.prototype.matches = Element.prototype.webkitMatchesSelector;
|
||||
}
|
||||
|
||||
(() => {
|
||||
describe('Pipelines', () => {
|
||||
preloadFixtures('static/pipeline_graph.html.raw');
|
||||
describe('Pipelines', () => {
|
||||
preloadFixtures('static/pipeline_graph.html.raw');
|
||||
|
||||
beforeEach(() => {
|
||||
loadFixtures('static/pipeline_graph.html.raw');
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(window.gl.Pipelines).toBeDefined();
|
||||
});
|
||||
|
||||
it('should create a `Pipelines` instance without options', () => {
|
||||
expect(() => { new window.gl.Pipelines(); }).not.toThrow(); //eslint-disable-line
|
||||
});
|
||||
|
||||
it('should create a `Pipelines` instance with options', () => {
|
||||
const pipelines = new window.gl.Pipelines({ foo: 'bar' });
|
||||
|
||||
expect(pipelines.pipelineGraph).toBeDefined();
|
||||
});
|
||||
beforeEach(() => {
|
||||
loadFixtures('static/pipeline_graph.html.raw');
|
||||
});
|
||||
})();
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(Pipelines).toBeDefined();
|
||||
});
|
||||
|
||||
it('should create a `Pipelines` instance without options', () => {
|
||||
expect(() => { new Pipelines(); }).not.toThrow(); //eslint-disable-line
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import getActionIcon from '~/vue_shared/ci_action_icons';
|
||||
import cancelSVG from 'icons/_icon_action_cancel.svg';
|
||||
import retrySVG from 'icons/_icon_action_retry.svg';
|
||||
import playSVG from 'icons/_icon_action_play.svg';
|
||||
|
||||
describe('getActionIcon', () => {
|
||||
it('should return an empty string', () => {
|
||||
expect(getActionIcon()).toEqual('');
|
||||
});
|
||||
|
||||
it('should return cancel svg', () => {
|
||||
expect(getActionIcon('icon_action_cancel')).toEqual(cancelSVG);
|
||||
});
|
||||
|
||||
it('should return retry svg', () => {
|
||||
expect(getActionIcon('icon_action_retry')).toEqual(retrySVG);
|
||||
});
|
||||
|
||||
it('should return play svg', () => {
|
||||
expect(getActionIcon('icon_action_play')).toEqual(playSVG);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
import { borderlessStatusIconEntityMap, statusIconEntityMap } from '~/vue_shared/ci_status_icons';
|
||||
|
||||
describe('CI status icons', () => {
|
||||
const statuses = [
|
||||
'icon_status_canceled',
|
||||
'icon_status_created',
|
||||
'icon_status_failed',
|
||||
'icon_status_manual',
|
||||
'icon_status_pending',
|
||||
'icon_status_running',
|
||||
'icon_status_skipped',
|
||||
'icon_status_success',
|
||||
'icon_status_warning',
|
||||
];
|
||||
|
||||
it('should have a dictionary for borderless icons', () => {
|
||||
statuses.forEach((status) => {
|
||||
expect(borderlessStatusIconEntityMap[status]).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should have a dictionary for icons', () => {
|
||||
statuses.forEach((status) => {
|
||||
expect(statusIconEntityMap[status]).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,130 @@
|
|||
import Vue from 'vue';
|
||||
import ciIcon from '~/vue_shared/components/ci_icon.vue';
|
||||
|
||||
describe('CI Icon component', () => {
|
||||
let CiIcon;
|
||||
beforeEach(() => {
|
||||
CiIcon = Vue.extend(ciIcon);
|
||||
});
|
||||
|
||||
it('should render a span element with an svg', () => {
|
||||
const component = new CiIcon({
|
||||
propsData: {
|
||||
status: {
|
||||
icon: 'icon_status_success',
|
||||
},
|
||||
},
|
||||
}).$mount();
|
||||
|
||||
expect(component.$el.tagName).toEqual('SPAN');
|
||||
expect(component.$el.querySelector('span > svg')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should render a success status', () => {
|
||||
const component = new CiIcon({
|
||||
propsData: {
|
||||
status: {
|
||||
icon: 'icon_status_success',
|
||||
},
|
||||
},
|
||||
}).$mount();
|
||||
|
||||
expect(component.$el.classList.contains('ci-status-icon-success')).toEqual(true);
|
||||
});
|
||||
|
||||
it('should render a failed status', () => {
|
||||
const component = new CiIcon({
|
||||
propsData: {
|
||||
status: {
|
||||
icon: 'icon_status_failed',
|
||||
},
|
||||
},
|
||||
}).$mount();
|
||||
|
||||
expect(component.$el.classList.contains('ci-status-icon-failed')).toEqual(true);
|
||||
});
|
||||
|
||||
it('should render success with warnings status', () => {
|
||||
const component = new CiIcon({
|
||||
propsData: {
|
||||
status: {
|
||||
icon: 'icon_status_warning',
|
||||
},
|
||||
},
|
||||
}).$mount();
|
||||
|
||||
expect(component.$el.classList.contains('ci-status-icon-warning')).toEqual(true);
|
||||
});
|
||||
|
||||
it('should render pending status', () => {
|
||||
const component = new CiIcon({
|
||||
propsData: {
|
||||
status: {
|
||||
icon: 'icon_status_pending',
|
||||
},
|
||||
},
|
||||
}).$mount();
|
||||
|
||||
expect(component.$el.classList.contains('ci-status-icon-pending')).toEqual(true);
|
||||
});
|
||||
|
||||
it('should render running status', () => {
|
||||
const component = new CiIcon({
|
||||
propsData: {
|
||||
status: {
|
||||
icon: 'icon_status_running',
|
||||
},
|
||||
},
|
||||
}).$mount();
|
||||
|
||||
expect(component.$el.classList.contains('ci-status-icon-running')).toEqual(true);
|
||||
});
|
||||
|
||||
it('should render created status', () => {
|
||||
const component = new CiIcon({
|
||||
propsData: {
|
||||
status: {
|
||||
icon: 'icon_status_created',
|
||||
},
|
||||
},
|
||||
}).$mount();
|
||||
|
||||
expect(component.$el.classList.contains('ci-status-icon-created')).toEqual(true);
|
||||
});
|
||||
|
||||
it('should render skipped status', () => {
|
||||
const component = new CiIcon({
|
||||
propsData: {
|
||||
status: {
|
||||
icon: 'icon_status_skipped',
|
||||
},
|
||||
},
|
||||
}).$mount();
|
||||
|
||||
expect(component.$el.classList.contains('ci-status-icon-skipped')).toEqual(true);
|
||||
});
|
||||
|
||||
it('should render canceled status', () => {
|
||||
const component = new CiIcon({
|
||||
propsData: {
|
||||
status: {
|
||||
icon: 'icon_status_canceled',
|
||||
},
|
||||
},
|
||||
}).$mount();
|
||||
|
||||
expect(component.$el.classList.contains('ci-status-icon-canceled')).toEqual(true);
|
||||
});
|
||||
|
||||
it('should render status for manual action', () => {
|
||||
const component = new CiIcon({
|
||||
propsData: {
|
||||
status: {
|
||||
icon: 'icon_status_manual',
|
||||
},
|
||||
},
|
||||
}).$mount();
|
||||
|
||||
expect(component.$el.classList.contains('ci-status-icon-manual')).toEqual(true);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue