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
|
@ -1,5 +1,3 @@
|
|||
/* eslint-disable no-new */
|
||||
|
||||
import $ from 'jquery';
|
||||
import flash from './flash';
|
||||
import axios from './lib/utils/axios_utils';
|
||||
|
@ -62,7 +60,7 @@ export default class MiniPipelineGraph {
|
|||
*/
|
||||
renderBuildsList(stageContainer, data) {
|
||||
const dropdownContainer = stageContainer.parentElement.querySelector(
|
||||
`${this.dropdownListSelector} .js-builds-dropdown-list`,
|
||||
`${this.dropdownListSelector} .js-builds-dropdown-list ul`,
|
||||
);
|
||||
|
||||
dropdownContainer.innerHTML = data;
|
||||
|
|
|
@ -61,7 +61,7 @@ export default {
|
|||
methods: {
|
||||
onClickAction() {
|
||||
$(this.$el).tooltip('hide');
|
||||
eventHub.$emit('graphAction', this.link);
|
||||
eventHub.$emit('postAction', this.link);
|
||||
this.linkRequested = this.link;
|
||||
this.isDisabled = true;
|
||||
},
|
||||
|
|
|
@ -87,7 +87,8 @@ export default {
|
|||
data-toggle="dropdown"
|
||||
data-container="body"
|
||||
class="dropdown-menu-toggle build-content"
|
||||
:title="tooltipText">
|
||||
:title="tooltipText"
|
||||
>
|
||||
|
||||
<job-name-component
|
||||
:name="job.name"
|
||||
|
@ -104,7 +105,8 @@ export default {
|
|||
<ul>
|
||||
<li
|
||||
v-for="(item, i) in job.jobs"
|
||||
:key="i">
|
||||
:key="i"
|
||||
>
|
||||
<job-component
|
||||
:job="item"
|
||||
css-class-job-name="mini-pipeline-graph-dropdown-item"
|
||||
|
|
|
@ -108,7 +108,7 @@ export default {
|
|||
<div
|
||||
v-else
|
||||
v-tooltip
|
||||
class="js-job-component-tooltip"
|
||||
class="js-job-component-tooltip non-details-job-component"
|
||||
:title="tooltipText"
|
||||
:class="cssClassJobName"
|
||||
data-html="true"
|
||||
|
|
|
@ -1,135 +1,140 @@
|
|||
<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
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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';
|
||||
import { __ } from '../../locale';
|
||||
import Flash from '../../flash';
|
||||
import axios from '../../lib/utils/axios_utils';
|
||||
import eventHub from '../event_hub';
|
||||
import Icon from '../../vue_shared/components/icon.vue';
|
||||
import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
|
||||
import JobComponent from './graph/job_component.vue';
|
||||
import tooltip from '../../vue_shared/directives/tooltip';
|
||||
|
||||
import $ from 'jquery';
|
||||
import Flash from '../../flash';
|
||||
import axios from '../../lib/utils/axios_utils';
|
||||
import eventHub from '../event_hub';
|
||||
import Icon from '../../vue_shared/components/icon.vue';
|
||||
import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
|
||||
import tooltip from '../../vue_shared/directives/tooltip';
|
||||
export default {
|
||||
components: {
|
||||
LoadingIcon,
|
||||
Icon,
|
||||
JobComponent,
|
||||
},
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LoadingIcon,
|
||||
Icon,
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
|
||||
props: {
|
||||
stage: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
directives: {
|
||||
tooltip,
|
||||
updateDropdown: {
|
||||
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: {
|
||||
stage: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
updateDropdown: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
triggerButtonClass() {
|
||||
return `ci-status-icon-${this.stage.status.group}`;
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
dropdownContent: '',
|
||||
};
|
||||
borderlessIcon() {
|
||||
return `${this.stage.status.icon}_borderless`;
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
dropdownClass() {
|
||||
return this.dropdownContent.length > 0 ? 'js-builds-dropdown-container' : 'js-builds-dropdown-loading';
|
||||
},
|
||||
|
||||
triggerButtonClass() {
|
||||
return `ci-status-icon-${this.stage.status.group}`;
|
||||
},
|
||||
|
||||
borderlessIcon() {
|
||||
return `${this.stage.status.icon}_borderless`;
|
||||
},
|
||||
watch: {
|
||||
updateDropdown() {
|
||||
if (this.updateDropdown && this.isDropdownOpen() && !this.isLoading) {
|
||||
this.fetchJobs();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
updateDropdown() {
|
||||
if (this.updateDropdown &&
|
||||
this.isDropdownOpen() &&
|
||||
!this.isLoading) {
|
||||
this.fetchJobs();
|
||||
}
|
||||
},
|
||||
},
|
||||
updated() {
|
||||
if (this.dropdownContent.length > 0) {
|
||||
this.stopDropdownClickPropagation();
|
||||
}
|
||||
},
|
||||
|
||||
updated() {
|
||||
if (this.dropdownContent.length > 0) {
|
||||
this.stopDropdownClickPropagation();
|
||||
methods: {
|
||||
onClickStage() {
|
||||
if (!this.isDropdownOpen()) {
|
||||
eventHub.$emit('clickedDropdown');
|
||||
this.isLoading = true;
|
||||
this.fetchJobs();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClickStage() {
|
||||
if (!this.isDropdownOpen()) {
|
||||
eventHub.$emit('clickedDropdown');
|
||||
this.isLoading = true;
|
||||
this.fetchJobs();
|
||||
}
|
||||
},
|
||||
fetchJobs() {
|
||||
axios
|
||||
.get(this.stage.dropdown_path)
|
||||
.then(({ data }) => {
|
||||
this.dropdownContent = data.latest_statuses;
|
||||
this.isLoading = false;
|
||||
})
|
||||
.catch(() => {
|
||||
this.closeDropdown();
|
||||
this.isLoading = false;
|
||||
|
||||
fetchJobs() {
|
||||
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');
|
||||
},
|
||||
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() {
|
||||
$(
|
||||
'.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>
|
||||
|
||||
<template>
|
||||
|
@ -168,7 +173,6 @@
|
|||
>
|
||||
|
||||
<li
|
||||
:class="dropdownClass"
|
||||
class="js-builds-dropdown-list scrollable-menu"
|
||||
>
|
||||
|
||||
|
@ -176,8 +180,16 @@
|
|||
|
||||
<ul
|
||||
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>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -29,10 +29,10 @@ export default () => {
|
|||
};
|
||||
},
|
||||
created() {
|
||||
eventHub.$on('graphAction', this.postAction);
|
||||
eventHub.$on('postAction', this.postAction);
|
||||
},
|
||||
beforeDestroy() {
|
||||
eventHub.$off('graphAction', this.postAction);
|
||||
eventHub.$off('postAction', this.postAction);
|
||||
},
|
||||
methods: {
|
||||
postAction(action) {
|
||||
|
|
|
@ -690,6 +690,8 @@ $stage-hover-bg: $gray-darker;
|
|||
$ci-action-icon-size: 22px;
|
||||
$pipeline-dropdown-line-height: 20px;
|
||||
$pipeline-dropdown-status-icon-size: 18px;
|
||||
$ci-action-dropdown-button-size: 24px;
|
||||
$ci-action-dropdown-svg-size: 12px;
|
||||
|
||||
/*
|
||||
CI variable lists
|
||||
|
|
|
@ -156,10 +156,6 @@
|
|||
.dropdown-menu {
|
||||
z-index: 300;
|
||||
}
|
||||
|
||||
.ci-action-icon-wrapper {
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.mini-pipeline-graph-dropdown-toggle {
|
||||
|
|
|
@ -22,7 +22,6 @@
|
|||
}
|
||||
|
||||
.ci-table {
|
||||
|
||||
.label {
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
@ -123,7 +122,6 @@
|
|||
}
|
||||
|
||||
.branch-commit {
|
||||
|
||||
.ref-name {
|
||||
font-weight: $gl-font-weight-bold;
|
||||
max-width: 100px;
|
||||
|
@ -481,43 +479,6 @@
|
|||
@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 {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
|
@ -548,7 +509,6 @@
|
|||
border: 1px solid $dropdown-toggle-active-border-color;
|
||||
}
|
||||
|
||||
|
||||
// Connect first build in each stage with right horizontal line
|
||||
&:first-child {
|
||||
&::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
|
||||
|
@ -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,
|
||||
.mini-pipeline-graph-dropdown-menu {
|
||||
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 {
|
||||
padding: 0;
|
||||
max-height: 245px;
|
||||
|
@ -731,74 +780,6 @@ button.mini-pipeline-graph-dropdown-toggle {
|
|||
@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
|
||||
.mini-pipeline-graph-dropdown-item {
|
||||
align-items: center;
|
||||
|
@ -808,6 +789,11 @@ button.mini-pipeline-graph-dropdown-toggle {
|
|||
line-height: $line-height-base;
|
||||
white-space: nowrap;
|
||||
|
||||
// Match dropdown.scss for all `a` tags
|
||||
&.non-details-job-component {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.ci-job-name-component {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
@ -939,7 +925,7 @@ button.mini-pipeline-graph-dropdown-toggle {
|
|||
&.dropdown-menu {
|
||||
transform: translate(-80%, 0);
|
||||
|
||||
@media(min-width: $screen-md-min) {
|
||||
@media (min-width: $screen-md-min) {
|
||||
transform: translate(-50%, 0);
|
||||
right: auto;
|
||||
left: 50%;
|
||||
|
|
|
@ -104,9 +104,18 @@ class Projects::PipelinesController < Projects::ApplicationController
|
|||
@stage = pipeline.legacy_stage(params[:stage])
|
||||
return not_found unless @stage
|
||||
|
||||
respond_to do |format|
|
||||
format.json { render json: { html: view_to_html_string('projects/pipelines/_stage') } }
|
||||
end
|
||||
render json: StageSerializer
|
||||
.new(project: @project, current_user: @current_user)
|
||||
.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
|
||||
|
||||
def retry
|
||||
|
|
|
@ -11,6 +11,12 @@ class StageEntity < Grape::Entity
|
|||
if: -> (_, opts) { opts[:grouped] },
|
||||
with: JobGroupEntity
|
||||
|
||||
expose :latest_statuses,
|
||||
if: -> (_, opts) { opts[:details] },
|
||||
with: JobEntity do |stage|
|
||||
latest_statuses
|
||||
end
|
||||
|
||||
expose :detailed_status, as: :status, with: StatusEntity
|
||||
|
||||
expose :path do |stage|
|
||||
|
@ -35,4 +41,14 @@ class StageEntity < Grape::Entity
|
|||
def detailed_status
|
||||
stage.detailed_status(request.current_user)
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
||||
- 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}")
|
||||
|
|
|
@ -6,12 +6,13 @@
|
|||
- status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}"
|
||||
|
||||
.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)
|
||||
= icon('caret-down')
|
||||
|
||||
%ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
|
||||
%li.js-builds-dropdown-list.scrollable-menu
|
||||
%ul
|
||||
|
||||
%li.js-builds-dropdown-loading.hidden
|
||||
.text-center
|
||||
|
|
|
@ -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
|
||||
get :stage
|
||||
get :stage_ajax
|
||||
post :cancel
|
||||
post :retry
|
||||
get :builds
|
||||
|
|
|
@ -109,8 +109,7 @@ describe Projects::PipelinesController do
|
|||
|
||||
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')
|
||||
expect(response).to match_response_schema('pipeline_stage')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -133,6 +132,42 @@ describe Projects::PipelinesController do
|
|||
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
|
||||
let(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
let(:status) { pipeline.detailed_status(double('user')) }
|
||||
|
|
|
@ -388,9 +388,9 @@ describe 'Pipelines', :js do
|
|||
|
||||
it 'should be possible to cancel pending build' do
|
||||
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
|
||||
end
|
||||
end
|
||||
|
@ -407,7 +407,7 @@ describe 'Pipelines', :js do
|
|||
|
||||
within('.js-builds-dropdown-list') do
|
||||
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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
%li.js-builds-dropdown-list.scrollable-menu
|
||||
%ul
|
||||
|
||||
%li.js-builds-dropdown-loading.hidden
|
||||
%span.fa.fa-spinner
|
||||
|
|
|
@ -22,7 +22,7 @@ describe('pipeline graph action component', () => {
|
|||
});
|
||||
|
||||
it('should emit an event with the provided link', () => {
|
||||
eventHub.$on('graphAction', link => {
|
||||
eventHub.$on('postAction', link => {
|
||||
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 eventHub from '~/pipelines/event_hub';
|
||||
import mountComponent from 'spec/helpers/vue_mount_component_helper';
|
||||
import { stageReply } from './mock_data';
|
||||
|
||||
describe('Pipelines stage component', () => {
|
||||
let StageComponent;
|
||||
|
@ -41,7 +42,7 @@ describe('Pipelines stage component', () => {
|
|||
|
||||
describe('with successfull request', () => {
|
||||
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 => {
|
||||
|
@ -51,7 +52,7 @@ describe('Pipelines stage component', () => {
|
|||
setTimeout(() => {
|
||||
expect(
|
||||
component.$el.querySelector('.js-builds-dropdown-container ul').textContent.trim(),
|
||||
).toEqual('foo');
|
||||
).toContain(stageReply.latest_statuses[0].name);
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
|
||||
done();
|
||||
}, 0);
|
||||
|
@ -74,7 +75,9 @@ describe('Pipelines stage component', () => {
|
|||
|
||||
describe('update endpoint correctly', () => {
|
||||
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 => {
|
||||
|
@ -93,7 +96,7 @@ describe('Pipelines stage component', () => {
|
|||
setTimeout(() => {
|
||||
expect(
|
||||
component.$el.querySelector('.js-builds-dropdown-container ul').textContent.trim(),
|
||||
).toEqual('this is the updated content');
|
||||
).toContain('this is the updated content');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue