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 UserCallout from './user_callout';
|
||||||
import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags';
|
import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags';
|
||||||
import ShortcutsWiki from './shortcuts_wiki';
|
import ShortcutsWiki from './shortcuts_wiki';
|
||||||
|
import Pipelines from './pipelines';
|
||||||
import BlobViewer from './blob/viewer/index';
|
import BlobViewer from './blob/viewer/index';
|
||||||
import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select';
|
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 { controllerAction } = document.querySelector('.js-pipeline-container').dataset;
|
||||||
const pipelineStatusUrl = `${document.querySelector('.js-pipeline-tab-link a').getAttribute('href')}/status.json`;
|
const pipelineStatusUrl = `${document.querySelector('.js-pipeline-tab-link a').getAttribute('href')}/status.json`;
|
||||||
|
|
||||||
new gl.Pipelines({
|
new Pipelines({
|
||||||
initTabs: true,
|
initTabs: true,
|
||||||
pipelineStatusUrl,
|
pipelineStatusUrl,
|
||||||
tabsOptions: {
|
tabsOptions: {
|
||||||
|
|
|
@ -31,82 +31,78 @@
|
||||||
*
|
*
|
||||||
* ### How to use
|
* ### How to use
|
||||||
*
|
*
|
||||||
* new window.gl.LinkedTabs({
|
* new LinkedTabs({
|
||||||
* action: "#{controller.action_name}",
|
* action: "#{controller.action_name}",
|
||||||
* defaultAction: 'tab1',
|
* defaultAction: 'tab1',
|
||||||
* parentEl: '.tab-links'
|
* parentEl: '.tab-links'
|
||||||
* });
|
* });
|
||||||
*/
|
*/
|
||||||
|
|
||||||
(() => {
|
export default class LinkedTabs {
|
||||||
window.gl = window.gl || {};
|
/**
|
||||||
|
* Binds the events and activates de default tab.
|
||||||
|
*
|
||||||
|
* @param {Object} options
|
||||||
|
*/
|
||||||
|
constructor(options = {}) {
|
||||||
|
this.options = options;
|
||||||
|
|
||||||
window.gl.LinkedTabs = class LinkedTabs {
|
this.defaultAction = this.options.defaultAction;
|
||||||
/**
|
this.action = this.options.action || this.defaultAction;
|
||||||
* Binds the events and activates de default tab.
|
|
||||||
*
|
|
||||||
* @param {Object} options
|
|
||||||
*/
|
|
||||||
constructor(options) {
|
|
||||||
this.options = options || {};
|
|
||||||
|
|
||||||
this.defaultAction = this.options.defaultAction;
|
if (this.action === 'show') {
|
||||||
this.action = this.options.action || this.defaultAction;
|
this.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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
this.currentLocation = window.location;
|
||||||
* 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');
|
|
||||||
|
|
||||||
return this.setCurrentAction(source);
|
const tabSelector = `${this.options.parentEl} a[data-toggle="tab"]`;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// since this is a custom event we need jQuery :(
|
||||||
* Updates the URL with the path that matched the given action.
|
$(document)
|
||||||
*
|
.off('shown.bs.tab', tabSelector)
|
||||||
* @param {String} source
|
.on('shown.bs.tab', tabSelector, e => this.tabShown(e));
|
||||||
* @return {String}
|
|
||||||
*/
|
|
||||||
setCurrentAction(source) {
|
|
||||||
const copySource = source;
|
|
||||||
|
|
||||||
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({
|
return this.setCurrentAction(source);
|
||||||
url: newState,
|
}
|
||||||
}, document.title, newState);
|
|
||||||
return newState;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given the current action activates the correct tab.
|
* Updates the URL with the path that matched the given action.
|
||||||
* http://getbootstrap.com/javascript/#tab-show
|
*
|
||||||
* Note: Will trigger `shown.bs.tab`
|
* @param {String} source
|
||||||
*/
|
* @return {String}
|
||||||
activateTab() {
|
*/
|
||||||
return $(`${this.options.parentEl} a[data-action='${this.action}']`).tab('show');
|
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');
|
export default class Pipelines {
|
||||||
|
constructor(options = {}) {
|
||||||
((global) => {
|
if (options.initTabs && options.tabsOptions) {
|
||||||
class Pipelines {
|
// eslint-disable-next-line no-new
|
||||||
constructor(options = {}) {
|
new LinkedTabs(options.tabsOptions);
|
||||||
if (options.initTabs && options.tabsOptions) {
|
|
||||||
new global.LinkedTabs(options.tabsOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.pipelineStatusUrl) {
|
|
||||||
gl.utils.setCiStatusFavicon(options.pipelineStatusUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.addMarginToBuildColumns();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addMarginToBuildColumns() {
|
if (options.pipelineStatusUrl) {
|
||||||
this.pipelineGraph = document.querySelector('.js-pipeline-graph');
|
gl.utils.setCiStatusFavicon(options.pipelineStatusUrl);
|
||||||
|
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
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 */
|
/* global Flash */
|
||||||
import StatusIconEntityMap from '../../ci_status_icons';
|
import { statusCssClasses, borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
@ -109,11 +109,11 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
triggerButtonClass() {
|
triggerButtonClass() {
|
||||||
return `ci-status-icon-${this.stage.status.group}`;
|
return `ci-status-icon-${statusCssClasses[this.stage.status.icon]}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
svgIcon() {
|
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;
|
color: $gl-text-color-secondary;
|
||||||
|
|
||||||
// Action Icons in big pipeline-graph nodes
|
// 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;
|
height: 30px;
|
||||||
width: 30px;
|
width: 30px;
|
||||||
background: $white-light;
|
background: $white-light;
|
||||||
|
@ -511,7 +511,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .ci-action-icon-container {
|
> div > .ci-action-icon-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 5px;
|
right: 5px;
|
||||||
top: 5px;
|
top: 5px;
|
||||||
|
@ -541,7 +541,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .build-content {
|
> div > .build-content {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 8px 10px 9px;
|
padding: 8px 10px 9px;
|
||||||
width: 100%;
|
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
|
// Connect first build in each stage with right horizontal line
|
||||||
&:first-child {
|
&:first-child {
|
||||||
&::after {
|
&::after {
|
||||||
|
@ -859,7 +831,8 @@
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
|
||||||
// build name
|
// build name
|
||||||
.ci-build-text {
|
.ci-build-text,
|
||||||
|
.ci-status-text {
|
||||||
font-weight: 200;
|
font-weight: 200;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
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
|
* 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
|
.tab-content
|
||||||
#js-tab-pipeline.tab-pane
|
#js-tab-pipeline.tab-pane
|
||||||
.build-content.middle-block.js-pipeline-graph
|
#js-pipeline-graph-vue{ data: { endpoint: namespace_project_pipeline_path(@project.namespace, @project, @pipeline, format: :json) } }
|
||||||
= render "projects/pipelines/graph", pipeline: pipeline
|
|
||||||
|
- 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
|
#js-tab-builds.tab-pane
|
||||||
- if pipeline.yaml_errors.present?
|
- 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',
|
pdf_viewer: './blob/pdf_viewer.js',
|
||||||
pipelines: './pipelines/index.js',
|
pipelines: './pipelines/index.js',
|
||||||
balsamiq_viewer: './blob/balsamiq_viewer.js',
|
balsamiq_viewer: './blob/balsamiq_viewer.js',
|
||||||
|
pipelines_graph: './pipelines/graph_bundle.js',
|
||||||
profile: './profile/profile_bundle.js',
|
profile: './profile/profile_bundle.js',
|
||||||
protected_branches: './protected_branches/protected_branches_bundle.js',
|
protected_branches: './protected_branches/protected_branches_bundle.js',
|
||||||
protected_tags: './protected_tags',
|
protected_tags: './protected_tags',
|
||||||
|
@ -145,6 +146,7 @@ var config = {
|
||||||
'pdf_viewer',
|
'pdf_viewer',
|
||||||
'pipelines',
|
'pipelines',
|
||||||
'balsamiq_viewer',
|
'balsamiq_viewer',
|
||||||
|
'pipelines_graph',
|
||||||
],
|
],
|
||||||
minChunks: function(module, count) {
|
minChunks: function(module, count) {
|
||||||
return module.resource && (/vue_shared/).test(module.resource);
|
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!
|
// TODO: remove this hack!
|
||||||
|
@ -25,7 +25,7 @@ require('~/lib/utils/bootstrap_linked_tabs');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should activate the tab correspondent to the given action', () => {
|
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',
|
action: 'tab1',
|
||||||
defaultAction: 'tab1',
|
defaultAction: 'tab1',
|
||||||
parentEl: '.linked-tabs',
|
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', () => {
|
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',
|
action: 'show',
|
||||||
defaultAction: 'tab1',
|
defaultAction: 'tab1',
|
||||||
parentEl: '.linked-tabs',
|
parentEl: '.linked-tabs',
|
||||||
|
@ -49,7 +49,7 @@ require('~/lib/utils/bootstrap_linked_tabs');
|
||||||
it('should change the url according to the clicked tab', () => {
|
it('should change the url according to the clicked tab', () => {
|
||||||
const historySpy = !phantomjs && spyOn(history, 'replaceState').and.callFake(() => {});
|
const historySpy = !phantomjs && spyOn(history, 'replaceState').and.callFake(() => {});
|
||||||
|
|
||||||
const linkedTabs = new window.gl.LinkedTabs({ // eslint-disable-line
|
const linkedTabs = new LinkedTabs({
|
||||||
action: 'show',
|
action: 'show',
|
||||||
defaultAction: 'tab1',
|
defaultAction: 'tab1',
|
||||||
parentEl: '.linked-tabs',
|
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
|
// Fix for phantomJS
|
||||||
if (!Element.prototype.matches && Element.prototype.webkitMatchesSelector) {
|
if (!Element.prototype.matches && Element.prototype.webkitMatchesSelector) {
|
||||||
Element.prototype.matches = Element.prototype.webkitMatchesSelector;
|
Element.prototype.matches = Element.prototype.webkitMatchesSelector;
|
||||||
}
|
}
|
||||||
|
|
||||||
(() => {
|
describe('Pipelines', () => {
|
||||||
describe('Pipelines', () => {
|
preloadFixtures('static/pipeline_graph.html.raw');
|
||||||
preloadFixtures('static/pipeline_graph.html.raw');
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
loadFixtures('static/pipeline_graph.html.raw');
|
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();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
})();
|
|
||||||
|
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