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
c1f89970
Commit
c1f89970
authored
Jan 14, 2021
by
Sarah Groff Hennigh-Palermo
Committed by
Natalia Tepluhina
Jan 14, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add link capability to graph
Includes new component, specs
parent
d89d8429
Changes
16
Hide whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
495 additions
and
78 deletions
+495
-78
app/assets/javascripts/pipelines/components/graph/graph_component.vue
...avascripts/pipelines/components/graph/graph_component.vue
+73
-35
app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
...scripts/pipelines/components/graph/job_group_dropdown.vue
+9
-1
app/assets/javascripts/pipelines/components/graph/job_item.vue
...ssets/javascripts/pipelines/components/graph/job_item.vue
+9
-0
app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
...pts/pipelines/components/graph/stage_column_component.vue
+8
-1
app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js
...cripts/pipelines/components/graph_shared/drawing_utils.js
+14
-10
app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue
...scripts/pipelines/components/graph_shared/links_inner.vue
+137
-0
app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue
...scripts/pipelines/components/graph_shared/links_layer.vue
+86
-0
app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
...ts/pipelines/components/pipeline_graph/pipeline_graph.vue
+1
-1
app/assets/javascripts/pipelines/pipeline_details_bundle.js
app/assets/javascripts/pipelines/pipeline_details_bundle.js
+1
-1
app/assets/javascripts/pipelines/utils.js
app/assets/javascripts/pipelines/utils.js
+0
-2
locale/gitlab.pot
locale/gitlab.pot
+6
-0
spec/frontend/pipeline_editor/mock_data.js
spec/frontend/pipeline_editor/mock_data.js
+1
-1
spec/frontend/pipelines/graph/graph_component_spec.js
spec/frontend/pipelines/graph/graph_component_spec.js
+9
-0
spec/frontend/pipelines/graph/stage_column_component_spec.js
spec/frontend/pipelines/graph/stage_column_component_spec.js
+41
-25
spec/frontend/pipelines/pipeline_graph/mock_data.js
spec/frontend/pipelines/pipeline_graph/mock_data.js
+1
-1
spec/frontend/pipelines/shared/links_layer_spec.js
spec/frontend/pipelines/shared/links_layer_spec.js
+99
-0
No files found.
app/assets/javascripts/pipelines/components/graph/graph_component.vue
View file @
c1f89970
<
script
>
<
script
>
import
LinkedGraphWrapper
from
'
../graph_shared/linked_graph_wrapper.vue
'
;
import
LinkedGraphWrapper
from
'
../graph_shared/linked_graph_wrapper.vue
'
;
import
LinksLayer
from
'
../graph_shared/links_layer.vue
'
;
import
LinkedPipelinesColumn
from
'
./linked_pipelines_column.vue
'
;
import
LinkedPipelinesColumn
from
'
./linked_pipelines_column.vue
'
;
import
StageColumnComponent
from
'
./stage_column_component.vue
'
;
import
StageColumnComponent
from
'
./stage_column_component.vue
'
;
import
{
DOWNSTREAM
,
MAIN
,
UPSTREAM
}
from
'
./constants
'
;
import
{
DOWNSTREAM
,
MAIN
,
UPSTREAM
}
from
'
./constants
'
;
...
@@ -8,6 +9,7 @@ import { reportToSentry } from './utils';
...
@@ -8,6 +9,7 @@ import { reportToSentry } from './utils';
export
default
{
export
default
{
name
:
'
PipelineGraph
'
,
name
:
'
PipelineGraph
'
,
components
:
{
components
:
{
LinksLayer
,
LinkedGraphWrapper
,
LinkedGraphWrapper
,
LinkedPipelinesColumn
,
LinkedPipelinesColumn
,
StageColumnComponent
,
StageColumnComponent
,
...
@@ -32,9 +34,15 @@ export default {
...
@@ -32,9 +34,15 @@ export default {
DOWNSTREAM
,
DOWNSTREAM
,
UPSTREAM
,
UPSTREAM
,
},
},
CONTAINER_REF
:
'
PIPELINE_LINKS_CONTAINER_REF
'
,
BASE_CONTAINER_ID
:
'
pipeline-links-container
'
,
data
()
{
data
()
{
return
{
return
{
hoveredJobName
:
''
,
hoveredJobName
:
''
,
measurements
:
{
width
:
0
,
height
:
0
,
},
pipelineExpanded
:
{
pipelineExpanded
:
{
jobName
:
''
,
jobName
:
''
,
expanded
:
false
,
expanded
:
false
,
...
@@ -42,6 +50,9 @@ export default {
...
@@ -42,6 +50,9 @@ export default {
};
};
},
},
computed
:
{
computed
:
{
containerId
()
{
return
`
${
this
.
$options
.
BASE_CONTAINER_ID
}
-
${
this
.
pipeline
.
id
}
`
;
},
downstreamPipelines
()
{
downstreamPipelines
()
{
return
this
.
hasDownstreamPipelines
?
this
.
pipeline
.
downstream
:
[];
return
this
.
hasDownstreamPipelines
?
this
.
pipeline
.
downstream
:
[];
},
},
...
@@ -54,12 +65,13 @@ export default {
...
@@ -54,12 +65,13 @@ export default {
hasUpstreamPipelines
()
{
hasUpstreamPipelines
()
{
return
Boolean
(
this
.
pipeline
?.
upstream
?.
length
>
0
);
return
Boolean
(
this
.
pipeline
?.
upstream
?.
length
>
0
);
},
},
// The
two show checks prevent upstream / downstream from
showing redundant linked columns
// The
show downstream check prevents
showing redundant linked columns
showDownstreamPipelines
()
{
showDownstreamPipelines
()
{
return
(
return
(
this
.
hasDownstreamPipelines
&&
this
.
type
!==
this
.
$options
.
pipelineTypeConstants
.
UPSTREAM
this
.
hasDownstreamPipelines
&&
this
.
type
!==
this
.
$options
.
pipelineTypeConstants
.
UPSTREAM
);
);
},
},
// The show upstream check prevents showing redundant linked columns
showUpstreamPipelines
()
{
showUpstreamPipelines
()
{
return
(
return
(
this
.
hasUpstreamPipelines
&&
this
.
type
!==
this
.
$options
.
pipelineTypeConstants
.
DOWNSTREAM
this
.
hasUpstreamPipelines
&&
this
.
type
!==
this
.
$options
.
pipelineTypeConstants
.
DOWNSTREAM
...
@@ -72,7 +84,19 @@ export default {
...
@@ -72,7 +84,19 @@ export default {
errorCaptured
(
err
,
_vm
,
info
)
{
errorCaptured
(
err
,
_vm
,
info
)
{
reportToSentry
(
this
.
$options
.
name
,
`error:
${
err
}
, info:
${
info
}
`
);
reportToSentry
(
this
.
$options
.
name
,
`error:
${
err
}
, info:
${
info
}
`
);
},
},
mounted
()
{
this
.
measurements
=
this
.
getMeasurements
();
},
methods
:
{
methods
:
{
getMeasurements
()
{
return
{
width
:
this
.
$refs
[
this
.
containerId
].
scrollWidth
,
height
:
this
.
$refs
[
this
.
containerId
].
scrollHeight
,
};
},
onError
(
errorType
)
{
this
.
$emit
(
'
error
'
,
errorType
);
},
setJob
(
jobName
)
{
setJob
(
jobName
)
{
this
.
hoveredJobName
=
jobName
;
this
.
hoveredJobName
=
jobName
;
},
},
...
@@ -88,43 +112,57 @@ export default {
...
@@ -88,43 +112,57 @@ export default {
<
template
>
<
template
>
<div
class=
"js-pipeline-graph"
>
<div
class=
"js-pipeline-graph"
>
<div
<div
:id=
"containerId"
:ref=
"containerId"
class=
"gl-pipeline-min-h gl-display-flex gl-position-relative gl-overflow-auto gl-bg-gray-10 gl-white-space-nowrap"
class=
"gl-pipeline-min-h gl-display-flex gl-position-relative gl-overflow-auto gl-bg-gray-10 gl-white-space-nowrap"
:class=
"
{ 'gl-py-5': !isLinkedPipeline }"
:class=
"
{ 'gl-py-5': !isLinkedPipeline }"
>
>
<linked-graph-wrapper>
<links-layer
<template
#upstream
>
:pipeline-data=
"graph"
<linked-pipelines-column
:pipeline-id=
"pipeline.id"
v-if=
"showUpstreamPipelines"
:container-id=
"containerId"
:linked-pipelines=
"upstreamPipelines"
:container-measurements=
"measurements"
:column-title=
"__('Upstream')"
:highlighted-job=
"hoveredJobName"
:type=
"$options.pipelineTypeConstants.UPSTREAM"
default-link-color=
"gl-stroke-transparent"
@
error=
"emit('error', errorType)"
@
error=
"onError"
/>
>
</
template
>
<linked-graph-wrapper>
<
template
#main
>
<template
#upstream
>
<stage-column-component
<linked-pipelines-column
v-for=
"stage in graph"
v-if=
"showUpstreamPipelines"
:key=
"stage.name"
:linked-pipelines=
"upstreamPipelines"
:title=
"stage.name"
:column-title=
"__('Upstream')"
:groups=
"stage.groups"
:type=
"$options.pipelineTypeConstants.UPSTREAM"
:action=
"stage.status.action"
@
error=
"onError"
:job-hovered=
"hoveredJobName"
/>
:pipeline-expanded=
"pipelineExpanded"
</
template
>
@
refreshPipelineGraph=
"$emit('refreshPipelineGraph')"
<
template
#main
>
/>
<stage-column-component
</
template
>
v-for=
"stage in graph"
<
template
#downstream
>
:key=
"stage.name"
<linked-pipelines-column
:title=
"stage.name"
v-if=
"showDownstreamPipelines"
:groups=
"stage.groups"
:linked-pipelines=
"downstreamPipelines"
:action=
"stage.status.action"
:column-title=
"__('Downstream')"
:job-hovered=
"hoveredJobName"
:type=
"$options.pipelineTypeConstants.DOWNSTREAM"
:pipeline-expanded=
"pipelineExpanded"
@
downstreamHovered=
"setJob"
:pipeline-id=
"pipeline.id"
@
pipelineExpandToggle=
"togglePipelineExpanded"
@
refreshPipelineGraph=
"$emit('refreshPipelineGraph')"
@
error=
"emit('error', errorType)"
@
jobHover=
"setJob"
/>
/>
</
template
>
</
template
>
</linked-graph-wrapper>
<
template
#downstream
>
<linked-pipelines-column
v-if=
"showDownstreamPipelines"
:linked-pipelines=
"downstreamPipelines"
:column-title=
"__('Downstream')"
:type=
"$options.pipelineTypeConstants.DOWNSTREAM"
@
downstreamHovered=
"setJob"
@
pipelineExpandToggle=
"togglePipelineExpanded"
@
error=
"onError"
/>
</
template
>
</linked-graph-wrapper>
</links-layer>
</div>
</div>
</div>
</div>
</template>
</template>
app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
View file @
c1f89970
...
@@ -23,8 +23,16 @@ export default {
...
@@ -23,8 +23,16 @@ export default {
type
:
Object
,
type
:
Object
,
required
:
true
,
required
:
true
,
},
},
pipelineId
:
{
type
:
Number
,
required
:
false
,
default
:
-
1
,
},
},
},
computed
:
{
computed
:
{
computedJobId
()
{
return
this
.
pipelineId
>
-
1
?
`
${
this
.
group
.
name
}
-
${
this
.
pipelineId
}
`
:
''
;
},
tooltipText
()
{
tooltipText
()
{
const
{
name
,
status
}
=
this
.
group
;
const
{
name
,
status
}
=
this
.
group
;
return
`
${
name
}
-
${
status
.
label
}
`
;
return
`
${
name
}
-
${
status
.
label
}
`
;
...
@@ -41,7 +49,7 @@ export default {
...
@@ -41,7 +49,7 @@ export default {
};
};
</
script
>
</
script
>
<
template
>
<
template
>
<div
class=
"ci-job-dropdown-container dropdown dropright"
>
<div
:id=
"computedJobId"
class=
"ci-job-dropdown-container dropdown dropright"
>
<button
<button
v-gl-tooltip.hover=
"
{ boundary: 'viewport' }"
v-gl-tooltip.hover=
"
{ boundary: 'viewport' }"
:title="tooltipText"
:title="tooltipText"
...
...
app/assets/javascripts/pipelines/components/graph/job_item.vue
View file @
c1f89970
...
@@ -74,6 +74,11 @@ export default {
...
@@ -74,6 +74,11 @@ export default {
required
:
false
,
required
:
false
,
default
:
()
=>
({}),
default
:
()
=>
({}),
},
},
pipelineId
:
{
type
:
Number
,
required
:
false
,
default
:
-
1
,
},
},
},
computed
:
{
computed
:
{
boundary
()
{
boundary
()
{
...
@@ -85,6 +90,9 @@ export default {
...
@@ -85,6 +90,9 @@ export default {
hasDetails
()
{
hasDetails
()
{
return
accessValue
(
this
.
dataMethod
,
'
hasDetails
'
,
this
.
status
);
return
accessValue
(
this
.
dataMethod
,
'
hasDetails
'
,
this
.
status
);
},
},
computedJobId
()
{
return
this
.
pipelineId
>
-
1
?
`
${
this
.
job
.
name
}
-
${
this
.
pipelineId
}
`
:
''
;
},
status
()
{
status
()
{
return
this
.
job
&&
this
.
job
.
status
?
this
.
job
.
status
:
{};
return
this
.
job
&&
this
.
job
.
status
?
this
.
job
.
status
:
{};
},
},
...
@@ -146,6 +154,7 @@ export default {
...
@@ -146,6 +154,7 @@ export default {
</
script
>
</
script
>
<
template
>
<
template
>
<div
<div
:id=
"computedJobId"
class=
"ci-job-component gl-display-flex gl-align-items-center gl-justify-content-space-between"
class=
"ci-job-component gl-display-flex gl-align-items-center gl-justify-content-space-between"
data-qa-selector=
"job_item_container"
data-qa-selector=
"job_item_container"
>
>
...
...
app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
View file @
c1f89970
...
@@ -24,6 +24,10 @@ export default {
...
@@ -24,6 +24,10 @@ export default {
type
:
Array
,
type
:
Array
,
required
:
true
,
required
:
true
,
},
},
pipelineId
:
{
type
:
Number
,
required
:
true
,
},
action
:
{
action
:
{
type
:
Object
,
type
:
Object
,
required
:
false
,
required
:
false
,
...
@@ -94,16 +98,19 @@ export default {
...
@@ -94,16 +98,19 @@ export default {
:key=
"getGroupId(group)"
:key=
"getGroupId(group)"
data-testid=
"stage-column-group"
data-testid=
"stage-column-group"
class=
"gl-relative gl-mb-3 gl-white-space-normal gl-pipeline-job-width"
class=
"gl-relative gl-mb-3 gl-white-space-normal gl-pipeline-job-width"
@
mouseenter=
"$emit('jobHover', group.name)"
@
mouseleave=
"$emit('jobHover', '')"
>
>
<job-item
<job-item
v-if=
"group.size === 1"
v-if=
"group.size === 1"
:job=
"group.jobs[0]"
:job=
"group.jobs[0]"
:job-hovered=
"jobHovered"
:job-hovered=
"jobHovered"
:pipeline-expanded=
"pipelineExpanded"
:pipeline-expanded=
"pipelineExpanded"
:pipeline-id=
"pipelineId"
css-class-job-name=
"gl-build-content"
css-class-job-name=
"gl-build-content"
@
pipelineActionRequestComplete=
"$emit('refreshPipelineGraph')"
@
pipelineActionRequestComplete=
"$emit('refreshPipelineGraph')"
/>
/>
<job-group-dropdown
v-else
:group=
"group"
/>
<job-group-dropdown
v-else
:group=
"group"
:pipeline-id=
"pipelineId"
/>
</div>
</div>
</
template
>
</
template
>
</main-graph-wrapper>
</main-graph-wrapper>
...
...
app/assets/javascripts/pipelines/components/
pipeline_graph
/drawing_utils.js
→
app/assets/javascripts/pipelines/components/
graph_shared
/drawing_utils.js
View file @
c1f89970
import
*
as
d3
from
'
d3
'
;
import
*
as
d3
from
'
d3
'
;
import
{
createUniqueLinkId
}
from
'
../../utils
'
;
export
const
createUniqueLinkId
=
(
stageName
,
jobName
)
=>
`
${
stageName
}
-
${
jobName
}
`
;
/**
/**
* This function expects its first argument data structure
* This function expects its first argument data structure
* to be the same shaped as the one generated by `parseData`,
* to be the same shaped as the one generated by `parseData`,
...
@@ -7,12 +9,11 @@ import { createUniqueLinkId } from '../../utils';
...
@@ -7,12 +9,11 @@ import { createUniqueLinkId } from '../../utils';
* we find the nodes in the graph, calculate their coordinates and
* we find the nodes in the graph, calculate their coordinates and
* trace the lines that represent the needs of each job.
* trace the lines that represent the needs of each job.
* @param {Object} nodeDict - Resulting object of `parseData` with nodes and links
* @param {Object} nodeDict - Resulting object of `parseData` with nodes and links
* @param {Object} jobs - An object where each key is the job name that contains the job data
* @param {String} containerID - Id for the svg the links will be draw in
* @param {ref} svg - Reference to the svg we draw in
* @returns {Array} Links that contain all the information about them
* @returns {Array} Links that contain all the information about them
*/
*/
export
const
generateLinksData
=
({
links
},
containerID
)
=>
{
export
const
generateLinksData
=
({
links
},
containerID
,
modifier
=
''
)
=>
{
const
containerEl
=
document
.
getElementById
(
containerID
);
const
containerEl
=
document
.
getElementById
(
containerID
);
return
links
.
map
((
link
)
=>
{
return
links
.
map
((
link
)
=>
{
const
path
=
d3
.
path
();
const
path
=
d3
.
path
();
...
@@ -20,8 +21,11 @@ export const generateLinksData = ({ links }, containerID) => {
...
@@ -20,8 +21,11 @@ export const generateLinksData = ({ links }, containerID) => {
const
sourceId
=
link
.
source
;
const
sourceId
=
link
.
source
;
const
targetId
=
link
.
target
;
const
targetId
=
link
.
target
;
const
sourceNodeEl
=
document
.
getElementById
(
sourceId
);
const
modifiedSourceId
=
`
${
sourceId
}${
modifier
}
`
;
const
targetNodeEl
=
document
.
getElementById
(
targetId
);
const
modifiedTargetId
=
`
${
targetId
}${
modifier
}
`
;
const
sourceNodeEl
=
document
.
getElementById
(
modifiedSourceId
);
const
targetNodeEl
=
document
.
getElementById
(
modifiedTargetId
);
const
sourceNodeCoordinates
=
sourceNodeEl
.
getBoundingClientRect
();
const
sourceNodeCoordinates
=
sourceNodeEl
.
getBoundingClientRect
();
const
targetNodeCoordinates
=
targetNodeEl
.
getBoundingClientRect
();
const
targetNodeCoordinates
=
targetNodeEl
.
getBoundingClientRect
();
...
@@ -35,11 +39,11 @@ export const generateLinksData = ({ links }, containerID) => {
...
@@ -35,11 +39,11 @@ export const generateLinksData = ({ links }, containerID) => {
// from the total to make sure it's aligned properly. We then make the line
// from the total to make sure it's aligned properly. We then make the line
// positioned in the center of the job node by adding half the height
// positioned in the center of the job node by adding half the height
// of the job pill.
// of the job pill.
const
paddingLeft
=
Number
(
const
paddingLeft
=
parseFloat
(
window
.
getComputedStyle
(
containerEl
,
null
).
getPropertyValue
(
'
padding-left
'
)
.
replace
(
'
px
'
,
''
)
,
window
.
getComputedStyle
(
containerEl
,
null
).
getPropertyValue
(
'
padding-left
'
),
);
);
const
paddingTop
=
Number
(
const
paddingTop
=
parseFloat
(
window
.
getComputedStyle
(
containerEl
,
null
).
getPropertyValue
(
'
padding-top
'
)
.
replace
(
'
px
'
,
''
)
,
window
.
getComputedStyle
(
containerEl
,
null
).
getPropertyValue
(
'
padding-top
'
),
);
);
const
sourceNodeX
=
sourceNodeCoordinates
.
right
-
containerCoordinates
.
x
-
paddingLeft
;
const
sourceNodeX
=
sourceNodeCoordinates
.
right
-
containerCoordinates
.
x
-
paddingLeft
;
...
...
app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue
0 → 100644
View file @
c1f89970
<
script
>
import
{
isEmpty
}
from
'
lodash
'
;
import
{
DRAW_FAILURE
}
from
'
../../constants
'
;
import
{
createJobsHash
,
generateJobNeedsDict
}
from
'
../../utils
'
;
import
{
parseData
}
from
'
../parsing_utils
'
;
import
{
generateLinksData
}
from
'
./drawing_utils
'
;
export
default
{
name
:
'
LinksInner
'
,
STROKE_WIDTH
:
2
,
props
:
{
containerId
:
{
type
:
String
,
required
:
true
,
},
containerMeasurements
:
{
type
:
Object
,
required
:
true
,
},
pipelineId
:
{
type
:
Number
,
required
:
true
,
},
pipelineData
:
{
type
:
Array
,
required
:
true
,
},
defaultLinkColor
:
{
type
:
String
,
required
:
false
,
default
:
'
gl-stroke-gray-200
'
,
},
highlightedJob
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
},
data
()
{
return
{
links
:
[],
needsObject
:
null
,
};
},
computed
:
{
hasHighlightedJob
()
{
return
Boolean
(
this
.
highlightedJob
);
},
isPipelineDataEmpty
()
{
return
isEmpty
(
this
.
pipelineData
);
},
highlightedJobs
()
{
// If you are hovering on a job, then the jobs we want to highlight are:
// The job you are currently hovering + all of its needs.
return
this
.
hasHighlightedJob
?
[
this
.
highlightedJob
,
...
this
.
needsObject
[
this
.
highlightedJob
]]
:
[];
},
highlightedLinks
()
{
// If you are hovering on a job, then the links we want to highlight are:
// All the links whose `source` and `target` are highlighted jobs.
if
(
this
.
hasHighlightedJob
)
{
const
filteredLinks
=
this
.
links
.
filter
((
link
)
=>
{
return
(
this
.
highlightedJobs
.
includes
(
link
.
source
)
&&
this
.
highlightedJobs
.
includes
(
link
.
target
)
);
});
return
filteredLinks
.
map
((
link
)
=>
link
.
ref
);
}
return
[];
},
viewBox
()
{
return
[
0
,
0
,
this
.
containerMeasurements
.
width
,
this
.
containerMeasurements
.
height
];
},
},
watch
:
{
highlightedJob
()
{
// On first hover, generate the needs reference
if
(
!
this
.
needsObject
)
{
const
jobs
=
createJobsHash
(
this
.
pipelineData
);
this
.
needsObject
=
generateJobNeedsDict
(
jobs
)
??
{};
}
},
},
mounted
()
{
if
(
!
isEmpty
(
this
.
pipelineData
))
{
this
.
prepareLinkData
();
}
},
methods
:
{
isLinkHighlighted
(
linkRef
)
{
return
this
.
highlightedLinks
.
includes
(
linkRef
);
},
prepareLinkData
()
{
try
{
const
arrayOfJobs
=
this
.
pipelineData
.
flatMap
(({
groups
})
=>
groups
);
const
parsedData
=
parseData
(
arrayOfJobs
);
this
.
links
=
generateLinksData
(
parsedData
,
this
.
containerId
,
`-
${
this
.
pipelineId
}
`
);
}
catch
{
this
.
$emit
(
'
error
'
,
DRAW_FAILURE
);
}
},
getLinkClasses
(
link
)
{
return
[
this
.
isLinkHighlighted
(
link
.
ref
)
?
'
gl-stroke-blue-400
'
:
this
.
defaultLinkColor
,
{
'
gl-opacity-3
'
:
this
.
hasHighlightedJob
&&
!
this
.
isLinkHighlighted
(
link
.
ref
)
},
];
},
},
};
</
script
>
<
template
>
<div
class=
"gl-display-flex gl-relative"
>
<svg
id=
"link-svg"
class=
"gl-absolute"
:viewBox=
"viewBox"
:width=
"`$
{containerMeasurements.width}px`"
:height="`${containerMeasurements.height}px`"
>
<template>
<path
v-for=
"link in links"
:key=
"link.path"
:ref=
"link.ref"
:d=
"link.path"
class=
"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease"
:class=
"getLinkClasses(link)"
:stroke-width=
"$options.STROKE_WIDTH"
/>
</
template
>
</svg>
<slot></slot>
</div>
</template>
app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue
0 → 100644
View file @
c1f89970
<
script
>
import
{
GlAlert
}
from
'
@gitlab/ui
'
;
import
{
__
}
from
'
~/locale
'
;
import
LinksInner
from
'
./links_inner.vue
'
;
export
default
{
name
:
'
LinksLayer
'
,
components
:
{
GlAlert
,
LinksInner
,
},
MAX_GROUPS
:
200
,
props
:
{
containerMeasurements
:
{
type
:
Object
,
required
:
true
,
},
pipelineData
:
{
type
:
Array
,
required
:
true
,
},
},
data
()
{
return
{
alertDismissed
:
false
,
showLinksOverride
:
false
,
};
},
i18n
:
{
showLinksAnyways
:
__
(
'
Show links anyways
'
),
tooManyJobs
:
__
(
'
This graph has a large number of jobs and showing the links between them may have performance implications.
'
,
),
},
computed
:
{
containerZero
()
{
return
!
this
.
containerMeasurements
.
width
||
!
this
.
containerMeasurements
.
height
;
},
numGroups
()
{
return
this
.
pipelineData
.
reduce
((
acc
,
{
groups
})
=>
{
return
acc
+
Number
(
groups
.
length
);
},
0
);
},
showAlert
()
{
return
!
this
.
showLinkedLayers
&&
!
this
.
alertDismissed
;
},
showLinkedLayers
()
{
return
(
!
this
.
containerZero
&&
(
this
.
showLinksOverride
||
this
.
numGroups
<
this
.
$options
.
MAX_GROUPS
)
);
},
},
methods
:
{
dismissAlert
()
{
this
.
alertDismissed
=
true
;
},
overrideShowLinks
()
{
this
.
dismissAlert
();
this
.
showLinksOverride
=
true
;
},
},
};
</
script
>
<
template
>
<links-inner
v-if=
"showLinkedLayers"
:container-measurements=
"containerMeasurements"
:pipeline-data=
"pipelineData"
v-bind=
"$attrs"
v-on=
"$listeners"
>
<slot></slot>
</links-inner>
<div
v-else
>
<gl-alert
v-if=
"showAlert"
class=
"gl-w-max-content gl-ml-4"
:primary-button-text=
"$options.i18n.showLinksAnyways"
@
primaryAction=
"overrideShowLinks"
@
dismiss=
"dismissAlert"
>
{{
$options
.
i18n
.
tooManyJobs
}}
</gl-alert>
<slot></slot>
</div>
</
template
>
app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
View file @
c1f89970
<
script
>
<
script
>
import
{
GlAlert
}
from
'
@gitlab/ui
'
;
import
{
GlAlert
}
from
'
@gitlab/ui
'
;
import
{
__
}
from
'
~/locale
'
;
import
{
__
}
from
'
~/locale
'
;
import
{
generateLinksData
}
from
'
../graph_shared/drawing_utils
'
;
import
JobPill
from
'
./job_pill.vue
'
;
import
JobPill
from
'
./job_pill.vue
'
;
import
StagePill
from
'
./stage_pill.vue
'
;
import
StagePill
from
'
./stage_pill.vue
'
;
import
{
generateLinksData
}
from
'
./drawing_utils
'
;
import
{
parseData
}
from
'
../parsing_utils
'
;
import
{
parseData
}
from
'
../parsing_utils
'
;
import
{
DRAW_FAILURE
,
DEFAULT
,
INVALID_CI_CONFIG
,
EMPTY_PIPELINE_DATA
}
from
'
../../constants
'
;
import
{
DRAW_FAILURE
,
DEFAULT
,
INVALID_CI_CONFIG
,
EMPTY_PIPELINE_DATA
}
from
'
../../constants
'
;
import
{
createJobsHash
,
generateJobNeedsDict
}
from
'
../../utils
'
;
import
{
createJobsHash
,
generateJobNeedsDict
}
from
'
../../utils
'
;
...
...
app/assets/javascripts/pipelines/pipeline_details_bundle.js
View file @
c1f89970
...
@@ -158,7 +158,7 @@ export default async function () {
...
@@ -158,7 +158,7 @@ export default async function () {
);
);
const
{
pipelineProjectPath
,
pipelineIid
}
=
dataset
;
const
{
pipelineProjectPath
,
pipelineIid
}
=
dataset
;
createPipelinesDetailApp
(
SELECTORS
.
PIPELINE_
DETAILS
,
pipelineProjectPath
,
pipelineIid
);
createPipelinesDetailApp
(
SELECTORS
.
PIPELINE_
GRAPH
,
pipelineProjectPath
,
pipelineIid
);
}
catch
{
}
catch
{
Flash
(
__
(
'
An error occurred while loading the pipeline.
'
));
Flash
(
__
(
'
An error occurred while loading the pipeline.
'
));
}
}
...
...
app/assets/javascripts/pipelines/utils.js
View file @
c1f89970
...
@@ -6,8 +6,6 @@ export const validateParams = (params) => {
...
@@ -6,8 +6,6 @@ export const validateParams = (params) => {
return
pickBy
(
params
,
(
val
,
key
)
=>
SUPPORTED_FILTER_PARAMETERS
.
includes
(
key
)
&&
val
);
return
pickBy
(
params
,
(
val
,
key
)
=>
SUPPORTED_FILTER_PARAMETERS
.
includes
(
key
)
&&
val
);
};
};
export
const
createUniqueLinkId
=
(
stageName
,
jobName
)
=>
`
${
stageName
}
-
${
jobName
}
`
;
/**
/**
* This function takes the stages array and transform it
* This function takes the stages array and transform it
* into a hash where each key is a job name and the job data
* into a hash where each key is a job name and the job data
...
...
locale/gitlab.pot
View file @
c1f89970
...
@@ -25947,6 +25947,9 @@ msgstr ""
...
@@ -25947,6 +25947,9 @@ msgstr ""
msgid "Show latest version"
msgid "Show latest version"
msgstr ""
msgstr ""
msgid "Show links anyways"
msgstr ""
msgid "Show list"
msgid "Show list"
msgstr ""
msgstr ""
...
@@ -28799,6 +28802,9 @@ msgstr ""
...
@@ -28799,6 +28802,9 @@ msgstr ""
msgid "This field is required."
msgid "This field is required."
msgstr ""
msgstr ""
msgid "This graph has a large number of jobs and showing the links between them may have performance implications."
msgstr ""
msgid "This group"
msgid "This group"
msgstr ""
msgstr ""
...
...
spec/frontend/pipeline_editor/mock_data.js
View file @
c1f89970
...
@@ -30,7 +30,7 @@ job_test_2:
...
@@ -30,7 +30,7 @@ job_test_2:
job_build:
job_build:
stage: build
stage: build
script:
script:
- echo "build"
- echo "build"
needs: ["job_test_2"]
needs: ["job_test_2"]
`
;
`
;
...
...
spec/frontend/pipelines/graph/graph_component_spec.js
View file @
c1f89970
...
@@ -2,6 +2,7 @@ import { mount, shallowMount } from '@vue/test-utils';
...
@@ -2,6 +2,7 @@ import { mount, shallowMount } from '@vue/test-utils';
import
PipelineGraph
from
'
~/pipelines/components/graph/graph_component.vue
'
;
import
PipelineGraph
from
'
~/pipelines/components/graph/graph_component.vue
'
;
import
StageColumnComponent
from
'
~/pipelines/components/graph/stage_column_component.vue
'
;
import
StageColumnComponent
from
'
~/pipelines/components/graph/stage_column_component.vue
'
;
import
LinkedPipelinesColumn
from
'
~/pipelines/components/graph/linked_pipelines_column.vue
'
;
import
LinkedPipelinesColumn
from
'
~/pipelines/components/graph/linked_pipelines_column.vue
'
;
import
LinksLayer
from
'
~/pipelines/components/graph_shared/links_layer.vue
'
;
import
{
GRAPHQL
}
from
'
~/pipelines/components/graph/constants
'
;
import
{
GRAPHQL
}
from
'
~/pipelines/components/graph/constants
'
;
import
{
import
{
generateResponse
,
generateResponse
,
...
@@ -13,6 +14,7 @@ describe('graph component', () => {
...
@@ -13,6 +14,7 @@ describe('graph component', () => {
let
wrapper
;
let
wrapper
;
const
findLinkedColumns
=
()
=>
wrapper
.
findAll
(
LinkedPipelinesColumn
);
const
findLinkedColumns
=
()
=>
wrapper
.
findAll
(
LinkedPipelinesColumn
);
const
findLinksLayer
=
()
=>
wrapper
.
find
(
LinksLayer
);
const
findStageColumns
=
()
=>
wrapper
.
findAll
(
StageColumnComponent
);
const
findStageColumns
=
()
=>
wrapper
.
findAll
(
StageColumnComponent
);
const
defaultProps
=
{
const
defaultProps
=
{
...
@@ -28,6 +30,9 @@ describe('graph component', () => {
...
@@ -28,6 +30,9 @@ describe('graph component', () => {
provide
:
{
provide
:
{
dataMethod
:
GRAPHQL
,
dataMethod
:
GRAPHQL
,
},
},
stubs
:
{
'
links-inner
'
:
true
,
},
});
});
};
};
...
@@ -45,6 +50,10 @@ describe('graph component', () => {
...
@@ -45,6 +50,10 @@ describe('graph component', () => {
expect
(
findStageColumns
()).
toHaveLength
(
defaultProps
.
pipeline
.
stages
.
length
);
expect
(
findStageColumns
()).
toHaveLength
(
defaultProps
.
pipeline
.
stages
.
length
);
});
});
it
(
'
renders the links layer
'
,
()
=>
{
expect
(
findLinksLayer
().
exists
()).
toBe
(
true
);
});
describe
(
'
when column requests a refresh
'
,
()
=>
{
describe
(
'
when column requests a refresh
'
,
()
=>
{
beforeEach
(()
=>
{
beforeEach
(()
=>
{
findStageColumns
().
at
(
0
).
vm
.
$emit
(
'
refreshPipelineGraph
'
);
findStageColumns
().
at
(
0
).
vm
.
$emit
(
'
refreshPipelineGraph
'
);
...
...
spec/frontend/pipelines/graph/stage_column_component_spec.js
View file @
c1f89970
...
@@ -30,6 +30,7 @@ const mockGroups = Array(4)
...
@@ -30,6 +30,7 @@ const mockGroups = Array(4)
const
defaultProps
=
{
const
defaultProps
=
{
title
:
'
Fish
'
,
title
:
'
Fish
'
,
groups
:
mockGroups
,
groups
:
mockGroups
,
pipelineId
:
159
,
};
};
describe
(
'
stage column component
'
,
()
=>
{
describe
(
'
stage column component
'
,
()
=>
{
...
@@ -92,36 +93,51 @@ describe('stage column component', () => {
...
@@ -92,36 +93,51 @@ describe('stage column component', () => {
});
});
describe
(
'
job
'
,
()
=>
{
describe
(
'
job
'
,
()
=>
{
beforeEach
(()
=>
{
describe
(
'
text handling
'
,
()
=>
{
createComponent
({
beforeEach
(()
=>
{
method
:
mount
,
createComponent
({
props
:
{
method
:
mount
,
groups
:
[
props
:
{
{
groups
:
[
id
:
4259
,
{
name
:
'
<img src=x onerror=alert(document.domain)>
'
,
id
:
4259
,
status
:
{
name
:
'
<img src=x onerror=alert(document.domain)>
'
,
icon
:
'
status_success
'
,
status
:
{
label
:
'
success
'
,
icon
:
'
status_success
'
,
tooltip
:
'
<img src=x onerror=alert(document.domain)>
'
,
label
:
'
success
'
,
tooltip
:
'
<img src=x onerror=alert(document.domain)>
'
,
},
},
},
}
,
]
,
]
,
title
:
'
test <img src=x onerror=alert(document.domain)>
'
,
title
:
'
test <img src=x onerror=alert(document.domain)>
'
,
}
,
}
,
}
);
});
});
});
it
(
'
capitalizes and escapes name
'
,
()
=>
{
it
(
'
capitalizes and escapes name
'
,
()
=>
{
expect
(
findStageColumnTitle
().
text
()).
toBe
(
expect
(
findStageColumnTitle
().
text
()).
toBe
(
'
Test <img src=x onerror=alert(document.domain)>
'
,
'
Test <img src=x onerror=alert(document.domain)>
'
,
);
);
});
it
(
'
escapes id
'
,
()
=>
{
expect
(
findStageColumnGroup
().
attributes
(
'
id
'
)).
toBe
(
'
ci-badge-<img src=x onerror=alert(document.domain)>
'
,
);
});
});
});
it
(
'
escapes id
'
,
()
=>
{
describe
(
'
interactions
'
,
()
=>
{
expect
(
findStageColumnGroup
().
attributes
(
'
id
'
)).
toBe
(
beforeEach
(()
=>
{
'
ci-badge-<img src=x onerror=alert(document.domain)>
'
,
createComponent
({
method
:
mount
});
);
});
it
(
'
emits jobHovered event on mouseenter and mouseleave
'
,
async
()
=>
{
await
findStageColumnGroup
().
trigger
(
'
mouseenter
'
);
expect
(
wrapper
.
emitted
().
jobHover
).
toEqual
([[
defaultProps
.
groups
[
0
].
name
]]);
await
findStageColumnGroup
().
trigger
(
'
mouseleave
'
);
expect
(
wrapper
.
emitted
().
jobHover
).
toEqual
([[
defaultProps
.
groups
[
0
].
name
],
[
''
]]);
});
});
});
});
});
...
...
spec/frontend/pipelines/pipeline_graph/mock_data.js
View file @
c1f89970
import
{
createUniqueLinkId
}
from
'
~/pipelines/utils
'
;
import
{
createUniqueLinkId
}
from
'
~/pipelines/
components/graph_shared/drawing_
utils
'
;
export
const
yamlString
=
`stages:
export
const
yamlString
=
`stages:
- empty
- empty
...
...
spec/frontend/pipelines/shared/links_layer_spec.js
0 → 100644
View file @
c1f89970
import
{
mount
,
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
GlAlert
,
GlButton
}
from
'
@gitlab/ui
'
;
import
LinksLayer
from
'
~/pipelines/components/graph_shared/links_layer.vue
'
;
import
LinksInner
from
'
~/pipelines/components/graph_shared/links_inner.vue
'
;
import
{
generateResponse
,
mockPipelineResponse
}
from
'
../graph/mock_data
'
;
describe
(
'
links layer component
'
,
()
=>
{
let
wrapper
;
const
findAlert
=
()
=>
wrapper
.
find
(
GlAlert
);
const
findShowAnyways
=
()
=>
findAlert
().
find
(
GlButton
);
const
findLinksInner
=
()
=>
wrapper
.
find
(
LinksInner
);
const
pipeline
=
generateResponse
(
mockPipelineResponse
,
'
root/fungi-xoxo
'
);
const
containerId
=
`pipeline-links-container-
${
pipeline
.
id
}
`
;
const
slotContent
=
"
<div>Ceci n'est pas un graphique</div>
"
;
const
tooManyStages
=
Array
(
101
)
.
fill
(
0
)
.
flatMap
(()
=>
pipeline
.
stages
);
const
defaultProps
=
{
containerId
,
containerMeasurements
:
{
width
:
400
,
height
:
400
},
pipelineId
:
pipeline
.
id
,
pipelineData
:
pipeline
.
stages
,
};
const
createComponent
=
({
mountFn
=
shallowMount
,
props
=
{}
}
=
{})
=>
{
wrapper
=
mountFn
(
LinksLayer
,
{
propsData
:
{
...
defaultProps
,
...
props
,
},
slots
:
{
default
:
slotContent
,
},
stubs
:
{
'
links-inner
'
:
true
,
},
});
};
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
=
null
;
});
describe
(
'
with data under max stages
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
();
});
it
(
'
renders the default slot
'
,
()
=>
{
expect
(
wrapper
.
html
()).
toContain
(
slotContent
);
});
it
(
'
renders the inner links component
'
,
()
=>
{
expect
(
findLinksInner
().
exists
()).
toBe
(
true
);
});
});
describe
(
'
with more than the max number of stages
'
,
()
=>
{
describe
(
'
rendering
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
({
props
:
{
pipelineData
:
tooManyStages
}
});
});
it
(
'
renders the default slot
'
,
()
=>
{
expect
(
wrapper
.
html
()).
toContain
(
slotContent
);
});
it
(
'
renders the alert component
'
,
()
=>
{
expect
(
findAlert
().
exists
()).
toBe
(
true
);
});
it
(
'
does not render the inner links component
'
,
()
=>
{
expect
(
findLinksInner
().
exists
()).
toBe
(
false
);
});
});
describe
(
'
interactions
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
({
mountFn
:
mount
,
props
:
{
pipelineData
:
tooManyStages
}
});
});
it
(
'
renders the disable button
'
,
()
=>
{
expect
(
findShowAnyways
().
exists
()).
toBe
(
true
);
expect
(
findShowAnyways
().
text
()).
toBe
(
wrapper
.
vm
.
$options
.
i18n
.
showLinksAnyways
);
});
it
(
'
shows links when override is clicked
'
,
async
()
=>
{
expect
(
findLinksInner
().
exists
()).
toBe
(
false
);
await
findShowAnyways
().
trigger
(
'
click
'
);
expect
(
findLinksInner
().
exists
()).
toBe
(
true
);
});
});
});
});
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