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
2ac47a71
Commit
2ac47a71
authored
Aug 18, 2020
by
Natalia Tepluhina
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'dag-graphql-fe' into 'master'
Convert DAG to Use GraphQL See merge request gitlab-org/gitlab!38085
parents
77614c16
182f2616
Changes
10
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
514 additions
and
347 deletions
+514
-347
app/assets/javascripts/pipelines/components/dag/dag.vue
app/assets/javascripts/pipelines/components/dag/dag.vue
+59
-35
app/assets/javascripts/pipelines/components/dag/parsing_utils.js
...ets/javascripts/pipelines/components/dag/parsing_utils.js
+16
-34
app/assets/javascripts/pipelines/graphql/queries/get_dag_vis_data.query.graphql
.../pipelines/graphql/queries/get_dag_vis_data.query.graphql
+27
-0
app/assets/javascripts/pipelines/pipeline_details_bundle.js
app/assets/javascripts/pipelines/pipeline_details_bundle.js
+1
-27
app/assets/javascripts/pipelines/pipeline_details_dag.js
app/assets/javascripts/pipelines/pipeline_details_dag.js
+39
-0
app/views/projects/pipelines/_with_tabs.html.haml
app/views/projects/pipelines/_with_tabs.html.haml
+1
-1
spec/frontend/pipelines/components/dag/dag_spec.js
spec/frontend/pipelines/components/dag/dag_spec.js
+34
-78
spec/frontend/pipelines/components/dag/drawing_utils_spec.js
spec/frontend/pipelines/components/dag/drawing_utils_spec.js
+2
-2
spec/frontend/pipelines/components/dag/mock_data.js
spec/frontend/pipelines/components/dag/mock_data.js
+313
-123
spec/frontend/pipelines/components/dag/parsing_utils_spec.js
spec/frontend/pipelines/components/dag/parsing_utils_spec.js
+22
-47
No files found.
app/assets/javascripts/pipelines/components/dag/dag.vue
View file @
2ac47a71
<
script
>
import
{
GlAlert
,
GlButton
,
GlEmptyState
,
GlSprintf
}
from
'
@gitlab/ui
'
;
import
{
isEmpty
}
from
'
lodash
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
{
__
}
from
'
~/locale
'
;
import
{
fetchPolicies
}
from
'
~/lib/graphql
'
;
import
getDagVisData
from
'
../../graphql/queries/get_dag_vis_data.query.graphql
'
;
import
DagGraph
from
'
./dag_graph.vue
'
;
import
DagAnnotations
from
'
./dag_annotations.vue
'
;
import
{
...
...
@@ -27,23 +28,58 @@ export default {
GlEmptyState
,
GlButton
,
},
props
:
{
graphUrl
:
{
type
:
String
,
required
:
false
,
default
:
''
,
inject
:
{
dagDocPath
:
{
default
:
null
,
},
emptySvgPath
:
{
type
:
String
,
required
:
true
,
default
:
''
,
},
dagDocPath
:
{
type
:
String
,
required
:
true
,
pipelineIid
:
{
default
:
''
,
},
pipelineProjectPath
:
{
default
:
''
,
},
},
apollo
:
{
graphData
:
{
fetchPolicy
:
fetchPolicies
.
CACHE_AND_NETWORK
,
query
:
getDagVisData
,
variables
()
{
return
{
projectPath
:
this
.
pipelineProjectPath
,
iid
:
this
.
pipelineIid
,
};
},
update
(
data
)
{
const
{
stages
:
{
nodes
:
stages
},
}
=
data
.
project
.
pipeline
;
const
unwrappedGroups
=
stages
.
map
(({
name
,
groups
:
{
nodes
:
groups
}
})
=>
{
return
groups
.
map
(
group
=>
{
return
{
category
:
name
,
...
group
};
});
})
.
flat
(
2
);
const
nodes
=
unwrappedGroups
.
map
(
group
=>
{
const
jobs
=
group
.
jobs
.
nodes
.
map
(({
name
,
needs
})
=>
{
return
{
name
,
needs
:
needs
.
nodes
.
map
(
need
=>
need
.
name
)
};
});
return
{
...
group
,
jobs
};
});
return
nodes
;
},
error
()
{
this
.
reportFailure
(
LOAD_FAILURE
);
},
},
},
data
()
{
return
{
annotationsMap
:
{},
...
...
@@ -90,32 +126,20 @@ export default {
default
:
return
{
text
:
this
.
$options
.
errorTexts
[
DEFAULT
],
va
t
iant
:
'
danger
'
,
va
r
iant
:
'
danger
'
,
};
}
},
processedData
()
{
return
this
.
processGraphData
(
this
.
graphData
);
},
shouldDisplayAnnotations
()
{
return
!
isEmpty
(
this
.
annotationsMap
);
},
shouldDisplayGraph
()
{
return
Boolean
(
!
this
.
showFailureAlert
&&
this
.
graphData
);
return
Boolean
(
!
this
.
showFailureAlert
&&
!
this
.
hasNoDependentJobs
&&
this
.
graphData
);
},
},
mounted
()
{
const
{
processGraphData
,
reportFailure
}
=
this
;
if
(
!
this
.
graphUrl
)
{
reportFailure
();
return
;
}
axios
.
get
(
this
.
graphUrl
)
.
then
(
response
=>
{
processGraphData
(
response
.
data
);
})
.
catch
(()
=>
reportFailure
(
LOAD_FAILURE
));
},
methods
:
{
addAnnotationToMap
({
uid
,
source
,
target
})
{
this
.
$set
(
this
.
annotationsMap
,
uid
,
{
source
,
target
});
...
...
@@ -124,25 +148,25 @@ export default {
let
parsed
;
try
{
parsed
=
parseData
(
data
.
stages
);
parsed
=
parseData
(
data
);
}
catch
{
this
.
reportFailure
(
PARSE_FAILURE
);
return
;
return
{}
;
}
if
(
parsed
.
links
.
length
===
1
)
{
this
.
reportFailure
(
UNSUPPORTED_DATA
);
return
;
return
{}
;
}
// If there are no links, we don't report failure
// as it simply means the user does not use job dependencies
if
(
parsed
.
links
.
length
===
0
)
{
this
.
hasNoDependentJobs
=
true
;
return
;
return
{}
;
}
this
.
graphData
=
parsed
;
return
parsed
;
},
hideAlert
()
{
this
.
showFailureAlert
=
false
;
...
...
@@ -182,7 +206,7 @@ export default {
<dag-annotations
v-if=
"shouldDisplayAnnotations"
:annotations=
"annotationsMap"
/>
<dag-graph
v-if=
"shouldDisplayGraph"
:graph-data=
"
graph
Data"
:graph-data=
"
processed
Data"
@
onFailure=
"reportFailure"
@
update-annotation=
"updateAnnotation"
/>
...
...
@@ -209,7 +233,7 @@ export default {
</p>
</div>
</template>
<
template
#actions
>
<
template
v-if=
"dagDocPath"
#actions
>
<gl-button
:href=
"dagDocPath"
target=
"__blank"
variant=
"success"
>
{{
$options
.
emptyStateTexts
.
button
}}
</gl-button>
...
...
app/assets/javascripts/pipelines/components/dag/parsing_utils.js
View file @
2ac47a71
...
...
@@ -5,14 +5,16 @@ import { uniqWith, isEqual } from 'lodash';
received from the endpoint into the format the d3 graph expects.
Input is of the form:
[stages]
stages: {name, groups}
groups: [{ name, size, jobs }]
name is a group name; in the case that the group has one job, it is
also the job name
size is the number of parallel jobs
jobs: [{ name, needs}]
job name is either the same as the group name or group x/y
[nodes]
nodes: [{category, name, jobs, size}]
category is the stage name
name is a group name; in the case that the group has one job, it is
also the job name
size is the number of parallel jobs
jobs: [{ name, needs}]
job name is either the same as the group name or group x/y
needs: [job-names]
needs is an array of job-name strings
Output is of the form:
{ nodes: [node], links: [link] }
...
...
@@ -20,30 +22,17 @@ import { uniqWith, isEqual } from 'lodash';
link: { source, target, value }, with source & target being node names
and value being a constant
We create nodes, create links, and then dedupe the links, so that in the case where
We create nodes in the GraphQL update function, and then here we create the node dictionary,
then create links, and then dedupe the links, so that in the case where
job 4 depends on job 1 and job 2, and job 2 depends on job 1, we show only a single link
from job 1 to job 2 then another from job 2 to job 4.
CREATE NODES
stage.name -> node.category
stage.group.name -> node.name (this is the group name if there are parallel jobs)
stage.group.jobs -> node.jobs
stage.group.size -> node.size
CREATE LINKS
stages.group
s.name -> target
stages.groups
.needs.each -> source (source is the name of the group, not the parallel job)
node
s.name -> target
nodes.name
.needs.each -> source (source is the name of the group, not the parallel job)
10 -> value (constant)
*/
export
const
createNodes
=
data
=>
{
return
data
.
flatMap
(({
groups
,
name
})
=>
{
return
groups
.
map
(
group
=>
{
return
{
...
group
,
category
:
name
};
});
});
};
export
const
createNodeDict
=
nodes
=>
{
return
nodes
.
reduce
((
acc
,
node
)
=>
{
const
newNode
=
{
...
...
@@ -62,13 +51,6 @@ export const createNodeDict = nodes => {
},
{});
};
export
const
createNodesStructure
=
data
=>
{
const
nodes
=
createNodes
(
data
);
const
nodeDict
=
createNodeDict
(
nodes
);
return
{
nodes
,
nodeDict
};
};
export
const
makeLinksFromNodes
=
(
nodes
,
nodeDict
)
=>
{
const
constantLinkValue
=
10
;
// all links are the same weight
return
nodes
...
...
@@ -126,8 +108,8 @@ export const filterByAncestors = (links, nodeDict) =>
return
!
allAncestors
.
includes
(
source
);
});
export
const
parseData
=
data
=>
{
const
{
nodes
,
nodeDict
}
=
createNodesStructure
(
data
);
export
const
parseData
=
nodes
=>
{
const
nodeDict
=
createNodeDict
(
nodes
);
const
allLinks
=
makeLinksFromNodes
(
nodes
,
nodeDict
);
const
filteredLinks
=
filterByAncestors
(
allLinks
,
nodeDict
);
const
links
=
uniqWith
(
filteredLinks
,
isEqual
);
...
...
app/assets/javascripts/pipelines/graphql/queries/get_dag_vis_data.query.graphql
0 → 100644
View file @
2ac47a71
query
getDagVisData
(
$projectPath
:
ID
!,
$iid
:
ID
!)
{
project
(
fullPath
:
$projectPath
)
{
pipeline
(
iid
:
$iid
)
{
stages
{
nodes
{
name
groups
{
nodes
{
name
size
jobs
{
nodes
{
name
needs
{
nodes
{
name
}
}
}
}
}
}
}
}
}
}
}
app/assets/javascripts/pipelines/pipeline_details_bundle.js
View file @
2ac47a71
...
...
@@ -4,7 +4,7 @@ import Translate from '~/vue_shared/translate';
import
{
__
}
from
'
~/locale
'
;
import
{
setUrlFragment
,
redirectTo
}
from
'
~/lib/utils/url_utility
'
;
import
pipelineGraph
from
'
./components/graph/graph_component.vue
'
;
import
Dag
from
'
./components/dag/dag.vue
'
;
import
createDagApp
from
'
./pipeline_details_dag
'
;
import
GraphBundleMixin
from
'
./mixins/graph_pipeline_bundle_mixin
'
;
import
PipelinesMediator
from
'
./pipeline_details_mediator
'
;
import
pipelineHeader
from
'
./components/header_component.vue
'
;
...
...
@@ -114,32 +114,6 @@ const createTestDetails = () => {
});
};
const
createDagApp
=
()
=>
{
if
(
!
window
.
gon
?.
features
?.
dagPipelineTab
)
{
return
;
}
const
el
=
document
.
querySelector
(
'
#js-pipeline-dag-vue
'
);
const
{
pipelineDataPath
,
emptySvgPath
,
dagDocPath
}
=
el
?.
dataset
;
// eslint-disable-next-line no-new
new
Vue
({
el
,
components
:
{
Dag
,
},
render
(
createElement
)
{
return
createElement
(
'
dag
'
,
{
props
:
{
graphUrl
:
pipelineDataPath
,
emptySvgPath
,
dagDocPath
,
},
});
},
});
};
export
default
()
=>
{
const
{
dataset
}
=
document
.
querySelector
(
'
.js-pipeline-details-vue
'
);
const
mediator
=
new
PipelinesMediator
({
endpoint
:
dataset
.
endpoint
});
...
...
app/assets/javascripts/pipelines/pipeline_details_dag.js
0 → 100644
View file @
2ac47a71
import
Vue
from
'
vue
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
createDefaultClient
from
'
~/lib/graphql
'
;
import
Dag
from
'
./components/dag/dag.vue
'
;
Vue
.
use
(
VueApollo
);
const
apolloProvider
=
new
VueApollo
({
defaultClient
:
createDefaultClient
(),
});
const
createDagApp
=
()
=>
{
if
(
!
window
.
gon
?.
features
?.
dagPipelineTab
)
{
return
;
}
const
el
=
document
.
querySelector
(
'
#js-pipeline-dag-vue
'
);
const
{
pipelineProjectPath
,
pipelineIid
,
emptySvgPath
,
dagDocPath
}
=
el
?.
dataset
;
// eslint-disable-next-line no-new
new
Vue
({
el
,
components
:
{
Dag
,
},
apolloProvider
,
provide
:
{
pipelineProjectPath
,
pipelineIid
,
emptySvgPath
,
dagDocPath
,
},
render
(
createElement
)
{
return
createElement
(
'
dag
'
,
{});
},
});
};
export
default
createDagApp
;
app/views/projects/pipelines/_with_tabs.html.haml
View file @
2ac47a71
...
...
@@ -81,7 +81,7 @@
-
if
dag_pipeline_tab_enabled
#js-tab-dag
.tab-pane
#js-pipeline-dag-vue
{
data:
{
pipeline_
data_path:
dag_project_pipeline_path
(
@project
,
@pipeline
)
,
empty_svg_path:
image_path
(
'illustrations/empty-state/empty-dag-md.svg'
),
dag_doc_path:
help_page_path
(
'ci/yaml/README.md'
,
anchor:
'needs'
)}
}
#js-pipeline-dag-vue
{
data:
{
pipeline_
project_path:
@project
.
full_path
,
pipeline_iid:
@pipeline
.
iid
,
empty_svg_path:
image_path
(
'illustrations/empty-state/empty-dag-md.svg'
),
dag_doc_path:
help_page_path
(
'ci/yaml/README.md'
,
anchor:
'needs'
)}
}
#js-tab-tests
.tab-pane
#js-pipeline-tests-detail
{
data:
{
summary_endpoint:
summary_project_pipeline_tests_path
(
@project
,
@pipeline
,
format: :json
),
...
...
spec/frontend/pipelines/components/dag/dag_spec.js
View file @
2ac47a71
import
{
mount
,
shallowMount
}
from
'
@vue/test-utils
'
;
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
{
GlAlert
,
GlEmptyState
}
from
'
@gitlab/ui
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
Dag
from
'
~/pipelines/components/dag/dag.vue
'
;
import
DagGraph
from
'
~/pipelines/components/dag/dag_graph.vue
'
;
import
DagAnnotations
from
'
~/pipelines/components/dag/dag_annotations.vue
'
;
...
...
@@ -11,13 +8,11 @@ import {
ADD_NOTE
,
REMOVE_NOTE
,
REPLACE_NOTES
,
DEFAULT
,
PARSE_FAILURE
,
LOAD_FAILURE
,
UNSUPPORTED_DATA
,
}
from
'
~/pipelines/components/dag//constants
'
;
import
{
mock
BaseData
,
mock
ParsedGraphQLNodes
,
tooSmallGraph
,
unparseableGraph
,
graphWithoutDependencies
,
...
...
@@ -27,7 +22,6 @@ import {
describe
(
'
Pipeline DAG graph wrapper
'
,
()
=>
{
let
wrapper
;
let
mock
;
const
getAlert
=
()
=>
wrapper
.
find
(
GlAlert
);
const
getAllAlerts
=
()
=>
wrapper
.
findAll
(
GlAlert
);
const
getGraph
=
()
=>
wrapper
.
find
(
DagGraph
);
...
...
@@ -35,45 +29,46 @@ describe('Pipeline DAG graph wrapper', () => {
const
getErrorText
=
type
=>
wrapper
.
vm
.
$options
.
errorTexts
[
type
];
const
getEmptyState
=
()
=>
wrapper
.
find
(
GlEmptyState
);
const
dataPath
=
'
/root/test/pipelines/90/dag.json
'
;
const
createComponent
=
(
propsData
=
{},
method
=
shallowMount
)
=>
{
const
createComponent
=
({
graphData
=
mockParsedGraphQLNodes
,
provideOverride
=
{},
method
=
shallowMount
,
}
=
{})
=>
{
if
(
wrapper
?.
destroy
)
{
wrapper
.
destroy
();
}
wrapper
=
method
(
Dag
,
{
propsData
:
{
provide
:
{
pipelineProjectPath
:
'
root/abc-dag
'
,
pipelineIid
:
'
1
'
,
emptySvgPath
:
'
/my-svg
'
,
dagDocPath
:
'
/my-doc
'
,
...
pro
psData
,
...
pro
videOverride
,
},
data
()
{
return
{
graphData
,
showFailureAlert
:
false
,
};
},
});
};
beforeEach
(()
=>
{
mock
=
new
MockAdapter
(
axios
);
});
afterEach
(()
=>
{
mock
.
restore
();
wrapper
.
destroy
();
wrapper
=
null
;
});
describe
(
'
when
there is no dataUrl
'
,
()
=>
{
describe
(
'
when
a query argument is undefined
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
({
graphUrl
:
undefined
});
createComponent
({
provideOverride
:
{
pipelineProjectPath
:
undefined
},
graphData
:
null
,
});
});
it
(
'
shows the DEFAULT alert and not the graph
'
,
()
=>
{
expect
(
getAlert
().
exists
()).
toBe
(
true
);
expect
(
getAlert
().
text
()).
toBe
(
getErrorText
(
DEFAULT
));
it
(
'
does not render the graph
'
,
async
()
=>
{
expect
(
getGraph
().
exists
()).
toBe
(
false
);
});
...
...
@@ -82,36 +77,12 @@ describe('Pipeline DAG graph wrapper', () => {
});
});
describe
(
'
when
there is a dataUrl
'
,
()
=>
{
describe
(
'
but the
data fetch
fails
'
,
()
=>
{
describe
(
'
when
all query variables are defined
'
,
()
=>
{
describe
(
'
but the
parse
fails
'
,
()
=>
{
beforeEach
(
async
()
=>
{
mock
.
onGet
(
dataPath
).
replyOnce
(
500
);
createComponent
({
graphUrl
:
dataPath
});
await
wrapper
.
vm
.
$nextTick
();
return
waitForPromises
();
});
it
(
'
shows the LOAD_FAILURE alert and not the graph
'
,
()
=>
{
expect
(
getAlert
().
exists
()).
toBe
(
true
);
expect
(
getAlert
().
text
()).
toBe
(
getErrorText
(
LOAD_FAILURE
));
expect
(
getGraph
().
exists
()).
toBe
(
false
);
});
it
(
'
does not render the empty state
'
,
()
=>
{
expect
(
getEmptyState
().
exists
()).
toBe
(
false
);
});
});
describe
(
'
the data fetch succeeds but the parse fails
'
,
()
=>
{
beforeEach
(
async
()
=>
{
mock
.
onGet
(
dataPath
).
replyOnce
(
200
,
unparseableGraph
);
createComponent
({
graphUrl
:
dataPath
});
await
wrapper
.
vm
.
$nextTick
();
return
waitForPromises
();
createComponent
({
graphData
:
unparseableGraph
,
});
});
it
(
'
shows the PARSE_FAILURE alert and not the graph
'
,
()
=>
{
...
...
@@ -125,14 +96,9 @@ describe('Pipeline DAG graph wrapper', () => {
});
});
describe
(
'
and the data fetch and
parse succeeds
'
,
()
=>
{
describe
(
'
parse succeeds
'
,
()
=>
{
beforeEach
(
async
()
=>
{
mock
.
onGet
(
dataPath
).
replyOnce
(
200
,
mockBaseData
);
createComponent
({
graphUrl
:
dataPath
},
mount
);
await
wrapper
.
vm
.
$nextTick
();
return
waitForPromises
();
createComponent
({
method
:
mount
});
});
it
(
'
shows the graph
'
,
()
=>
{
...
...
@@ -144,14 +110,11 @@ describe('Pipeline DAG graph wrapper', () => {
});
});
describe
(
'
the data fetch and
parse succeeds, but the resulting graph is too small
'
,
()
=>
{
describe
(
'
parse succeeds, but the resulting graph is too small
'
,
()
=>
{
beforeEach
(
async
()
=>
{
mock
.
onGet
(
dataPath
).
replyOnce
(
200
,
tooSmallGraph
);
createComponent
({
graphUrl
:
dataPath
});
await
wrapper
.
vm
.
$nextTick
();
return
waitForPromises
();
createComponent
({
graphData
:
tooSmallGraph
,
});
});
it
(
'
shows the UNSUPPORTED_DATA alert and not the graph
'
,
()
=>
{
...
...
@@ -165,14 +128,12 @@ describe('Pipeline DAG graph wrapper', () => {
});
});
describe
(
'
the
data fetch succeeds but the
returned data is empty
'
,
()
=>
{
describe
(
'
the returned data is empty
'
,
()
=>
{
beforeEach
(
async
()
=>
{
mock
.
onGet
(
dataPath
).
replyOnce
(
200
,
graphWithoutDependencies
);
createComponent
({
graphUrl
:
dataPath
},
mount
);
await
wrapper
.
vm
.
$nextTick
();
return
waitForPromises
();
createComponent
({
method
:
mount
,
graphData
:
graphWithoutDependencies
,
});
});
it
(
'
does not render an error alert or the graph
'
,
()
=>
{
...
...
@@ -188,12 +149,7 @@ describe('Pipeline DAG graph wrapper', () => {
describe
(
'
annotations
'
,
()
=>
{
beforeEach
(
async
()
=>
{
mock
.
onGet
(
dataPath
).
replyOnce
(
200
,
mockBaseData
);
createComponent
({
graphUrl
:
dataPath
},
mount
);
await
wrapper
.
vm
.
$nextTick
();
return
waitForPromises
();
createComponent
();
});
it
(
'
toggles on link mouseover and mouseout
'
,
async
()
=>
{
...
...
spec/frontend/pipelines/components/dag/drawing_utils_spec.js
View file @
2ac47a71
import
{
createSankey
}
from
'
~/pipelines/components/dag/drawing_utils
'
;
import
{
parseData
}
from
'
~/pipelines/components/dag/parsing_utils
'
;
import
{
mock
BaseData
}
from
'
./mock_data
'
;
import
{
mock
ParsedGraphQLNodes
}
from
'
./mock_data
'
;
describe
(
'
DAG visualization drawing utilities
'
,
()
=>
{
const
parsed
=
parseData
(
mock
BaseData
.
stag
es
);
const
parsed
=
parseData
(
mock
ParsedGraphQLNod
es
);
const
layoutSettings
=
{
width
:
200
,
...
...
spec/frontend/pipelines/components/dag/mock_data.js
View file @
2ac47a71
/*
It is important that the simple base include parallel jobs
as well as non-parallel jobs with spaces in the name to prevent
us relying on spaces as an indicator.
*/
export
const
mockBaseData
=
{
stages
:
[
{
name
:
'
test
'
,
groups
:
[
{
name
:
'
jest
'
,
size
:
2
,
jobs
:
[{
name
:
'
jest 1/2
'
,
needs
:
[
'
frontend fixtures
'
]
},
{
name
:
'
jest 2/2
'
}],
},
{
name
:
'
rspec
'
,
size
:
1
,
jobs
:
[{
name
:
'
rspec
'
,
needs
:
[
'
frontend fixtures
'
]
}],
},
],
},
{
name
:
'
fixtures
'
,
groups
:
[
{
name
:
'
frontend fixtures
'
,
size
:
1
,
jobs
:
[{
name
:
'
frontend fixtures
'
}],
},
],
},
{
name
:
'
un-needed
'
,
groups
:
[
{
name
:
'
un-needed
'
,
size
:
1
,
jobs
:
[{
name
:
'
un-needed
'
}],
},
],
},
],
};
export
const
tooSmallGraph
=
{
stages
:
[
{
name
:
'
test
'
,
groups
:
[
{
name
:
'
jest
'
,
size
:
2
,
jobs
:
[{
name
:
'
jest 1/2
'
},
{
name
:
'
jest 2/2
'
}],
},
{
name
:
'
rspec
'
,
size
:
1
,
jobs
:
[{
name
:
'
rspec
'
,
needs
:
[
'
frontend fixtures
'
]
}],
},
],
},
{
name
:
'
fixtures
'
,
groups
:
[
{
name
:
'
frontend fixtures
'
,
size
:
1
,
jobs
:
[{
name
:
'
frontend fixtures
'
}],
},
],
},
{
name
:
'
un-needed
'
,
groups
:
[
{
name
:
'
un-needed
'
,
size
:
1
,
jobs
:
[{
name
:
'
un-needed
'
}],
},
],
},
],
};
export
const
tooSmallGraph
=
[
{
category
:
'
test
'
,
name
:
'
jest
'
,
size
:
2
,
jobs
:
[{
name
:
'
jest 1/2
'
},
{
name
:
'
jest 2/2
'
}],
},
{
category
:
'
test
'
,
name
:
'
rspec
'
,
size
:
1
,
jobs
:
[{
name
:
'
rspec
'
,
needs
:
[
'
frontend fixtures
'
]
}],
},
{
category
:
'
fixtures
'
,
name
:
'
frontend fixtures
'
,
size
:
1
,
jobs
:
[{
name
:
'
frontend fixtures
'
}],
},
{
category
:
'
un-needed
'
,
name
:
'
un-needed
'
,
size
:
1
,
jobs
:
[{
name
:
'
un-needed
'
}],
},
];
export
const
graphWithoutDependencies
=
{
stages
:
[
{
name
:
'
test
'
,
groups
:
[
{
name
:
'
jest
'
,
size
:
2
,
jobs
:
[{
name
:
'
jest 1/2
'
},
{
name
:
'
jest 2/2
'
}],
},
{
name
:
'
rspec
'
,
size
:
1
,
jobs
:
[{
name
:
'
rspec
'
}],
},
],
},
{
name
:
'
fixtures
'
,
groups
:
[
{
name
:
'
frontend fixtures
'
,
size
:
1
,
jobs
:
[{
name
:
'
frontend fixtures
'
}],
},
],
},
{
name
:
'
un-needed
'
,
groups
:
[
{
name
:
'
un-needed
'
,
size
:
1
,
jobs
:
[{
name
:
'
un-needed
'
}],
},
],
},
],
};
export
const
graphWithoutDependencies
=
[
{
category
:
'
test
'
,
name
:
'
jest
'
,
size
:
2
,
jobs
:
[{
name
:
'
jest 1/2
'
},
{
name
:
'
jest 2/2
'
}],
},
{
category
:
'
test
'
,
name
:
'
rspec
'
,
size
:
1
,
jobs
:
[{
name
:
'
rspec
'
}],
},
{
category
:
'
fixtures
'
,
name
:
'
frontend fixtures
'
,
size
:
1
,
jobs
:
[{
name
:
'
frontend fixtures
'
}],
},
{
category
:
'
un-needed
'
,
name
:
'
un-needed
'
,
size
:
1
,
jobs
:
[{
name
:
'
un-needed
'
}],
},
];
export
const
unparseableGraph
=
[
{
...
...
@@ -468,3 +397,264 @@ export const multiNote = {
},
},
};
/*
It is important that the base include parallel jobs
as well as non-parallel jobs with spaces in the name to prevent
us relying on spaces as an indicator.
*/
export
const
mockParsedGraphQLNodes
=
[
{
category
:
'
build
'
,
name
:
'
build_a
'
,
size
:
1
,
jobs
:
[
{
name
:
'
build_a
'
,
needs
:
[],
},
],
__typename
:
'
CiGroup
'
,
},
{
category
:
'
build
'
,
name
:
'
build_b
'
,
size
:
1
,
jobs
:
[
{
name
:
'
build_b
'
,
needs
:
[],
},
],
__typename
:
'
CiGroup
'
,
},
{
category
:
'
test
'
,
name
:
'
test_a
'
,
size
:
1
,
jobs
:
[
{
name
:
'
test_a
'
,
needs
:
[
'
build_a
'
],
},
],
__typename
:
'
CiGroup
'
,
},
{
category
:
'
test
'
,
name
:
'
test_b
'
,
size
:
1
,
jobs
:
[
{
name
:
'
test_b
'
,
needs
:
[],
},
],
__typename
:
'
CiGroup
'
,
},
{
category
:
'
test
'
,
name
:
'
test_c
'
,
size
:
1
,
jobs
:
[
{
name
:
'
test_c
'
,
needs
:
[],
},
],
__typename
:
'
CiGroup
'
,
},
{
category
:
'
test
'
,
name
:
'
test_d
'
,
size
:
1
,
jobs
:
[
{
name
:
'
test_d
'
,
needs
:
[],
},
],
__typename
:
'
CiGroup
'
,
},
{
category
:
'
post-test
'
,
name
:
'
post_test_a
'
,
size
:
1
,
jobs
:
[
{
name
:
'
post_test_a
'
,
needs
:
[],
},
],
__typename
:
'
CiGroup
'
,
},
{
category
:
'
post-test
'
,
name
:
'
post_test_b
'
,
size
:
1
,
jobs
:
[
{
name
:
'
post_test_b
'
,
needs
:
[],
},
],
__typename
:
'
CiGroup
'
,
},
{
category
:
'
post-test
'
,
name
:
'
post_test_c
'
,
size
:
1
,
jobs
:
[
{
name
:
'
post_test_c
'
,
needs
:
[
'
test_b
'
,
'
test_a
'
],
},
],
__typename
:
'
CiGroup
'
,
},
{
category
:
'
staging
'
,
name
:
'
staging_a
'
,
size
:
1
,
jobs
:
[
{
name
:
'
staging_a
'
,
needs
:
[
'
post_test_a
'
],
},
],
__typename
:
'
CiGroup
'
,
},
{
category
:
'
staging
'
,
name
:
'
staging_b
'
,
size
:
1
,
jobs
:
[
{
name
:
'
staging_b
'
,
needs
:
[
'
post_test_b
'
],
},
],
__typename
:
'
CiGroup
'
,
},
{
category
:
'
staging
'
,
name
:
'
staging_c
'
,
size
:
1
,
jobs
:
[
{
name
:
'
staging_c
'
,
needs
:
[],
},
],
__typename
:
'
CiGroup
'
,
},
{
category
:
'
staging
'
,
name
:
'
staging_d
'
,
size
:
1
,
jobs
:
[
{
name
:
'
staging_d
'
,
needs
:
[],
},
],
__typename
:
'
CiGroup
'
,
},
{
category
:
'
staging
'
,
name
:
'
staging_e
'
,
size
:
1
,
jobs
:
[
{
name
:
'
staging_e
'
,
needs
:
[],
},
],
__typename
:
'
CiGroup
'
,
},
{
category
:
'
canary
'
,
name
:
'
canary_a
'
,
size
:
1
,
jobs
:
[
{
name
:
'
canary_a
'
,
needs
:
[
'
staging_b
'
,
'
staging_a
'
],
},
],
__typename
:
'
CiGroup
'
,
},
{
category
:
'
canary
'
,
name
:
'
canary_b
'
,
size
:
1
,
jobs
:
[
{
name
:
'
canary_b
'
,
needs
:
[],
},
],
__typename
:
'
CiGroup
'
,
},
{
category
:
'
canary
'
,
name
:
'
canary_c
'
,
size
:
1
,
jobs
:
[
{
name
:
'
canary_c
'
,
needs
:
[
'
staging_b
'
],
},
],
__typename
:
'
CiGroup
'
,
},
{
category
:
'
production
'
,
name
:
'
production_a
'
,
size
:
1
,
jobs
:
[
{
name
:
'
production_a
'
,
needs
:
[
'
canary_a
'
],
},
],
__typename
:
'
CiGroup
'
,
},
{
category
:
'
production
'
,
name
:
'
production_b
'
,
size
:
1
,
jobs
:
[
{
name
:
'
production_b
'
,
needs
:
[],
},
],
__typename
:
'
CiGroup
'
,
},
{
category
:
'
production
'
,
name
:
'
production_c
'
,
size
:
1
,
jobs
:
[
{
name
:
'
production_c
'
,
needs
:
[],
},
],
__typename
:
'
CiGroup
'
,
},
{
category
:
'
production
'
,
name
:
'
production_d
'
,
size
:
1
,
jobs
:
[
{
name
:
'
production_d
'
,
needs
:
[
'
canary_c
'
],
},
],
__typename
:
'
CiGroup
'
,
},
];
spec/frontend/pipelines/components/dag/parsing_utils_spec.js
View file @
2ac47a71
import
{
createNode
sStructure
,
createNode
Dict
,
makeLinksFromNodes
,
filterByAncestors
,
parseData
,
...
...
@@ -8,56 +8,17 @@ import {
}
from
'
~/pipelines/components/dag/parsing_utils
'
;
import
{
createSankey
}
from
'
~/pipelines/components/dag/drawing_utils
'
;
import
{
mock
BaseData
}
from
'
./mock_data
'
;
import
{
mock
ParsedGraphQLNodes
}
from
'
./mock_data
'
;
describe
(
'
DAG visualization parsing utilities
'
,
()
=>
{
const
{
nodes
,
nodeDict
}
=
createNodesStructure
(
mockBaseData
.
stages
);
const
unfilteredLinks
=
makeLinksFromNodes
(
nodes
,
nodeDict
);
const
parsed
=
parseData
(
mockBaseData
.
stages
);
const
layoutSettings
=
{
width
:
200
,
height
:
200
,
nodeWidth
:
10
,
nodePadding
:
20
,
paddingForLabels
:
100
,
};
const
sankeyLayout
=
createSankey
(
layoutSettings
)(
parsed
);
describe
(
'
createNodesStructure
'
,
()
=>
{
const
parallelGroupName
=
'
jest
'
;
const
parallelJobName
=
'
jest 1/2
'
;
const
singleJobName
=
'
frontend fixtures
'
;
const
{
name
,
jobs
,
size
}
=
mockBaseData
.
stages
[
0
].
groups
[
0
];
it
(
'
returns the expected node structure
'
,
()
=>
{
expect
(
nodes
[
0
]).
toHaveProperty
(
'
category
'
,
mockBaseData
.
stages
[
0
].
name
);
expect
(
nodes
[
0
]).
toHaveProperty
(
'
name
'
,
name
);
expect
(
nodes
[
0
]).
toHaveProperty
(
'
jobs
'
,
jobs
);
expect
(
nodes
[
0
]).
toHaveProperty
(
'
size
'
,
size
);
});
it
(
'
adds needs to top level of nodeDict entries
'
,
()
=>
{
expect
(
nodeDict
[
parallelGroupName
]).
toHaveProperty
(
'
needs
'
);
expect
(
nodeDict
[
parallelJobName
]).
toHaveProperty
(
'
needs
'
);
expect
(
nodeDict
[
singleJobName
]).
toHaveProperty
(
'
needs
'
);
});
it
(
'
makes entries in nodeDict for jobs and parallel jobs
'
,
()
=>
{
const
nodeNames
=
Object
.
keys
(
nodeDict
);
expect
(
nodeNames
.
includes
(
parallelGroupName
)).
toBe
(
true
);
expect
(
nodeNames
.
includes
(
parallelJobName
)).
toBe
(
true
);
expect
(
nodeNames
.
includes
(
singleJobName
)).
toBe
(
true
);
});
});
const
nodeDict
=
createNodeDict
(
mockParsedGraphQLNodes
);
const
unfilteredLinks
=
makeLinksFromNodes
(
mockParsedGraphQLNodes
,
nodeDict
);
const
parsed
=
parseData
(
mockParsedGraphQLNodes
);
describe
(
'
makeLinksFromNodes
'
,
()
=>
{
it
(
'
returns the expected link structure
'
,
()
=>
{
expect
(
unfilteredLinks
[
0
]).
toHaveProperty
(
'
source
'
,
'
frontend fixtures
'
);
expect
(
unfilteredLinks
[
0
]).
toHaveProperty
(
'
target
'
,
'
jest
'
);
expect
(
unfilteredLinks
[
0
]).
toHaveProperty
(
'
source
'
,
'
build_a
'
);
expect
(
unfilteredLinks
[
0
]).
toHaveProperty
(
'
target
'
,
'
test_a
'
);
expect
(
unfilteredLinks
[
0
]).
toHaveProperty
(
'
value
'
,
10
);
});
});
...
...
@@ -107,8 +68,22 @@ describe('DAG visualization parsing utilities', () => {
describe
(
'
removeOrphanNodes
'
,
()
=>
{
it
(
'
removes sankey nodes that have no needs and are not needed
'
,
()
=>
{
const
layoutSettings
=
{
width
:
200
,
height
:
200
,
nodeWidth
:
10
,
nodePadding
:
20
,
paddingForLabels
:
100
,
};
const
sankeyLayout
=
createSankey
(
layoutSettings
)(
parsed
);
const
cleanedNodes
=
removeOrphanNodes
(
sankeyLayout
.
nodes
);
expect
(
cleanedNodes
).
toHaveLength
(
sankeyLayout
.
nodes
.
length
-
1
);
/*
These lengths are determined by the mock data.
If the data changes, the numbers may also change.
*/
expect
(
parsed
.
nodes
).
toHaveLength
(
21
);
expect
(
cleanedNodes
).
toHaveLength
(
12
);
});
});
...
...
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