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
6fe5b635
Commit
6fe5b635
authored
Feb 18, 2020
by
Ezekiel Kigbo
Committed by
Phil Hughes
Feb 18, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Added recover hidden stages dropdown
Adds additional getters for getting active stages or hidden stages.
parent
a343011d
Changes
10
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
213 additions
and
34 deletions
+213
-34
app/assets/stylesheets/pages/cycle_analytics.scss
app/assets/stylesheets/pages/cycle_analytics.scss
+1
-1
ee/app/assets/javascripts/analytics/cycle_analytics/components/base.vue
...javascripts/analytics/cycle_analytics/components/base.vue
+2
-1
ee/app/assets/javascripts/analytics/cycle_analytics/components/custom_stage_form.vue
...nalytics/cycle_analytics/components/custom_stage_form.vue
+33
-2
ee/app/assets/javascripts/analytics/cycle_analytics/store/getters.js
...ts/javascripts/analytics/cycle_analytics/store/getters.js
+6
-0
ee/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js
.../javascripts/analytics/cycle_analytics/store/mutations.js
+1
-1
ee/spec/features/analytics/cycle_analytics/cycle_analytics_spec.rb
...eatures/analytics/cycle_analytics/cycle_analytics_spec.rb
+62
-14
ee/spec/frontend/analytics/cycle_analytics/components/__snapshots__/custom_stage_form_spec.js.snap
...s/components/__snapshots__/custom_stage_form_spec.js.snap
+2
-2
ee/spec/frontend/analytics/cycle_analytics/components/custom_stage_form_spec.js
...tics/cycle_analytics/components/custom_stage_form_spec.js
+80
-13
ee/spec/frontend/analytics/cycle_analytics/store/getters_spec.js
.../frontend/analytics/cycle_analytics/store/getters_spec.js
+17
-0
locale/gitlab.pot
locale/gitlab.pot
+9
-0
No files found.
app/assets/stylesheets/pages/cycle_analytics.scss
View file @
6fe5b635
...
...
@@ -192,7 +192,7 @@
.stage-events
{
width
:
60%
;
overflow
:
scroll
;
height
:
467px
;
min-
height
:
467px
;
}
.stage-event-list
{
...
...
ee/app/assets/javascripts/analytics/cycle_analytics/components/base.vue
View file @
6fe5b635
...
...
@@ -76,6 +76,7 @@ export default {
'
durationChartPlottableData
'
,
'
tasksByTypeChartData
'
,
'
durationChartMedianData
'
,
'
activeStages
'
,
]),
shouldRenderEmptyState
()
{
return
!
this
.
selectedGroup
;
...
...
@@ -271,7 +272,7 @@ export default {
v-if=
"selectedStage"
class=
"js-stage-table"
:current-stage=
"selectedStage"
:stages=
"
s
tages"
:stages=
"
activeS
tages"
:medians=
"medians"
:is-loading=
"isLoadingStage"
:is-empty-stage=
"isEmptyStage"
...
...
ee/app/assets/javascripts/analytics/cycle_analytics/components/custom_stage_form.vue
View file @
6fe5b635
<
script
>
import
{
mapGetters
}
from
'
vuex
'
;
import
{
isEqual
}
from
'
underscore
'
;
import
{
GlFormGroup
,
GlFormInput
,
GlFormSelect
,
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
{
GlFormGroup
,
GlFormInput
,
GlFormSelect
,
GlLoadingIcon
,
GlDropdown
,
GlDropdownHeader
,
GlDropdownItem
,
}
from
'
@gitlab/ui
'
;
import
{
s__
}
from
'
~/locale
'
;
import
{
convertObjectPropsToSnakeCase
}
from
'
~/lib/utils/common_utils
'
;
import
LabelsSelector
from
'
./labels_selector.vue
'
;
...
...
@@ -30,6 +39,9 @@ export default {
GlFormSelect
,
GlLoadingIcon
,
LabelsSelector
,
GlDropdown
,
GlDropdownHeader
,
GlDropdownItem
,
},
props
:
{
events
:
{
...
...
@@ -79,6 +91,7 @@ export default {
};
},
computed
:
{
...
mapGetters
([
'
hiddenStages
'
]),
startEventOptions
()
{
return
[
{
value
:
null
,
text
:
s__
(
'
CustomCycleAnalytics|Select start event
'
)
},
...
...
@@ -148,6 +161,9 @@ export default {
?
s__
(
'
CustomCycleAnalytics|Editing stage
'
)
:
s__
(
'
CustomCycleAnalytics|New stage
'
);
},
hasHiddenStages
()
{
return
this
.
hiddenStages
.
length
;
},
},
watch
:
{
initialFields
(
newFields
)
{
...
...
@@ -202,13 +218,28 @@ export default {
onUpdateEndEventField
()
{
this
.
$set
(
this
.
fieldErrors
,
'
endEventIdentifier
'
,
null
);
},
handleRecoverStage
(
id
)
{
this
.
$emit
(
STAGE_ACTIONS
.
UPDATE
,
{
id
,
hidden
:
false
});
},
},
};
</
script
>
<
template
>
<form
class=
"custom-stage-form m-4 mt-0"
>
<div
class=
"mb-1"
>
<div
class=
"mb-1
d-flex flex-row justify-content-between
"
>
<h4>
{{
formTitle
}}
</h4>
<gl-dropdown
:text=
"__('Recover hidden stage')"
class=
"js-recover-hidden-stage-dropdown"
>
<gl-dropdown-header>
{{
__
(
'
Default stages
'
)
}}
</gl-dropdown-header>
<template
v-if=
"hasHiddenStages"
>
<gl-dropdown-item
v-for=
"stage in hiddenStages"
:key=
"stage.id"
@
click=
"handleRecoverStage(stage.id)"
>
{{
stage
.
title
}}
</gl-dropdown-item
>
</
template
>
<p
v-else
class=
"mx-3 my-2"
>
{{ __('All default stages are currently visible') }}
</p>
</gl-dropdown>
</div>
<gl-form-group
...
...
ee/app/assets/javascripts/analytics/cycle_analytics/store/getters.js
View file @
6fe5b635
...
...
@@ -48,3 +48,9 @@ export const tasksByTypeChartData = ({ tasksByType, startDate, endDate }) => {
}
return
{
groupBy
:
[],
data
:
[],
seriesNames
:
[]
};
};
const
filterStagesByHiddenStatus
=
(
stages
=
[],
isHidden
=
true
)
=>
stages
.
filter
(({
hidden
=
false
})
=>
hidden
===
isHidden
);
export
const
hiddenStages
=
({
stages
})
=>
filterStagesByHiddenStatus
(
stages
);
export
const
activeStages
=
({
stages
})
=>
filterStagesByHiddenStatus
(
stages
,
false
);
ee/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js
View file @
6fe5b635
...
...
@@ -137,7 +137,7 @@ export default {
},
[
types
.
RECEIVE_GROUP_STAGES_AND_EVENTS_SUCCESS
](
state
,
data
)
{
const
{
events
=
[],
stages
=
[]
}
=
data
;
state
.
stages
=
transformRawStages
(
stages
.
filter
(({
hidden
=
false
})
=>
!
hidden
)
);
state
.
stages
=
transformRawStages
(
stages
);
state
.
customStageFormEvents
=
events
.
map
(
ev
=>
convertObjectPropsToCamelCase
(
ev
,
{
deep
:
true
}),
...
...
ee/spec/features/analytics/cycle_analytics/cycle_analytics_spec.rb
View file @
6fe5b635
...
...
@@ -299,7 +299,7 @@ describe 'Group Value Stream Analytics', :js do
start_label_event
=
:issue_label_added
stop_label_event
=
:issue_label_removed
let
(
:
button_class
)
{
'.js-add-stage-button'
}
let
(
:
add_stage_button
)
{
'.js-add-stage-button'
}
let
(
:params
)
{
{
name:
custom_stage_name
,
start_event_identifier:
start_event_identifier
,
end_event_identifier:
end_event_identifier
}
}
let
(
:first_default_stage
)
{
page
.
find
(
'.stage-nav-item-cell'
,
text:
"Issue"
).
ancestor
(
".stage-nav-item"
)
}
let
(
:first_custom_stage
)
{
page
.
find
(
'.stage-nav-item-cell'
,
text:
custom_stage_name
).
ancestor
(
".stage-nav-item"
)
}
...
...
@@ -334,34 +334,34 @@ describe 'Group Value Stream Analytics', :js do
context
'Add a stage button'
do
it
'is visible'
do
expect
(
page
).
to
have_selector
(
button_class
,
visible:
true
)
expect
(
page
).
to
have_selector
(
add_stage_button
,
visible:
true
)
expect
(
page
).
to
have_text
(
'Add a stage'
)
end
it
'becomes active when clicked'
do
expect
(
page
).
not_to
have_selector
(
"
#{
button_class
}
.active"
)
expect
(
page
).
not_to
have_selector
(
"
#{
add_stage_button
}
.active"
)
find
(
button_class
).
click
find
(
add_stage_button
).
click
expect
(
page
).
to
have_selector
(
"
#{
button_class
}
.active"
)
expect
(
page
).
to
have_selector
(
"
#{
add_stage_button
}
.active"
)
end
it
'displays the custom stage form when clicked'
do
expect
(
page
).
not_to
have_text
(
'New stage'
)
page
.
find
(
button_class
).
click
page
.
find
(
add_stage_button
).
click
expect
(
page
).
to
have_text
(
'New stage'
)
end
end
context
'Custom stage form'
do
let
(
:show_form_
button_class
)
{
'.js-add-stage-button'
}
let
(
:show_form_
add_stage_button
)
{
'.js-add-stage-button'
}
before
do
select_group
page
.
find
(
show_form_
button_class
).
click
page
.
find
(
show_form_
add_stage_button
).
click
wait_for_requests
end
...
...
@@ -535,6 +535,25 @@ describe 'Group Value Stream Analytics', :js do
context
'Stage table'
do
context
'default stages'
do
let
(
:nav
)
{
page
.
find
(
stage_nav_selector
)
}
def
open_recover_stage_dropdown
find
(
add_stage_button
).
click
expect
(
page
).
to
have_content
(
'New stage'
)
expect
(
page
).
to
have_content
(
'Recover hidden stage'
)
click_button
"Recover hidden stage"
within
(
:css
,
'.js-recover-hidden-stage-dropdown'
)
do
expect
(
find
(
".dropdown-menu"
)).
to
have_content
(
'Default stages'
)
end
end
def
active_stages
page
.
all
(
".stage-nav .stage-name"
).
collect
(
&
:text
)
end
before
do
select_group
...
...
@@ -553,14 +572,43 @@ describe 'Group Value Stream Analytics', :js do
expect
(
first_default_stage
.
find
(
'.more-actions-dropdown'
)).
not_to
have_text
"Remove stage"
end
it
'will not appear in the stage table after being
hidden'
do
nav
=
page
.
find
(
stage_nav_selector
)
expect
(
nav
).
to
have_text
(
"Issue"
)
context
'
hidden'
do
before
do
click_button
"Hide stage"
click_button
"Hide stage"
# wait for the stage list to laod
expect
(
nav
).
to
have_content
(
"Plan"
)
end
expect
(
page
.
find
(
'.flash-notice'
)).
to
have_text
'Stage data updated'
expect
(
nav
).
not_to
have_text
(
"Issue"
)
it
'will not appear in the stage table'
do
expect
(
active_stages
).
not_to
include
(
"Issue"
)
end
it
'can be recovered'
do
open_recover_stage_dropdown
expect
(
page
.
find
(
'.js-recover-hidden-stage-dropdown'
)).
to
have_text
(
'Issue'
)
end
end
context
'recovered'
do
before
do
click_button
"Hide stage"
# wait for the stage list to laod
expect
(
nav
).
to
have_content
(
"Plan"
)
end
it
'will appear in the stage table'
do
open_recover_stage_dropdown
click_button
(
"Issue"
)
# wait for the stage list to laod
expect
(
nav
).
to
have_content
(
"Plan"
)
expect
(
page
.
find
(
'.flash-notice'
)).
to
have_content
'Stage data updated'
expect
(
active_stages
).
to
include
(
"Issue"
)
end
end
end
...
...
ee/spec/frontend/analytics/cycle_analytics/components/__snapshots__/custom_stage_form_spec.js.snap
View file @
6fe5b635
...
...
@@ -7,7 +7,7 @@ exports[`CustomStageForm Editing a custom stage isSavingCustomStage=true display
`;
exports[`CustomStageForm Start event with events does not select events with canBeStartEvent=false for the start events dropdown 1`] = `
"<select name=\\"custom-stage-start-event\\" required=\\"required\\" aria-required=\\"true\\" class=\\"gl-form-select custom-select\\" id=\\"__BVID__1
23
\\">
"<select name=\\"custom-stage-start-event\\" required=\\"required\\" aria-required=\\"true\\" class=\\"gl-form-select custom-select\\" id=\\"__BVID__1
77
\\">
<option value=\\"\\">Select start event</option>
<option value=\\"issue_created\\">Issue created</option>
<option value=\\"issue_first_mentioned_in_commit\\">Issue first mentioned in a commit</option>
...
...
@@ -30,7 +30,7 @@ exports[`CustomStageForm Start event with events does not select events with can
`;
exports[`CustomStageForm Start event with events selects events with canBeStartEvent=true for the start events dropdown 1`] = `
"<select name=\\"custom-stage-start-event\\" required=\\"required\\" aria-required=\\"true\\" class=\\"gl-form-select custom-select\\" id=\\"__BVID__
95
\\">
"<select name=\\"custom-stage-start-event\\" required=\\"required\\" aria-required=\\"true\\" class=\\"gl-form-select custom-select\\" id=\\"__BVID__
137
\\">
<option value=\\"\\">Select start event</option>
<option value=\\"issue_created\\">Issue created</option>
<option value=\\"issue_first_mentioned_in_commit\\">Issue first mentioned in a commit</option>
...
...
ee/spec/frontend/analytics/cycle_analytics/components/custom_stage_form_spec.js
View file @
6fe5b635
import
Vue
from
'
vue
'
;
import
{
mount
}
from
'
@vue/test-utils
'
;
import
Vuex
from
'
vuex
'
;
import
createStore
from
'
ee/analytics/cycle_analytics/store
'
;
import
{
createLocalVue
,
mount
}
from
'
@vue/test-utils
'
;
import
CustomStageForm
from
'
ee/analytics/cycle_analytics/components/custom_stage_form.vue
'
;
import
{
STAGE_ACTIONS
}
from
'
ee/analytics/cycle_analytics/constants
'
;
import
{
...
...
@@ -21,14 +23,22 @@ const initData = {
endEventLabelId
:
groupLabels
[
1
].
id
,
};
let
store
=
null
;
const
localVue
=
createLocalVue
();
localVue
.
use
(
Vuex
);
describe
(
'
CustomStageForm
'
,
()
=>
{
function
createComponent
(
props
)
{
function
createComponent
(
props
=
{},
stubs
=
{})
{
store
=
createStore
();
return
mount
(
CustomStageForm
,
{
localVue
,
store
,
propsData
:
{
events
,
labels
:
groupLabels
,
...
props
,
},
stubs
,
});
}
...
...
@@ -44,6 +54,9 @@ describe('CustomStageForm', () => {
submit
:
'
.js-save-stage
'
,
cancel
:
'
.js-save-stage-cancel
'
,
invalidFeedback
:
'
.invalid-feedback
'
,
recoverStageDropdown
:
'
.js-recover-hidden-stage-dropdown
'
,
recoverStageDropdownTrigger
:
'
.js-recover-hidden-stage-dropdown .dropdown-toggle
'
,
hiddenStageDropdownOption
:
'
.js-recover-hidden-stage-dropdown .dropdown-item
'
,
};
function
getDropdownOption
(
_wrapper
,
dropdown
,
index
)
{
...
...
@@ -122,7 +135,7 @@ describe('CustomStageForm', () => {
describe
(
'
start event label
'
,
()
=>
{
beforeEach
(()
=>
{
wrapper
=
createComponent
(
{},
false
);
wrapper
=
createComponent
();
});
afterEach
(()
=>
{
...
...
@@ -174,7 +187,7 @@ describe('CustomStageForm', () => {
const
currAllowed
=
startEvents
[
startEventArrayIndex
].
allowedEndEvents
;
beforeEach
(()
=>
{
wrapper
=
createComponent
(
{},
false
);
wrapper
=
createComponent
();
});
it
(
'
notifies that a start event needs to be selected first
'
,
()
=>
{
...
...
@@ -251,7 +264,7 @@ describe('CustomStageForm', () => {
describe
(
'
with a stop event selected and a change to the start event
'
,
()
=>
{
beforeEach
(()
=>
{
wrapper
=
createComponent
(
{}
);
wrapper
=
createComponent
();
wrapper
.
setData
({
fields
:
{
...
...
@@ -518,15 +531,12 @@ describe('CustomStageForm', () => {
describe
(
'
Editing a custom stage
'
,
()
=>
{
beforeEach
(()
=>
{
wrapper
=
createComponent
(
{
isEditingCustomStage
:
true
,
initialFields
:
{
...
initData
,
},
wrapper
=
createComponent
({
isEditingCustomStage
:
true
,
initialFields
:
{
...
initData
,
},
false
,
);
});
wrapper
.
setData
({
fields
:
{
...
...
@@ -682,4 +692,61 @@ describe('CustomStageForm', () => {
expect
(
wrapper
.
find
({
ref
:
'
startEventIdentifier
'
}).
html
()).
toContain
(
'
cant be blank
'
);
});
});
describe
(
'
recover stage dropdown
'
,
()
=>
{
const
formFieldStubs
=
{
'
gl-form-group
'
:
true
,
'
gl-form-select
'
:
true
,
'
labels-selector
'
:
true
,
};
beforeEach
(()
=>
{
wrapper
=
createComponent
({},
formFieldStubs
);
});
describe
(
'
without hidden stages
'
,
()
=>
{
it
(
'
has the recover stage dropdown
'
,
()
=>
{
expect
(
wrapper
.
find
(
sel
.
recoverStageDropdown
).
exists
()).
toBe
(
true
);
});
it
(
'
has no stages available to recover
'
,
()
=>
{
wrapper
.
find
(
sel
.
recoverStageDropdownTrigger
).
trigger
(
'
click
'
);
return
wrapper
.
vm
.
$nextTick
().
then
(()
=>
{
expect
(
wrapper
.
find
(
sel
.
recoverStageDropdown
).
text
()).
toContain
(
'
All default stages are currently visible
'
,
);
});
});
});
describe
(
'
with hidden stages
'
,
()
=>
{
beforeEach
(()
=>
{
wrapper
=
createComponent
({},
formFieldStubs
);
store
.
state
.
stages
=
[{
id
:
'
my-stage
'
,
title
:
'
My default stage
'
,
hidden
:
true
}];
});
it
(
'
has stages available to recover
'
,
()
=>
{
wrapper
.
find
(
sel
.
recoverStageDropdownTrigger
).
trigger
(
'
click
'
);
return
wrapper
.
vm
.
$nextTick
().
then
(()
=>
{
const
txt
=
wrapper
.
find
(
sel
.
recoverStageDropdown
).
text
();
expect
(
txt
).
not
.
toContain
(
'
All default stages are currently visible
'
);
expect
(
txt
).
toContain
(
'
My default stage
'
);
});
});
it
(
`emits the
${
STAGE_ACTIONS
.
UPDATE
}
action when clicking on a stage to recover`
,
()
=>
{
wrapper
.
find
(
sel
.
recoverStageDropdownTrigger
).
trigger
(
'
click
'
);
return
wrapper
.
vm
.
$nextTick
().
then
(()
=>
{
wrapper
.
findAll
(
sel
.
hiddenStageDropdownOption
)
.
at
(
0
)
.
trigger
(
'
click
'
);
expect
(
wrapper
.
emitted
()).
toEqual
({
[
STAGE_ACTIONS
.
UPDATE
]:
[[{
hidden
:
false
,
id
:
'
my-stage
'
}]],
});
});
});
});
});
});
ee/spec/frontend/analytics/cycle_analytics/store/getters_spec.js
View file @
6fe5b635
...
...
@@ -6,6 +6,7 @@ import {
transformedDurationMedianData
,
durationChartPlottableData
,
durationChartPlottableMedianData
,
allowedStages
,
}
from
'
../mock_data
'
;
let
state
=
null
;
...
...
@@ -121,4 +122,20 @@ describe('Cycle analytics getters', () => {
expect
(
getters
.
durationChartMedianData
(
stateWithDurationMedianData
)).
toEqual
([]);
});
});
const
hiddenStage
=
{
...
allowedStages
[
2
],
hidden
:
true
};
const
givenStages
=
[
allowedStages
[
0
],
allowedStages
[
1
],
hiddenStage
];
describe
.
each
`
func | givenStages | expectedStages
${
'
hiddenStages
'
}
|
${
givenStages
}
|
${[
hiddenStage
]}
${
'
activeStages
'
}
|
${
givenStages
}
|
${[
allowedStages
[
0
],
allowedStages
[
1
]]}
`
(
'
hiddenStages
'
,
({
func
,
expectedStages
,
givenStages
:
stages
})
=>
{
it
(
`'
${
func
}
' returns
${
expectedStages
.
length
}
stages`
,
()
=>
{
expect
(
getters
[
func
]({
stages
})).
toEqual
(
expectedStages
);
});
it
(
`'
${
func
}
' returns an empty array if there are no stages`
,
()
=>
{
expect
(
getters
[
func
]({
stages
:
[]
})).
toEqual
([]);
});
});
});
locale/gitlab.pot
View file @
6fe5b635
...
...
@@ -1538,6 +1538,9 @@ msgstr ""
msgid "All changes are committed"
msgstr ""
msgid "All default stages are currently visible"
msgstr ""
msgid "All email addresses will be used to identify your commits."
msgstr ""
...
...
@@ -6110,6 +6113,9 @@ msgstr ""
msgid "Default projects limit"
msgstr ""
msgid "Default stages"
msgstr ""
msgid "Default: Directly import the Google Code email address or username"
msgstr ""
...
...
@@ -15727,6 +15733,9 @@ msgstr ""
msgid "Recipe"
msgstr ""
msgid "Recover hidden stage"
msgstr ""
msgid "Recovery Codes"
msgstr ""
...
...
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