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
186f2eee
Commit
186f2eee
authored
Apr 22, 2021
by
Sarah Groff Hennigh-Palermo
Committed by
Andrew Fontaine
Apr 22, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add hover tip for show links toggle
parent
3fb47386
Changes
9
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
246 additions
and
40 deletions
+246
-40
app/assets/javascripts/pipelines/components/graph/graph_component.vue
...avascripts/pipelines/components/graph/graph_component.vue
+1
-1
app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
...ts/pipelines/components/graph/graph_component_wrapper.vue
+35
-0
app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue
...cripts/pipelines/components/graph/graph_view_selector.vue
+54
-30
app/assets/javascripts/pipelines/components/notification/pipeline_notification.vue
...pelines/components/notification/pipeline_notification.vue
+2
-2
app/assets/javascripts/pipelines/graphql/mutations/dismiss_pipeline_notification.graphql
...s/graphql/mutations/dismiss_pipeline_notification.graphql
+1
-1
locale/gitlab.pot
locale/gitlab.pot
+3
-0
spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
.../frontend/pipelines/graph/graph_component_wrapper_spec.js
+64
-4
spec/frontend/pipelines/graph/graph_view_selector_spec.js
spec/frontend/pipelines/graph/graph_view_selector_spec.js
+67
-2
spec/frontend/pipelines/graph/mock_data.js
spec/frontend/pipelines/graph/mock_data.js
+19
-0
No files found.
app/assets/javascripts/pipelines/components/graph/graph_component.vue
View file @
186f2eee
...
...
@@ -165,7 +165,7 @@ export default {
<div
class=
"js-pipeline-graph"
>
<div
ref=
"mainPipelineContainer"
class=
"gl-display-flex gl-position-relative gl-bg-gray-10 gl-white-space-nowrap"
class=
"gl-display-flex gl-position-relative gl-bg-gray-10 gl-white-space-nowrap
gl-border-t-solid gl-border-t-1 gl-border-gray-100
"
:class=
"
{ 'gl-pipeline-min-h gl-py-5 gl-overflow-auto': !isLinkedPipeline }"
>
<linked-graph-wrapper>
...
...
app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
View file @
186f2eee
...
...
@@ -5,6 +5,8 @@ import { __ } from '~/locale';
import
LocalStorageSync
from
'
~/vue_shared/components/local_storage_sync.vue
'
;
import
glFeatureFlagMixin
from
'
~/vue_shared/mixins/gl_feature_flags_mixin
'
;
import
{
DEFAULT
,
DRAW_FAILURE
,
LOAD_FAILURE
}
from
'
../../constants
'
;
import
DismissPipelineGraphCallout
from
'
../../graphql/mutations/dismiss_pipeline_notification.graphql
'
;
import
getUserCallouts
from
'
../../graphql/queries/get_user_callouts.query.graphql
'
;
import
{
reportToSentry
}
from
'
../../utils
'
;
import
{
listByLayers
}
from
'
../parsing_utils
'
;
import
{
IID_FAILURE
,
LAYER_VIEW
,
STAGE_VIEW
,
VIEW_TYPE_KEY
}
from
'
./constants
'
;
...
...
@@ -17,6 +19,9 @@ import {
unwrapPipelineData
,
}
from
'
./utils
'
;
const
featureName
=
'
pipeline_needs_hover_tip
'
;
const
enumFeatureName
=
featureName
.
toUpperCase
();
export
default
{
name
:
'
PipelineGraphWrapper
'
,
components
:
{
...
...
@@ -44,6 +49,7 @@ export default {
data
()
{
return
{
alertType
:
null
,
callouts
:
[],
currentViewType
:
STAGE_VIEW
,
pipeline
:
null
,
pipelineLayers
:
null
,
...
...
@@ -60,6 +66,18 @@ export default {
[
DEFAULT
]:
__
(
'
An unknown error occurred while loading this graph.
'
),
},
apollo
:
{
callouts
:
{
query
:
getUserCallouts
,
update
(
data
)
{
return
data
?.
currentUser
?.
callouts
?.
nodes
.
map
((
callout
)
=>
callout
.
featureName
);
},
error
(
err
)
{
reportToSentry
(
this
.
$options
.
name
,
`type: callout_load_failure, info:
${
serializeLoadErrors
(
err
)}
`
,
);
},
},
pipeline
:
{
context
()
{
return
getQueryHeaders
(
this
.
graphqlResourceEtag
);
...
...
@@ -142,6 +160,9 @@ export default {
/* This prevents reading view type off the localStorage value if it does not apply. */
return
this
.
showGraphViewSelector
?
this
.
currentViewType
:
STAGE_VIEW
;
},
hoverTipPreviouslyDismissed
()
{
return
this
.
callouts
.
includes
(
enumFeatureName
);
},
showLoadingIcon
()
{
/*
Shows the icon only when the graph is empty, not when it is is
...
...
@@ -171,6 +192,18 @@ export default {
return
this
.
pipelineLayers
;
},
handleTipDismissal
()
{
try
{
this
.
$apollo
.
mutate
({
mutation
:
DismissPipelineGraphCallout
,
variables
:
{
featureName
,
},
});
}
catch
(
err
)
{
reportToSentry
(
this
.
$options
.
name
,
`type: callout_dismiss_failure, info:
${
err
}
`
);
}
},
hideAlert
()
{
this
.
showAlert
=
false
;
this
.
alertType
=
null
;
...
...
@@ -211,6 +244,8 @@ export default {
v-if=
"showGraphViewSelector"
:type=
"graphViewType"
:show-links=
"showLinks"
:tip-previously-dismissed=
"hoverTipPreviouslyDismissed"
@
dismissHoverTip=
"handleTipDismissal"
@
updateViewType=
"updateViewType"
@
updateShowLinksState=
"updateShowLinksState"
/>
...
...
app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue
View file @
186f2eee
<
script
>
import
{
GlLoadingIcon
,
GlSegmentedControl
,
GlToggle
}
from
'
@gitlab/ui
'
;
import
{
Gl
Alert
,
Gl
LoadingIcon
,
GlSegmentedControl
,
GlToggle
}
from
'
@gitlab/ui
'
;
import
{
__
}
from
'
~/locale
'
;
import
{
STAGE_VIEW
,
LAYER_VIEW
}
from
'
./constants
'
;
export
default
{
name
:
'
GraphViewSelector
'
,
components
:
{
GlAlert
,
GlLoadingIcon
,
GlSegmentedControl
,
GlToggle
,
...
...
@@ -15,6 +16,10 @@ export default {
type
:
Boolean
,
required
:
true
,
},
tipPreviouslyDismissed
:
{
type
:
Boolean
,
required
:
true
,
},
type
:
{
type
:
String
,
required
:
true
,
...
...
@@ -22,15 +27,17 @@ export default {
},
data
()
{
return
{
currentViewType
:
this
.
type
,
showLinksActive
:
false
,
hoverTipDismissed
:
false
,
isToggleLoading
:
false
,
isSwitcherLoading
:
false
,
segmentSelectedType
:
this
.
type
,
showLinksActive
:
false
,
};
},
i18n
:
{
viewLabelText
:
__
(
'
Group jobs by
'
),
hoverTipText
:
__
(
'
Tip: Hover over a job to see the jobs it depends on to run.
'
),
linksLabelText
:
__
(
'
Show dependencies
'
),
viewLabelText
:
__
(
'
Group jobs by
'
),
},
views
:
{
[
STAGE_VIEW
]:
{
...
...
@@ -48,7 +55,15 @@ export default {
},
computed
:
{
showLinksToggle
()
{
return
this
.
currentViewType
===
LAYER_VIEW
;
return
this
.
segmentSelectedType
===
LAYER_VIEW
;
},
showTip
()
{
return
(
this
.
showLinks
&&
this
.
showLinksActive
&&
!
this
.
tipPreviouslyDismissed
&&
!
this
.
hoverTipDismissed
);
},
viewTypesList
()
{
return
Object
.
keys
(
this
.
$options
.
views
).
map
((
key
)
=>
{
...
...
@@ -77,6 +92,10 @@ export default {
},
},
methods
:
{
dismissTip
()
{
this
.
hoverTipDismissed
=
true
;
this
.
$emit
(
'
dismissHoverTip
'
);
},
/*
In both toggle methods, we use setTimeout so that the loading indicator displays,
then the work is done to update the DOM. The process is:
...
...
@@ -108,33 +127,38 @@ export default {
</
script
>
<
template
>
<div
class=
"gl-relative gl-display-flex gl-align-items-center gl-w-max-content gl-my-4"
>
<gl-loading-icon
v-if=
"isSwitcherLoading"
data-testid=
"switcher-loading-state"
class=
"gl-absolute gl-w-full gl-bg-white gl-opacity-5 gl-z-index-2"
size=
"lg"
/>
<span
class=
"gl-font-weight-bold"
>
{{
$options
.
i18n
.
viewLabelText
}}
</span>
<gl-segmented-control
v-model=
"currentViewType"
:options=
"viewTypesList"
:disabled=
"isSwitcherLoading"
data-testid=
"pipeline-view-selector"
class=
"gl-mx-4"
@
input=
"toggleView"
/>
<div
v-if=
"showLinksToggle"
>
<gl-toggle
v-model=
"showLinksActive"
data-testid=
"show-links-toggle"
<div>
<div
class=
"gl-relative gl-display-flex gl-align-items-center gl-w-max-content gl-my-4"
>
<gl-loading-icon
v-if=
"isSwitcherLoading"
data-testid=
"switcher-loading-state"
class=
"gl-absolute gl-w-full gl-bg-white gl-opacity-5 gl-z-index-2"
size=
"lg"
/>
<span
class=
"gl-font-weight-bold"
>
{{
$options
.
i18n
.
viewLabelText
}}
</span>
<gl-segmented-control
v-model=
"segmentSelectedType"
:options=
"viewTypesList"
:disabled=
"isSwitcherLoading"
data-testid=
"pipeline-view-selector"
class=
"gl-mx-4"
:label=
"$options.i18n.linksLabelText"
:is-loading=
"isToggleLoading"
label-position=
"left"
@
change=
"toggleShowLinksActive"
@
input=
"toggleView"
/>
<div
v-if=
"showLinksToggle"
class=
"gl-display-flex gl-align-items-center"
>
<gl-toggle
v-model=
"showLinksActive"
data-testid=
"show-links-toggle"
class=
"gl-mx-4"
:label=
"$options.i18n.linksLabelText"
:is-loading=
"isToggleLoading"
label-position=
"left"
@
change=
"toggleShowLinksActive"
/>
</div>
</div>
<gl-alert
v-if=
"showTip"
class=
"gl-my-5"
variant=
"tip"
@
dismiss=
"dismissTip"
>
{{
$options
.
i18n
.
hoverTipText
}}
</gl-alert>
</div>
</
template
>
app/assets/javascripts/pipelines/components/notification/pipeline_notification.vue
View file @
186f2eee
...
...
@@ -2,7 +2,7 @@
import
{
GlBanner
,
GlLink
,
GlSprintf
}
from
'
@gitlab/ui
'
;
import
createFlash
from
'
~/flash
'
;
import
{
__
}
from
'
~/locale
'
;
import
DismissPipeline
Notification
from
'
../../graphql/mutations/dismiss_pipeline_notification.graphql
'
;
import
DismissPipeline
GraphCallout
from
'
../../graphql/mutations/dismiss_pipeline_notification.graphql
'
;
import
getUserCallouts
from
'
../../graphql/queries/get_user_callouts.query.graphql
'
;
const
featureName
=
'
pipeline_needs_banner
'
;
...
...
@@ -55,7 +55,7 @@ export default {
this
.
dismissedAlert
=
true
;
try
{
this
.
$apollo
.
mutate
({
mutation
:
DismissPipeline
Notification
,
mutation
:
DismissPipeline
GraphCallout
,
variables
:
{
featureName
,
},
...
...
app/assets/javascripts/pipelines/graphql/mutations/dismiss_pipeline_notification.graphql
View file @
186f2eee
mutation
DismissPipeline
Notification
(
$featureName
:
String
!)
{
mutation
DismissPipeline
GraphCallout
(
$featureName
:
String
!)
{
userCalloutCreate
(
input
:
{
featureName
:
$featureName
})
{
errors
}
...
...
locale/gitlab.pot
View file @
186f2eee
...
...
@@ -32974,6 +32974,9 @@ msgstr ""
msgid "Tip:"
msgstr ""
msgid "Tip: Hover over a job to see the jobs it depends on to run."
msgstr ""
msgid "Tip: add a"
msgstr ""
...
...
spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
View file @
186f2eee
...
...
@@ -17,7 +17,8 @@ import GraphViewSelector from '~/pipelines/components/graph/graph_view_selector.
import
StageColumnComponent
from
'
~/pipelines/components/graph/stage_column_component.vue
'
;
import
LinksLayer
from
'
~/pipelines/components/graph_shared/links_layer.vue
'
;
import
*
as
parsingUtils
from
'
~/pipelines/components/parsing_utils
'
;
import
{
mockPipelineResponse
}
from
'
./mock_data
'
;
import
getUserCallouts
from
'
~/pipelines/graphql/queries/get_user_callouts.query.graphql
'
;
import
{
mapCallouts
,
mockCalloutsResponse
,
mockPipelineResponse
}
from
'
./mock_data
'
;
const
defaultProvide
=
{
graphqlResourceEtag
:
'
frog/amphibirama/etag/
'
,
...
...
@@ -31,15 +32,16 @@ describe('Pipeline graph wrapper', () => {
useLocalStorageSpy
();
let
wrapper
;
const
getAlert
=
()
=>
wrapper
.
find
(
GlAlert
);
const
getAlert
=
()
=>
wrapper
.
find
Component
(
GlAlert
);
const
getDependenciesToggle
=
()
=>
wrapper
.
find
(
'
[data-testid="show-links-toggle"]
'
);
const
getLoadingIcon
=
()
=>
wrapper
.
find
(
GlLoadingIcon
);
const
getLoadingIcon
=
()
=>
wrapper
.
find
Component
(
GlLoadingIcon
);
const
getLinksLayer
=
()
=>
wrapper
.
findComponent
(
LinksLayer
);
const
getGraph
=
()
=>
wrapper
.
find
(
PipelineGraph
);
const
getStageColumnTitle
=
()
=>
wrapper
.
find
(
'
[data-testid="stage-column-title"]
'
);
const
getAllStageColumnGroupsInColumn
=
()
=>
wrapper
.
find
(
StageColumnComponent
).
findAll
(
'
[data-testid="stage-column-group"]
'
);
const
getViewSelector
=
()
=>
wrapper
.
find
(
GraphViewSelector
);
const
getViewSelectorTrip
=
()
=>
getViewSelector
().
findComponent
(
GlAlert
);
const
createComponent
=
({
apolloProvider
,
...
...
@@ -62,12 +64,19 @@ describe('Pipeline graph wrapper', () => {
};
const
createComponentWithApollo
=
({
calloutsList
=
[],
data
=
{},
getPipelineDetailsHandler
=
jest
.
fn
().
mockResolvedValue
(
mockPipelineResponse
),
mountFn
=
shallowMount
,
provide
=
{},
}
=
{})
=>
{
const
requestHandlers
=
[[
getPipelineDetails
,
getPipelineDetailsHandler
]];
const
callouts
=
mapCallouts
(
calloutsList
);
const
getUserCalloutsHandler
=
jest
.
fn
().
mockResolvedValue
(
mockCalloutsResponse
(
callouts
));
const
requestHandlers
=
[
[
getPipelineDetails
,
getPipelineDetailsHandler
],
[
getUserCallouts
,
getUserCalloutsHandler
],
];
const
apolloProvider
=
createMockApollo
(
requestHandlers
);
createComponent
({
apolloProvider
,
data
,
provide
,
mountFn
});
...
...
@@ -325,6 +334,57 @@ describe('Pipeline graph wrapper', () => {
});
});
describe
(
'
when pipelineGraphLayersView feature flag is on, layers view is selected, and links are active
'
,
()
=>
{
beforeEach
(
async
()
=>
{
createComponentWithApollo
({
provide
:
{
glFeatures
:
{
pipelineGraphLayersView
:
true
,
},
},
data
:
{
currentViewType
:
LAYER_VIEW
,
showLinks
:
true
,
},
mountFn
:
mount
,
});
jest
.
runOnlyPendingTimers
();
await
wrapper
.
vm
.
$nextTick
();
});
it
(
'
shows the hover tip in the view selector
'
,
async
()
=>
{
await
getViewSelector
().
setData
({
showLinksActive
:
true
});
expect
(
getViewSelectorTrip
().
exists
()).
toBe
(
true
);
});
});
describe
(
'
when hover tip would otherwise show, but it has been previously dismissed
'
,
()
=>
{
beforeEach
(
async
()
=>
{
createComponentWithApollo
({
provide
:
{
glFeatures
:
{
pipelineGraphLayersView
:
true
,
},
},
data
:
{
currentViewType
:
LAYER_VIEW
,
showLinks
:
true
,
},
mountFn
:
mount
,
calloutsList
:
[
'
pipeline_needs_hover_tip
'
.
toUpperCase
()],
});
jest
.
runOnlyPendingTimers
();
await
wrapper
.
vm
.
$nextTick
();
});
it
(
'
does not show the hover tip
'
,
async
()
=>
{
await
getViewSelector
().
setData
({
showLinksActive
:
true
});
expect
(
getViewSelectorTrip
().
exists
()).
toBe
(
false
);
});
});
describe
(
'
when feature flag is on and local storage is set
'
,
()
=>
{
beforeEach
(
async
()
=>
{
localStorage
.
setItem
(
VIEW_TYPE_KEY
,
LAYER_VIEW
);
...
...
spec/frontend/pipelines/graph/graph_view_selector_spec.js
View file @
186f2eee
import
{
GlLoadingIcon
,
GlSegmentedControl
}
from
'
@gitlab/ui
'
;
import
{
Gl
Alert
,
Gl
LoadingIcon
,
GlSegmentedControl
}
from
'
@gitlab/ui
'
;
import
{
mount
,
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
LAYER_VIEW
,
STAGE_VIEW
}
from
'
~/pipelines/components/graph/constants
'
;
import
GraphViewSelector
from
'
~/pipelines/components/graph/graph_view_selector.vue
'
;
...
...
@@ -12,16 +12,19 @@ describe('the graph view selector component', () => {
const
findLayersViewLabel
=
()
=>
findViewTypeSelector
().
findAll
(
'
label
'
).
at
(
1
);
const
findSwitcherLoader
=
()
=>
wrapper
.
find
(
'
[data-testid="switcher-loading-state"]
'
);
const
findToggleLoader
=
()
=>
findDependenciesToggle
().
find
(
GlLoadingIcon
);
const
findHoverTip
=
()
=>
wrapper
.
findComponent
(
GlAlert
);
const
defaultProps
=
{
showLinks
:
false
,
tipPreviouslyDismissed
:
false
,
type
:
STAGE_VIEW
,
};
const
defaultData
=
{
showLinksActive
:
false
,
hoverTipDismissed
:
false
,
isToggleLoading
:
false
,
isSwitcherLoading
:
false
,
showLinksActive
:
false
,
};
const
createComponent
=
({
data
=
{},
mountFn
=
shallowMount
,
props
=
{}
}
=
{})
=>
{
...
...
@@ -121,4 +124,66 @@ describe('the graph view selector component', () => {
expect
(
wrapper
.
emitted
().
updateShowLinksState
).
toEqual
([[
true
]]);
});
});
describe
(
'
hover tip callout
'
,
()
=>
{
describe
(
'
when links are live and it has not been previously dismissed
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
({
props
:
{
showLinks
:
true
,
},
data
:
{
showLinksActive
:
true
,
},
mountFn
:
mount
,
});
});
it
(
'
is displayed
'
,
()
=>
{
expect
(
findHoverTip
().
exists
()).
toBe
(
true
);
expect
(
findHoverTip
().
text
()).
toBe
(
wrapper
.
vm
.
$options
.
i18n
.
hoverTipText
);
});
it
(
'
emits dismissHoverTip event when the tip is dismissed
'
,
async
()
=>
{
expect
(
wrapper
.
emitted
().
dismissHoverTip
).
toBeUndefined
();
await
findHoverTip
().
find
(
'
button
'
).
trigger
(
'
click
'
);
expect
(
wrapper
.
emitted
().
dismissHoverTip
).
toHaveLength
(
1
);
});
});
describe
(
'
when links are live and it has been previously dismissed
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
({
props
:
{
showLinks
:
true
,
tipPreviouslyDismissed
:
true
,
},
data
:
{
showLinksActive
:
true
,
},
});
});
it
(
'
is not displayed
'
,
()
=>
{
expect
(
findHoverTip
().
exists
()).
toBe
(
false
);
});
});
describe
(
'
when links are not live
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
({
props
:
{
showLinks
:
true
,
},
data
:
{
showLinksActive
:
false
,
},
});
});
it
(
'
is not displayed
'
,
()
=>
{
expect
(
findHoverTip
().
exists
()).
toBe
(
false
);
});
});
});
});
spec/frontend/pipelines/graph/mock_data.js
View file @
186f2eee
...
...
@@ -669,3 +669,22 @@ export const pipelineWithUpstreamDownstream = (base) => {
return
generateResponse
(
pip
,
'
root/abcd-dag
'
);
};
export
const
mapCallouts
=
(
callouts
)
=>
callouts
.
map
((
callout
)
=>
{
return
{
featureName
:
callout
,
__typename
:
'
UserCallout
'
};
});
export
const
mockCalloutsResponse
=
(
mappedCallouts
)
=>
({
data
:
{
currentUser
:
{
id
:
45
,
__typename
:
'
User
'
,
callouts
:
{
id
:
5
,
__typename
:
'
UserCalloutConnection
'
,
nodes
:
mappedCallouts
,
},
},
},
});
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