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
160f182b
Commit
160f182b
authored
Jul 15, 2021
by
Ezekiel Kigbo
Committed by
Enrique Alcántara
Jul 15, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Fetch project VSA medians from group VSA endpoint
parent
856a51d3
Changes
17
Hide whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
350 additions
and
54 deletions
+350
-54
app/assets/javascripts/api/analytics_api.js
app/assets/javascripts/api/analytics_api.js
+19
-0
app/assets/javascripts/cycle_analytics/index.js
app/assets/javascripts/cycle_analytics/index.js
+14
-1
app/assets/javascripts/cycle_analytics/store/actions.js
app/assets/javascripts/cycle_analytics/store/actions.js
+54
-6
app/assets/javascripts/cycle_analytics/store/getters.js
app/assets/javascripts/cycle_analytics/store/getters.js
+29
-0
app/assets/javascripts/cycle_analytics/store/mutation_types.js
...ssets/javascripts/cycle_analytics/store/mutation_types.js
+4
-0
app/assets/javascripts/cycle_analytics/store/mutations.js
app/assets/javascripts/cycle_analytics/store/mutations.js
+35
-3
app/assets/javascripts/cycle_analytics/store/state.js
app/assets/javascripts/cycle_analytics/store/state.js
+5
-0
app/assets/javascripts/cycle_analytics/utils.js
app/assets/javascripts/cycle_analytics/utils.js
+20
-0
app/controllers/projects/cycle_analytics_controller.rb
app/controllers/projects/cycle_analytics_controller.rb
+4
-0
app/views/projects/cycle_analytics/show.html.haml
app/views/projects/cycle_analytics/show.html.haml
+1
-1
ee/app/assets/javascripts/analytics/cycle_analytics/store/actions.js
...ts/javascripts/analytics/cycle_analytics/store/actions.js
+2
-1
ee/app/assets/javascripts/api.js
ee/app/assets/javascripts/api.js
+0
-6
ee/spec/frontend/api_spec.js
ee/spec/frontend/api_spec.js
+0
-23
spec/frontend/cycle_analytics/mock_data.js
spec/frontend/cycle_analytics/mock_data.js
+9
-0
spec/frontend/cycle_analytics/store/actions_spec.js
spec/frontend/cycle_analytics/store/actions_spec.js
+86
-2
spec/frontend/cycle_analytics/store/mutations_spec.js
spec/frontend/cycle_analytics/store/mutations_spec.js
+58
-11
spec/frontend/cycle_analytics/utils_spec.js
spec/frontend/cycle_analytics/utils_spec.js
+10
-0
No files found.
app/assets/javascripts/api/analytics_api.js
View file @
160f182b
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
{
buildApiUrl
}
from
'
./api_utils
'
;
const
GROUP_VSA_PATH_BASE
=
'
/groups/:id/-/analytics/value_stream_analytics/value_streams/:value_stream_id/stages/:stage_id
'
;
const
PROJECT_VSA_PATH_BASE
=
'
/:project_path/-/analytics/value_stream_analytics/value_streams
'
;
const
PROJECT_VSA_STAGES_PATH
=
`
${
PROJECT_VSA_PATH_BASE
}
/:value_stream_id/stages`
;
...
...
@@ -13,6 +15,12 @@ const buildProjectValueStreamPath = (projectPath, valueStreamId = null) => {
return
buildApiUrl
(
PROJECT_VSA_PATH_BASE
).
replace
(
'
:project_path
'
,
projectPath
);
};
const
buildGroupValueStreamPath
=
({
groupId
,
valueStreamId
=
null
,
stageId
=
null
})
=>
buildApiUrl
(
GROUP_VSA_PATH_BASE
)
.
replace
(
'
:id
'
,
groupId
)
.
replace
(
'
:value_stream_id
'
,
valueStreamId
)
.
replace
(
'
:stage_id
'
,
stageId
);
export
const
getProjectValueStreams
=
(
projectPath
)
=>
{
const
url
=
buildProjectValueStreamPath
(
projectPath
);
return
axios
.
get
(
url
);
...
...
@@ -30,3 +38,14 @@ export const getProjectValueStreamStageData = ({ requestPath, stageId, params })
export
const
getProjectValueStreamMetrics
=
(
requestPath
,
params
)
=>
axios
.
get
(
requestPath
,
{
params
});
/**
* Shared group VSA paths
* We share some endpoints across and group and project level VSA
* When used for project level VSA, requests should include the `project_id` in the params object
*/
export
const
getValueStreamStageMedian
=
({
groupId
,
valueStreamId
,
stageId
},
params
=
{})
=>
{
const
stageBase
=
buildGroupValueStreamPath
({
groupId
,
valueStreamId
,
stageId
});
return
axios
.
get
(
`
${
stageBase
}
/median`
,
{
params
});
};
app/assets/javascripts/cycle_analytics/index.js
View file @
160f182b
...
...
@@ -8,11 +8,24 @@ Vue.use(Translate);
export
default
()
=>
{
const
store
=
createStore
();
const
el
=
document
.
querySelector
(
'
#js-cycle-analytics
'
);
const
{
noAccessSvgPath
,
noDataSvgPath
,
requestPath
,
fullPath
}
=
el
.
dataset
;
const
{
noAccessSvgPath
,
noDataSvgPath
,
requestPath
,
fullPath
,
projectId
,
groupPath
,
}
=
el
.
dataset
;
store
.
dispatch
(
'
initializeVsa
'
,
{
projectId
:
parseInt
(
projectId
,
10
),
groupPath
,
requestPath
,
fullPath
,
features
:
{
cycleAnalyticsForGroups
:
(
groupPath
&&
gon
?.
licensed_features
?.
cycleAnalyticsForGroups
)
||
false
,
},
});
// eslint-disable-next-line no-new
...
...
app/assets/javascripts/cycle_analytics/store/actions.js
View file @
160f182b
...
...
@@ -3,6 +3,7 @@ import {
getProjectValueStreams
,
getProjectValueStreamStageData
,
getProjectValueStreamMetrics
,
getValueStreamStageMedian
,
}
from
'
~/api/analytics_api
'
;
import
createFlash
from
'
~/flash
'
;
import
{
__
}
from
'
~/locale
'
;
...
...
@@ -35,21 +36,33 @@ export const receiveValueStreamsSuccess = ({ commit, dispatch }, data = []) => {
};
export
const
fetchValueStreams
=
({
commit
,
dispatch
,
state
})
=>
{
const
{
fullPath
}
=
state
;
const
{
fullPath
,
features
:
{
cycleAnalyticsForGroups
},
}
=
state
;
commit
(
types
.
REQUEST_VALUE_STREAMS
);
const
stageRequests
=
[
'
setSelectedStage
'
];
if
(
cycleAnalyticsForGroups
)
{
stageRequests
.
push
(
'
fetchStageMedians
'
);
}
return
getProjectValueStreams
(
fullPath
)
.
then
(({
data
})
=>
dispatch
(
'
receiveValueStreamsSuccess
'
,
data
))
.
then
(()
=>
dispatch
(
'
setSelectedStage
'
))
.
then
(()
=>
Promise
.
all
(
stageRequests
.
map
((
r
)
=>
dispatch
(
r
))
))
.
catch
(({
response
:
{
status
}
})
=>
{
commit
(
types
.
RECEIVE_VALUE_STREAMS_ERROR
,
status
);
});
};
export
const
fetchCycleAnalyticsData
=
({
state
:
{
requestPath
,
startDate
},
commit
})
=>
{
export
const
fetchCycleAnalyticsData
=
({
state
:
{
requestPath
},
getters
:
{
legacyFilterParams
},
commit
,
})
=>
{
commit
(
types
.
REQUEST_CYCLE_ANALYTICS_DATA
);
return
getProjectValueStreamMetrics
(
requestPath
,
{
'
cycle_analytics[start_date]
'
:
startDate
}
)
return
getProjectValueStreamMetrics
(
requestPath
,
legacyFilterParams
)
.
then
(({
data
})
=>
commit
(
types
.
RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS
,
data
))
.
catch
(()
=>
{
commit
(
types
.
RECEIVE_CYCLE_ANALYTICS_DATA_ERROR
);
...
...
@@ -59,13 +72,17 @@ export const fetchCycleAnalyticsData = ({ state: { requestPath, startDate }, com
});
};
export
const
fetchStageData
=
({
state
:
{
requestPath
,
selectedStage
,
startDate
},
commit
})
=>
{
export
const
fetchStageData
=
({
state
:
{
requestPath
,
selectedStage
},
getters
:
{
legacyFilterParams
},
commit
,
})
=>
{
commit
(
types
.
REQUEST_STAGE_DATA
);
return
getProjectValueStreamStageData
({
requestPath
,
stageId
:
selectedStage
.
id
,
params
:
{
'
cycle_analytics[start_date]
'
:
startDate
}
,
params
:
legacyFilterParams
,
})
.
then
(({
data
})
=>
{
// when there's a query timeout, the request succeeds but the error is encoded in the response data
...
...
@@ -78,6 +95,37 @@ export const fetchStageData = ({ state: { requestPath, selectedStage, startDate
.
catch
(()
=>
commit
(
types
.
RECEIVE_STAGE_DATA_ERROR
));
};
const
getStageMedians
=
({
stageId
,
vsaParams
,
filterParams
=
{}
})
=>
{
return
getValueStreamStageMedian
({
...
vsaParams
,
stageId
},
filterParams
).
then
(({
data
})
=>
({
id
:
stageId
,
value
:
data
?.
value
||
null
,
}));
};
export
const
fetchStageMedians
=
({
state
:
{
stages
},
getters
:
{
requestParams
:
vsaParams
,
filterParams
},
commit
,
})
=>
{
commit
(
types
.
REQUEST_STAGE_MEDIANS
);
return
Promise
.
all
(
stages
.
map
(({
id
:
stageId
})
=>
getStageMedians
({
vsaParams
,
stageId
,
filterParams
,
}),
),
)
.
then
((
data
)
=>
commit
(
types
.
RECEIVE_STAGE_MEDIANS_SUCCESS
,
data
))
.
catch
((
error
)
=>
{
commit
(
types
.
RECEIVE_STAGE_MEDIANS_ERROR
,
error
);
createFlash
({
message
:
__
(
'
There was an error fetching median data for stages
'
),
});
});
};
export
const
setSelectedStage
=
({
dispatch
,
commit
,
state
:
{
stages
}
},
selectedStage
=
null
)
=>
{
const
stage
=
selectedStage
||
stages
[
0
];
commit
(
types
.
SET_SELECTED_STAGE
,
stage
);
...
...
app/assets/javascripts/cycle_analytics/store/getters.js
View file @
160f182b
import
dateFormat
from
'
dateformat
'
;
import
{
dateFormats
}
from
'
~/analytics/shared/constants
'
;
import
{
transformStagesForPathNavigation
,
filterStagesByHiddenStatus
}
from
'
../utils
'
;
export
const
pathNavigationData
=
({
stages
,
medians
,
stageCounts
,
selectedStage
})
=>
{
...
...
@@ -8,3 +10,30 @@ export const pathNavigationData = ({ stages, medians, stageCounts, selectedStage
selectedStage
,
});
};
export
const
requestParams
=
(
state
)
=>
{
const
{
selectedStage
:
{
id
:
stageId
=
null
},
groupPath
:
groupId
,
selectedValueStream
:
{
id
:
valueStreamId
},
}
=
state
;
return
{
valueStreamId
,
groupId
,
stageId
};
};
const
dateRangeParams
=
({
createdAfter
,
createdBefore
})
=>
({
created_after
:
createdAfter
?
dateFormat
(
createdAfter
,
dateFormats
.
isoDate
)
:
null
,
created_before
:
createdBefore
?
dateFormat
(
createdBefore
,
dateFormats
.
isoDate
)
:
null
,
});
export
const
legacyFilterParams
=
({
startDate
})
=>
{
return
{
'
cycle_analytics[start_date]
'
:
startDate
,
};
};
export
const
filterParams
=
({
id
,
...
rest
})
=>
{
return
{
project_ids
:
[
id
],
...
dateRangeParams
(
rest
),
};
};
app/assets/javascripts/cycle_analytics/store/mutation_types.js
View file @
160f182b
...
...
@@ -20,3 +20,7 @@ export const RECEIVE_CYCLE_ANALYTICS_DATA_ERROR = 'RECEIVE_CYCLE_ANALYTICS_DATA_
export
const
REQUEST_STAGE_DATA
=
'
REQUEST_STAGE_DATA
'
;
export
const
RECEIVE_STAGE_DATA_SUCCESS
=
'
RECEIVE_STAGE_DATA_SUCCESS
'
;
export
const
RECEIVE_STAGE_DATA_ERROR
=
'
RECEIVE_STAGE_DATA_ERROR
'
;
export
const
REQUEST_STAGE_MEDIANS
=
'
REQUEST_STAGE_MEDIANS
'
;
export
const
RECEIVE_STAGE_MEDIANS_SUCCESS
=
'
RECEIVE_STAGE_MEDIANS_SUCCESS
'
;
export
const
RECEIVE_STAGE_MEDIANS_ERROR
=
'
RECEIVE_STAGE_MEDIANS_ERROR
'
;
app/assets/javascripts/cycle_analytics/store/mutations.js
View file @
160f182b
import
{
convertObjectPropsToCamelCase
}
from
'
~/lib/utils/common_utils
'
;
import
{
decorateData
,
decorateEvents
,
formatMedianValues
}
from
'
../utils
'
;
import
{
DEFAULT_DAYS_TO_DISPLAY
}
from
'
../constants
'
;
import
{
decorateData
,
decorateEvents
,
formatMedianValues
,
calculateFormattedDayInPast
,
}
from
'
../utils
'
;
import
*
as
types
from
'
./mutation_types
'
;
export
default
{
[
types
.
INITIALIZE_VSA
](
state
,
{
requestPath
,
fullPath
})
{
[
types
.
INITIALIZE_VSA
](
state
,
{
requestPath
,
fullPath
,
groupPath
,
projectId
,
features
})
{
state
.
requestPath
=
requestPath
;
state
.
fullPath
=
fullPath
;
state
.
groupPath
=
groupPath
;
state
.
id
=
projectId
;
const
{
now
,
past
}
=
calculateFormattedDayInPast
(
DEFAULT_DAYS_TO_DISPLAY
);
state
.
createdBefore
=
now
;
state
.
createdAfter
=
past
;
state
.
features
=
features
;
},
[
types
.
SET_LOADING
](
state
,
loadingState
)
{
state
.
isLoading
=
loadingState
;
...
...
@@ -18,6 +30,9 @@ export default {
},
[
types
.
SET_DATE_RANGE
](
state
,
{
startDate
})
{
state
.
startDate
=
startDate
;
const
{
now
,
past
}
=
calculateFormattedDayInPast
(
startDate
);
state
.
createdBefore
=
now
;
state
.
createdAfter
=
past
;
},
[
types
.
REQUEST_VALUE_STREAMS
](
state
)
{
state
.
valueStreams
=
[];
...
...
@@ -46,17 +61,25 @@ export default {
[
types
.
REQUEST_CYCLE_ANALYTICS_DATA
](
state
)
{
state
.
isLoading
=
true
;
state
.
hasError
=
false
;
if
(
!
state
.
features
.
cycleAnalyticsForGroups
)
{
state
.
medians
=
{};
}
},
[
types
.
RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS
](
state
,
data
)
{
const
{
summary
,
medians
}
=
decorateData
(
data
);
if
(
!
state
.
features
.
cycleAnalyticsForGroups
)
{
state
.
medians
=
formatMedianValues
(
medians
);
}
state
.
permissions
=
data
.
permissions
;
state
.
summary
=
summary
;
state
.
medians
=
formatMedianValues
(
medians
);
state
.
hasError
=
false
;
},
[
types
.
RECEIVE_CYCLE_ANALYTICS_DATA_ERROR
](
state
)
{
state
.
isLoading
=
false
;
state
.
hasError
=
true
;
if
(
!
state
.
features
.
cycleAnalyticsForGroups
)
{
state
.
medians
=
{};
}
},
[
types
.
REQUEST_STAGE_DATA
](
state
)
{
state
.
isLoadingStage
=
true
;
...
...
@@ -78,4 +101,13 @@ export default {
state
.
hasError
=
true
;
state
.
selectedStageError
=
error
;
},
[
types
.
REQUEST_STAGE_MEDIANS
](
state
)
{
state
.
medians
=
{};
},
[
types
.
RECEIVE_STAGE_MEDIANS_SUCCESS
](
state
,
medians
)
{
state
.
medians
=
formatMedianValues
(
medians
);
},
[
types
.
RECEIVE_STAGE_MEDIANS_ERROR
](
state
)
{
state
.
medians
=
{};
},
};
app/assets/javascripts/cycle_analytics/store/state.js
View file @
160f182b
import
{
DEFAULT_DAYS_TO_DISPLAY
}
from
'
../constants
'
;
export
default
()
=>
({
features
:
{},
id
:
null
,
requestPath
:
''
,
fullPath
:
''
,
startDate
:
DEFAULT_DAYS_TO_DISPLAY
,
createdAfter
:
null
,
createdBefore
:
null
,
stages
:
[],
summary
:
[],
analytics
:
[],
...
...
@@ -19,4 +23,5 @@ export default () => ({
isLoadingStage
:
false
,
isEmptyStage
:
false
,
permissions
:
{},
parentPath
:
null
,
});
app/assets/javascripts/cycle_analytics/utils.js
View file @
160f182b
import
dateFormat
from
'
dateformat
'
;
import
{
unescape
}
from
'
lodash
'
;
import
{
dateFormats
}
from
'
~/analytics/shared/constants
'
;
import
{
sanitize
}
from
'
~/lib/dompurify
'
;
import
{
roundToNearestHalf
,
convertObjectPropsToCamelCase
}
from
'
~/lib/utils/common_utils
'
;
import
{
getDateInPast
}
from
'
~/lib/utils/datetime/date_calculation_utility
'
;
import
{
parseSeconds
}
from
'
~/lib/utils/datetime_utility
'
;
import
{
s__
,
sprintf
}
from
'
../locale
'
;
import
DEFAULT_EVENT_OBJECTS
from
'
./default_event_objects
'
;
...
...
@@ -115,3 +118,20 @@ export const formatMedianValues = (medians = []) =>
export
const
filterStagesByHiddenStatus
=
(
stages
=
[],
isHidden
=
true
)
=>
stages
.
filter
(({
hidden
=
false
})
=>
hidden
===
isHidden
);
const
toIsoFormat
=
(
d
)
=>
dateFormat
(
d
,
dateFormats
.
isoDate
);
/**
* Takes an integer specifying the number of days to subtract
* from the date specified will return the 2 dates, formatted as ISO dates
*
* @param {Number} daysInPast - Number of days in the past to subtract
* @param {Date} [today=new Date] - Date to subtract days from, defaults to today
* @returns {Object} Returns 'now' and the 'past' date formatted as ISO dates
*/
export
const
calculateFormattedDayInPast
=
(
daysInPast
,
today
=
new
Date
())
=>
{
return
{
now
:
toIsoFormat
(
today
),
past
:
toIsoFormat
(
getDateInPast
(
today
,
daysInPast
)),
};
};
app/controllers/projects/cycle_analytics_controller.rb
View file @
160f182b
...
...
@@ -13,6 +13,10 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
feature_category
:planning_analytics
before_action
do
push_licensed_feature
(
:cycle_analytics_for_groups
)
if
project
.
licensed_feature_available?
(
:cycle_analytics_for_groups
)
end
def
show
@cycle_analytics
=
Analytics
::
CycleAnalytics
::
ProjectLevel
.
new
(
project:
@project
,
options:
options
(
cycle_analytics_project_params
))
...
...
app/views/projects/cycle_analytics/show.html.haml
View file @
160f182b
-
page_title
_
(
"Value Stream Analytics"
)
-
add_page_specific_style
'page_bundles/cycle_analytics'
-
svgs
=
{
empty_state_svg_path:
image_path
(
"illustrations/analytics/cycle-analytics-empty-chart.svg"
),
no_data_svg_path:
image_path
(
"illustrations/analytics/cycle-analytics-empty-chart.svg"
),
no_access_svg_path:
image_path
(
"illustrations/analytics/no-access.svg"
)
}
-
initial_data
=
{
request_path:
project_cycle_analytics_path
(
@project
),
full_path:
@project
.
full_path
}.
merge!
(
svgs
)
-
initial_data
=
{
project_id:
@project
.
id
,
group_path:
@project
.
group
&
.
path
,
request_path:
project_cycle_analytics_path
(
@project
),
full_path:
@project
.
full_path
}.
merge!
(
svgs
)
#js-cycle-analytics
{
data:
initial_data
}
ee/app/assets/javascripts/analytics/cycle_analytics/store/actions.js
View file @
160f182b
import
Api
from
'
ee/api
'
;
import
{
getValueStreamStageMedian
}
from
'
~/api/analytics_api
'
;
import
createFlash
from
'
~/flash
'
;
import
{
normalizeHeaders
,
parseIntPagination
}
from
'
~/lib/utils/common_utils
'
;
import
httpStatus
from
'
~/lib/utils/http_status
'
;
...
...
@@ -99,7 +100,7 @@ export const receiveStageMedianValuesError = ({ commit }, error) => {
};
const
fetchStageMedian
=
({
groupId
,
valueStreamId
,
stageId
,
params
})
=>
Api
.
cycleAnalyticsStageMedian
({
groupId
,
valueStreamId
,
stageId
,
params
}
).
then
(({
data
})
=>
{
getValueStreamStageMedian
({
groupId
,
valueStreamId
,
stageId
},
params
).
then
(({
data
})
=>
{
return
{
id
:
stageId
,
...(
data
?.
error
...
...
ee/app/assets/javascripts/api.js
View file @
160f182b
...
...
@@ -169,12 +169,6 @@ export default {
return
axios
.
get
(
url
,
{
params
});
},
cycleAnalyticsStageMedian
({
groupId
,
valueStreamId
,
stageId
,
params
=
{}
})
{
const
stageBase
=
this
.
cycleAnalyticsStageUrl
({
groupId
,
valueStreamId
,
stageId
});
const
url
=
`
${
stageBase
}
/median`
;
return
axios
.
get
(
url
,
{
params
});
},
cycleAnalyticsStageCount
({
groupId
,
valueStreamId
,
stageId
,
params
=
{}
})
{
const
stageBase
=
this
.
cycleAnalyticsStageUrl
({
groupId
,
valueStreamId
,
stageId
});
const
url
=
`
${
stageBase
}
/count`
;
...
...
ee/spec/frontend/api_spec.js
View file @
160f182b
...
...
@@ -455,29 +455,6 @@ describe('Api', () => {
});
});
describe
(
'
cycleAnalyticsStageMedian
'
,
()
=>
{
it
(
'
fetches stage events
'
,
(
done
)
=>
{
const
response
=
{
value
:
'
5 days ago
'
};
const
params
=
{
...
defaultParams
};
const
expectedUrl
=
valueStreamBaseUrl
({
id
:
valueStreamId
,
resource
:
`stages/
${
stageId
}
/median`
,
});
mock
.
onGet
(
expectedUrl
).
reply
(
httpStatus
.
OK
,
response
);
Api
.
cycleAnalyticsStageMedian
({
groupId
,
valueStreamId
,
stageId
,
params
})
.
then
((
responseObj
)
=>
expectRequestWithCorrectParameters
(
responseObj
,
{
response
,
params
,
expectedUrl
,
}),
)
.
then
(
done
)
.
catch
(
done
.
fail
);
});
});
describe
(
'
cycleAnalyticsDurationChart
'
,
()
=>
{
it
(
'
fetches stage duration data
'
,
(
done
)
=>
{
const
response
=
[];
...
...
spec/frontend/cycle_analytics/mock_data.js
View file @
160f182b
...
...
@@ -174,6 +174,15 @@ export const stageMedians = {
staging
:
388800
,
};
export
const
formattedStageMedians
=
{
issue
:
'
2d
'
,
plan
:
'
1d
'
,
review
:
'
1w
'
,
code
:
'
1d
'
,
test
:
'
3d
'
,
staging
:
'
4d
'
,
};
export
const
allowedStages
=
[
issueStage
,
planStage
,
codeStage
];
export
const
transformedProjectStagePathData
=
[
...
...
spec/frontend/cycle_analytics/store/actions_spec.js
View file @
160f182b
...
...
@@ -3,7 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import
testAction
from
'
helpers/vuex_action_helper
'
;
import
*
as
actions
from
'
~/cycle_analytics/store/actions
'
;
import
httpStatusCodes
from
'
~/lib/utils/http_status
'
;
import
{
selectedStage
,
selectedValueStream
}
from
'
../mock_data
'
;
import
{
allowedStages
,
selectedStage
,
selectedValueStream
}
from
'
../mock_data
'
;
const
mockRequestPath
=
'
some/cool/path
'
;
const
mockFullPath
=
'
/namespace/-/analytics/value_stream_analytics/value_streams
'
;
...
...
@@ -25,6 +25,10 @@ const mockRequestedDataMutations = [
},
];
const
features
=
{
cycleAnalyticsForGroups
:
true
,
};
describe
(
'
Project Value Stream Analytics actions
'
,
()
=>
{
let
state
;
let
mock
;
...
...
@@ -175,6 +179,7 @@ describe('Project Value Stream Analytics actions', () => {
beforeEach
(()
=>
{
state
=
{
features
,
fullPath
:
mockFullPath
,
};
mock
=
new
MockAdapter
(
axios
);
...
...
@@ -187,9 +192,33 @@ describe('Project Value Stream Analytics actions', () => {
state
,
payload
:
{},
expectedMutations
:
[{
type
:
'
REQUEST_VALUE_STREAMS
'
}],
expectedActions
:
[{
type
:
'
receiveValueStreamsSuccess
'
},
{
type
:
'
setSelectedStage
'
}],
expectedActions
:
[
{
type
:
'
receiveValueStreamsSuccess
'
},
{
type
:
'
setSelectedStage
'
},
{
type
:
'
fetchStageMedians
'
},
],
}));
describe
(
'
with cycleAnalyticsForGroups=false
'
,
()
=>
{
beforeEach
(()
=>
{
state
=
{
features
:
{
cycleAnalyticsForGroups
:
false
},
fullPath
:
mockFullPath
,
};
mock
=
new
MockAdapter
(
axios
);
mock
.
onGet
(
mockValueStreamPath
).
reply
(
httpStatusCodes
.
OK
);
});
it
(
"
does not dispatch the 'fetchStageMedians' request
"
,
()
=>
testAction
({
action
:
actions
.
fetchValueStreams
,
state
,
payload
:
{},
expectedMutations
:
[{
type
:
'
REQUEST_VALUE_STREAMS
'
}],
expectedActions
:
[{
type
:
'
receiveValueStreamsSuccess
'
},
{
type
:
'
setSelectedStage
'
}],
}));
});
describe
(
'
with a failing request
'
,
()
=>
{
beforeEach
(()
=>
{
mock
=
new
MockAdapter
(
axios
);
...
...
@@ -280,4 +309,59 @@ describe('Project Value Stream Analytics actions', () => {
}));
});
});
describe
(
'
fetchStageMedians
'
,
()
=>
{
const
mockValueStreamPath
=
/median/
;
const
stageMediansPayload
=
[
{
id
:
'
issue
'
,
value
:
null
},
{
id
:
'
plan
'
,
value
:
null
},
{
id
:
'
code
'
,
value
:
null
},
];
const
stageMedianError
=
new
Error
(
`Request failed with status code
${
httpStatusCodes
.
BAD_REQUEST
}
`
,
);
beforeEach
(()
=>
{
state
=
{
fullPath
:
mockFullPath
,
selectedValueStream
,
stages
:
allowedStages
,
};
mock
=
new
MockAdapter
(
axios
);
mock
.
onGet
(
mockValueStreamPath
).
reply
(
httpStatusCodes
.
OK
);
});
it
(
`commits the 'REQUEST_STAGE_MEDIANS' and 'RECEIVE_STAGE_MEDIANS_SUCCESS' mutations`
,
()
=>
testAction
({
action
:
actions
.
fetchStageMedians
,
state
,
payload
:
{},
expectedMutations
:
[
{
type
:
'
REQUEST_STAGE_MEDIANS
'
},
{
type
:
'
RECEIVE_STAGE_MEDIANS_SUCCESS
'
,
payload
:
stageMediansPayload
},
],
expectedActions
:
[],
}));
describe
(
'
with a failing request
'
,
()
=>
{
beforeEach
(()
=>
{
mock
=
new
MockAdapter
(
axios
);
mock
.
onGet
(
mockValueStreamPath
).
reply
(
httpStatusCodes
.
BAD_REQUEST
);
});
it
(
`commits the 'RECEIVE_VALUE_STREAM_STAGES_ERROR' mutation`
,
()
=>
testAction
({
action
:
actions
.
fetchStageMedians
,
state
,
payload
:
{},
expectedMutations
:
[
{
type
:
'
REQUEST_STAGE_MEDIANS
'
},
{
type
:
'
RECEIVE_STAGE_MEDIANS_ERROR
'
,
payload
:
stageMedianError
},
],
expectedActions
:
[],
}));
});
});
});
spec/frontend/cycle_analytics/store/mutations_spec.js
View file @
160f182b
import
{
useFakeDate
}
from
'
helpers/fake_date
'
;
import
{
DEFAULT_DAYS_TO_DISPLAY
}
from
'
~/cycle_analytics/constants
'
;
import
*
as
types
from
'
~/cycle_analytics/store/mutation_types
'
;
import
mutations
from
'
~/cycle_analytics/store/mutations
'
;
import
{
...
...
@@ -9,15 +11,23 @@ import {
selectedValueStream
,
rawValueStreamStages
,
valueStreamStages
,
rawStageMedians
,
formattedStageMedians
,
}
from
'
../mock_data
'
;
let
state
;
const
mockRequestPath
=
'
fake/request/path
'
;
const
mockStartData
=
'
2021-04-20
'
;
const
mockCreatedAfter
=
'
2020-06-18
'
;
const
mockCreatedBefore
=
'
2020-07-18
'
;
const
features
=
{
cycleAnalyticsForGroups
:
true
,
};
describe
(
'
Project Value Stream Analytics mutations
'
,
()
=>
{
useFakeDate
(
2020
,
6
,
18
);
beforeEach
(()
=>
{
state
=
{};
state
=
{
features
};
});
afterEach
(()
=>
{
...
...
@@ -46,6 +56,8 @@ describe('Project Value Stream Analytics mutations', () => {
${
types
.
RECEIVE_STAGE_DATA_ERROR
}
|
${
'
selectedStageEvents
'
}
|
${[]}
${
types
.
RECEIVE_STAGE_DATA_ERROR
}
|
${
'
hasError
'
}
|
${
true
}
${
types
.
RECEIVE_STAGE_DATA_ERROR
}
|
${
'
isEmptyStage
'
}
|
${
true
}
${
types
.
REQUEST_STAGE_MEDIANS
}
|
${
'
medians
'
}
|
${{}}
$
{
types
.
RECEIVE_STAGE_MEDIANS_ERROR
}
|
${
'
medians
'
}
|
${{}}
`('$mutation will set $stateKey to $value', ({ mutation, stateKey, value }) => {
mutations[mutation](state, {});
...
...
@@ -53,15 +65,19 @@ describe('Project Value Stream Analytics mutations', () => {
});
it.each`
mutation | payload | stateKey | value
${
types
.
INITIALIZE_VSA
}
|
${{
requestPath
:
mockRequestPath
}
} |
${
'
requestPath
'
}
|
${
mockRequestPath
}
${
types
.
SET_DATE_RANGE
}
|
${{
startDate
:
mockStartData
}
} |
${
'
startDate
'
}
|
${
mockStartData
}
${
types
.
SET_LOADING
}
|
${
true
}
|
${
'
isLoading
'
}
|
${
true
}
${
types
.
SET_LOADING
}
|
${
false
}
|
${
'
isLoading
'
}
|
${
false
}
${
types
.
SET_SELECTED_VALUE_STREAM
}
|
${
selectedValueStream
}
|
${
'
selectedValueStream
'
}
|
${
selectedValueStream
}
${
types
.
RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS
}
|
${
rawData
}
|
${
'
summary
'
}
|
${
convertedData
.
summary
}
${
types
.
RECEIVE_VALUE_STREAMS_SUCCESS
}
|
${[
selectedValueStream
]}
|
${
'
valueStreams
'
}
|
${[
selectedValueStream
]}
${
types
.
RECEIVE_VALUE_STREAM_STAGES_SUCCESS
}
|
${{
stages
:
rawValueStreamStages
}
} |
${
'
stages
'
}
|
${
valueStreamStages
}
mutation
|
payload
|
stateKey
|
value
$
{
types
.
INITIALIZE_VSA
}
|
${{
requestPath
:
mockRequestPath
}
} |
${
'
requestPath
'
}
|
${
mockRequestPath
}
${
types
.
SET_DATE_RANGE
}
|
${{
startDate
:
DEFAULT_DAYS_TO_DISPLAY
}
} |
${
'
startDate
'
}
|
${
DEFAULT_DAYS_TO_DISPLAY
}
${
types
.
SET_DATE_RANGE
}
|
${{
startDate
:
DEFAULT_DAYS_TO_DISPLAY
}
} |
${
'
createdAfter
'
}
|
${
mockCreatedAfter
}
${
types
.
SET_DATE_RANGE
}
|
${{
startDate
:
DEFAULT_DAYS_TO_DISPLAY
}
} |
${
'
createdBefore
'
}
|
${
mockCreatedBefore
}
${
types
.
SET_LOADING
}
|
${
true
}
|
${
'
isLoading
'
}
|
${
true
}
${
types
.
SET_LOADING
}
|
${
false
}
|
${
'
isLoading
'
}
|
${
false
}
${
types
.
SET_SELECTED_VALUE_STREAM
}
|
${
selectedValueStream
}
|
${
'
selectedValueStream
'
}
|
${
selectedValueStream
}
${
types
.
RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS
}
|
${
rawData
}
|
${
'
summary
'
}
|
${
convertedData
.
summary
}
${
types
.
RECEIVE_VALUE_STREAMS_SUCCESS
}
|
${[
selectedValueStream
]}
|
${
'
valueStreams
'
}
|
${[
selectedValueStream
]}
${
types
.
RECEIVE_VALUE_STREAM_STAGES_SUCCESS
}
|
${{
stages
:
rawValueStreamStages
}
} |
${
'
stages
'
}
|
${
valueStreamStages
}
${
types
.
RECEIVE_VALUE_STREAMS_SUCCESS
}
|
${[
selectedValueStream
]}
|
${
'
valueStreams
'
}
|
${[
selectedValueStream
]}
${
types
.
RECEIVE_STAGE_MEDIANS_SUCCESS
}
|
${
rawStageMedians
}
|
${
'
medians
'
}
|
${
formattedStageMedians
}
`
(
'
$mutation with $payload will set $stateKey to $value
'
,
({
mutation
,
payload
,
stateKey
,
value
})
=>
{
...
...
@@ -92,4 +108,35 @@ describe('Project Value Stream Analytics mutations', () => {
},
);
});
describe
(
'
with cycleAnalyticsForGroups=false
'
,
()
=>
{
useFakeDate
(
2020
,
6
,
18
);
beforeEach
(()
=>
{
state
=
{
features
:
{
cycleAnalyticsForGroups
:
false
}
};
});
const
formattedMedians
=
{
code
:
'
2d
'
,
issue
:
'
-
'
,
plan
:
'
21h
'
,
review
:
'
-
'
,
staging
:
'
2d
'
,
test
:
'
4h
'
,
};
it
.
each
`
mutation | payload | stateKey | value
${
types
.
RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS
}
|
${
rawData
}
|
${
'
medians
'
}
|
${
formattedMedians
}
${
types
.
REQUEST_CYCLE_ANALYTICS_DATA
}
|
${{}}
|
$
{
'
medians
'
}
|
${{}}
$
{
types
.
RECEIVE_CYCLE_ANALYTICS_DATA_ERROR
}
|
${{}}
|
$
{
'
medians
'
}
|
${{}}
`(
'$mutation with $payload will set $stateKey to $value',
({ mutation, payload, stateKey, value }) => {
mutations[mutation](state, payload);
expect(state).toMatchObject({ [stateKey]: value });
},
);
});
});
spec/frontend/cycle_analytics/utils_spec.js
View file @
160f182b
import
{
useFakeDate
}
from
'
helpers/fake_date
'
;
import
{
decorateEvents
,
decorateData
,
...
...
@@ -6,6 +7,7 @@ import {
medianTimeToParsedSeconds
,
formatMedianValues
,
filterStagesByHiddenStatus
,
calculateFormattedDayInPast
,
}
from
'
~/cycle_analytics/utils
'
;
import
{
selectedStage
,
...
...
@@ -149,4 +151,12 @@ describe('Value stream analytics utils', () => {
expect
(
filterStagesByHiddenStatus
(
mockStages
,
isHidden
)).
toEqual
(
result
);
});
});
describe
(
'
calculateFormattedDayInPast
'
,
()
=>
{
useFakeDate
(
1815
,
11
,
10
);
it
(
'
will return 2 dates, now and past
'
,
()
=>
{
expect
(
calculateFormattedDayInPast
(
5
)).
toEqual
({
now
:
'
1815-12-10
'
,
past
:
'
1815-12-05
'
});
});
});
});
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