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
9124f81f
Commit
9124f81f
authored
Feb 02, 2022
by
Martin Wortschack
Committed by
Brandon Labuschagne
Feb 02, 2022
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
CI/CD analytics: Add metric tiles
Changelog: added EE: true
parent
032df049
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
154 additions
and
32 deletions
+154
-32
app/assets/javascripts/cycle_analytics/components/value_stream_metrics.vue
...ripts/cycle_analytics/components/value_stream_metrics.vue
+11
-17
app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue
...red/components/ci_cd_analytics/ci_cd_analytics_charts.vue
+2
-1
ee/app/assets/javascripts/dora/components/deployment_frequency_charts.vue
...vascripts/dora/components/deployment_frequency_charts.vue
+35
-1
ee/spec/frontend/dora/components/deployment_frequency_charts_spec.js
...ntend/dora/components/deployment_frequency_charts_spec.js
+38
-1
spec/frontend/cycle_analytics/value_stream_metrics_spec.js
spec/frontend/cycle_analytics/value_stream_metrics_spec.js
+35
-2
spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js
...ipelines/charts/components/ci_cd_analytics_charts_spec.js
+33
-10
No files found.
app/assets/javascripts/cycle_analytics/components/value_stream_metrics.vue
View file @
9124f81f
<
script
>
import
{
GlDeprecatedSkeletonLoading
as
GlSkeletonLoading
}
from
'
@gitlab/ui
'
;
import
{
flatten
}
from
'
lodash
'
;
import
{
flatten
,
isEqual
}
from
'
lodash
'
;
import
createFlash
from
'
~/flash
'
;
import
{
sprintf
,
s__
}
from
'
~/locale
'
;
import
{
redirectTo
}
from
'
~/lib/utils/url_utility
'
;
import
{
METRICS_POPOVER_CONTENT
}
from
'
../constants
'
;
import
{
removeFlash
,
prepareTimeMetricsData
}
from
'
../utils
'
;
import
MetricTile
from
'
./metric_tile.vue
'
;
...
...
@@ -48,6 +47,11 @@ export default {
type
:
Array
,
required
:
true
,
},
filterFn
:
{
type
:
Function
,
required
:
false
,
default
:
null
,
},
},
data
()
{
return
{
...
...
@@ -56,8 +60,10 @@ export default {
};
},
watch
:
{
requestParams
()
{
this
.
fetchData
();
requestParams
(
newVal
,
oldVal
)
{
if
(
!
isEqual
(
newVal
,
oldVal
))
{
this
.
fetchData
();
}
},
},
mounted
()
{
...
...
@@ -69,25 +75,13 @@ export default {
this
.
isLoading
=
true
;
return
fetchMetricsData
(
this
.
requests
,
this
.
requestPath
,
this
.
requestParams
)
.
then
((
data
)
=>
{
this
.
metrics
=
data
;
this
.
metrics
=
this
.
filterFn
?
this
.
filterFn
(
data
)
:
data
;
this
.
isLoading
=
false
;
})
.
catch
(()
=>
{
this
.
isLoading
=
false
;
});
},
hasLinks
(
links
)
{
return
links
?.
length
&&
links
[
0
].
url
;
},
clickHandler
({
links
})
{
if
(
this
.
hasLinks
(
links
))
{
redirectTo
(
links
[
0
].
url
);
}
},
getDecimalPlaces
(
value
)
{
const
parsedFloat
=
parseFloat
(
value
);
return
Number
.
isNaN
(
parsedFloat
)
||
Number
.
isInteger
(
parsedFloat
)
?
0
:
1
;
},
},
};
</
script
>
...
...
app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue
View file @
9124f81f
...
...
@@ -45,7 +45,8 @@ export default {
:chart-data=
"chart.data"
:area-chart-options=
"chartOptions"
>
{{
dateRange
}}
<p>
{{
dateRange
}}
</p>
<slot
name=
"metrics"
:selected-chart=
"selectedChart"
></slot>
<template
#tooltip-title
>
<slot
name=
"tooltip-title"
></slot>
</
template
>
...
...
ee/app/assets/javascripts/dora/components/deployment_frequency_charts.vue
View file @
9124f81f
<
script
>
import
*
as
Sentry
from
'
@sentry/browser
'
;
import
*
as
DoraApi
from
'
ee/api/dora_api
'
;
import
{
toYmd
}
from
'
~/analytics/shared/utils
'
;
import
createFlash
from
'
~/flash
'
;
import
{
s__
,
sprintf
}
from
'
~/locale
'
;
import
ValueStreamMetrics
from
'
~/cycle_analytics/components/value_stream_metrics.vue
'
;
import
{
SUMMARY_METRICS_REQUEST
}
from
'
~/cycle_analytics/constants
'
;
import
CiCdAnalyticsCharts
from
'
~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue
'
;
import
DoraChartHeader
from
'
./dora_chart_header.vue
'
;
import
{
...
...
@@ -19,11 +22,16 @@ import {
}
from
'
./static_data/deployment_frequency
'
;
import
{
apiDataToChartSeries
,
seriesToAverageSeries
}
from
'
./util
'
;
const
VISIBLE_METRICS
=
[
'
deploys
'
,
'
deployment-frequency
'
,
'
deployment_frequency
'
];
const
filterFn
=
(
data
)
=>
data
.
filter
((
d
)
=>
VISIBLE_METRICS
.
includes
(
d
.
identifier
)).
map
(({
links
,
...
rest
})
=>
rest
);
export
default
{
name
:
'
DeploymentFrequencyCharts
'
,
components
:
{
CiCdAnalyticsCharts
,
DoraChartHeader
,
ValueStreamMetrics
,
},
inject
:
{
projectPath
:
{
...
...
@@ -56,6 +64,9 @@ export default {
data
:
this
.
chartData
[
chart
.
id
],
}));
},
metricsRequestPath
()
{
return
this
.
projectPath
?
this
.
projectPath
:
`groups/
${
this
.
groupPath
}
`
;
},
},
async
mounted
()
{
const
results
=
await
Promise
.
allSettled
(
...
...
@@ -114,9 +125,23 @@ export default {
);
}
},
methods
:
{
getMetricsRequestParams
(
selectedChart
)
{
const
{
requestParams
:
{
start_date
},
}
=
allChartDefinitions
[
selectedChart
];
return
{
created_after
:
toYmd
(
start_date
),
};
},
},
areaChartOptions
,
chartDescriptionText
,
chartDocumentationHref
,
metricsRequest
:
SUMMARY_METRICS_REQUEST
,
filterFn
,
};
</
script
>
<
template
>
...
...
@@ -126,6 +151,15 @@ export default {
:chart-description-text=
"$options.chartDescriptionText"
:chart-documentation-href=
"$options.chartDocumentationHref"
/>
<ci-cd-analytics-charts
:charts=
"charts"
:chart-options=
"$options.areaChartOptions"
/>
<ci-cd-analytics-charts
:charts=
"charts"
:chart-options=
"$options.areaChartOptions"
>
<template
#metrics
="
{ selectedChart }">
<value-stream-metrics
:request-path=
"metricsRequestPath"
:requests=
"$options.metricsRequest"
:request-params=
"getMetricsRequestParams(selectedChart)"
:filter-fn=
"$options.filterFn"
/>
</
template
>
</ci-cd-analytics-charts>
</div>
</template>
ee/spec/frontend/dora/components/deployment_frequency_charts_spec.js
View file @
9124f81f
...
...
@@ -5,6 +5,8 @@ import lastWeekData from 'test_fixtures/api/dora/metrics/daily_deployment_freque
import
lastMonthData
from
'
test_fixtures/api/dora/metrics/daily_deployment_frequency_for_last_month.json
'
;
import
last90DaysData
from
'
test_fixtures/api/dora/metrics/daily_deployment_frequency_for_last_90_days.json
'
;
import
{
useFixturesFakeDate
}
from
'
helpers/fake_date
'
;
import
{
extendedWrapper
}
from
'
helpers/vue_test_utils_helper
'
;
import
ValueStreamMetrics
from
'
~/cycle_analytics/components/value_stream_metrics.vue
'
;
import
createFlash
from
'
~/flash
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
httpStatus
from
'
~/lib/utils/http_status
'
;
...
...
@@ -12,6 +14,14 @@ import CiCdAnalyticsCharts from '~/vue_shared/components/ci_cd_analytics/ci_cd_a
jest
.
mock
(
'
~/flash
'
);
const
makeMockCiCdAnalyticsCharts
=
({
selectedChart
=
0
}
=
{})
=>
({
render
()
{
return
this
.
$scopedSlots
.
metrics
({
selectedChart
,
});
},
});
describe
(
'
deployment_frequency_charts.vue
'
,
()
=>
{
useFixturesFakeDate
();
...
...
@@ -36,7 +46,7 @@ describe('deployment_frequency_charts.vue', () => {
};
const
createComponent
=
(
mountOptions
=
defaultMountOptions
)
=>
{
wrapper
=
shallowMount
(
DeploymentFrequencyCharts
,
mountOptions
);
wrapper
=
extendedWrapper
(
shallowMount
(
DeploymentFrequencyCharts
,
mountOptions
)
);
};
// Initializes the mock endpoint to return a specific set of deployment
...
...
@@ -55,6 +65,8 @@ describe('deployment_frequency_charts.vue', () => {
.
replyOnce
(
httpStatus
.
OK
,
data
);
};
const
findValueStreamMetrics
=
()
=>
wrapper
.
findComponent
(
ValueStreamMetrics
);
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
=
null
;
...
...
@@ -99,6 +111,31 @@ describe('deployment_frequency_charts.vue', () => {
it
(
'
renders a header
'
,
()
=>
{
expect
(
wrapper
.
findComponent
(
DoraChartHeader
).
exists
()).
toBe
(
true
);
});
describe
(
'
value stream metrics
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
({
...
defaultMountOptions
,
stubs
:
{
CiCdAnalyticsCharts
:
makeMockCiCdAnalyticsCharts
({
selectedChart
:
1
,
}),
},
});
});
it
(
'
renders the value stream metrics component
'
,
()
=>
{
const
metricsComponent
=
findValueStreamMetrics
();
expect
(
metricsComponent
.
exists
()).
toBe
(
true
);
});
it
(
'
passes the selectedChart correctly and computes the requestParams
'
,
()
=>
{
const
metricsComponent
=
findValueStreamMetrics
();
expect
(
metricsComponent
.
props
(
'
requestParams
'
)).
toMatchObject
({
created_after
:
'
2015-06-04
'
,
});
});
});
});
describe
(
'
when there are network errors
'
,
()
=>
{
...
...
spec/frontend/cycle_analytics/value_stream_metrics_spec.js
View file @
9124f81f
...
...
@@ -5,6 +5,8 @@ import metricsData from 'test_fixtures/projects/analytics/value_stream_analytics
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
{
METRIC_TYPE_SUMMARY
}
from
'
~/api/analytics_api
'
;
import
ValueStreamMetrics
from
'
~/cycle_analytics/components/value_stream_metrics.vue
'
;
import
{
METRICS_POPOVER_CONTENT
}
from
'
~/cycle_analytics/constants
'
;
import
{
prepareTimeMetricsData
}
from
'
~/cycle_analytics/utils
'
;
import
MetricTile
from
'
~/cycle_analytics/components/metric_tile.vue
'
;
import
createFlash
from
'
~/flash
'
;
import
{
group
}
from
'
./mock_data
'
;
...
...
@@ -14,6 +16,7 @@ jest.mock('~/flash');
describe
(
'
ValueStreamMetrics
'
,
()
=>
{
let
wrapper
;
let
mockGetValueStreamSummaryMetrics
;
let
mockFilterFn
;
const
{
full_path
:
requestPath
}
=
group
;
const
fakeReqName
=
'
Mock metrics
'
;
...
...
@@ -23,12 +26,13 @@ describe('ValueStreamMetrics', () => {
name
:
fakeReqName
,
});
const
createComponent
=
(
{
requestParams
=
{}
}
=
{})
=>
{
const
createComponent
=
(
props
=
{})
=>
{
return
shallowMount
(
ValueStreamMetrics
,
{
propsData
:
{
requestPath
,
requestParams
,
requestParams
:
{}
,
requests
:
[
metricsRequestFactory
()],
...
props
,
},
});
};
...
...
@@ -104,6 +108,35 @@ describe('ValueStreamMetrics', () => {
expect
(
wrapper
.
find
(
GlSkeletonLoading
).
exists
()).
toBe
(
false
);
});
describe
(
'
filterFn
'
,
()
=>
{
const
transferedMetricsData
=
prepareTimeMetricsData
(
metricsData
,
METRICS_POPOVER_CONTENT
);
it
(
'
with a filter function, will call the function with the metrics data
'
,
async
()
=>
{
const
filteredData
=
[
{
identifier
:
'
issues
'
,
value
:
'
3
'
,
title
:
'
New Issues
'
,
description
:
'
foo
'
},
];
mockFilterFn
=
jest
.
fn
(()
=>
filteredData
);
wrapper
=
createComponent
({
filterFn
:
mockFilterFn
,
});
await
waitForPromises
();
expect
(
mockFilterFn
).
toHaveBeenCalledWith
(
transferedMetricsData
);
expect
(
wrapper
.
vm
.
metrics
).
toEqual
(
filteredData
);
});
it
(
'
without a filter function, it will only update the metrics
'
,
async
()
=>
{
wrapper
=
createComponent
();
await
waitForPromises
();
expect
(
mockFilterFn
).
not
.
toHaveBeenCalled
();
expect
(
wrapper
.
vm
.
metrics
).
toEqual
(
transferedMetricsData
);
});
});
describe
(
'
with additional params
'
,
()
=>
{
beforeEach
(
async
()
=>
{
wrapper
=
createComponent
({
...
...
spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js
View file @
9124f81f
import
{
GlSegmentedControl
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
nextTick
}
from
'
vue
'
;
import
{
GlSegmentedControl
}
from
'
@gitlab/ui
'
;
import
{
shallowMountExtended
}
from
'
helpers/vue_test_utils_helper
'
;
import
CiCdAnalyticsAreaChart
from
'
~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_area_chart.vue
'
;
import
CiCdAnalyticsCharts
from
'
~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue
'
;
import
{
transformedAreaChartData
,
chartOptions
}
from
'
../mock_data
'
;
...
...
@@ -29,12 +29,15 @@ const DEFAULT_PROPS = {
describe
(
'
~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue
'
,
()
=>
{
let
wrapper
;
const
createWrapper
=
(
props
=
{})
=>
shallowMount
(
CiCdAnalyticsCharts
,
{
const
createWrapper
=
(
props
=
{}
,
slots
=
{}
)
=>
shallowMount
Extended
(
CiCdAnalyticsCharts
,
{
propsData
:
{
...
DEFAULT_PROPS
,
...
props
,
},
scopedSlots
:
{
...
slots
,
},
});
afterEach
(()
=>
{
...
...
@@ -44,20 +47,20 @@ describe('~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue', (
}
});
describe
(
'
segmented control
'
,
()
=>
{
let
segmentedControl
;
const
findMetricsSlot
=
()
=>
wrapper
.
findByTestId
(
'
metrics-slot
'
);
const
findSegmentedControl
=
()
=>
wrapper
.
findComponent
(
GlSegmentedControl
)
;
describe
(
'
segmented control
'
,
()
=>
{
beforeEach
(()
=>
{
wrapper
=
createWrapper
();
segmentedControl
=
wrapper
.
find
(
GlSegmentedControl
);
});
it
(
'
should default to the first chart
'
,
()
=>
{
expect
(
segmentedControl
.
props
(
'
checked
'
)).
toBe
(
0
);
expect
(
findSegmentedControl
()
.
props
(
'
checked
'
)).
toBe
(
0
);
});
it
(
'
should use the title and index as values
'
,
()
=>
{
const
options
=
segmentedControl
.
props
(
'
options
'
);
const
options
=
findSegmentedControl
()
.
props
(
'
options
'
);
expect
(
options
).
toHaveLength
(
3
);
expect
(
options
).
toEqual
([
{
...
...
@@ -76,7 +79,7 @@ describe('~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue', (
});
it
(
'
should select a different chart on change
'
,
async
()
=>
{
segmentedControl
.
vm
.
$emit
(
'
input
'
,
1
);
findSegmentedControl
()
.
vm
.
$emit
(
'
input
'
,
1
);
const
chart
=
wrapper
.
find
(
CiCdAnalyticsAreaChart
);
...
...
@@ -91,4 +94,24 @@ describe('~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue', (
wrapper
=
createWrapper
({
charts
:
[]
});
expect
(
wrapper
.
find
(
CiCdAnalyticsAreaChart
).
exists
()).
toBe
(
false
);
});
describe
(
'
slots
'
,
()
=>
{
beforeEach
(()
=>
{
wrapper
=
createWrapper
(
{},
{
metrics
:
'
<div data-testid="metrics-slot">selected chart: {{props.selectedChart}}</div>
'
,
},
);
});
it
(
'
renders a metrics slot
'
,
async
()
=>
{
const
selectedChart
=
1
;
findSegmentedControl
().
vm
.
$emit
(
'
input
'
,
selectedChart
);
await
nextTick
();
expect
(
findMetricsSlot
().
text
()).
toBe
(
`selected chart:
${
selectedChart
}
`
);
});
});
});
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