Commit d840535c authored by Kamil Trzciński's avatar Kamil Trzciński

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
parents 67c9f822 7dabb22f
/* 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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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"
......
<script> <script>
/**
/** * Renders each stage of the pipeline mini graph.
* Renders each stage of the pipeline mini graph. *
* * Given the provided endpoint will make a request to
* Given the provided endpoint will make a request to * fetch the dropdown data when the stage is clicked.
* fetch the dropdown data when the stage is clicked. *
* * Request is made inside this component to make it reusable between:
* Request is made inside this component to make it reusable between: * 1. Pipelines main table
* 1. Pipelines main table * 2. Pipelines table in commit and Merge request views
* 2. Pipelines table in commit and Merge request views * 3. Merge request widget
* 3. Merge request widget * 4. Commit widget
* 4. Commit widget */
*/
import $ from 'jquery';
import $ from 'jquery'; import { __ } from '../../locale';
import Flash from '../../flash'; import Flash from '../../flash';
import axios from '../../lib/utils/axios_utils'; import axios from '../../lib/utils/axios_utils';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import Icon from '../../vue_shared/components/icon.vue'; import Icon from '../../vue_shared/components/icon.vue';
import LoadingIcon from '../../vue_shared/components/loading_icon.vue'; import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import JobComponent from './graph/job_component.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
components: { export default {
LoadingIcon, components: {
Icon, LoadingIcon,
Icon,
JobComponent,
},
directives: {
tooltip,
},
props: {
stage: {
type: Object,
required: true,
}, },
directives: { updateDropdown: {
tooltip, type: Boolean,
required: false,
default: false,
}, },
},
props: {
stage: { data() {
type: Object, return {
required: true, isLoading: false,
}, dropdownContent: '',
};
updateDropdown: { },
type: Boolean,
required: false, computed: {
default: false, dropdownClass() {
}, return this.dropdownContent.length > 0
? 'js-builds-dropdown-container'
: 'js-builds-dropdown-loading';
}, },
data() { triggerButtonClass() {
return { return `ci-status-icon-${this.stage.status.group}`;
isLoading: false,
dropdownContent: '',
};
}, },
computed: { borderlessIcon() {
dropdownClass() { return `${this.stage.status.icon}_borderless`;
return this.dropdownContent.length > 0 ? 'js-builds-dropdown-container' : 'js-builds-dropdown-loading'; },
}, },
triggerButtonClass() { watch: {
return `ci-status-icon-${this.stage.status.group}`; updateDropdown() {
}, if (this.updateDropdown && this.isDropdownOpen() && !this.isLoading) {
this.fetchJobs();
}
},
},
updated() {
if (this.dropdownContent.length > 0) {
this.stopDropdownClickPropagation();
}
},
methods: {
onClickStage() {
if (!this.isDropdownOpen()) {
eventHub.$emit('clickedDropdown');
this.isLoading = true;
this.fetchJobs();
}
},
borderlessIcon() { fetchJobs() {
return `${this.stage.status.icon}_borderless`; axios
}, .get(this.stage.dropdown_path)
.then(({ data }) => {
this.dropdownContent = data.latest_statuses;
this.isLoading = false;
})
.catch(() => {
this.closeDropdown();
this.isLoading = false;
Flash(__('Something went wrong on our end.'));
});
}, },
watch: { /**
updateDropdown() { * When the user right clicks or cmd/ctrl + click in the job name
if (this.updateDropdown && * the dropdown should not be closed and the link should open in another tab,
this.isDropdownOpen() && * so we stop propagation of the click event inside the dropdown.
!this.isLoading) { *
this.fetchJobs(); * 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();
});
}, },
updated() { closeDropdown() {
if (this.dropdownContent.length > 0) { if (this.isDropdownOpen()) {
this.stopDropdownClickPropagation(); $(this.$refs.dropdown).dropdown('toggle');
} }
}, },
methods: { isDropdownOpen() {
onClickStage() { return this.$el.classList.contains('open');
if (!this.isDropdownOpen()) {
eventHub.$emit('clickedDropdown');
this.isLoading = true;
this.fetchJobs();
}
},
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');
},
}, },
}; },
};
</script> </script>
<template> <template>
...@@ -168,7 +173,6 @@ ...@@ -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 @@ ...@@ -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 () => { ...@@ -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; ...@@ -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 @@ ...@@ -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 @@ ...@@ -22,7 +22,6 @@
} }
.ci-table { .ci-table {
.label { .label {
margin-bottom: 3px; margin-bottom: 3px;
} }
...@@ -123,7 +122,6 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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,93 +707,77 @@ button.mini-pipeline-graph-dropdown-toggle { ...@@ -710,93 +707,77 @@ 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;
.scrollable-menu { // override dropdown.scss
&.dropdown-menu li button,
&.dropdown-menu li a.ci-action-icon-container {
padding: 0; padding: 0;
max-height: 245px; text-align: center;
overflow: auto;
} }
li { .ci-action-icon-container {
position: relative; position: absolute;
right: 8px;
top: 8px;
// ensure .mini-pipeline-graph-dropdown-item has hover style when action-icon is hovered &.ci-action-icon-wrapper {
&:hover > .mini-pipeline-graph-dropdown-item, height: $ci-action-dropdown-button-size;
&:hover > .ci-job-component > .mini-pipeline-graph-dropdown-item { width: $ci-action-dropdown-button-size;
@extend .mini-pipeline-graph-dropdown-item:hover;
}
// Action icon on the right background: $white-light;
a.ci-action-icon-wrapper {
border-radius: 50%;
border: 1px solid $border-color; border: 1px solid $border-color;
width: $ci-action-icon-size; border-radius: 50%;
height: $ci-action-icon-size; display: block;
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, &:hover {
&:focus {
background-color: $stage-hover-bg; background-color: $stage-hover-bg;
border: 1px solid $dropdown-toggle-active-border-color; border: 1px solid $dropdown-toggle-active-border-color;
svg {
fill: $gl-text-color;
}
} }
svg { svg {
width: $ci-action-dropdown-svg-size;
height: $ci-action-dropdown-svg-size;
fill: $gl-text-color-secondary; fill: $gl-text-color-secondary;
width: #{$ci-action-icon-size - 6};
height: #{$ci-action-icon-size - 6};
left: -3px;
position: relative; position: relative;
top: -1px; top: 0;
vertical-align: initial;
&.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, // SVGs in the commit widget and mr widget
&:focus svg { a.ci-action-icon-container.ci-action-icon-wrapper svg {
fill: $gl-text-color; top: 2px;
} }
&.icon-action-retry, .scrollable-menu {
&.icon-action-play { padding: 0;
svg { max-height: 245px;
width: #{$ci-action-icon-size - 6}; overflow: auto;
height: #{$ci-action-icon-size - 6}; }
left: 8px;
}
}
li {
position: relative;
// ensure .mini-pipeline-graph-dropdown-item has hover style when action-icon is hovered
&:hover > .mini-pipeline-graph-dropdown-item,
&:hover > .ci-job-component > .mini-pipeline-graph-dropdown-item {
@extend .mini-pipeline-graph-dropdown-item:hover;
} }
// link to the build // link to the build
...@@ -808,6 +789,11 @@ button.mini-pipeline-graph-dropdown-toggle { ...@@ -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 { ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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
class StageSerializer < BaseSerializer
include WithPagination
InvalidResourceError = Class.new(StandardError)
entity StageEntity
end
...@@ -16,5 +16,5 @@ ...@@ -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 @@ ...@@ -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
......
---
title: Use VueJS for rendering pipeline stages
merge_request:
author:
type: changed
...@@ -182,6 +182,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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
......
{
"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
}
{
"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
}
{
"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 @@ ...@@ -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', () => { ...@@ -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');
}); });
}); });
......
This diff is collapsed.
...@@ -4,6 +4,7 @@ import axios from '~/lib/utils/axios_utils'; ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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();
}); });
}); });
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment