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
166c573f
Commit
166c573f
authored
Jun 05, 2020
by
Sarah Groff Hennigh-Palermo
Committed by
Andrew Fontaine
Jun 05, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add constant selectors
Allows selectors to be constant across files
parent
c78ada3f
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
303 additions
and
23 deletions
+303
-23
app/assets/javascripts/pipelines/components/dag/constants.js
app/assets/javascripts/pipelines/components/dag/constants.js
+6
-0
app/assets/javascripts/pipelines/components/dag/dag_graph.vue
...assets/javascripts/pipelines/components/dag/dag_graph.vue
+40
-21
app/assets/javascripts/pipelines/components/dag/interactions.js
...sets/javascripts/pipelines/components/dag/interactions.js
+134
-0
spec/frontend/pipelines/components/dag/dag_graph_spec.js
spec/frontend/pipelines/components/dag/dag_graph_spec.js
+123
-2
No files found.
app/assets/javascripts/pipelines/components/dag/constants.js
View file @
166c573f
/* Error constants */
export
const
PARSE_FAILURE
=
'
parse_failure
'
;
export
const
LOAD_FAILURE
=
'
load_failure
'
;
export
const
UNSUPPORTED_DATA
=
'
unsupported_data
'
;
export
const
DEFAULT
=
'
default
'
;
/* Interaction handles */
export
const
IS_HIGHLIGHTED
=
'
dag-highlighted
'
;
export
const
LINK_SELECTOR
=
'
dag-link
'
;
export
const
NODE_SELECTOR
=
'
dag-node
'
;
app/assets/javascripts/pipelines/components/dag/dag_graph.vue
View file @
166c573f
<
script
>
import
*
as
d3
from
'
d3
'
;
import
{
uniqueId
}
from
'
lodash
'
;
import
{
PARSE_FAILURE
}
from
'
./constants
'
;
import
{
LINK_SELECTOR
,
NODE_SELECTOR
,
PARSE_FAILURE
}
from
'
./constants
'
;
import
{
highlightLinks
,
restoreLinks
,
toggleLinkHighlight
,
togglePathHighlights
,
}
from
'
./interactions
'
;
import
{
getMaxNodes
,
removeOrphanNodes
}
from
'
./parsing_utils
'
;
import
{
calculateClip
,
createLinkPath
,
createSankey
,
labelPosition
}
from
'
./drawing_utils
'
;
...
...
@@ -16,11 +21,7 @@ export default {
paddingForLabels
:
100
,
labelMargin
:
8
,
// can plausibly applied through CSS instead, TBD
baseOpacity
:
0.8
,
highlightIn
:
1
,
highlightOut
:
0.2
,
containerClasses
:
[
'
dag-graph-container
'
,
'
gl-display-flex
'
,
'
gl-flex-direction-column
'
].
join
(
'
'
,
),
...
...
@@ -88,6 +89,20 @@ export default {
);
},
appendLinkInteractions
(
link
)
{
return
link
.
on
(
'
mouseover
'
,
highlightLinks
)
.
on
(
'
mouseout
'
,
restoreLinks
.
bind
(
null
,
this
.
$options
.
viewOptions
.
baseOpacity
))
.
on
(
'
click
'
,
toggleLinkHighlight
.
bind
(
null
,
this
.
$options
.
viewOptions
.
baseOpacity
));
},
appendNodeInteractions
(
node
)
{
return
node
.
on
(
'
click
'
,
togglePathHighlights
.
bind
(
null
,
this
.
$options
.
viewOptions
.
baseOpacity
),
);
},
appendLabelAsForeignObject
(
d
,
i
,
n
)
{
const
currentNode
=
n
[
i
];
const
{
height
,
wrapperWidth
,
width
,
x
,
y
,
textAlign
}
=
labelPosition
(
d
,
{
...
...
@@ -163,15 +178,17 @@ export default {
},
createLinks
(
svg
,
linksData
)
{
const
link
=
this
.
generateLinks
(
svg
,
linksData
);
this
.
createGradient
(
link
);
this
.
createClip
(
link
);
this
.
appendLinks
(
link
);
const
links
=
this
.
generateLinks
(
svg
,
linksData
);
this
.
createGradient
(
links
);
this
.
createClip
(
links
);
this
.
appendLinks
(
links
);
this
.
appendLinkInteractions
(
links
);
},
createNodes
(
svg
,
nodeData
)
{
this
.
generateNodes
(
svg
,
nodeData
);
const
nodes
=
this
.
generateNodes
(
svg
,
nodeData
);
this
.
labelNodes
(
svg
,
nodeData
);
this
.
appendNodeInteractions
(
nodes
);
},
drawGraph
({
maxNodesPerLayer
,
linksAndNodes
})
{
...
...
@@ -202,37 +219,39 @@ export default {
},
generateLinks
(
svg
,
linksData
)
{
const
linkContainerName
=
'
dag-link
'
;
return
svg
.
append
(
'
g
'
)
.
attr
(
'
fill
'
,
'
none
'
)
.
attr
(
'
stroke-opacity
'
,
this
.
$options
.
viewOptions
.
baseOpacity
)
.
selectAll
(
`.
${
linkContainerName
}
`
)
.
selectAll
(
`.
${
LINK_SELECTOR
}
`
)
.
data
(
linksData
)
.
enter
()
.
append
(
'
g
'
)
.
attr
(
'
id
'
,
d
=>
{
return
this
.
createAndAssignId
(
d
,
'
uid
'
,
linkContainerName
);
return
this
.
createAndAssignId
(
d
,
'
uid
'
,
LINK_SELECTOR
);
})
.
classed
(
`
${
linkContainerName
}
gl-cursor-pointer`
,
true
);
.
classed
(
`
${
LINK_SELECTOR
}
gl-cursor-pointer`
,
true
);
},
generateNodes
(
svg
,
nodeData
)
{
const
nodeContainerName
=
'
dag-node
'
;
const
{
nodeWidth
}
=
this
.
$options
.
viewOptions
;
return
svg
.
append
(
'
g
'
)
.
selectAll
(
`.
${
nodeContainerName
}
`
)
.
selectAll
(
`.
${
NODE_SELECTOR
}
`
)
.
data
(
nodeData
)
.
enter
()
.
append
(
'
line
'
)
.
classed
(
`
${
nodeContainerName
}
gl-cursor-pointer`
,
true
)
.
classed
(
`
${
NODE_SELECTOR
}
gl-cursor-pointer`
,
true
)
.
attr
(
'
id
'
,
d
=>
{
return
this
.
createAndAssignId
(
d
,
'
uid
'
,
nodeContainerName
);
return
this
.
createAndAssignId
(
d
,
'
uid
'
,
NODE_SELECTOR
);
})
.
attr
(
'
stroke
'
,
d
=>
{
const
color
=
this
.
color
(
d
);
/* eslint-disable-next-line no-param-reassign */
d
.
color
=
color
;
return
color
;
})
.
attr
(
'
stroke
'
,
this
.
color
)
.
attr
(
'
stroke-width
'
,
nodeWidth
)
.
attr
(
'
stroke-linecap
'
,
'
round
'
)
.
attr
(
'
x1
'
,
d
=>
Math
.
floor
((
d
.
x1
+
d
.
x0
)
/
2
))
...
...
app/assets/javascripts/pipelines/components/dag/interactions.js
0 → 100644
View file @
166c573f
import
*
as
d3
from
'
d3
'
;
import
{
LINK_SELECTOR
,
NODE_SELECTOR
,
IS_HIGHLIGHTED
}
from
'
./constants
'
;
export
const
highlightIn
=
1
;
export
const
highlightOut
=
0.2
;
const
getCurrent
=
(
idx
,
collection
)
=>
d3
.
select
(
collection
[
idx
]);
const
currentIsLive
=
(
idx
,
collection
)
=>
getCurrent
(
idx
,
collection
).
classed
(
IS_HIGHLIGHTED
);
const
getOtherLinks
=
()
=>
d3
.
selectAll
(
`.
${
LINK_SELECTOR
}
:not(.
${
IS_HIGHLIGHTED
}
)`
);
const
getNodesNotLive
=
()
=>
d3
.
selectAll
(
`.
${
NODE_SELECTOR
}
:not(.
${
IS_HIGHLIGHTED
}
)`
);
const
backgroundLinks
=
selection
=>
selection
.
style
(
'
stroke-opacity
'
,
highlightOut
);
const
backgroundNodes
=
selection
=>
selection
.
attr
(
'
stroke
'
,
'
#f2f2f2
'
);
const
foregroundLinks
=
selection
=>
selection
.
style
(
'
stroke-opacity
'
,
highlightIn
);
const
foregroundNodes
=
selection
=>
selection
.
attr
(
'
stroke
'
,
d
=>
d
.
color
);
const
renewLinks
=
(
selection
,
baseOpacity
)
=>
selection
.
style
(
'
stroke-opacity
'
,
baseOpacity
);
const
renewNodes
=
selection
=>
selection
.
attr
(
'
stroke
'
,
d
=>
d
.
color
);
const
getAllLinkAncestors
=
node
=>
{
if
(
node
.
targetLinks
)
{
return
node
.
targetLinks
.
flatMap
(
n
=>
{
return
[
n
.
uid
,
...
getAllLinkAncestors
(
n
.
source
)];
});
}
return
[];
};
const
getAllNodeAncestors
=
node
=>
{
let
allNodes
=
[];
if
(
node
.
targetLinks
)
{
allNodes
=
node
.
targetLinks
.
flatMap
(
n
=>
{
return
getAllNodeAncestors
(
n
.
source
);
});
}
return
[...
allNodes
,
node
.
uid
];
};
export
const
highlightLinks
=
(
d
,
idx
,
collection
)
=>
{
const
currentLink
=
getCurrent
(
idx
,
collection
);
const
currentSourceNode
=
d3
.
select
(
`#
${
d
.
source
.
uid
}
`
);
const
currentTargetNode
=
d3
.
select
(
`#
${
d
.
target
.
uid
}
`
);
/* Higlight selected link, de-emphasize others */
backgroundLinks
(
getOtherLinks
());
foregroundLinks
(
currentLink
);
/* Do the same to related nodes */
backgroundNodes
(
getNodesNotLive
());
foregroundNodes
(
currentSourceNode
);
foregroundNodes
(
currentTargetNode
);
};
const
highlightPath
=
(
parentLinks
,
parentNodes
)
=>
{
/* de-emphasize everything else */
backgroundLinks
(
getOtherLinks
());
backgroundNodes
(
getNodesNotLive
());
/* highlight correct links */
parentLinks
.
forEach
(
id
=>
{
foregroundLinks
(
d3
.
select
(
`#
${
id
}
`
)).
classed
(
IS_HIGHLIGHTED
,
true
);
});
/* highlight correct nodes */
parentNodes
.
forEach
(
id
=>
{
foregroundNodes
(
d3
.
select
(
`#
${
id
}
`
)).
classed
(
IS_HIGHLIGHTED
,
true
);
});
};
const
restorePath
=
(
parentLinks
,
parentNodes
,
baseOpacity
)
=>
{
parentLinks
.
forEach
(
id
=>
{
renewLinks
(
d3
.
select
(
`#
${
id
}
`
),
baseOpacity
).
classed
(
IS_HIGHLIGHTED
,
false
);
});
parentNodes
.
forEach
(
id
=>
{
d3
.
select
(
`#
${
id
}
`
).
classed
(
IS_HIGHLIGHTED
,
false
);
});
if
(
d3
.
selectAll
(
`.
${
IS_HIGHLIGHTED
}
`
).
empty
())
{
renewLinks
(
getOtherLinks
(),
baseOpacity
);
renewNodes
(
getNodesNotLive
());
return
;
}
backgroundLinks
(
getOtherLinks
());
backgroundNodes
(
getNodesNotLive
());
};
export
const
restoreLinks
=
(
baseOpacity
,
d
,
idx
,
collection
)
=>
{
/* in this case, it has just been clicked */
if
(
currentIsLive
(
idx
,
collection
))
{
return
;
}
/*
if there exist live links, reset to highlight out / pale
otherwise, reset to base
*/
if
(
d3
.
selectAll
(
`.
${
IS_HIGHLIGHTED
}
`
).
empty
())
{
renewLinks
(
d3
.
selectAll
(
`.
${
LINK_SELECTOR
}
`
),
baseOpacity
);
renewNodes
(
d3
.
selectAll
(
`.
${
NODE_SELECTOR
}
`
));
return
;
}
backgroundLinks
(
getOtherLinks
());
backgroundNodes
(
getNodesNotLive
());
};
export
const
toggleLinkHighlight
=
(
baseOpacity
,
d
,
idx
,
collection
)
=>
{
if
(
currentIsLive
(
idx
,
collection
))
{
restorePath
([
d
.
uid
],
[
d
.
source
.
uid
,
d
.
target
.
uid
],
baseOpacity
);
return
;
}
highlightPath
([
d
.
uid
],
[
d
.
source
.
uid
,
d
.
target
.
uid
]);
};
export
const
togglePathHighlights
=
(
baseOpacity
,
d
,
idx
,
collection
)
=>
{
const
parentLinks
=
getAllLinkAncestors
(
d
);
const
parentNodes
=
getAllNodeAncestors
(
d
);
const
currentNode
=
getCurrent
(
idx
,
collection
);
/* if this node is already live, make it unlive and reset its path */
if
(
currentIsLive
(
idx
,
collection
))
{
currentNode
.
classed
(
IS_HIGHLIGHTED
,
false
);
restorePath
(
parentLinks
,
parentNodes
,
baseOpacity
);
return
;
}
highlightPath
(
parentLinks
,
parentNodes
);
};
spec/frontend/pipelines/components/dag/dag_graph_spec.js
View file @
166c573f
import
{
mount
}
from
'
@vue/test-utils
'
;
import
DagGraph
from
'
~/pipelines/components/dag/dag_graph.vue
'
;
import
{
IS_HIGHLIGHTED
,
LINK_SELECTOR
,
NODE_SELECTOR
}
from
'
~/pipelines/components/dag/constants
'
;
import
{
highlightIn
,
highlightOut
}
from
'
~/pipelines/components/dag/interactions
'
;
import
{
createSankey
}
from
'
~/pipelines/components/dag/drawing_utils
'
;
import
{
removeOrphanNodes
}
from
'
~/pipelines/components/dag/parsing_utils
'
;
import
{
parsedData
}
from
'
./mock_data
'
;
...
...
@@ -8,8 +10,8 @@ describe('The DAG graph', () => {
let
wrapper
;
const
getGraph
=
()
=>
wrapper
.
find
(
'
.dag-graph-container > svg
'
);
const
getAllLinks
=
()
=>
wrapper
.
findAll
(
'
.dag-link
'
);
const
getAllNodes
=
()
=>
wrapper
.
findAll
(
'
.dag-node
'
);
const
getAllLinks
=
()
=>
wrapper
.
findAll
(
`.
${
LINK_SELECTOR
}
`
);
const
getAllNodes
=
()
=>
wrapper
.
findAll
(
`.
${
NODE_SELECTOR
}
`
);
const
getAllLabels
=
()
=>
wrapper
.
findAll
(
'
foreignObject
'
);
const
createComponent
=
(
propsData
=
{})
=>
{
...
...
@@ -94,4 +96,123 @@ describe('The DAG graph', () => {
});
});
});
describe
(
'
interactions
'
,
()
=>
{
const
strokeOpacity
=
opacity
=>
`stroke-opacity:
${
opacity
}
;`
;
const
baseOpacity
=
()
=>
wrapper
.
vm
.
$options
.
viewOptions
.
baseOpacity
;
describe
(
'
links
'
,
()
=>
{
const
liveLink
=
()
=>
getAllLinks
().
at
(
4
);
const
otherLink
=
()
=>
getAllLinks
().
at
(
1
);
describe
(
'
on hover
'
,
()
=>
{
it
(
'
sets the link opacity to baseOpacity and background links to 0.2
'
,
()
=>
{
liveLink
().
trigger
(
'
mouseover
'
);
expect
(
liveLink
().
attributes
(
'
style
'
)).
toBe
(
strokeOpacity
(
highlightIn
));
expect
(
otherLink
().
attributes
(
'
style
'
)).
toBe
(
strokeOpacity
(
highlightOut
));
});
it
(
'
reverts the styles on mouseout
'
,
()
=>
{
liveLink
().
trigger
(
'
mouseover
'
);
liveLink
().
trigger
(
'
mouseout
'
);
expect
(
liveLink
().
attributes
(
'
style
'
)).
toBe
(
strokeOpacity
(
baseOpacity
()));
expect
(
otherLink
().
attributes
(
'
style
'
)).
toBe
(
strokeOpacity
(
baseOpacity
()));
});
});
describe
(
'
on click
'
,
()
=>
{
describe
(
'
toggles link liveness
'
,
()
=>
{
it
(
'
turns link on
'
,
()
=>
{
liveLink
().
trigger
(
'
click
'
);
expect
(
liveLink
().
attributes
(
'
style
'
)).
toBe
(
strokeOpacity
(
highlightIn
));
expect
(
otherLink
().
attributes
(
'
style
'
)).
toBe
(
strokeOpacity
(
highlightOut
));
});
it
(
'
turns link off on second click
'
,
()
=>
{
liveLink
().
trigger
(
'
click
'
);
liveLink
().
trigger
(
'
click
'
);
expect
(
liveLink
().
attributes
(
'
style
'
)).
toBe
(
strokeOpacity
(
baseOpacity
()));
expect
(
otherLink
().
attributes
(
'
style
'
)).
toBe
(
strokeOpacity
(
baseOpacity
()));
});
});
it
(
'
the link remains live even after mouseout
'
,
()
=>
{
liveLink
().
trigger
(
'
click
'
);
liveLink
().
trigger
(
'
mouseout
'
);
expect
(
liveLink
().
attributes
(
'
style
'
)).
toBe
(
strokeOpacity
(
highlightIn
));
expect
(
otherLink
().
attributes
(
'
style
'
)).
toBe
(
strokeOpacity
(
highlightOut
));
});
it
(
'
preserves state when multiple links are toggled on and off
'
,
()
=>
{
const
anotherLiveLink
=
()
=>
getAllLinks
().
at
(
2
);
liveLink
().
trigger
(
'
click
'
);
anotherLiveLink
().
trigger
(
'
click
'
);
expect
(
liveLink
().
attributes
(
'
style
'
)).
toBe
(
strokeOpacity
(
highlightIn
));
expect
(
anotherLiveLink
().
attributes
(
'
style
'
)).
toBe
(
strokeOpacity
(
highlightIn
));
expect
(
otherLink
().
attributes
(
'
style
'
)).
toBe
(
strokeOpacity
(
highlightOut
));
anotherLiveLink
().
trigger
(
'
click
'
);
expect
(
liveLink
().
attributes
(
'
style
'
)).
toBe
(
strokeOpacity
(
highlightIn
));
expect
(
anotherLiveLink
().
attributes
(
'
style
'
)).
toBe
(
strokeOpacity
(
highlightOut
));
expect
(
otherLink
().
attributes
(
'
style
'
)).
toBe
(
strokeOpacity
(
highlightOut
));
liveLink
().
trigger
(
'
click
'
);
expect
(
liveLink
().
attributes
(
'
style
'
)).
toBe
(
strokeOpacity
(
baseOpacity
()));
expect
(
anotherLiveLink
().
attributes
(
'
style
'
)).
toBe
(
strokeOpacity
(
baseOpacity
()));
expect
(
otherLink
().
attributes
(
'
style
'
)).
toBe
(
strokeOpacity
(
baseOpacity
()));
});
});
});
describe
(
'
nodes
'
,
()
=>
{
const
liveNode
=
()
=>
getAllNodes
().
at
(
10
);
const
anotherLiveNode
=
()
=>
getAllNodes
().
at
(
5
);
const
nodesNotHighlighted
=
()
=>
getAllNodes
().
filter
(
n
=>
!
n
.
classes
(
IS_HIGHLIGHTED
));
const
linksNotHighlighted
=
()
=>
getAllLinks
().
filter
(
n
=>
!
n
.
classes
(
IS_HIGHLIGHTED
));
const
nodesHighlighted
=
()
=>
getAllNodes
().
filter
(
n
=>
n
.
classes
(
IS_HIGHLIGHTED
));
const
linksHighlighted
=
()
=>
getAllLinks
().
filter
(
n
=>
n
.
classes
(
IS_HIGHLIGHTED
));
describe
(
'
on click
'
,
()
=>
{
it
(
'
highlights the clicked node and predecessors
'
,
()
=>
{
liveNode
().
trigger
(
'
click
'
);
expect
(
nodesNotHighlighted
().
length
<
getAllNodes
().
length
).
toBe
(
true
);
expect
(
linksNotHighlighted
().
length
<
getAllLinks
().
length
).
toBe
(
true
);
linksHighlighted
().
wrappers
.
forEach
(
link
=>
{
expect
(
link
.
attributes
(
'
style
'
)).
toBe
(
strokeOpacity
(
highlightIn
));
});
nodesHighlighted
().
wrappers
.
forEach
(
node
=>
{
expect
(
node
.
attributes
(
'
stroke
'
)).
not
.
toBe
(
'
#f2f2f2
'
);
});
linksNotHighlighted
().
wrappers
.
forEach
(
link
=>
{
expect
(
link
.
attributes
(
'
style
'
)).
toBe
(
strokeOpacity
(
highlightOut
));
});
nodesNotHighlighted
().
wrappers
.
forEach
(
node
=>
{
expect
(
node
.
attributes
(
'
stroke
'
)).
toBe
(
'
#f2f2f2
'
);
});
});
it
(
'
toggles path off on second click
'
,
()
=>
{
liveNode
().
trigger
(
'
click
'
);
liveNode
().
trigger
(
'
click
'
);
expect
(
nodesNotHighlighted
().
length
).
toBe
(
getAllNodes
().
length
);
expect
(
linksNotHighlighted
().
length
).
toBe
(
getAllLinks
().
length
);
});
it
(
'
preserves state when multiple nodes are toggled on and off
'
,
()
=>
{
anotherLiveNode
().
trigger
(
'
click
'
);
liveNode
().
trigger
(
'
click
'
);
anotherLiveNode
().
trigger
(
'
click
'
);
expect
(
nodesNotHighlighted
().
length
<
getAllNodes
().
length
).
toBe
(
true
);
expect
(
linksNotHighlighted
().
length
<
getAllLinks
().
length
).
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