Merge branch '33697-pipelines-json-endpoint' into 'master'
Resolve "CI retry/cancel job or pipeline redirect the user and can't be open in a new tab" Closes #33697 See merge request gitlab-org/gitlab-ce!18451
This commit is contained in:
commit
d840535c33
25 changed files with 497 additions and 254 deletions
|
@ -1,5 +1,3 @@
|
||||||
/* eslint-disable no-new */
|
|
||||||
|
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import flash from './flash';
|
import flash from './flash';
|
||||||
import axios from './lib/utils/axios_utils';
|
import axios from './lib/utils/axios_utils';
|
||||||
|
@ -62,7 +60,7 @@ export default class MiniPipelineGraph {
|
||||||
*/
|
*/
|
||||||
renderBuildsList(stageContainer, data) {
|
renderBuildsList(stageContainer, data) {
|
||||||
const dropdownContainer = stageContainer.parentElement.querySelector(
|
const dropdownContainer = stageContainer.parentElement.querySelector(
|
||||||
`${this.dropdownListSelector} .js-builds-dropdown-list`,
|
`${this.dropdownListSelector} .js-builds-dropdown-list ul`,
|
||||||
);
|
);
|
||||||
|
|
||||||
dropdownContainer.innerHTML = data;
|
dropdownContainer.innerHTML = data;
|
||||||
|
|
|
@ -61,7 +61,7 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
onClickAction() {
|
onClickAction() {
|
||||||
$(this.$el).tooltip('hide');
|
$(this.$el).tooltip('hide');
|
||||||
eventHub.$emit('graphAction', this.link);
|
eventHub.$emit('postAction', this.link);
|
||||||
this.linkRequested = this.link;
|
this.linkRequested = this.link;
|
||||||
this.isDisabled = true;
|
this.isDisabled = true;
|
||||||
},
|
},
|
||||||
|
|
|
@ -87,7 +87,8 @@ export default {
|
||||||
data-toggle="dropdown"
|
data-toggle="dropdown"
|
||||||
data-container="body"
|
data-container="body"
|
||||||
class="dropdown-menu-toggle build-content"
|
class="dropdown-menu-toggle build-content"
|
||||||
:title="tooltipText">
|
:title="tooltipText"
|
||||||
|
>
|
||||||
|
|
||||||
<job-name-component
|
<job-name-component
|
||||||
:name="job.name"
|
:name="job.name"
|
||||||
|
@ -104,7 +105,8 @@ export default {
|
||||||
<ul>
|
<ul>
|
||||||
<li
|
<li
|
||||||
v-for="(item, i) in job.jobs"
|
v-for="(item, i) in job.jobs"
|
||||||
:key="i">
|
:key="i"
|
||||||
|
>
|
||||||
<job-component
|
<job-component
|
||||||
:job="item"
|
:job="item"
|
||||||
css-class-job-name="mini-pipeline-graph-dropdown-item"
|
css-class-job-name="mini-pipeline-graph-dropdown-item"
|
||||||
|
|
|
@ -108,7 +108,7 @@ export default {
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
v-tooltip
|
v-tooltip
|
||||||
class="js-job-component-tooltip"
|
class="js-job-component-tooltip non-details-job-component"
|
||||||
:title="tooltipText"
|
:title="tooltipText"
|
||||||
:class="cssClassJobName"
|
:class="cssClassJobName"
|
||||||
data-html="true"
|
data-html="true"
|
||||||
|
|
|
@ -1,135 +1,140 @@
|
||||||
<script>
|
<script>
|
||||||
|
/**
|
||||||
|
* Renders each stage of the pipeline mini graph.
|
||||||
|
*
|
||||||
|
* Given the provided endpoint will make a request to
|
||||||
|
* fetch the dropdown data when the stage is clicked.
|
||||||
|
*
|
||||||
|
* Request is made inside this component to make it reusable between:
|
||||||
|
* 1. Pipelines main table
|
||||||
|
* 2. Pipelines table in commit and Merge request views
|
||||||
|
* 3. Merge request widget
|
||||||
|
* 4. Commit widget
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
import $ from 'jquery';
|
||||||
* Renders each stage of the pipeline mini graph.
|
import { __ } from '../../locale';
|
||||||
*
|
import Flash from '../../flash';
|
||||||
* Given the provided endpoint will make a request to
|
import axios from '../../lib/utils/axios_utils';
|
||||||
* fetch the dropdown data when the stage is clicked.
|
import eventHub from '../event_hub';
|
||||||
*
|
import Icon from '../../vue_shared/components/icon.vue';
|
||||||
* Request is made inside this component to make it reusable between:
|
import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
|
||||||
* 1. Pipelines main table
|
import JobComponent from './graph/job_component.vue';
|
||||||
* 2. Pipelines table in commit and Merge request views
|
import tooltip from '../../vue_shared/directives/tooltip';
|
||||||
* 3. Merge request widget
|
|
||||||
* 4. Commit widget
|
|
||||||
*/
|
|
||||||
|
|
||||||
import $ from 'jquery';
|
export default {
|
||||||
import Flash from '../../flash';
|
components: {
|
||||||
import axios from '../../lib/utils/axios_utils';
|
LoadingIcon,
|
||||||
import eventHub from '../event_hub';
|
Icon,
|
||||||
import Icon from '../../vue_shared/components/icon.vue';
|
JobComponent,
|
||||||
import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
|
},
|
||||||
import tooltip from '../../vue_shared/directives/tooltip';
|
|
||||||
|
|
||||||
export default {
|
directives: {
|
||||||
components: {
|
tooltip,
|
||||||
LoadingIcon,
|
},
|
||||||
Icon,
|
|
||||||
|
props: {
|
||||||
|
stage: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
directives: {
|
updateDropdown: {
|
||||||
tooltip,
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
dropdownContent: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
dropdownClass() {
|
||||||
|
return this.dropdownContent.length > 0
|
||||||
|
? 'js-builds-dropdown-container'
|
||||||
|
: 'js-builds-dropdown-loading';
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
triggerButtonClass() {
|
||||||
stage: {
|
return `ci-status-icon-${this.stage.status.group}`;
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
updateDropdown: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
borderlessIcon() {
|
||||||
return {
|
return `${this.stage.status.icon}_borderless`;
|
||||||
isLoading: false,
|
|
||||||
dropdownContent: '',
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
|
||||||
computed: {
|
watch: {
|
||||||
dropdownClass() {
|
updateDropdown() {
|
||||||
return this.dropdownContent.length > 0 ? 'js-builds-dropdown-container' : 'js-builds-dropdown-loading';
|
if (this.updateDropdown && this.isDropdownOpen() && !this.isLoading) {
|
||||||
},
|
this.fetchJobs();
|
||||||
|
}
|
||||||
triggerButtonClass() {
|
|
||||||
return `ci-status-icon-${this.stage.status.group}`;
|
|
||||||
},
|
|
||||||
|
|
||||||
borderlessIcon() {
|
|
||||||
return `${this.stage.status.icon}_borderless`;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
|
||||||
watch: {
|
updated() {
|
||||||
updateDropdown() {
|
if (this.dropdownContent.length > 0) {
|
||||||
if (this.updateDropdown &&
|
this.stopDropdownClickPropagation();
|
||||||
this.isDropdownOpen() &&
|
}
|
||||||
!this.isLoading) {
|
},
|
||||||
this.fetchJobs();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
updated() {
|
methods: {
|
||||||
if (this.dropdownContent.length > 0) {
|
onClickStage() {
|
||||||
this.stopDropdownClickPropagation();
|
if (!this.isDropdownOpen()) {
|
||||||
|
eventHub.$emit('clickedDropdown');
|
||||||
|
this.isLoading = true;
|
||||||
|
this.fetchJobs();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
fetchJobs() {
|
||||||
onClickStage() {
|
axios
|
||||||
if (!this.isDropdownOpen()) {
|
.get(this.stage.dropdown_path)
|
||||||
eventHub.$emit('clickedDropdown');
|
.then(({ data }) => {
|
||||||
this.isLoading = true;
|
this.dropdownContent = data.latest_statuses;
|
||||||
this.fetchJobs();
|
this.isLoading = false;
|
||||||
}
|
})
|
||||||
},
|
.catch(() => {
|
||||||
|
this.closeDropdown();
|
||||||
|
this.isLoading = false;
|
||||||
|
|
||||||
fetchJobs() {
|
Flash(__('Something went wrong on our end.'));
|
||||||
axios.get(this.stage.dropdown_path)
|
});
|
||||||
.then(({ data }) => {
|
|
||||||
this.dropdownContent = data.html;
|
|
||||||
this.isLoading = false;
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
this.closeDropdown();
|
|
||||||
this.isLoading = false;
|
|
||||||
|
|
||||||
Flash('Something went wrong on our end.');
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When the user right clicks or cmd/ctrl + click in the job name
|
|
||||||
* the dropdown should not be closed and the link should open in another tab,
|
|
||||||
* so we stop propagation of the click event inside the dropdown.
|
|
||||||
*
|
|
||||||
* Since this component is rendered multiple times per page we need to guarantee we only
|
|
||||||
* target the click event of this component.
|
|
||||||
*/
|
|
||||||
stopDropdownClickPropagation() {
|
|
||||||
$(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item'))
|
|
||||||
.on('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
closeDropdown() {
|
|
||||||
if (this.isDropdownOpen()) {
|
|
||||||
$(this.$refs.dropdown).dropdown('toggle');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
isDropdownOpen() {
|
|
||||||
return this.$el.classList.contains('open');
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
|
||||||
|
/**
|
||||||
|
* When the user right clicks or cmd/ctrl + click in the job name
|
||||||
|
* the dropdown should not be closed and the link should open in another tab,
|
||||||
|
* so we stop propagation of the click event inside the dropdown.
|
||||||
|
*
|
||||||
|
* Since this component is rendered multiple times per page we need to guarantee we only
|
||||||
|
* target the click event of this component.
|
||||||
|
*/
|
||||||
|
stopDropdownClickPropagation() {
|
||||||
|
$(
|
||||||
|
'.js-builds-dropdown-list button, .js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item',
|
||||||
|
this.$el,
|
||||||
|
).on('click', e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
closeDropdown() {
|
||||||
|
if (this.isDropdownOpen()) {
|
||||||
|
$(this.$refs.dropdown).dropdown('toggle');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isDropdownOpen() {
|
||||||
|
return this.$el.classList.contains('open');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -168,7 +173,6 @@
|
||||||
>
|
>
|
||||||
|
|
||||||
<li
|
<li
|
||||||
:class="dropdownClass"
|
|
||||||
class="js-builds-dropdown-list scrollable-menu"
|
class="js-builds-dropdown-list scrollable-menu"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
@ -176,8 +180,16 @@
|
||||||
|
|
||||||
<ul
|
<ul
|
||||||
v-else
|
v-else
|
||||||
v-html="dropdownContent"
|
|
||||||
>
|
>
|
||||||
|
<li
|
||||||
|
v-for="job in dropdownContent"
|
||||||
|
:key="job.id"
|
||||||
|
>
|
||||||
|
<job-component
|
||||||
|
:job="job"
|
||||||
|
css-class-job-name="mini-pipeline-graph-dropdown-item"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -29,10 +29,10 @@ export default () => {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
eventHub.$on('graphAction', this.postAction);
|
eventHub.$on('postAction', this.postAction);
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
eventHub.$off('graphAction', this.postAction);
|
eventHub.$off('postAction', this.postAction);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
postAction(action) {
|
postAction(action) {
|
||||||
|
|
|
@ -690,6 +690,8 @@ $stage-hover-bg: $gray-darker;
|
||||||
$ci-action-icon-size: 22px;
|
$ci-action-icon-size: 22px;
|
||||||
$pipeline-dropdown-line-height: 20px;
|
$pipeline-dropdown-line-height: 20px;
|
||||||
$pipeline-dropdown-status-icon-size: 18px;
|
$pipeline-dropdown-status-icon-size: 18px;
|
||||||
|
$ci-action-dropdown-button-size: 24px;
|
||||||
|
$ci-action-dropdown-svg-size: 12px;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
CI variable lists
|
CI variable lists
|
||||||
|
|
|
@ -156,10 +156,6 @@
|
||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
z-index: 300;
|
z-index: 300;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ci-action-icon-wrapper {
|
|
||||||
line-height: 16px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mini-pipeline-graph-dropdown-toggle {
|
.mini-pipeline-graph-dropdown-toggle {
|
||||||
|
|
|
@ -22,7 +22,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.ci-table {
|
.ci-table {
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
margin-bottom: 3px;
|
margin-bottom: 3px;
|
||||||
}
|
}
|
||||||
|
@ -123,7 +122,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.branch-commit {
|
.branch-commit {
|
||||||
|
|
||||||
.ref-name {
|
.ref-name {
|
||||||
font-weight: $gl-font-weight-bold;
|
font-weight: $gl-font-weight-bold;
|
||||||
max-width: 100px;
|
max-width: 100px;
|
||||||
|
@ -481,43 +479,6 @@
|
||||||
@extend .build-content:hover;
|
@extend .build-content:hover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ci-action-icon-container {
|
|
||||||
position: absolute;
|
|
||||||
right: 5px;
|
|
||||||
top: 5px;
|
|
||||||
|
|
||||||
// Action Icons in big pipeline-graph nodes
|
|
||||||
&.ci-action-icon-wrapper {
|
|
||||||
height: 30px;
|
|
||||||
width: 30px;
|
|
||||||
background: $white-light;
|
|
||||||
border: 1px solid $border-color;
|
|
||||||
border-radius: 100%;
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: $stage-hover-bg;
|
|
||||||
border: 1px solid $dropdown-toggle-active-border-color;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
fill: $gl-text-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
fill: $gl-text-color-secondary;
|
|
||||||
position: relative;
|
|
||||||
top: -1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.play {
|
|
||||||
svg {
|
|
||||||
left: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ci-status-icon svg {
|
.ci-status-icon svg {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
|
@ -548,7 +509,6 @@
|
||||||
border: 1px solid $dropdown-toggle-active-border-color;
|
border: 1px solid $dropdown-toggle-active-border-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 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 {
|
||||||
|
@ -602,6 +562,43 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ci-action-icon-container {
|
||||||
|
position: absolute;
|
||||||
|
right: 5px;
|
||||||
|
top: 5px;
|
||||||
|
|
||||||
|
// Action Icons in big pipeline-graph nodes
|
||||||
|
&.ci-action-icon-wrapper {
|
||||||
|
height: 30px;
|
||||||
|
width: 30px;
|
||||||
|
background: $white-light;
|
||||||
|
border: 1px solid $border-color;
|
||||||
|
border-radius: 100%;
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $stage-hover-bg;
|
||||||
|
border: 1px solid $dropdown-toggle-active-border-color;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: $gl-text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: $gl-text-color-secondary;
|
||||||
|
position: relative;
|
||||||
|
top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.play {
|
||||||
|
svg {
|
||||||
|
left: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Triggers the dropdown in the big pipeline graph
|
// Triggers the dropdown in the big pipeline graph
|
||||||
|
@ -710,12 +707,64 @@ button.mini-pipeline-graph-dropdown-toggle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// dropdown content for big and mini pipeline
|
/**
|
||||||
|
Action icons inside dropdowns:
|
||||||
|
- mini graph in pipelines table
|
||||||
|
- dropdown in big graph
|
||||||
|
- mini graph in MR widget pipeline
|
||||||
|
- mini graph in Commit widget pipeline
|
||||||
|
*/
|
||||||
.big-pipeline-graph-dropdown-menu,
|
.big-pipeline-graph-dropdown-menu,
|
||||||
.mini-pipeline-graph-dropdown-menu {
|
.mini-pipeline-graph-dropdown-menu {
|
||||||
width: 240px;
|
width: 240px;
|
||||||
max-width: 240px;
|
max-width: 240px;
|
||||||
|
|
||||||
|
// override dropdown.scss
|
||||||
|
&.dropdown-menu li button,
|
||||||
|
&.dropdown-menu li a.ci-action-icon-container {
|
||||||
|
padding: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ci-action-icon-container {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
top: 8px;
|
||||||
|
|
||||||
|
&.ci-action-icon-wrapper {
|
||||||
|
height: $ci-action-dropdown-button-size;
|
||||||
|
width: $ci-action-dropdown-button-size;
|
||||||
|
|
||||||
|
background: $white-light;
|
||||||
|
border: 1px solid $border-color;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $stage-hover-bg;
|
||||||
|
border: 1px solid $dropdown-toggle-active-border-color;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: $gl-text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: $ci-action-dropdown-svg-size;
|
||||||
|
height: $ci-action-dropdown-svg-size;
|
||||||
|
fill: $gl-text-color-secondary;
|
||||||
|
position: relative;
|
||||||
|
top: 0;
|
||||||
|
vertical-align: initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SVGs in the commit widget and mr widget
|
||||||
|
a.ci-action-icon-container.ci-action-icon-wrapper svg {
|
||||||
|
top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.scrollable-menu {
|
.scrollable-menu {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
max-height: 245px;
|
max-height: 245px;
|
||||||
|
@ -731,74 +780,6 @@ button.mini-pipeline-graph-dropdown-toggle {
|
||||||
@extend .mini-pipeline-graph-dropdown-item:hover;
|
@extend .mini-pipeline-graph-dropdown-item:hover;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Action icon on the right
|
|
||||||
a.ci-action-icon-wrapper {
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 1px solid $border-color;
|
|
||||||
width: $ci-action-icon-size;
|
|
||||||
height: $ci-action-icon-size;
|
|
||||||
padding: 2px 0 0 5px;
|
|
||||||
font-size: 12px;
|
|
||||||
background-color: $white-light;
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
right: $gl-padding;
|
|
||||||
margin-top: -#{$ci-action-icon-size / 2};
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus {
|
|
||||||
background-color: $stage-hover-bg;
|
|
||||||
border: 1px solid $dropdown-toggle-active-border-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
fill: $gl-text-color-secondary;
|
|
||||||
width: #{$ci-action-icon-size - 6};
|
|
||||||
height: #{$ci-action-icon-size - 6};
|
|
||||||
left: -3px;
|
|
||||||
position: relative;
|
|
||||||
top: -1px;
|
|
||||||
|
|
||||||
&.icon-action-stop,
|
|
||||||
&.icon-action-cancel {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
top: 1px;
|
|
||||||
left: -1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.icon-action-play {
|
|
||||||
width: 11px;
|
|
||||||
height: 11px;
|
|
||||||
top: 1px;
|
|
||||||
left: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.icon-action-retry {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
top: 0;
|
|
||||||
left: -3px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover svg,
|
|
||||||
&:focus svg {
|
|
||||||
fill: $gl-text-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.icon-action-retry,
|
|
||||||
&.icon-action-play {
|
|
||||||
svg {
|
|
||||||
width: #{$ci-action-icon-size - 6};
|
|
||||||
height: #{$ci-action-icon-size - 6};
|
|
||||||
left: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// link to the build
|
// link to the build
|
||||||
.mini-pipeline-graph-dropdown-item {
|
.mini-pipeline-graph-dropdown-item {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -808,6 +789,11 @@ button.mini-pipeline-graph-dropdown-toggle {
|
||||||
line-height: $line-height-base;
|
line-height: $line-height-base;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
|
// Match dropdown.scss for all `a` tags
|
||||||
|
&.non-details-job-component {
|
||||||
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.ci-job-name-component {
|
.ci-job-name-component {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -939,7 +925,7 @@ button.mini-pipeline-graph-dropdown-toggle {
|
||||||
&.dropdown-menu {
|
&.dropdown-menu {
|
||||||
transform: translate(-80%, 0);
|
transform: translate(-80%, 0);
|
||||||
|
|
||||||
@media(min-width: $screen-md-min) {
|
@media (min-width: $screen-md-min) {
|
||||||
transform: translate(-50%, 0);
|
transform: translate(-50%, 0);
|
||||||
right: auto;
|
right: auto;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
|
|
@ -104,9 +104,18 @@ class Projects::PipelinesController < Projects::ApplicationController
|
||||||
@stage = pipeline.legacy_stage(params[:stage])
|
@stage = pipeline.legacy_stage(params[:stage])
|
||||||
return not_found unless @stage
|
return not_found unless @stage
|
||||||
|
|
||||||
respond_to do |format|
|
render json: StageSerializer
|
||||||
format.json { render json: { html: view_to_html_string('projects/pipelines/_stage') } }
|
.new(project: @project, current_user: @current_user)
|
||||||
end
|
.represent(@stage, details: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO: This endpoint is used by mini-pipeline-graph
|
||||||
|
# TODO: This endpoint should be migrated to `stage.json`
|
||||||
|
def stage_ajax
|
||||||
|
@stage = pipeline.legacy_stage(params[:stage])
|
||||||
|
return not_found unless @stage
|
||||||
|
|
||||||
|
render json: { html: view_to_html_string('projects/pipelines/_stage') }
|
||||||
end
|
end
|
||||||
|
|
||||||
def retry
|
def retry
|
||||||
|
|
|
@ -11,6 +11,12 @@ class StageEntity < Grape::Entity
|
||||||
if: -> (_, opts) { opts[:grouped] },
|
if: -> (_, opts) { opts[:grouped] },
|
||||||
with: JobGroupEntity
|
with: JobGroupEntity
|
||||||
|
|
||||||
|
expose :latest_statuses,
|
||||||
|
if: -> (_, opts) { opts[:details] },
|
||||||
|
with: JobEntity do |stage|
|
||||||
|
latest_statuses
|
||||||
|
end
|
||||||
|
|
||||||
expose :detailed_status, as: :status, with: StatusEntity
|
expose :detailed_status, as: :status, with: StatusEntity
|
||||||
|
|
||||||
expose :path do |stage|
|
expose :path do |stage|
|
||||||
|
@ -35,4 +41,14 @@ class StageEntity < Grape::Entity
|
||||||
def detailed_status
|
def detailed_status
|
||||||
stage.detailed_status(request.current_user)
|
stage.detailed_status(request.current_user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def grouped_statuses
|
||||||
|
@grouped_statuses ||= stage.statuses.latest_ordered.group_by(&:status)
|
||||||
|
end
|
||||||
|
|
||||||
|
def latest_statuses
|
||||||
|
HasStatus::ORDERED_STATUSES.map do |ordered_status|
|
||||||
|
grouped_statuses.fetch(ordered_status, [])
|
||||||
|
end.flatten
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
7
app/serializers/stage_serializer.rb
Normal file
7
app/serializers/stage_serializer.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
class StageSerializer < BaseSerializer
|
||||||
|
include WithPagination
|
||||||
|
|
||||||
|
InvalidResourceError = Class.new(StandardError)
|
||||||
|
|
||||||
|
entity StageEntity
|
||||||
|
end
|
|
@ -16,5 +16,5 @@
|
||||||
%span.ci-build-text= subject.name
|
%span.ci-build-text= subject.name
|
||||||
|
|
||||||
- if status.has_action?
|
- if status.has_action?
|
||||||
= link_to status.action_path, class: "ci-action-icon-wrapper js-ci-action-icon", method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' } do
|
= link_to status.action_path, class: "ci-action-icon-container ci-action-icon-wrapper js-ci-action-icon", method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' } do
|
||||||
= sprite_icon(status.action_icon, css_class: "icon-action-#{status.action_icon}")
|
= sprite_icon(status.action_icon, css_class: "icon-action-#{status.action_icon}")
|
||||||
|
|
|
@ -6,12 +6,13 @@
|
||||||
- status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}"
|
- status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}"
|
||||||
|
|
||||||
.stage-container.dropdown{ class: klass }
|
.stage-container.dropdown{ class: klass }
|
||||||
%button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_project_pipeline_path(pipeline.project, pipeline, stage: stage.name) } }
|
%button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_ajax_project_pipeline_path(pipeline.project, pipeline, stage: stage.name) } }
|
||||||
= sprite_icon(icon_status)
|
= sprite_icon(icon_status)
|
||||||
= icon('caret-down')
|
= icon('caret-down')
|
||||||
|
|
||||||
%ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
|
%ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
|
||||||
%li.js-builds-dropdown-list.scrollable-menu
|
%li.js-builds-dropdown-list.scrollable-menu
|
||||||
|
%ul
|
||||||
|
|
||||||
%li.js-builds-dropdown-loading.hidden
|
%li.js-builds-dropdown-loading.hidden
|
||||||
.text-center
|
.text-center
|
||||||
|
|
5
changelogs/unreleased/33697-pipelines-json-endpoint.yml
Normal file
5
changelogs/unreleased/33697-pipelines-json-endpoint.yml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Use VueJS for rendering pipeline stages
|
||||||
|
merge_request:
|
||||||
|
author:
|
||||||
|
type: changed
|
|
@ -182,6 +182,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
||||||
|
|
||||||
member do
|
member do
|
||||||
get :stage
|
get :stage
|
||||||
|
get :stage_ajax
|
||||||
post :cancel
|
post :cancel
|
||||||
post :retry
|
post :retry
|
||||||
get :builds
|
get :builds
|
||||||
|
|
|
@ -109,8 +109,7 @@ describe Projects::PipelinesController do
|
||||||
|
|
||||||
it 'returns html source for stage dropdown' do
|
it 'returns html source for stage dropdown' do
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
expect(response).to render_template('projects/pipelines/_stage')
|
expect(response).to match_response_schema('pipeline_stage')
|
||||||
expect(json_response).to include('html')
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -133,6 +132,42 @@ describe Projects::PipelinesController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'GET stages_ajax.json' do
|
||||||
|
let(:pipeline) { create(:ci_pipeline, project: project) }
|
||||||
|
|
||||||
|
context 'when accessing existing stage' do
|
||||||
|
before do
|
||||||
|
create(:ci_build, pipeline: pipeline, stage: 'build')
|
||||||
|
|
||||||
|
get_stage_ajax('build')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns html source for stage dropdown' do
|
||||||
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
expect(response).to render_template('projects/pipelines/_stage')
|
||||||
|
expect(json_response).to include('html')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when accessing unknown stage' do
|
||||||
|
before do
|
||||||
|
get_stage_ajax('test')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'responds with not found' do
|
||||||
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_stage_ajax(name)
|
||||||
|
get :stage_ajax, namespace_id: project.namespace,
|
||||||
|
project_id: project,
|
||||||
|
id: pipeline.id,
|
||||||
|
stage: name,
|
||||||
|
format: :json
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'GET status.json' do
|
describe 'GET status.json' do
|
||||||
let(:pipeline) { create(:ci_pipeline, project: project) }
|
let(:pipeline) { create(:ci_pipeline, project: project) }
|
||||||
let(:status) { pipeline.detailed_status(double('user')) }
|
let(:status) { pipeline.detailed_status(double('user')) }
|
||||||
|
|
|
@ -388,9 +388,9 @@ describe 'Pipelines', :js do
|
||||||
|
|
||||||
it 'should be possible to cancel pending build' do
|
it 'should be possible to cancel pending build' do
|
||||||
find('.js-builds-dropdown-button').click
|
find('.js-builds-dropdown-button').click
|
||||||
find('a.js-ci-action-icon').click
|
find('.js-ci-action').click
|
||||||
|
wait_for_requests
|
||||||
|
|
||||||
expect(page).to have_content('canceled')
|
|
||||||
expect(build.reload).to be_canceled
|
expect(build.reload).to be_canceled
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -407,7 +407,7 @@ describe 'Pipelines', :js do
|
||||||
|
|
||||||
within('.js-builds-dropdown-list') do
|
within('.js-builds-dropdown-list') do
|
||||||
build_element = page.find('.mini-pipeline-graph-dropdown-item')
|
build_element = page.find('.mini-pipeline-graph-dropdown-item')
|
||||||
expect(build_element['data-title']).to eq('build - failed <br> (unknown failure)')
|
expect(build_element['data-original-title']).to eq('build - failed <br> (unknown failure)')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
24
spec/fixtures/api/schemas/ci_detailed_status.json
vendored
Normal file
24
spec/fixtures/api/schemas/ci_detailed_status.json
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required" : [
|
||||||
|
"icon",
|
||||||
|
"text",
|
||||||
|
"label",
|
||||||
|
"group",
|
||||||
|
"tooltip",
|
||||||
|
"has_details",
|
||||||
|
"details_path",
|
||||||
|
"favicon"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"icon": { "type": "string" },
|
||||||
|
"text": { "type": "string" },
|
||||||
|
"label": { "type": "string" },
|
||||||
|
"group": { "type": "string" },
|
||||||
|
"tooltip": { "type": "string" },
|
||||||
|
"has_details": { "type": "boolean" },
|
||||||
|
"details_path": { "type": "string" },
|
||||||
|
"favicon": { "type": "string" }
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
24
spec/fixtures/api/schemas/job.json
vendored
Normal file
24
spec/fixtures/api/schemas/job.json
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"started",
|
||||||
|
"build_path",
|
||||||
|
"playable",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"status"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "integer" },
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"started": { "type": "boolean" } ,
|
||||||
|
"build_path": { "type": "string" },
|
||||||
|
"playable": { "type": "boolean" },
|
||||||
|
"created_at": { "type": "string" },
|
||||||
|
"updated_at": { "type": "string" },
|
||||||
|
"status": { "$ref": "ci_detailed_status.json" }
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
24
spec/fixtures/api/schemas/pipeline_stage.json
vendored
Normal file
24
spec/fixtures/api/schemas/pipeline_stage.json
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required" : [
|
||||||
|
"name",
|
||||||
|
"title",
|
||||||
|
"status",
|
||||||
|
"path",
|
||||||
|
"dropdown_path"
|
||||||
|
],
|
||||||
|
"properties" : {
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"title": { "type": "string" },
|
||||||
|
"groups": { "optional": true },
|
||||||
|
"latest_statuses": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "job.json" },
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"status": { "$ref": "ci_detailed_status.json" },
|
||||||
|
"path": { "type": "string" },
|
||||||
|
"dropdown_path": { "type": "string" }
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
%ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
|
%ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
|
||||||
%li.js-builds-dropdown-list.scrollable-menu
|
%li.js-builds-dropdown-list.scrollable-menu
|
||||||
|
%ul
|
||||||
|
|
||||||
%li.js-builds-dropdown-loading.hidden
|
%li.js-builds-dropdown-loading.hidden
|
||||||
%span.fa.fa-spinner
|
%span.fa.fa-spinner
|
||||||
|
|
|
@ -22,7 +22,7 @@ describe('pipeline graph action component', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit an event with the provided link', () => {
|
it('should emit an event with the provided link', () => {
|
||||||
eventHub.$on('graphAction', link => {
|
eventHub.$on('postAction', link => {
|
||||||
expect(link).toEqual('foo');
|
expect(link).toEqual('foo');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -4,6 +4,7 @@ import axios from '~/lib/utils/axios_utils';
|
||||||
import stage from '~/pipelines/components/stage.vue';
|
import stage from '~/pipelines/components/stage.vue';
|
||||||
import eventHub from '~/pipelines/event_hub';
|
import eventHub from '~/pipelines/event_hub';
|
||||||
import mountComponent from 'spec/helpers/vue_mount_component_helper';
|
import mountComponent from 'spec/helpers/vue_mount_component_helper';
|
||||||
|
import { stageReply } from './mock_data';
|
||||||
|
|
||||||
describe('Pipelines stage component', () => {
|
describe('Pipelines stage component', () => {
|
||||||
let StageComponent;
|
let StageComponent;
|
||||||
|
@ -41,7 +42,7 @@ describe('Pipelines stage component', () => {
|
||||||
|
|
||||||
describe('with successfull request', () => {
|
describe('with successfull request', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mock.onGet('path.json').reply(200, { html: 'foo' });
|
mock.onGet('path.json').reply(200, stageReply);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render the received data and emit `clickedDropdown` event', done => {
|
it('should render the received data and emit `clickedDropdown` event', done => {
|
||||||
|
@ -51,7 +52,7 @@ describe('Pipelines stage component', () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
expect(
|
expect(
|
||||||
component.$el.querySelector('.js-builds-dropdown-container ul').textContent.trim(),
|
component.$el.querySelector('.js-builds-dropdown-container ul').textContent.trim(),
|
||||||
).toEqual('foo');
|
).toContain(stageReply.latest_statuses[0].name);
|
||||||
expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
|
expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
|
||||||
done();
|
done();
|
||||||
}, 0);
|
}, 0);
|
||||||
|
@ -74,7 +75,9 @@ describe('Pipelines stage component', () => {
|
||||||
|
|
||||||
describe('update endpoint correctly', () => {
|
describe('update endpoint correctly', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mock.onGet('bar.json').reply(200, { html: 'this is the updated content' });
|
const copyStage = Object.assign({}, stageReply);
|
||||||
|
copyStage.latest_statuses[0].name = 'this is the updated content';
|
||||||
|
mock.onGet('bar.json').reply(200, copyStage);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update the stage to request the new endpoint provided', done => {
|
it('should update the stage to request the new endpoint provided', done => {
|
||||||
|
@ -93,7 +96,7 @@ describe('Pipelines stage component', () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
expect(
|
expect(
|
||||||
component.$el.querySelector('.js-builds-dropdown-container ul').textContent.trim(),
|
component.$el.querySelector('.js-builds-dropdown-container ul').textContent.trim(),
|
||||||
).toEqual('this is the updated content');
|
).toContain('this is the updated content');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue