Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
738d27e8
Commit
738d27e8
authored
Dec 03, 2018
by
Filipa Lacerda
Committed by
Phil Hughes
Dec 03, 2018
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Updates linked pipelines to render inline
parent
60bb9eb4
Changes
16
Show whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
508 additions
and
145 deletions
+508
-145
app/assets/javascripts/pipelines/components/graph/graph_component.vue
...avascripts/pipelines/components/graph/graph_component.vue
+48
-25
app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
...pts/pipelines/components/graph/stage_column_component.vue
+2
-2
app/assets/javascripts/pipelines/pipeline_details_bundle.js
app/assets/javascripts/pipelines/pipeline_details_bundle.js
+15
-1
app/assets/javascripts/pipelines/pipeline_details_mediator.js
...assets/javascripts/pipelines/pipeline_details_mediator.js
+2
-2
ee/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
...avascripts/pipelines/components/graph/linked_pipeline.vue
+23
-22
ee/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
...ts/pipelines/components/graph/linked_pipelines_column.vue
+6
-3
ee/app/assets/javascripts/pipelines/mixins/graph_component_mixin.js
...ets/javascripts/pipelines/mixins/graph_component_mixin.js
+70
-0
ee/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js
...vascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js
+42
-0
ee/app/assets/javascripts/pipelines/pipeline_details_mediator.js
...assets/javascripts/pipelines/pipeline_details_mediator.js
+82
-0
ee/app/assets/javascripts/pipelines/stores/pipeline_store.js
ee/app/assets/javascripts/pipelines/stores/pipeline_store.js
+23
-7
ee/app/assets/stylesheets/pages/pipelines.scss
ee/app/assets/stylesheets/pages/pipelines.scss
+40
-6
ee/changelogs/unreleased/2122-transform-linked-into-button.yml
...angelogs/unreleased/2122-transform-linked-into-button.yml
+5
-0
ee/spec/javascripts/pipelines/graph/linked_pipeline_spec.js
ee/spec/javascripts/pipelines/graph/linked_pipeline_spec.js
+37
-25
ee/spec/javascripts/pipelines/graph/linked_pipelines_column_spec.js
...vascripts/pipelines/graph/linked_pipelines_column_spec.js
+17
-22
locale/gitlab.pot
locale/gitlab.pot
+12
-0
spec/javascripts/pipelines/graph/graph_component_spec.js
spec/javascripts/pipelines/graph/graph_component_spec.js
+84
-30
No files found.
app/assets/javascripts/pipelines/components/graph/graph_component.vue
View file @
738d27e8
<
script
>
import
_
from
'
underscore
'
;
import
{
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
LinkedPipelinesColumn
from
'
ee/pipelines/components/graph/linked_pipelines_column.vue
'
;
import
EEGraphMixin
from
'
ee/pipelines/mixins/graph_component_mixin
'
;
import
StageColumnComponent
from
'
./stage_column_component.vue
'
;
import
LinkedPipelinesColumn
from
'
ee/pipelines/components/graph/linked_pipelines_column.vue
'
;
// eslint-disable-line import/order
export
default
{
components
:
{
...
...
@@ -10,6 +11,7 @@ export default {
StageColumnComponent
,
GlLoadingIcon
,
},
mixins
:
[
EEGraphMixin
],
props
:
{
isLoading
:
{
type
:
Boolean
,
...
...
@@ -20,36 +22,19 @@ export default {
required
:
true
,
},
},
computed
:
{
graph
()
{
return
this
.
pipeline
.
details
&&
this
.
pipeline
.
details
.
stages
;
},
triggered
()
{
return
this
.
pipeline
.
triggered
||
[];
},
triggeredBy
()
{
const
response
=
this
.
pipeline
.
triggered_by
;
return
response
?
[
response
]
:
[];
},
hasTriggered
()
{
return
!!
this
.
triggered
.
length
;
},
hasTriggeredBy
()
{
return
!!
this
.
triggeredBy
.
length
;
},
},
methods
:
{
capitalizeStageName
(
name
)
{
const
escapedName
=
_
.
escape
(
name
);
return
escapedName
.
charAt
(
0
).
toUpperCase
()
+
escapedName
.
slice
(
1
);
},
isFirstColumn
(
index
)
{
return
index
===
0
;
},
stageConnectorClass
(
index
,
stage
)
{
let
className
;
...
...
@@ -63,10 +48,12 @@ export default {
return
className
;
},
refreshPipelineGraph
()
{
this
.
$emit
(
'
refreshPipelineGraph
'
);
},
hasOnlyOneJob
(
stage
)
{
return
stage
.
groups
.
length
===
1
;
},
},
};
</
script
>
...
...
@@ -75,11 +62,27 @@ export default {
<div
class=
"pipeline-visualization pipeline-graph pipeline-tab-content"
>
<div
class=
"text-center"
><gl-loading-icon
v-if=
"isLoading"
:size=
"3"
/></div>
<ul
v-if=
"shouldRenderTriggeredByPipeline"
class=
"d-inline-block upstream-pipeline align-top"
>
<stage-column-component
v-for=
"(stage, indexUpstream) in triggeredByGraph"
:key=
"stage.name"
:class=
"
{
'has-only-one-job': hasOnlyOneJob(stage),
}"
:title="capitalizeStageName(stage.name)"
:groups="stage.groups"
:stage-connector-class="stageConnectorClass(indexUpstream, stage)"
:is-first-column="isFirstColumn(indexUpstream)"
@refreshPipelineGraph="refreshTriggeredByPipelineGraph"
/>
</ul>
<linked-pipelines-column
v-if=
"hasTriggeredBy"
:linked-pipelines=
"triggeredBy"
column-title=
"Upstream
"
:linked-pipelines=
"triggeredBy
Pipelines
"
:column-title=
"__('Upstream')
"
graph-position=
"left"
@
linkedPipelineClick=
"pipeline => $emit('onClickTriggeredBy', pipeline)"
/>
<ul
...
...
@@ -87,7 +90,7 @@ export default {
:class=
"
{
'has-linked-pipelines': hasTriggered || hasTriggeredBy,
}"
class="stage-column-list"
class="stage-column-list
align-top
"
>
<stage-column-component
v-for=
"(stage, index) in graph"
...
...
@@ -95,7 +98,7 @@ export default {
:class=
"
{
'has-upstream': index === 0
&&
hasTriggeredBy,
'has-downstream': index === graph.length - 1
&&
hasTriggered,
'has-only-one-job':
stage.groups.length === 1
,
'has-only-one-job':
hasOnlyOneJob(stage)
,
}"
:title="capitalizeStageName(stage.name)"
:groups="stage.groups"
...
...
@@ -108,10 +111,30 @@ export default {
<linked-pipelines-column
v-if=
"hasTriggered"
:linked-pipelines=
"triggered"
column-title=
"Downstream
"
:linked-pipelines=
"triggered
Pipelines
"
:column-title=
"__('Downstream')
"
graph-position=
"right"
@
linkedPipelineClick=
"handleClickedDownstream"
/>
<ul
v-if=
"shouldRenderTriggeredPipeline"
class=
"d-inline-block downstream-pipeline position-relative align-top"
:style=
"
{ 'margin-top': marginTop }"
>
<stage-column-component
v-for=
"(stage, indexDownstream) in triggeredGraph"
:key=
"stage.name"
:class=
"
{
'has-only-one-job': hasOnlyOneJob(stage),
}"
:title="capitalizeStageName(stage.name)"
:groups="stage.groups"
:stage-connector-class="stageConnectorClass(indexDownstream, stage)"
:is-first-column="isFirstColumn(indexDownstream)"
@refreshPipelineGraph="refreshTriggeredPipelineGraph"
/>
</ul>
</div>
</div>
</
template
>
app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
View file @
738d27e8
...
...
@@ -27,10 +27,10 @@ export default {
required
:
false
,
default
:
''
,
},
hasTriggeredBy
:
{
type
:
Boolean
,
required
:
true
,
required
:
false
,
default
:
false
,
},
},
methods
:
{
...
...
app/assets/javascripts/pipelines/pipeline_details_bundle.js
View file @
738d27e8
...
...
@@ -2,10 +2,11 @@ import Vue from 'vue';
import
Flash
from
'
~/flash
'
;
import
Translate
from
'
~/vue_shared/translate
'
;
import
{
__
}
from
'
~/locale
'
;
import
PipelinesMediator
from
'
.
/pipeline_details_mediator
'
;
import
PipelinesMediator
from
'
ee/pipelines
/pipeline_details_mediator
'
;
import
pipelineGraph
from
'
./components/graph/graph_component.vue
'
;
import
pipelineHeader
from
'
./components/header_component.vue
'
;
import
eventHub
from
'
./event_hub
'
;
import
GraphEEMixin
from
'
ee/pipelines/mixins/graph_pipeline_bundle_mixin
'
;
// eslint-disable-line import/order
Vue
.
use
(
Translate
);
...
...
@@ -22,6 +23,7 @@ export default () => {
components
:
{
pipelineGraph
,
},
mixins
:
[
GraphEEMixin
],
data
()
{
return
{
mediator
,
...
...
@@ -41,9 +43,21 @@ export default () => {
props
:
{
isLoading
:
this
.
mediator
.
state
.
isLoading
,
pipeline
:
this
.
mediator
.
store
.
state
.
pipeline
,
// EE-only start
triggeredPipelines
:
this
.
mediator
.
store
.
state
.
triggeredPipelines
,
triggered
:
this
.
mediator
.
store
.
state
.
triggered
,
triggeredByPipelines
:
this
.
mediator
.
store
.
state
.
triggeredByPipelines
,
triggeredBy
:
this
.
mediator
.
store
.
state
.
triggeredBy
,
// EE-only end
},
on
:
{
refreshPipelineGraph
:
this
.
requestRefreshPipelineGraph
,
// EE-only start
refreshTriggeredPipelineGraph
:
this
.
mediator
.
refreshTriggeredByPipelineGraph
,
refreshTriggeredByPipelineGraph
:
this
.
mediator
.
refreshTriggeredByPipelineGraph
,
onClickTriggeredBy
:
pipeline
=>
this
.
clickTriggeredBy
(
pipeline
),
onClickTriggered
:
pipeline
=>
this
.
clickTriggered
(
pipeline
),
// EE-only end
},
});
},
...
...
app/assets/javascripts/pipelines/pipeline_details_mediator.js
View file @
738d27e8
...
...
@@ -2,8 +2,8 @@ import Visibility from 'visibilityjs';
import
Flash
from
'
../flash
'
;
import
Poll
from
'
../lib/utils/poll
'
;
import
{
__
}
from
'
../locale
'
;
import
PipelineStore
from
'
./stores/pipeline_store
'
;
import
PipelineService
from
'
./services/pipeline_service
'
;
import
PipelineStore
from
'
ee/pipelines/stores/pipeline_store
'
;
// eslint-disable-line import/order
import
PipelineService
from
'
ee/pipelines/services/pipeline_service
'
;
// eslint-disable-line import/order
export
default
class
pipelinesMediator
{
constructor
(
options
=
{})
{
...
...
ee/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
View file @
738d27e8
<
script
>
import
{
GlLoadingIcon
,
GlTooltipDirective
,
Gl
Link
}
from
'
@gitlab/ui
'
;
import
{
GlLoadingIcon
,
GlTooltipDirective
,
Gl
Button
}
from
'
@gitlab/ui
'
;
import
CiStatus
from
'
~/vue_shared/components/ci_icon.vue
'
;
export
default
{
...
...
@@ -9,17 +9,13 @@ export default {
components
:
{
CiStatus
,
GlLoadingIcon
,
Gl
Link
,
Gl
Button
,
},
props
:
{
pipelineId
:
{
type
:
Number
,
required
:
true
,
},
pipelinePath
:
{
type
:
String
,
required
:
true
,
},
pipelineStatus
:
{
type
:
Object
,
required
:
true
,
...
...
@@ -38,6 +34,15 @@ export default {
tooltipText
()
{
return
`
${
this
.
projectName
}
-
${
this
.
pipelineStatus
.
label
}
`
;
},
buttonId
()
{
return
`js-linked-pipeline-
${
this
.
pipelineId
}
`
;
},
},
methods
:
{
onClickLinkedPipeline
()
{
this
.
$root
.
$emit
(
'
bv::hide::tooltip
'
,
this
.
buttonId
);
this
.
$emit
(
'
pipelineClicked
'
);
},
},
};
</
script
>
...
...
@@ -45,21 +50,17 @@ export default {
<
template
>
<li
class=
"linked-pipeline build"
>
<div
class=
"curve"
></div>
<
div>
<gl-link
<
gl-button
:id=
"buttonId"
v-gl-tooltip
:href=
"pipelinePath"
:title=
"tooltipText"
class=
"js-linked-pipeline-content linked-pipeline-content"
@
click=
"onClickLinkedPipeline"
>
<span
class=
"js-linked-pipeline-status ci-status-text"
>
<gl-loading-icon
v-if=
"isLoading"
class=
"js-linked-pipeline-loading"
/>
<gl-loading-icon
v-if=
"isLoading"
class=
"js-linked-pipeline-loading d-inline"
/>
<ci-status
v-else
:status=
"pipelineStatus"
class=
"js-linked-pipeline-status"
/>
</span>
<span
class=
"linked-pipeline-project-name"
>
{{
projectName
}}
</span>
<span
class=
"project-name-pipeline-id-separator"
>
•
</span>
<span
class=
"js-linked-pipeline-id"
>
#
{{
pipelineId
}}
</span>
</gl-link>
</div>
<span
class=
"str-truncated align-bottom"
>
{{
projectName
}}
•
#
{{
pipelineId
}}
</span>
</gl-button>
</li>
</
template
>
ee/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
View file @
738d27e8
<
script
>
import
l
inkedPipeline
from
'
./linked_pipeline.vue
'
;
import
L
inkedPipeline
from
'
./linked_pipeline.vue
'
;
export
default
{
components
:
{
l
inkedPipeline
,
L
inkedPipeline
,
},
props
:
{
columnTitle
:
{
...
...
@@ -19,7 +19,6 @@ export default {
required
:
true
,
},
},
computed
:
{
columnClass
()
{
return
`graph-position-
${
this
.
graphPosition
}
`
;
...
...
@@ -38,11 +37,15 @@ export default {
:key=
"pipeline.id"
:class=
"
{
'flat-connector-before': index === 0
&&
graphPosition === 'right',
active: !pipeline.isCollapsed || pipeline.isLoading,
'left-connector': !pipeline.isCollapsed
&&
graphPosition === 'left',
}"
:pipeline-id="pipeline.id"
:project-name="pipeline.project.name"
:pipeline-status="pipeline.details.status"
:pipeline-path="pipeline.path"
:is-loading="pipeline.isLoading"
@pipelineClicked="$emit('linkedPipelineClick', pipeline, index);"
/>
</ul>
</div>
...
...
ee/app/assets/javascripts/pipelines/mixins/graph_component_mixin.js
0 → 100644
View file @
738d27e8
import
_
from
'
underscore
'
;
export
default
{
props
:
{
triggered
:
{
type
:
Object
,
required
:
false
,
default
:
()
=>
({}),
},
triggeredBy
:
{
type
:
Object
,
required
:
false
,
default
:
()
=>
({}),
},
triggeredByPipelines
:
{
type
:
Array
,
required
:
false
,
default
:
()
=>
[],
},
triggeredPipelines
:
{
type
:
Array
,
required
:
false
,
default
:
()
=>
[],
},
},
data
()
{
return
{
triggeredTopIndex
:
1
,
};
},
computed
:
{
triggeredGraph
()
{
return
this
.
triggered
&&
this
.
triggered
.
details
&&
this
.
triggered
.
details
.
stages
;
},
triggeredByGraph
()
{
return
this
.
triggeredBy
&&
this
.
triggeredBy
.
details
&&
this
.
triggeredBy
.
details
.
stages
;
},
hasTriggered
()
{
return
this
.
triggeredPipelines
.
length
>
0
;
},
hasTriggeredBy
()
{
return
this
.
triggeredByPipelines
.
length
>
0
;
},
shouldRenderTriggeredPipeline
()
{
return
!
this
.
isLoading
&&
!
_
.
isEmpty
(
this
.
triggered
);
},
shouldRenderTriggeredByPipeline
()
{
return
!
this
.
isLoading
&&
!
_
.
isEmpty
(
this
.
triggeredBy
);
},
/**
* Calculates the margin top of the clicked downstream pipeline by
* adding the height of each linked pipeline and the margin
*/
marginTop
()
{
return
`
${
this
.
triggeredTopIndex
*
52
}
px`
;
},
},
methods
:
{
refreshTriggeredPipelineGraph
()
{
this
.
$emit
(
'
refreshTriggeredPipelineGraph
'
);
},
refreshTriggeredByPipelineGraph
()
{
this
.
$emit
(
'
refreshTriggeredByPipelineGraph
'
);
},
handleClickedDownstream
(
pipeline
,
clickedIndex
)
{
this
.
triggeredTopIndex
=
clickedIndex
;
this
.
$emit
(
'
onClickTriggered
'
,
pipeline
);
},
},
};
ee/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js
0 → 100644
View file @
738d27e8
import
pipelinesKeys
from
'
ee/pipelines/constants
'
;
export
default
{
methods
:
{
/**
* Called when a linked pipeline is clicked.
*
* If the pipeline is collapsed we will start polling it & we will reset the other pipelines.
* If the pipeline is expanded we will close it.
*
* @param {String} method Method to fetch the pipeline
* @param {String} storeKey Store property that will be updates
* @param {String} resetStoreKey Store key for the visible pipeline that will need to be reset
* @param {Object} pipeline The clicked pipeline
*/
clickPipeline
(
method
,
storeKey
,
resetStoreKey
,
pipeline
,
pollKey
)
{
if
(
pipeline
.
isCollapsed
)
{
this
.
mediator
[
method
](
pipeline
);
}
else
{
this
.
mediator
.
resetPipeline
(
storeKey
,
pipeline
,
resetStoreKey
,
pollKey
);
}
},
clickTriggered
(
triggered
)
{
this
.
clickPipeline
(
'
fetchTriggeredPipeline
'
,
pipelinesKeys
.
triggeredPipelines
,
pipelinesKeys
.
triggered
,
triggered
,
'
pollTriggered
'
,
);
},
clickTriggeredBy
(
triggeredBy
)
{
this
.
clickPipeline
(
'
fetchTriggeredByPipeline
'
,
pipelinesKeys
.
triggeredByPipelines
,
pipelinesKeys
.
triggeredBy
,
triggeredBy
,
'
pollTriggeredBy
'
,
);
},
},
};
ee/app/assets/javascripts/pipelines/pipeline_details_mediator.js
0 → 100644
View file @
738d27e8
import
CePipelineMediator
from
'
~/pipelines/pipeline_details_mediator
'
;
import
createFlash
from
'
~/flash
'
;
import
Poll
from
'
~/lib/utils/poll
'
;
import
{
__
}
from
'
~/locale
'
;
import
PipelineService
from
'
ee/pipelines/services/pipeline_service
'
;
/**
* Extends CE mediator with the logic to handle the upstream/downstream pipelines
*/
export
default
class
EePipelineMediator
extends
CePipelineMediator
{
/**
* Requests the clicked downstream pipeline pipeline
*
* @param {Object} pipeline
*/
fetchTriggeredPipeline
(
pipeline
)
{
if
(
this
.
pollTriggered
)
{
this
.
pollTriggered
.
stop
();
this
.
pollTriggered
=
null
;
}
this
.
store
.
requestTriggeredPipeline
(
pipeline
);
this
.
pollTriggered
=
new
Poll
({
resource
:
PipelineService
,
method
:
'
getUpstreamDownstream
'
,
data
:
pipeline
.
path
,
successCallback
:
({
data
})
=>
this
.
store
.
receiveTriggeredPipelineSuccess
(
pipeline
,
data
),
errorCallback
:
()
=>
{
this
.
store
.
receiveTriggeredPipelineError
(
pipeline
);
createFlash
(
__
(
'
An error occured while fetching this downstream pipeline. Please try again
'
),
);
},
});
this
.
pollTriggered
.
makeRequest
();
}
refreshTriggeredPipelineGraph
()
{
this
.
pollTriggered
.
stop
();
this
.
pollTriggered
.
restart
();
}
/**
* Requests the clicked upstream pipeline pipeline
* @param {*} pipeline
*/
fetchTriggeredByPipeline
(
pipeline
)
{
if
(
this
.
pollTriggeredBy
)
{
this
.
pollTriggeredBy
.
stop
();
this
.
pollTriggeredBy
=
null
;
}
this
.
store
.
requestTriggeredByPipeline
(
pipeline
);
this
.
pollTriggeredBy
=
new
Poll
({
resource
:
PipelineService
,
method
:
'
getUpstreamDownstream
'
,
data
:
pipeline
.
path
,
successCallback
:
({
data
})
=>
this
.
store
.
receiveTriggeredByPipelineSuccess
(
pipeline
,
data
),
errorCallback
:
()
=>
{
this
.
store
.
receiveTriggeredByPipelineError
(
pipeline
);
createFlash
(
__
(
'
An error occured while fetching this upstream pipeline. Please try again
'
));
},
});
this
.
pollTriggeredBy
.
makeRequest
();
}
refreshTriggeredByPipelineGraph
()
{
this
.
pollTriggeredBy
.
stop
();
this
.
pollTriggeredBy
.
restart
();
}
resetPipeline
(
storeKey
,
pipeline
,
resetStoreKey
,
pollKey
)
{
this
[
pollKey
].
stop
();
this
.
store
.
closePipeline
(
storeKey
,
pipeline
,
resetStoreKey
);
}
}
ee/app/assets/javascripts/pipelines/stores/pipeline_store.js
View file @
738d27e8
...
...
@@ -33,13 +33,30 @@ export default class PipelineStore extends CePipelineStore {
super
.
storePipeline
(
pipeline
);
if
(
pipeline
.
triggered
&&
pipeline
.
triggered
.
length
)
{
this
.
state
.
triggeredPipelines
=
pipeline
.
triggered
.
map
(
triggered
=>
PipelineStore
.
parsePipeline
(
triggered
),
this
.
state
.
triggeredPipelines
=
pipeline
.
triggered
.
map
(
triggered
=>
{
// because we are polling we need to make sure we do not hijack user's clicks.
const
oldPipeline
=
this
.
state
.
triggeredPipelines
.
find
(
oldValue
=>
oldValue
.
id
===
triggered
.
id
,
);
return
Object
.
assign
({},
triggered
,
{
isCollapsed
:
oldPipeline
?
oldPipeline
.
isCollapsed
:
true
,
isLoading
:
oldPipeline
?
oldPipeline
.
isLoading
:
false
,
});
});
}
if
(
pipeline
.
triggered_by
)
{
this
.
state
.
triggeredByPipelines
=
[
PipelineStore
.
parsePipeline
(
pipeline
.
triggered_by
)];
this
.
state
.
triggeredByPipelines
=
[
Object
.
assign
({},
pipeline
.
triggered_by
,
{
isCollapsed
:
this
.
state
.
triggeredByPipelines
.
length
?
this
.
state
.
triggeredByPipelines
[
0
].
isCollapsed
:
true
,
isLoading
:
this
.
state
.
triggeredByPipelines
.
length
?
this
.
state
.
triggeredByPipelines
[
0
].
isLoading
:
false
,
}),
];
}
}
...
...
@@ -72,7 +89,7 @@ export default class PipelineStore extends CePipelineStore {
this
.
updatePipeline
(
pipelinesKeys
.
triggeredPipelines
,
pipeline
,
{
isLoading
:
false
},
{
isLoading
:
false
,
isCollapsed
:
false
},
pipelinesKeys
.
triggered
,
response
,
);
...
...
@@ -121,7 +138,7 @@ export default class PipelineStore extends CePipelineStore {
this
.
updatePipeline
(
pipelinesKeys
.
triggeredByPipelines
,
pipeline
,
{
isLoading
:
false
},
{
isLoading
:
false
,
isCollapsed
:
false
},
pipelinesKeys
.
triggeredBy
,
response
,
);
...
...
@@ -183,7 +200,6 @@ export default class PipelineStore extends CePipelineStore {
if
(
triggered
.
id
===
pipeline
.
id
)
{
return
Object
.
assign
({},
triggered
,
{
isLoading
:
true
,
isCollapsed
:
false
});
}
// reset the others, in case another was one opened
return
PipelineStore
.
parsePipeline
(
triggered
);
});
...
...
ee/app/assets/stylesheets/pages/pipelines.scss
View file @
738d27e8
...
...
@@ -98,6 +98,10 @@
display
:
inline-block
;
}
.upstream-pipeline
{
margin-right
:
84px
;
}
.linked-pipelines-column.stage-column
{
position
:
relative
;
...
...
@@ -119,6 +123,16 @@
.cross-project-triangle
{
left
:
-64px
;
}
// reset connectors for the downstream pipeline
.linked-pipeline.build
{
.
curve
:
:
before
,
&::
after
{
content
:
''
;
width
:
0
;
border
:
0
;
}
}
}
.linked-pipeline.build
{
...
...
@@ -129,16 +143,36 @@
@include
flat-connector-before
(
$linked-project-column-margin
);
}
&
.active
,
{
.linked-pipeline-content
,
.linked-pipeline-content
:hover
,
.linked-pipeline-content
:focus
,
{
background-color
:
$blue-100
;
}
&
.left-connector
{
@include
flat-connector-before
(
88px
)
}
&
:
:
after
{
right
:
-
$linked-project-column-margin
;
width
:
$linked-project-column-margin
;
content
:
''
;
position
:
absolute
;
top
:
48%
;
right
:
-88px
;
border-top
:
2px
solid
$border-color
;
width
:
88px
;
height
:
1px
;
}
}
.linked-pipeline-content
{
@include
build-content
(
0
);
white-space
:
nowrap
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
text-align
:
inherit
;
min-height
:
42px
;
svg
{
top
:
0
;
}
}
}
}
...
...
ee/changelogs/unreleased/2122-transform-linked-into-button.yml
0 → 100644
View file @
738d27e8
---
title
:
Renders upstream and downstream pipelines in the main pipeline graph
merge_request
:
8607
author
:
type
:
fixed
ee/spec/javascripts/pipelines/graph/linked_pipeline_spec.js
View file @
738d27e8
...
...
@@ -48,43 +48,26 @@ describe('Linked pipeline', () => {
expect
(
vm
.
$el
.
tagName
).
toBe
(
'
LI
'
);
});
it
(
'
should render a
link
'
,
()
=>
{
it
(
'
should render a
button
'
,
()
=>
{
const
linkElement
=
vm
.
$el
.
querySelector
(
'
.js-linked-pipeline-content
'
);
expect
(
linkElement
).
not
.
toBeNull
();
});
it
(
'
should link to the correct path
'
,
()
=>
{
const
linkElement
=
vm
.
$el
.
querySelector
(
'
.js-linked-pipeline-content
'
);
expect
(
linkElement
.
getAttribute
(
'
href
'
)).
toBe
(
props
.
pipelinePath
);
});
it
(
'
should render the project name
'
,
()
=>
{
const
projectNameElement
=
vm
.
$el
.
querySelector
(
'
.linked-pipeline-project-name
'
);
expect
(
projectNameElement
.
innerText
).
toContain
(
props
.
projectName
);
expect
(
vm
.
$el
.
innerText
).
toContain
(
props
.
projectName
);
});
it
(
'
should render an svg within the status container
'
,
()
=>
{
console
.
log
(
vm
.
$el
);
const
pipelineStatusElement
=
vm
.
$el
.
querySelector
(
'
.js-linked-pipeline-status
'
);
expect
(
pipelineStatusElement
.
querySelector
(
'
svg
'
)).
not
.
toBeNull
();
});
it
(
'
should render the pipeline status icon svg
'
,
()
=>
{
const
pipelineStatusElement
=
vm
.
$el
.
querySelector
(
'
.js-linked-pipeline-status
'
);
expect
(
pipelineStatusElement
.
querySelector
(
'
.ci-status-icon-running
'
)).
not
.
toBeNull
();
expect
(
pipelineStatusElement
.
innerHTML
).
toContain
(
'
<svg
'
);
});
it
(
'
should render the correct pipeline status icon style selector
'
,
()
=>
{
const
pipelineStatusElement
=
vm
.
$el
.
querySelector
(
'
.js-linked-pipeline-status
'
);
expect
(
pipelineStatusElement
.
firstChild
.
classList
.
contains
(
'
ci-status-icon-running
'
)).
toBe
(
true
,
);
expect
(
vm
.
$el
.
querySelector
(
'
.js-ci-status-icon-running
'
)).
not
.
toBeNull
();
expect
(
vm
.
$el
.
querySelector
(
'
.js-ci-status-icon-running
'
).
innerHTML
).
toContain
(
'
<svg
'
);
});
it
(
'
should have a ci-status child component
'
,
()
=>
{
...
...
@@ -92,9 +75,7 @@ describe('Linked pipeline', () => {
});
it
(
'
should render the pipeline id
'
,
()
=>
{
const
pipelineIdElement
=
vm
.
$el
.
querySelector
(
'
.js-linked-pipeline-id
'
);
expect
(
pipelineIdElement
.
innerText
).
toContain
(
`#
${
props
.
pipelineId
}
`
);
expect
(
vm
.
$el
.
innerText
).
toContain
(
`#
${
props
.
pipelineId
}
`
);
});
it
(
'
should correctly compute the tooltip text
'
,
()
=>
{
...
...
@@ -110,4 +91,35 @@ describe('Linked pipeline', () => {
expect
(
titleAttr
).
toContain
(
mockPipeline
.
details
.
status
.
label
);
});
});
describe
(
'
on click
'
,
()
=>
{
const
props
=
{
pipelineId
:
mockPipeline
.
id
,
pipelinePath
:
mockPipeline
.
path
,
pipelineStatus
:
mockPipeline
.
details
.
status
,
projectName
:
mockPipeline
.
project
.
name
,
isLoading
:
false
,
};
beforeEach
(()
=>
{
vm
=
mountComponent
(
Component
,
props
);
});
it
(
'
emits `pipelineClicked` event
'
,
()
=>
{
spyOn
(
vm
,
'
$emit
'
);
vm
.
$el
.
querySelector
(
'
button
'
).
click
();
expect
(
vm
.
$emit
).
toHaveBeenCalledWith
(
'
pipelineClicked
'
);
});
it
(
'
should emit `bv::hide::tooltip` to close the tooltip
'
,
()
=>
{
spyOn
(
vm
.
$root
,
'
$emit
'
);
vm
.
$el
.
querySelector
(
'
button
'
).
click
();
expect
(
vm
.
$root
.
$emit
).
toHaveBeenCalledWith
(
'
bv::hide::tooltip
'
,
`js-linked-pipeline-
${
props
.
pipelineId
}
`
,
);
});
});
});
ee/spec/javascripts/pipelines/graph/linked_pipelines_column_spec.js
View file @
738d27e8
import
Vue
from
'
vue
'
;
import
LinkedPipelinesColumn
from
'
ee/pipelines/components/graph/linked_pipelines_column.vue
'
;
import
mountComponent
from
'
spec/helpers/vue_mount_component_helper
'
;
import
mockData
from
'
./linked_pipelines_mock_data
'
;
const
LinkedPipelinesColumnComponent
=
Vue
.
extend
(
LinkedPipelinesColumn
);
describe
(
'
Linked Pipelines Column
'
,
function
()
{
beforeEach
(()
=>
{
this
.
propsData
=
{
describe
(
'
Linked Pipelines Column
'
,
()
=>
{
const
Component
=
Vue
.
extend
(
LinkedPipelinesColumn
);
const
props
=
{
columnTitle
:
'
Upstream
'
,
linkedPipelines
:
mockData
.
triggered
,
graphPosition
:
'
right
'
,
};
let
vm
;
this
.
linkedPipelinesColumn
=
new
LinkedPipelinesColumnComponent
({
propsData
:
this
.
propsData
,
}).
$mount
();
beforeEach
(()
=>
{
vm
=
mountComponent
(
Component
,
props
);
});
it
(
'
instantiates a defined Vue component
'
,
()
=>
{
expect
(
this
.
linkedPipelinesColumn
).
toBeDefined
();
afterEach
(
()
=>
{
vm
.
$destroy
();
});
it
(
'
renders the pipeline orientation
'
,
()
=>
{
const
titleElement
=
this
.
linkedPipelinesColumn
.
$el
.
querySelector
(
'
.linked-pipelines-column-title
'
,
);
const
titleElement
=
vm
.
$el
.
querySelector
(
'
.linked-pipelines-column-title
'
);
expect
(
titleElement
.
innerText
).
toContain
(
this
.
propsData
.
columnTitle
);
expect
(
titleElement
.
innerText
).
toContain
(
props
.
columnTitle
);
});
it
(
'
has the correct number of linked pipeline child components
'
,
()
=>
{
expect
(
this
.
linkedPipelinesColumn
.
$children
.
length
).
toBe
(
this
.
propsData
.
linkedPipelines
.
length
);
expect
(
vm
.
$children
.
length
).
toBe
(
props
.
linkedPipelines
.
length
);
});
it
(
'
renders the correct number of linked pipelines
'
,
()
=>
{
const
linkedPipelineElements
=
this
.
linkedPipelinesColumn
.
$el
.
querySelectorAll
(
'
.linked-pipeline
'
,
);
const
linkedPipelineElements
=
vm
.
$el
.
querySelectorAll
(
'
.linked-pipeline
'
);
expect
(
linkedPipelineElements
.
length
).
toBe
(
this
.
propsData
.
linkedPipelines
.
length
);
expect
(
linkedPipelineElements
.
length
).
toBe
(
props
.
linkedPipelines
.
length
);
});
});
locale/gitlab.pot
View file @
738d27e8
...
...
@@ -608,6 +608,12 @@ msgstr ""
msgid "An error has occurred"
msgstr ""
msgid "An error occured while fetching this downstream pipeline. Please try again"
msgstr ""
msgid "An error occured while fetching this upstream pipeline. Please try again"
msgstr ""
msgid "An error occurred adding a draft to the discussion."
msgstr ""
...
...
@@ -2998,6 +3004,9 @@ msgstr ""
msgid "DownloadSource|Download"
msgstr ""
msgid "Downstream"
msgstr ""
msgid "Downvotes"
msgstr ""
...
...
@@ -9043,6 +9052,9 @@ msgstr ""
msgid "UploadLink|click to upload"
msgstr ""
msgid "Upstream"
msgstr ""
msgid "Upvotes"
msgstr ""
...
...
spec/javascripts/pipelines/graph/graph_component_spec.js
View file @
738d27e8
...
...
@@ -28,28 +28,30 @@ describe('graph component', () => {
});
});
describe
(
'
when linked pipelines are present
'
,
function
()
{
beforeEach
(
function
()
{
describe
(
'
when linked pipelines are present
'
,
()
=>
{
beforeEach
(
()
=>
{
component
=
mountComponent
(
GraphComponent
,
{
isLoading
:
false
,
pipeline
:
graphJSON
,
triggeredByPipelines
:
[
linkedPipelineJSON
.
triggered_by
],
triggeredPipelines
:
linkedPipelineJSON
.
triggered
,
});
});
describe
(
'
rendered output
'
,
function
()
{
it
(
'
should include the pipelines graph
'
,
function
()
{
describe
(
'
rendered output
'
,
()
=>
{
it
(
'
should include the pipelines graph
'
,
()
=>
{
expect
(
component
.
$el
.
classList
.
contains
(
'
js-pipeline-graph
'
)).
toEqual
(
true
);
});
it
(
'
should not include the loading icon
'
,
function
()
{
it
(
'
should not include the loading icon
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.fa-spinner
'
)).
toBeNull
();
});
it
(
'
should include the stage column list
'
,
function
()
{
it
(
'
should include the stage column list
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.stage-column-list
'
)).
not
.
toBeNull
();
});
it
(
'
should include the no-margin class on the first child
'
,
function
()
{
it
(
'
should include the no-margin class on the first child
'
,
()
=>
{
const
firstStageColumnElement
=
component
.
$el
.
querySelector
(
'
.stage-column-list .stage-column
'
,
);
...
...
@@ -57,7 +59,7 @@ describe('graph component', () => {
expect
(
firstStageColumnElement
.
classList
.
contains
(
'
no-margin
'
)).
toEqual
(
true
);
});
it
(
'
should include the has-only-one-job class on the first child
'
,
function
()
{
it
(
'
should include the has-only-one-job class on the first child
'
,
()
=>
{
const
firstStageColumnElement
=
component
.
$el
.
querySelector
(
'
.stage-column-list .stage-column
'
,
);
...
...
@@ -65,7 +67,7 @@ describe('graph component', () => {
expect
(
firstStageColumnElement
.
classList
.
contains
(
'
has-only-one-job
'
)).
toEqual
(
true
);
});
it
(
'
should include the left-margin class on the second child
'
,
function
()
{
it
(
'
should include the left-margin class on the second child
'
,
()
=>
{
const
firstStageColumnElement
=
component
.
$el
.
querySelector
(
'
.stage-column-list .stage-column:last-child
'
,
);
...
...
@@ -73,44 +75,96 @@ describe('graph component', () => {
expect
(
firstStageColumnElement
.
classList
.
contains
(
'
left-margin
'
)).
toEqual
(
true
);
});
it
(
'
should include the has-linked-pipelines flag
'
,
function
()
{
it
(
'
should include the has-linked-pipelines flag
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.has-linked-pipelines
'
)).
not
.
toBeNull
();
});
});
describe
(
'
computeds and methods
'
,
function
()
{
describe
(
'
capitalizeStageName
'
,
function
()
{
it
(
'
it capitalizes the stage name
'
,
function
()
{
describe
(
'
computeds and methods
'
,
()
=>
{
describe
(
'
capitalizeStageName
'
,
()
=>
{
it
(
'
it capitalizes the stage name
'
,
()
=>
{
expect
(
component
.
capitalizeStageName
(
'
mystage
'
)).
toBe
(
'
Mystage
'
);
});
});
describe
(
'
stageConnectorClass
'
,
function
()
{
it
(
'
it returns left-margin when there is a triggerer
'
,
function
()
{
describe
(
'
stageConnectorClass
'
,
()
=>
{
it
(
'
it returns left-margin when there is a triggerer
'
,
()
=>
{
expect
(
component
.
stageConnectorClass
(
0
,
{
groups
:
[
'
job
'
]
})).
toBe
(
'
no-margin
'
);
});
});
});
describe
(
'
linked pipelines components
'
,
function
()
{
it
(
'
should coerce triggeredBy into a collection
'
,
function
()
{
expect
(
component
.
triggeredBy
.
length
).
toBe
(
1
);
});
it
(
'
should render an upstream pipelines column
'
,
function
()
{
describe
(
'
linked pipelines components
'
,
()
=>
{
it
(
'
should render an upstream pipelines column
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.linked-pipelines-column
'
)).
not
.
toBeNull
();
expect
(
component
.
$el
.
innerHTML
).
toContain
(
'
Upstream
'
);
});
it
(
'
should render a downstream pipelines column
'
,
function
()
{
it
(
'
should render a downstream pipelines column
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.linked-pipelines-column
'
)).
not
.
toBeNull
();
expect
(
component
.
$el
.
innerHTML
).
toContain
(
'
Downstream
'
);
});
describe
(
'
triggered by
'
,
()
=>
{
it
(
'
should emit `onClickTriggeredBy` when triggered by linked pipeline is clicked
'
,
()
=>
{
spyOn
(
component
,
'
$emit
'
);
component
.
$el
.
querySelector
(
'
#js-linked-pipeline-129
'
).
click
();
expect
(
component
.
$emit
).
toHaveBeenCalledWith
(
'
onClickTriggeredBy
'
,
linkedPipelineJSON
.
triggered_by
,
);
});
describe
(
'
with expanded triggered by pipeline
'
,
()
=>
{
it
(
'
should render expanded upstream pipeline
'
,
()
=>
{
component
=
mountComponent
(
GraphComponent
,
{
isLoading
:
false
,
pipeline
:
graphJSON
,
triggeredByPipelines
:
[
Object
.
assign
({},
linkedPipelineJSON
.
triggered_by
,
{
isCollapsed
:
false
}),
],
triggeredPipelines
:
linkedPipelineJSON
.
triggered
,
triggeredBy
:
linkedPipelineJSON
.
triggered_by
,
});
expect
(
component
.
$el
.
querySelector
(
'
.upstream-pipeline
'
)).
not
.
toBeNull
();
});
});
});
describe
(
'
triggered
'
,
()
=>
{
it
(
'
should emit `onClickTriggered` when triggered linked pipeline is clicked
'
,
()
=>
{
spyOn
(
component
,
'
$emit
'
);
component
.
$el
.
querySelector
(
'
#js-linked-pipeline-132
'
).
click
();
expect
(
component
.
$emit
).
toHaveBeenCalledWith
(
'
onClickTriggered
'
,
linkedPipelineJSON
.
triggered
[
0
],
);
});
describe
(
'
with expanded triggered pipeline
'
,
()
=>
{
it
(
'
should render expanded downstream pipeline
'
,
()
=>
{
component
=
mountComponent
(
GraphComponent
,
{
isLoading
:
false
,
pipeline
:
graphJSON
,
triggeredByPipelines
:
[
linkedPipelineJSON
.
triggered_by
],
triggeredPipelines
:
[
Object
.
assign
({},
linkedPipelineJSON
.
triggered
[
0
],
{
isCollapsed
:
false
}),
],
triggered
:
linkedPipelineJSON
.
triggered
[
0
],
});
expect
(
component
.
$el
.
querySelector
(
'
.downstream-pipeline
'
)).
not
.
toBeNull
();
});
});
});
});
});
describe
(
'
when linked pipelines are not present
'
,
function
()
{
beforeEach
(
function
()
{
describe
(
'
when linked pipelines are not present
'
,
()
=>
{
beforeEach
(
()
=>
{
const
pipeline
=
Object
.
assign
(
graphJSON
,
{
triggered
:
null
,
triggered_by
:
null
});
component
=
mountComponent
(
GraphComponent
,
{
isLoading
:
false
,
...
...
@@ -118,24 +172,24 @@ describe('graph component', () => {
});
});
describe
(
'
rendered output
'
,
function
()
{
it
(
'
should include the first column with a no margin
'
,
function
()
{
describe
(
'
rendered output
'
,
()
=>
{
it
(
'
should include the first column with a no margin
'
,
()
=>
{
const
firstColumn
=
component
.
$el
.
querySelector
(
'
.stage-column:first-child
'
);
expect
(
firstColumn
.
classList
.
contains
(
'
no-margin
'
)).
toEqual
(
true
);
});
it
(
'
should not render a linked pipelines column
'
,
function
()
{
it
(
'
should not render a linked pipelines column
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.linked-pipelines-column
'
)).
toBeNull
();
});
});
describe
(
'
stageConnectorClass
'
,
function
()
{
it
(
'
it returns left-margin when no triggerer and there is one job
'
,
function
()
{
describe
(
'
stageConnectorClass
'
,
()
=>
{
it
(
'
it returns left-margin when no triggerer and there is one job
'
,
()
=>
{
expect
(
component
.
stageConnectorClass
(
0
,
{
groups
:
[
'
job
'
]
})).
toBe
(
'
no-margin
'
);
});
it
(
'
it returns left-margin when no triggerer and not the first stage
'
,
function
()
{
it
(
'
it returns left-margin when no triggerer and not the first stage
'
,
()
=>
{
expect
(
component
.
stageConnectorClass
(
99
,
{
groups
:
[
'
job
'
]
})).
toBe
(
'
left-margin
'
);
});
});
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment