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
182f2616
Commit
182f2616
authored
Aug 18, 2020
by
Sarah Groff Hennigh-Palermo
Committed by
Natalia Tepluhina
Aug 18, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Rework DAG for graphQL
Adds new structural files, add specs, and revises current query method
parent
86abb0af
Changes
10
Show 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 @
182f2616
<
script
>
<
script
>
import
{
GlAlert
,
GlButton
,
GlEmptyState
,
GlSprintf
}
from
'
@gitlab/ui
'
;
import
{
GlAlert
,
GlButton
,
GlEmptyState
,
GlSprintf
}
from
'
@gitlab/ui
'
;
import
{
isEmpty
}
from
'
lodash
'
;
import
{
isEmpty
}
from
'
lodash
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
{
__
}
from
'
~/locale
'
;
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
DagGraph
from
'
./dag_graph.vue
'
;
import
DagAnnotations
from
'
./dag_annotations.vue
'
;
import
DagAnnotations
from
'
./dag_annotations.vue
'
;
import
{
import
{
...
@@ -27,22 +28,57 @@ export default {
...
@@ -27,22 +28,57 @@ export default {
GlEmptyState
,
GlEmptyState
,
GlButton
,
GlButton
,
},
},
props
:
{
inject
:
{
graphUrl
:
{
dagDocPath
:
{
type
:
String
,
default
:
null
,
required
:
false
,
default
:
''
,
},
},
emptySvgPath
:
{
emptySvgPath
:
{
type
:
String
,
required
:
true
,
default
:
''
,
default
:
''
,
},
},
dagDocPath
:
{
pipelineIid
:
{
type
:
String
,
required
:
true
,
default
:
''
,
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
()
{
data
()
{
return
{
return
{
...
@@ -90,31 +126,19 @@ export default {
...
@@ -90,31 +126,19 @@ export default {
default
:
default
:
return
{
return
{
text
:
this
.
$options
.
errorTexts
[
DEFAULT
],
text
:
this
.
$options
.
errorTexts
[
DEFAULT
],
va
t
iant
:
'
danger
'
,
va
r
iant
:
'
danger
'
,
};
};
}
}
},
},
processedData
()
{
return
this
.
processGraphData
(
this
.
graphData
);
},
shouldDisplayAnnotations
()
{
shouldDisplayAnnotations
()
{
return
!
isEmpty
(
this
.
annotationsMap
);
return
!
isEmpty
(
this
.
annotationsMap
);
},
},
shouldDisplayGraph
()
{
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
:
{
methods
:
{
addAnnotationToMap
({
uid
,
source
,
target
})
{
addAnnotationToMap
({
uid
,
source
,
target
})
{
...
@@ -124,25 +148,25 @@ export default {
...
@@ -124,25 +148,25 @@ export default {
let
parsed
;
let
parsed
;
try
{
try
{
parsed
=
parseData
(
data
.
stages
);
parsed
=
parseData
(
data
);
}
catch
{
}
catch
{
this
.
reportFailure
(
PARSE_FAILURE
);
this
.
reportFailure
(
PARSE_FAILURE
);
return
;
return
{}
;
}
}
if
(
parsed
.
links
.
length
===
1
)
{
if
(
parsed
.
links
.
length
===
1
)
{
this
.
reportFailure
(
UNSUPPORTED_DATA
);
this
.
reportFailure
(
UNSUPPORTED_DATA
);
return
;
return
{}
;
}
}
// If there are no links, we don't report failure
// If there are no links, we don't report failure
// as it simply means the user does not use job dependencies
// as it simply means the user does not use job dependencies
if
(
parsed
.
links
.
length
===
0
)
{
if
(
parsed
.
links
.
length
===
0
)
{
this
.
hasNoDependentJobs
=
true
;
this
.
hasNoDependentJobs
=
true
;
return
;
return
{}
;
}
}
this
.
graphData
=
parsed
;
return
parsed
;
},
},
hideAlert
()
{
hideAlert
()
{
this
.
showFailureAlert
=
false
;
this
.
showFailureAlert
=
false
;
...
@@ -182,7 +206,7 @@ export default {
...
@@ -182,7 +206,7 @@ export default {
<dag-annotations
v-if=
"shouldDisplayAnnotations"
:annotations=
"annotationsMap"
/>
<dag-annotations
v-if=
"shouldDisplayAnnotations"
:annotations=
"annotationsMap"
/>
<dag-graph
<dag-graph
v-if=
"shouldDisplayGraph"
v-if=
"shouldDisplayGraph"
:graph-data=
"
graph
Data"
:graph-data=
"
processed
Data"
@
onFailure=
"reportFailure"
@
onFailure=
"reportFailure"
@
update-annotation=
"updateAnnotation"
@
update-annotation=
"updateAnnotation"
/>
/>
...
@@ -209,7 +233,7 @@ export default {
...
@@ -209,7 +233,7 @@ export default {
</p>
</p>
</div>
</div>
</template>
</template>
<
template
#actions
>
<
template
v-if=
"dagDocPath"
#actions
>
<gl-button
:href=
"dagDocPath"
target=
"__blank"
variant=
"success"
>
<gl-button
:href=
"dagDocPath"
target=
"__blank"
variant=
"success"
>
{{
$options
.
emptyStateTexts
.
button
}}
{{
$options
.
emptyStateTexts
.
button
}}
</gl-button>
</gl-button>
...
...
app/assets/javascripts/pipelines/components/dag/parsing_utils.js
View file @
182f2616
...
@@ -5,14 +5,16 @@ import { uniqWith, isEqual } from 'lodash';
...
@@ -5,14 +5,16 @@ import { uniqWith, isEqual } from 'lodash';
received from the endpoint into the format the d3 graph expects.
received from the endpoint into the format the d3 graph expects.
Input is of the form:
Input is of the form:
[
stag
es]
[
nod
es]
stages: {name, groups}
nodes: [{category, name, jobs, size}]
groups: [{ name, size, jobs }]
category is the stage name
name is a group name; in the case that the group has one job, it is
name is a group name; in the case that the group has one job, it is
also the job name
also the job name
size is the number of parallel jobs
size is the number of parallel jobs
jobs: [{ name, needs}]
jobs: [{ name, needs}]
job name is either the same as the group name or group x/y
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:
Output is of the form:
{ nodes: [node], links: [link] }
{ nodes: [node], links: [link] }
...
@@ -20,30 +22,17 @@ import { uniqWith, isEqual } from 'lodash';
...
@@ -20,30 +22,17 @@ import { uniqWith, isEqual } from 'lodash';
link: { source, target, value }, with source & target being node names
link: { source, target, value }, with source & target being node names
and value being a constant
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
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.
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
CREATE LINKS
stages.group
s.name -> target
node
s.name -> target
stages.groups
.needs.each -> source (source is the name of the group, not the parallel job)
nodes.name
.needs.each -> source (source is the name of the group, not the parallel job)
10 -> value (constant)
10 -> value (constant)
*/
*/
export
const
createNodes
=
data
=>
{
return
data
.
flatMap
(({
groups
,
name
})
=>
{
return
groups
.
map
(
group
=>
{
return
{
...
group
,
category
:
name
};
});
});
};
export
const
createNodeDict
=
nodes
=>
{
export
const
createNodeDict
=
nodes
=>
{
return
nodes
.
reduce
((
acc
,
node
)
=>
{
return
nodes
.
reduce
((
acc
,
node
)
=>
{
const
newNode
=
{
const
newNode
=
{
...
@@ -62,13 +51,6 @@ export const createNodeDict = nodes => {
...
@@ -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
)
=>
{
export
const
makeLinksFromNodes
=
(
nodes
,
nodeDict
)
=>
{
const
constantLinkValue
=
10
;
// all links are the same weight
const
constantLinkValue
=
10
;
// all links are the same weight
return
nodes
return
nodes
...
@@ -126,8 +108,8 @@ export const filterByAncestors = (links, nodeDict) =>
...
@@ -126,8 +108,8 @@ export const filterByAncestors = (links, nodeDict) =>
return
!
allAncestors
.
includes
(
source
);
return
!
allAncestors
.
includes
(
source
);
});
});
export
const
parseData
=
data
=>
{
export
const
parseData
=
nodes
=>
{
const
{
nodes
,
nodeDict
}
=
createNodesStructure
(
data
);
const
nodeDict
=
createNodeDict
(
nodes
);
const
allLinks
=
makeLinksFromNodes
(
nodes
,
nodeDict
);
const
allLinks
=
makeLinksFromNodes
(
nodes
,
nodeDict
);
const
filteredLinks
=
filterByAncestors
(
allLinks
,
nodeDict
);
const
filteredLinks
=
filterByAncestors
(
allLinks
,
nodeDict
);
const
links
=
uniqWith
(
filteredLinks
,
isEqual
);
const
links
=
uniqWith
(
filteredLinks
,
isEqual
);
...
...
app/assets/javascripts/pipelines/graphql/queries/get_dag_vis_data.query.graphql
0 → 100644
View file @
182f2616
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 @
182f2616
...
@@ -4,7 +4,7 @@ import Translate from '~/vue_shared/translate';
...
@@ -4,7 +4,7 @@ import Translate from '~/vue_shared/translate';
import
{
__
}
from
'
~/locale
'
;
import
{
__
}
from
'
~/locale
'
;
import
{
setUrlFragment
,
redirectTo
}
from
'
~/lib/utils/url_utility
'
;
import
{
setUrlFragment
,
redirectTo
}
from
'
~/lib/utils/url_utility
'
;
import
pipelineGraph
from
'
./components/graph/graph_component.vue
'
;
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
GraphBundleMixin
from
'
./mixins/graph_pipeline_bundle_mixin
'
;
import
PipelinesMediator
from
'
./pipeline_details_mediator
'
;
import
PipelinesMediator
from
'
./pipeline_details_mediator
'
;
import
pipelineHeader
from
'
./components/header_component.vue
'
;
import
pipelineHeader
from
'
./components/header_component.vue
'
;
...
@@ -114,32 +114,6 @@ const createTestDetails = () => {
...
@@ -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
()
=>
{
export
default
()
=>
{
const
{
dataset
}
=
document
.
querySelector
(
'
.js-pipeline-details-vue
'
);
const
{
dataset
}
=
document
.
querySelector
(
'
.js-pipeline-details-vue
'
);
const
mediator
=
new
PipelinesMediator
({
endpoint
:
dataset
.
endpoint
});
const
mediator
=
new
PipelinesMediator
({
endpoint
:
dataset
.
endpoint
});
...
...
app/assets/javascripts/pipelines/pipeline_details_dag.js
0 → 100644
View file @
182f2616
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 @
182f2616
...
@@ -81,7 +81,7 @@
...
@@ -81,7 +81,7 @@
-
if
dag_pipeline_tab_enabled
-
if
dag_pipeline_tab_enabled
#js-tab-dag
.tab-pane
#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-tab-tests
.tab-pane
#js-pipeline-tests-detail
{
data:
{
summary_endpoint:
summary_project_pipeline_tests_path
(
@project
,
@pipeline
,
format: :json
),
#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 @
182f2616
import
{
mount
,
shallowMount
}
from
'
@vue/test-utils
'
;
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
{
GlAlert
,
GlEmptyState
}
from
'
@gitlab/ui
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
Dag
from
'
~/pipelines/components/dag/dag.vue
'
;
import
Dag
from
'
~/pipelines/components/dag/dag.vue
'
;
import
DagGraph
from
'
~/pipelines/components/dag/dag_graph.vue
'
;
import
DagGraph
from
'
~/pipelines/components/dag/dag_graph.vue
'
;
import
DagAnnotations
from
'
~/pipelines/components/dag/dag_annotations.vue
'
;
import
DagAnnotations
from
'
~/pipelines/components/dag/dag_annotations.vue
'
;
...
@@ -11,13 +8,11 @@ import {
...
@@ -11,13 +8,11 @@ import {
ADD_NOTE
,
ADD_NOTE
,
REMOVE_NOTE
,
REMOVE_NOTE
,
REPLACE_NOTES
,
REPLACE_NOTES
,
DEFAULT
,
PARSE_FAILURE
,
PARSE_FAILURE
,
LOAD_FAILURE
,
UNSUPPORTED_DATA
,
UNSUPPORTED_DATA
,
}
from
'
~/pipelines/components/dag//constants
'
;
}
from
'
~/pipelines/components/dag//constants
'
;
import
{
import
{
mock
BaseData
,
mock
ParsedGraphQLNodes
,
tooSmallGraph
,
tooSmallGraph
,
unparseableGraph
,
unparseableGraph
,
graphWithoutDependencies
,
graphWithoutDependencies
,
...
@@ -27,7 +22,6 @@ import {
...
@@ -27,7 +22,6 @@ import {
describe
(
'
Pipeline DAG graph wrapper
'
,
()
=>
{
describe
(
'
Pipeline DAG graph wrapper
'
,
()
=>
{
let
wrapper
;
let
wrapper
;
let
mock
;
const
getAlert
=
()
=>
wrapper
.
find
(
GlAlert
);
const
getAlert
=
()
=>
wrapper
.
find
(
GlAlert
);
const
getAllAlerts
=
()
=>
wrapper
.
findAll
(
GlAlert
);
const
getAllAlerts
=
()
=>
wrapper
.
findAll
(
GlAlert
);
const
getGraph
=
()
=>
wrapper
.
find
(
DagGraph
);
const
getGraph
=
()
=>
wrapper
.
find
(
DagGraph
);
...
@@ -35,67 +29,46 @@ describe('Pipeline DAG graph wrapper', () => {
...
@@ -35,67 +29,46 @@ describe('Pipeline DAG graph wrapper', () => {
const
getErrorText
=
type
=>
wrapper
.
vm
.
$options
.
errorTexts
[
type
];
const
getErrorText
=
type
=>
wrapper
.
vm
.
$options
.
errorTexts
[
type
];
const
getEmptyState
=
()
=>
wrapper
.
find
(
GlEmptyState
);
const
getEmptyState
=
()
=>
wrapper
.
find
(
GlEmptyState
);
const
dataPath
=
'
/root/test/pipelines/90/dag.json
'
;
const
createComponent
=
({
graphData
=
mockParsedGraphQLNodes
,
const
createComponent
=
(
propsData
=
{},
method
=
shallowMount
)
=>
{
provideOverride
=
{},
method
=
shallowMount
,
}
=
{})
=>
{
if
(
wrapper
?.
destroy
)
{
if
(
wrapper
?.
destroy
)
{
wrapper
.
destroy
();
wrapper
.
destroy
();
}
}
wrapper
=
method
(
Dag
,
{
wrapper
=
method
(
Dag
,
{
propsData
:
{
provide
:
{
pipelineProjectPath
:
'
root/abc-dag
'
,
pipelineIid
:
'
1
'
,
emptySvgPath
:
'
/my-svg
'
,
emptySvgPath
:
'
/my-svg
'
,
dagDocPath
:
'
/my-doc
'
,
dagDocPath
:
'
/my-doc
'
,
...
pro
psData
,
...
pro
videOverride
,
},
},
data
()
{
data
()
{
return
{
return
{
graphData
,
showFailureAlert
:
false
,
showFailureAlert
:
false
,
};
};
},
},
});
});
};
};
beforeEach
(()
=>
{
mock
=
new
MockAdapter
(
axios
);
});
afterEach
(()
=>
{
afterEach
(()
=>
{
mock
.
restore
();
wrapper
.
destroy
();
wrapper
.
destroy
();
wrapper
=
null
;
wrapper
=
null
;
});
});
describe
(
'
when
there is no dataUrl
'
,
()
=>
{
describe
(
'
when
a query argument is undefined
'
,
()
=>
{
beforeEach
(()
=>
{
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
));
expect
(
getGraph
().
exists
()).
toBe
(
false
);
});
});
it
(
'
does not render the empty state
'
,
()
=>
{
it
(
'
does not render the graph
'
,
async
()
=>
{
expect
(
getEmptyState
().
exists
()).
toBe
(
false
);
});
});
describe
(
'
when there is a dataUrl
'
,
()
=>
{
describe
(
'
but the data fetch 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
);
expect
(
getGraph
().
exists
()).
toBe
(
false
);
});
});
...
@@ -104,14 +77,12 @@ describe('Pipeline DAG graph wrapper', () => {
...
@@ -104,14 +77,12 @@ describe('Pipeline DAG graph wrapper', () => {
});
});
});
});
describe
(
'
the data fetch succeeds but the parse fails
'
,
()
=>
{
describe
(
'
when all query variables are defined
'
,
()
=>
{
describe
(
'
but the parse fails
'
,
()
=>
{
beforeEach
(
async
()
=>
{
beforeEach
(
async
()
=>
{
mock
.
onGet
(
dataPath
).
replyOnce
(
200
,
unparseableGraph
);
createComponent
({
createComponent
({
graphUrl
:
dataPath
});
graphData
:
unparseableGraph
,
});
await
wrapper
.
vm
.
$nextTick
();
return
waitForPromises
();
});
});
it
(
'
shows the PARSE_FAILURE alert and not the graph
'
,
()
=>
{
it
(
'
shows the PARSE_FAILURE alert and not the graph
'
,
()
=>
{
...
@@ -125,14 +96,9 @@ describe('Pipeline DAG graph wrapper', () => {
...
@@ -125,14 +96,9 @@ describe('Pipeline DAG graph wrapper', () => {
});
});
});
});
describe
(
'
and the data fetch and
parse succeeds
'
,
()
=>
{
describe
(
'
parse succeeds
'
,
()
=>
{
beforeEach
(
async
()
=>
{
beforeEach
(
async
()
=>
{
mock
.
onGet
(
dataPath
).
replyOnce
(
200
,
mockBaseData
);
createComponent
({
method
:
mount
});
createComponent
({
graphUrl
:
dataPath
},
mount
);
await
wrapper
.
vm
.
$nextTick
();
return
waitForPromises
();
});
});
it
(
'
shows the graph
'
,
()
=>
{
it
(
'
shows the graph
'
,
()
=>
{
...
@@ -144,14 +110,11 @@ describe('Pipeline DAG graph wrapper', () => {
...
@@ -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
()
=>
{
beforeEach
(
async
()
=>
{
mock
.
onGet
(
dataPath
).
replyOnce
(
200
,
tooSmallGraph
);
createComponent
({
createComponent
({
graphUrl
:
dataPath
});
graphData
:
tooSmallGraph
,
});
await
wrapper
.
vm
.
$nextTick
();
return
waitForPromises
();
});
});
it
(
'
shows the UNSUPPORTED_DATA alert and not the graph
'
,
()
=>
{
it
(
'
shows the UNSUPPORTED_DATA alert and not the graph
'
,
()
=>
{
...
@@ -165,14 +128,12 @@ describe('Pipeline DAG graph wrapper', () => {
...
@@ -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
()
=>
{
beforeEach
(
async
()
=>
{
mock
.
onGet
(
dataPath
).
replyOnce
(
200
,
graphWithoutDependencies
);
createComponent
({
createComponent
({
graphUrl
:
dataPath
},
mount
);
method
:
mount
,
graphData
:
graphWithoutDependencies
,
await
wrapper
.
vm
.
$nextTick
();
});
return
waitForPromises
();
});
});
it
(
'
does not render an error alert or the graph
'
,
()
=>
{
it
(
'
does not render an error alert or the graph
'
,
()
=>
{
...
@@ -188,12 +149,7 @@ describe('Pipeline DAG graph wrapper', () => {
...
@@ -188,12 +149,7 @@ describe('Pipeline DAG graph wrapper', () => {
describe
(
'
annotations
'
,
()
=>
{
describe
(
'
annotations
'
,
()
=>
{
beforeEach
(
async
()
=>
{
beforeEach
(
async
()
=>
{
mock
.
onGet
(
dataPath
).
replyOnce
(
200
,
mockBaseData
);
createComponent
();
createComponent
({
graphUrl
:
dataPath
},
mount
);
await
wrapper
.
vm
.
$nextTick
();
return
waitForPromises
();
});
});
it
(
'
toggles on link mouseover and mouseout
'
,
async
()
=>
{
it
(
'
toggles on link mouseover and mouseout
'
,
async
()
=>
{
...
...
spec/frontend/pipelines/components/dag/drawing_utils_spec.js
View file @
182f2616
import
{
createSankey
}
from
'
~/pipelines/components/dag/drawing_utils
'
;
import
{
createSankey
}
from
'
~/pipelines/components/dag/drawing_utils
'
;
import
{
parseData
}
from
'
~/pipelines/components/dag/parsing_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
'
,
()
=>
{
describe
(
'
DAG visualization drawing utilities
'
,
()
=>
{
const
parsed
=
parseData
(
mock
BaseData
.
stag
es
);
const
parsed
=
parseData
(
mock
ParsedGraphQLNod
es
);
const
layoutSettings
=
{
const
layoutSettings
=
{
width
:
200
,
width
:
200
,
...
...
spec/frontend/pipelines/components/dag/mock_data.js
View file @
182f2616
/*
export
const
tooSmallGraph
=
[
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
:
[
{
{
category
:
'
test
'
,
name
:
'
jest
'
,
name
:
'
jest
'
,
size
:
2
,
size
:
2
,
jobs
:
[{
name
:
'
jest 1/2
'
},
{
name
:
'
jest 2/2
'
}],
jobs
:
[{
name
:
'
jest 1/2
'
},
{
name
:
'
jest 2/2
'
}],
},
},
{
{
category
:
'
test
'
,
name
:
'
rspec
'
,
name
:
'
rspec
'
,
size
:
1
,
size
:
1
,
jobs
:
[{
name
:
'
rspec
'
,
needs
:
[
'
frontend fixtures
'
]
}],
jobs
:
[{
name
:
'
rspec
'
,
needs
:
[
'
frontend fixtures
'
]
}],
},
},
],
},
{
name
:
'
fixtures
'
,
groups
:
[
{
{
category
:
'
fixtures
'
,
name
:
'
frontend fixtures
'
,
name
:
'
frontend fixtures
'
,
size
:
1
,
size
:
1
,
jobs
:
[{
name
:
'
frontend fixtures
'
}],
jobs
:
[{
name
:
'
frontend fixtures
'
}],
},
},
],
},
{
name
:
'
un-needed
'
,
groups
:
[
{
{
category
:
'
un-needed
'
,
name
:
'
un-needed
'
,
name
:
'
un-needed
'
,
size
:
1
,
size
:
1
,
jobs
:
[{
name
:
'
un-needed
'
}],
jobs
:
[{
name
:
'
un-needed
'
}],
},
},
],
];
},
],
};
export
const
graphWithoutDependencies
=
{
export
const
graphWithoutDependencies
=
[
stages
:
[
{
name
:
'
test
'
,
groups
:
[
{
{
category
:
'
test
'
,
name
:
'
jest
'
,
name
:
'
jest
'
,
size
:
2
,
size
:
2
,
jobs
:
[{
name
:
'
jest 1/2
'
},
{
name
:
'
jest 2/2
'
}],
jobs
:
[{
name
:
'
jest 1/2
'
},
{
name
:
'
jest 2/2
'
}],
},
},
{
{
category
:
'
test
'
,
name
:
'
rspec
'
,
name
:
'
rspec
'
,
size
:
1
,
size
:
1
,
jobs
:
[{
name
:
'
rspec
'
}],
jobs
:
[{
name
:
'
rspec
'
}],
},
},
],
},
{
name
:
'
fixtures
'
,
groups
:
[
{
{
category
:
'
fixtures
'
,
name
:
'
frontend fixtures
'
,
name
:
'
frontend fixtures
'
,
size
:
1
,
size
:
1
,
jobs
:
[{
name
:
'
frontend fixtures
'
}],
jobs
:
[{
name
:
'
frontend fixtures
'
}],
},
},
],
},
{
name
:
'
un-needed
'
,
groups
:
[
{
{
category
:
'
un-needed
'
,
name
:
'
un-needed
'
,
name
:
'
un-needed
'
,
size
:
1
,
size
:
1
,
jobs
:
[{
name
:
'
un-needed
'
}],
jobs
:
[{
name
:
'
un-needed
'
}],
},
},
],
];
},
],
};
export
const
unparseableGraph
=
[
export
const
unparseableGraph
=
[
{
{
...
@@ -468,3 +397,264 @@ export const multiNote = {
...
@@ -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 @
182f2616
import
{
import
{
createNode
sStructure
,
createNode
Dict
,
makeLinksFromNodes
,
makeLinksFromNodes
,
filterByAncestors
,
filterByAncestors
,
parseData
,
parseData
,
...
@@ -8,56 +8,17 @@ import {
...
@@ -8,56 +8,17 @@ import {
}
from
'
~/pipelines/components/dag/parsing_utils
'
;
}
from
'
~/pipelines/components/dag/parsing_utils
'
;
import
{
createSankey
}
from
'
~/pipelines/components/dag/drawing_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
'
,
()
=>
{
describe
(
'
DAG visualization parsing utilities
'
,
()
=>
{
const
{
nodes
,
nodeDict
}
=
createNodesStructure
(
mockBaseData
.
stages
);
const
nodeDict
=
createNodeDict
(
mockParsedGraphQLNodes
);
const
unfilteredLinks
=
makeLinksFromNodes
(
nodes
,
nodeDict
);
const
unfilteredLinks
=
makeLinksFromNodes
(
mockParsedGraphQLNodes
,
nodeDict
);
const
parsed
=
parseData
(
mockBaseData
.
stages
);
const
parsed
=
parseData
(
mockParsedGraphQLNodes
);
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
);
});
});
describe
(
'
makeLinksFromNodes
'
,
()
=>
{
describe
(
'
makeLinksFromNodes
'
,
()
=>
{
it
(
'
returns the expected link structure
'
,
()
=>
{
it
(
'
returns the expected link structure
'
,
()
=>
{
expect
(
unfilteredLinks
[
0
]).
toHaveProperty
(
'
source
'
,
'
frontend fixtures
'
);
expect
(
unfilteredLinks
[
0
]).
toHaveProperty
(
'
source
'
,
'
build_a
'
);
expect
(
unfilteredLinks
[
0
]).
toHaveProperty
(
'
target
'
,
'
jest
'
);
expect
(
unfilteredLinks
[
0
]).
toHaveProperty
(
'
target
'
,
'
test_a
'
);
expect
(
unfilteredLinks
[
0
]).
toHaveProperty
(
'
value
'
,
10
);
expect
(
unfilteredLinks
[
0
]).
toHaveProperty
(
'
value
'
,
10
);
});
});
});
});
...
@@ -107,8 +68,22 @@ describe('DAG visualization parsing utilities', () => {
...
@@ -107,8 +68,22 @@ describe('DAG visualization parsing utilities', () => {
describe
(
'
removeOrphanNodes
'
,
()
=>
{
describe
(
'
removeOrphanNodes
'
,
()
=>
{
it
(
'
removes sankey nodes that have no needs and are not needed
'
,
()
=>
{
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
);
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