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
24281d62
Commit
24281d62
authored
Sep 24, 2019
by
Martin Wortschack
Committed by
Filipa Lacerda
Sep 24, 2019
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add metric chart component to app
- Leverage MetricChart component in productivity analytics app
parent
91aea099
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
333 additions
and
461 deletions
+333
-461
ee/app/assets/javascripts/analytics/productivity_analytics/components/app.vue
...ripts/analytics/productivity_analytics/components/app.vue
+99
-175
ee/app/assets/javascripts/analytics/productivity_analytics/store/modules/charts/getters.js
...cs/productivity_analytics/store/modules/charts/getters.js
+1
-2
ee/spec/frontend/analytics/productivity_analytics/components/app_spec.js
...d/analytics/productivity_analytics/components/app_spec.js
+219
-280
ee/spec/frontend/analytics/productivity_analytics/store/modules/charts/getters_spec.js
...oductivity_analytics/store/modules/charts/getters_spec.js
+13
-0
locale/gitlab.pot
locale/gitlab.pot
+1
-4
No files found.
ee/app/assets/javascripts/analytics/productivity_analytics/components/app.vue
View file @
24281d62
...
@@ -10,6 +10,7 @@ import {
...
@@ -10,6 +10,7 @@ import {
}
from
'
@gitlab/ui
'
;
}
from
'
@gitlab/ui
'
;
import
{
GlColumnChart
}
from
'
@gitlab/ui/dist/charts
'
;
import
{
GlColumnChart
}
from
'
@gitlab/ui/dist/charts
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
MetricChart
from
'
./metric_chart.vue
'
;
import
MergeRequestTable
from
'
./mr_table.vue
'
;
import
MergeRequestTable
from
'
./mr_table.vue
'
;
import
{
chartKeys
}
from
'
../constants
'
;
import
{
chartKeys
}
from
'
../constants
'
;
...
@@ -22,6 +23,7 @@ export default {
...
@@ -22,6 +23,7 @@ export default {
GlColumnChart
,
GlColumnChart
,
GlButton
,
GlButton
,
Icon
,
Icon
,
MetricChart
,
MergeRequestTable
,
MergeRequestTable
,
},
},
directives
:
{
directives
:
{
...
@@ -47,7 +49,7 @@ export default {
...
@@ -47,7 +49,7 @@ export default {
};
};
},
},
computed
:
{
computed
:
{
...
mapState
(
'
filters
'
,
[
'
groupNamespace
'
,
'
projectPath
'
]),
...
mapState
(
'
filters
'
,
[
'
groupNamespace
'
]),
...
mapState
(
'
table
'
,
[
'
isLoadingTable
'
,
'
mergeRequests
'
,
'
pageInfo
'
,
'
columnMetric
'
]),
...
mapState
(
'
table
'
,
[
'
isLoadingTable
'
,
'
mergeRequests
'
,
'
pageInfo
'
,
'
columnMetric
'
]),
...
mapGetters
([
'
getMetricTypes
'
]),
...
mapGetters
([
'
getMetricTypes
'
]),
...
mapGetters
(
'
charts
'
,
[
...
mapGetters
(
'
charts
'
,
[
...
@@ -56,7 +58,7 @@ export default {
...
@@ -56,7 +58,7 @@ export default {
'
getChartData
'
,
'
getChartData
'
,
'
getColumnChartDatazoomOption
'
,
'
getColumnChartDatazoomOption
'
,
'
getMetricDropdownLabel
'
,
'
getMetricDropdownLabel
'
,
'
is
SelectedMetric
'
,
'
get
SelectedMetric
'
,
'
hasNoAccessError
'
,
'
hasNoAccessError
'
,
]),
]),
...
mapGetters
(
'
table
'
,
[
...
mapGetters
(
'
table
'
,
[
...
@@ -85,7 +87,6 @@ export default {
...
@@ -85,7 +87,6 @@ export default {
},
},
methods
:
{
methods
:
{
...
mapActions
([
'
setEndpoint
'
]),
...
mapActions
([
'
setEndpoint
'
]),
...
mapActions
(
'
filters
'
,
[
'
setProjectPath
'
]),
...
mapActions
(
'
charts
'
,
[
'
fetchChartData
'
,
'
setMetricType
'
,
'
chartItemClicked
'
]),
...
mapActions
(
'
charts
'
,
[
'
fetchChartData
'
,
'
setMetricType
'
,
'
chartItemClicked
'
]),
...
mapActions
(
'
table
'
,
[
...
mapActions
(
'
table
'
,
[
'
setSortField
'
,
'
setSortField
'
,
...
@@ -141,136 +142,109 @@ export default {
...
@@ -141,136 +142,109 @@ export default {
/>
/>
<template
v-if=
"showAppContent"
>
<template
v-if=
"showAppContent"
>
<h4>
{{
__
(
'
Merge Requests
'
)
}}
</h4>
<h4>
{{
__
(
'
Merge Requests
'
)
}}
</h4>
<div
class=
"qa-time-to-merge mb-4"
>
<metric-chart
<h5>
{{
__
(
'
Time to merge
'
)
}}
</h5>
ref=
"mainChart"
<gl-loading-icon
v-if=
"chartLoading(chartKeys.main)"
size=
"md"
class=
"my-4 py-4"
/>
class=
"mb-4"
<template
v-else
>
:title=
"__('Time to merge')"
<div
v-if=
"!chartHasData(chartKeys.main)"
class=
"bs-callout bs-callout-info"
>
:description=
"
{{
__
(
'
There is no data available. Please change your selection.
'
)
}}
__('You can filter by \'days to merge\' by clicking on the columns in the chart.')
</div>
"
<template
v-else
>
:is-loading=
"chartLoading(chartKeys.main)"
<p
class=
"text-muted"
>
:chart-data=
"getChartData(chartKeys.main)"
{{
__
(
'
You can filter by "days to merge" by clicking on the columns in the chart.
'
)
}}
>
</p>
<gl-column-chart
<gl-column-chart
:data=
"
{ full: getChartData(chartKeys.main) }"
:data=
"
{ full: getChartData(chartKeys.main) }"
:option="getColumnChartOption(chartKeys.main)"
:option="getColumnChartOption(chartKeys.main)"
:y-axis-title="__('Merge requests')"
:y-axis-title="__('Merge requests')"
:x-axis-title="__('Days')"
:x-axis-title="__('Days')"
x-axis-type="category"
x-axis-type="category"
@chartItemClicked="onMainChartItemClicked"
@chartItemClicked="onMainChartItemClicked"
/>
/>
</metric-chart>
</
template
>
</template>
</div>
<template
v-if=
"showSecondaryCharts"
>
<template
v-if=
"showSecondaryCharts"
>
<div
class=
"row"
>
<div
ref=
"secondaryCharts"
>
<div
class=
"qa-time-based col-lg-6 col-sm-12 mb-4"
>
<div
class=
"row"
>
<gl-loading-icon
<metric-chart
v-if=
"chartLoading(chartKeys.timeBasedHistogram)"
ref=
"timeBasedChart"
size=
"md"
class=
"col-lg-6 col-sm-12 mb-4"
class=
"my-4 py-4"
:description=
"
/>
__(
<template
v-else
>
'Not all data has been processed yet, the accuracy of the chart for the selected timeframe is limited.',
<div
)
v-if=
"!chartHasData(chartKeys.timeBasedHistogram)"
"
class=
"bs-callout bs-callout-info"
:is-loading=
"chartLoading(chartKeys.timeBasedHistogram)"
>
:metric-types=
"getMetricTypes(chartKeys.timeBasedHistogram)"
{{
__
(
'
There is no data for the selected metric. Please change your selection.
'
)
}}
:selected-metric=
"getSelectedMetric(chartKeys.timeBasedHistogram)"
</div>
:chart-data=
"getChartData(chartKeys.timeBasedHistogram)"
<template
v-else
>
@
metricTypeChange=
"
<gl-dropdown
metric =>
class=
"mb-4 metric-dropdown"
setMetricType(
{ metricType: metric, chartKey: chartKeys.timeBasedHistogram })
toggle-class=
"dropdown-menu-toggle w-100"
"
menu-class=
"w-100 mw-100"
>
:text=
"getMetricDropdownLabel(chartKeys.timeBasedHistogram)"
<gl-column-chart
>
:data=
"
{ full: getChartData(chartKeys.timeBasedHistogram) }"
<gl-dropdown-item
:option="getColumnChartOption(chartKeys.timeBasedHistogram)"
v-for=
"metric in getMetricTypes(chartKeys.timeBasedHistogram)"
:y-axis-title="__('Merge requests')"
:key=
"metric.key"
:x-axis-title="__('Hours')"
active-class=
"is-active"
x-axis-type="category"
class=
"w-100"
/>
@
click=
"
</metric-chart>
setMetricType(
{
metricType: metric.key,
<metric-chart
chartKey: chartKeys.timeBasedHistogram,
ref=
"commitBasedChart"
})
class=
"col-lg-6 col-sm-12 mb-4"
"
:description=
"
>
__(
<span
class=
"d-flex"
>
'Not all data has been processed yet, the accuracy of the chart for the selected timeframe is limited.',
<icon
)
class=
"flex-shrink-0 append-right-4"
"
:class=
"
{
:is-loading=
"chartLoading(chartKeys.commitBasedHistogram)"
invisible: !isSelectedMetric({
:metric-types=
"getMetricTypes(chartKeys.commitBasedHistogram)"
metric: metric.key,
:selected-metric=
"getSelectedMetric(chartKeys.commitBasedHistogram)"
chartKey: chartKeys.timeBasedHistogram,
:chart-data=
"getChartData(chartKeys.commitBasedHistogram)"
}),
@
metricTypeChange=
"
}"
metric =>
name="mobile-issue-close"
setMetricType(
{ metricType: metric, chartKey: chartKeys.commitBasedHistogram })
/>
"
{{
metric
.
label
}}
>
</span>
<gl-column-chart
</gl-dropdown-item>
:data=
"
{ full: getChartData(chartKeys.commitBasedHistogram) }"
</gl-dropdown>
:option="getColumnChartOption(chartKeys.commitBasedHistogram)"
<p
class=
"text-muted"
>
:y-axis-title="__('Merge requests')"
{{
:x-axis-title="getMetricDropdownLabel(chartKeys.commitBasedHistogram)"
__
(
x-axis-type="category"
'
Not all data has been processed yet, the accuracy of the chart for the selected timeframe is limited.
'
,
/>
)
</metric-chart>
}}
</p>
<gl-column-chart
:data=
"
{ full: getChartData(chartKeys.timeBasedHistogram) }"
:option="getColumnChartOption(chartKeys.timeBasedHistogram)"
:y-axis-title="__('Merge requests')"
:x-axis-title="__('Hours')"
x-axis-type="category"
/>
</
template
>
</template>
</div>
</div>
<div
class=
"qa-commit-based col-lg-6 col-sm-12 mb-4"
>
<div
<gl-loading-icon
class=
"js-mr-table-sort d-flex flex-column flex-md-row align-items-md-center justify-content-between mb-2"
v-if=
"chartLoading(chartKeys.commitBasedHistogram)"
>
size=
"md"
<h5>
{{
__
(
'
List
'
)
}}
</h5>
class=
"my-4 py-4"
<div
/>
v-if=
"showMergeRequestTable"
<
template
v-else
>
class=
"d-flex flex-column flex-md-row align-items-md-center"
<div
>
v-if=
"!chartHasData(chartKeys.commitBasedHistogram)"
<strong
class=
"mr-2"
>
{{
__
(
'
Sort by
'
)
}}
</strong>
class=
"bs-callout bs-callout-info"
<div
class=
"d-flex"
>
>
{{
__
(
'
There is no data for the selected metric. Please change your selection.
'
)
}}
</div>
<template
v-else
>
<gl-dropdown
<gl-dropdown
class=
"mb-4 metric-dropdown"
class=
"mr-2 flex-grow"
toggle-class=
"dropdown-menu-toggle w-100"
toggle-class=
"dropdown-menu-toggle"
menu-class=
"w-100 mw-100"
:text=
"sortFieldDropdownLabel"
:text=
"getMetricDropdownLabel(chartKeys.commitBasedHistogram)"
>
>
<gl-dropdown-item
<gl-dropdown-item
v-for=
"metric in
getMetricTypes(chartKeys.commitBasedHistogram)
"
v-for=
"metric in
tableSortOptions
"
:key=
"metric.key"
:key=
"metric.key"
active-class=
"is-active"
active-class=
"is-active"
class=
"w-100"
class=
"w-100"
@
click=
"
@
click=
"setSortField(metric.key)"
setMetricType(
{
metricType: metric.key,
chartKey: chartKeys.commitBasedHistogram,
})
"
>
>
<span
class=
"d-flex"
>
<span
class=
"d-flex"
>
<icon
<icon
class=
"flex-shrink-0 append-right-4"
class=
"flex-shrink-0 append-right-4"
:class=
"
{
:class=
"
{
invisible: !isSelectedMetric({
invisible: !isSelectedSortField(metric.key),
metric: metric.key,
chartKey: chartKeys.commitBasedHistogram,
}),
}"
}"
name="mobile-issue-close"
name="mobile-issue-close"
/>
/>
...
@@ -278,66 +252,16 @@ export default {
...
@@ -278,66 +252,16 @@ export default {
</span>
</span>
</gl-dropdown-item>
</gl-dropdown-item>
</gl-dropdown>
</gl-dropdown>
<p
class=
"text-muted"
>
<gl-button
v-gl-tooltip
.
hover
:title=
"sortTooltipTitle"
@
click=
"toggleSortOrder"
>
{{
<icon
:name=
"sortIcon"
/>
__
(
</gl-button>
'
Not all data has been processed yet, the accuracy of the chart for the selected timeframe is limited.
'
,
</div>
)
}}
</p>
<gl-column-chart
:data=
"
{ full: getChartData(chartKeys.commitBasedHistogram) }"
:option="getColumnChartOption(chartKeys.commitBasedHistogram)"
:y-axis-title="__('Merge requests')"
:x-axis-title="getMetricDropdownLabel(chartKeys.commitBasedHistogram)"
x-axis-type="category"
/>
</
template
>
</template>
</div>
</div>
<div
class=
"qa-mr-table-sort d-flex flex-column flex-md-row align-items-md-center justify-content-between mb-2"
>
<h5>
{{ __('List') }}
</h5>
<div
v-if=
"showMergeRequestTable"
class=
"d-flex flex-column flex-md-row align-items-md-center"
>
<strong
class=
"mr-2"
>
{{ __('Sort by') }}
</strong>
<div
class=
"d-flex"
>
<gl-dropdown
class=
"mr-2 flex-grow"
toggle-class=
"dropdown-menu-toggle"
:text=
"sortFieldDropdownLabel"
>
<gl-dropdown-item
v-for=
"metric in tableSortOptions"
:key=
"metric.key"
active-class=
"is-active"
class=
"w-100"
@
click=
"setSortField(metric.key)"
>
<span
class=
"d-flex"
>
<icon
class=
"flex-shrink-0 append-right-4"
:class=
"{
invisible: !isSelectedSortField(metric.key),
}"
name=
"mobile-issue-close"
/>
{{ metric.label }}
</span>
</gl-dropdown-item>
</gl-dropdown>
<gl-button
v-gl-tooltip
.
hover
:title=
"sortTooltipTitle"
@
click=
"toggleSortOrder"
>
<icon
:name=
"sortIcon"
/>
</gl-button>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class=
"qa-mr-table"
>
<div
class=
"js-mr-table"
>
<div
ref=
"foo"
></div>
<gl-loading-icon
v-if=
"isLoadingTable"
size=
"md"
class=
"my-4 py-4"
/>
<gl-loading-icon
v-if=
"isLoadingTable"
size=
"md"
class=
"my-4 py-4"
/>
<merge-request-table
<merge-request-table
v-if=
"showMergeRequestTable"
v-if=
"showMergeRequestTable"
...
...
ee/app/assets/javascripts/analytics/productivity_analytics/store/modules/charts/getters.js
View file @
24281d62
...
@@ -107,8 +107,7 @@ export const getColumnChartDatazoomOption = state => chartKey => {
...
@@ -107,8 +107,7 @@ export const getColumnChartDatazoomOption = state => chartKey => {
};
};
};
};
export
const
isSelectedMetric
=
state
=>
({
metric
,
chartKey
})
=>
export
const
getSelectedMetric
=
state
=>
chartKey
=>
state
.
charts
[
chartKey
].
params
.
metricType
;
state
.
charts
[
chartKey
].
params
.
metricType
===
metric
;
export
const
hasNoAccessError
=
state
=>
export
const
hasNoAccessError
=
state
=>
state
.
charts
[
chartKeys
.
main
].
errorCode
===
httpStatus
.
FORBIDDEN
;
state
.
charts
[
chartKeys
.
main
].
errorCode
===
httpStatus
.
FORBIDDEN
;
...
...
ee/spec/frontend/analytics/productivity_analytics/components/app_spec.js
View file @
24281d62
import
{
createLocalVue
,
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
createLocalVue
,
shallowMount
}
from
'
@vue/test-utils
'
;
import
Vuex
from
'
vuex
'
;
import
Vuex
from
'
vuex
'
;
import
axios
from
'
axios
'
;
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
ProductivityApp
from
'
ee/analytics/productivity_analytics/components/app.vue
'
;
import
ProductivityApp
from
'
ee/analytics/productivity_analytics/components/app.vue
'
;
import
MergeRequestTable
from
'
ee/analytics/productivity_analytics/components/mr_table.vue
'
;
import
MergeRequestTable
from
'
ee/analytics/productivity_analytics/components/mr_table.vue
'
;
import
store
from
'
ee/analytics/productivity_analytics/store
'
;
import
store
from
'
ee/analytics/productivity_analytics/store
'
;
...
@@ -7,13 +9,13 @@ import { chartKeys } from 'ee/analytics/productivity_analytics/constants';
...
@@ -7,13 +9,13 @@ import { chartKeys } from 'ee/analytics/productivity_analytics/constants';
import
{
TEST_HOST
}
from
'
helpers/test_constants
'
;
import
{
TEST_HOST
}
from
'
helpers/test_constants
'
;
import
{
GlEmptyState
,
GlLoadingIcon
,
GlDropdown
,
GlDropdownItem
,
GlButton
}
from
'
@gitlab/ui
'
;
import
{
GlEmptyState
,
GlLoadingIcon
,
GlDropdown
,
GlDropdownItem
,
GlButton
}
from
'
@gitlab/ui
'
;
import
{
GlColumnChart
}
from
'
@gitlab/ui/dist/charts
'
;
import
{
GlColumnChart
}
from
'
@gitlab/ui/dist/charts
'
;
import
resetStore
from
'
../helpers
'
;
const
localVue
=
createLocalVue
();
const
localVue
=
createLocalVue
();
localVue
.
use
(
Vuex
);
localVue
.
use
(
Vuex
);
describe
(
'
ProductivityApp component
'
,
()
=>
{
describe
(
'
ProductivityApp component
'
,
()
=>
{
let
wrapper
;
let
wrapper
;
let
mock
;
const
propsData
=
{
const
propsData
=
{
endpoint
:
TEST_HOST
,
endpoint
:
TEST_HOST
,
...
@@ -30,7 +32,10 @@ describe('ProductivityApp component', () => {
...
@@ -30,7 +32,10 @@ describe('ProductivityApp component', () => {
setColumnMetric
:
jest
.
fn
(),
setColumnMetric
:
jest
.
fn
(),
};
};
const
mainChartData
=
{
1
:
2
,
2
:
3
};
beforeEach
(()
=>
{
beforeEach
(()
=>
{
mock
=
new
MockAdapter
(
axios
);
wrapper
=
shallowMount
(
localVue
.
extend
(
ProductivityApp
),
{
wrapper
=
shallowMount
(
localVue
.
extend
(
ProductivityApp
),
{
localVue
,
localVue
,
store
,
store
,
...
@@ -40,23 +45,22 @@ describe('ProductivityApp component', () => {
...
@@ -40,23 +45,22 @@ describe('ProductivityApp component', () => {
...
actionSpies
,
...
actionSpies
,
},
},
});
});
jest
.
spyOn
(
store
,
'
dispatch
'
).
mockImplementation
();
});
});
afterEach
(()
=>
{
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
.
destroy
();
resetStore
(
store
);
mock
.
restore
(
);
});
});
const
findTimeToMergeSection
=
()
=>
wrapper
.
find
(
'
.qa-time-to-merge
'
);
const
findMainMetricChart
=
()
=>
wrapper
.
find
({
ref
:
'
mainChart
'
});
const
findMrTableSortSection
=
()
=>
wrapper
.
find
(
'
.qa-mr-table-sort
'
);
const
findSecondaryChartsSection
=
()
=>
wrapper
.
find
({
ref
:
'
secondaryCharts
'
});
const
findMrTableSection
=
()
=>
wrapper
.
find
(
'
.qa-mr-table
'
);
const
findTimeBasedMetricChart
=
()
=>
wrapper
.
find
({
ref
:
'
timeBasedChart
'
});
const
findMrTable
=
()
=>
findMrTableSection
().
find
(
MergeRequestTable
);
const
findCommitBasedMetricChart
=
()
=>
wrapper
.
find
({
ref
:
'
commitBasedChart
'
});
const
findMrTableSortSection
=
()
=>
wrapper
.
find
(
'
.js-mr-table-sort
'
);
const
findSortFieldDropdown
=
()
=>
findMrTableSortSection
().
find
(
GlDropdown
);
const
findSortFieldDropdown
=
()
=>
findMrTableSortSection
().
find
(
GlDropdown
);
const
findSortOrderToggle
=
()
=>
findMrTableSortSection
().
find
(
GlButton
);
const
findSortOrderToggle
=
()
=>
findMrTableSortSection
().
find
(
GlButton
);
const
find
TimeBasedSection
=
()
=>
wrapper
.
find
(
'
.qa-time-based
'
);
const
find
MrTableSection
=
()
=>
wrapper
.
find
(
'
.js-mr-table
'
);
const
find
CommitBasedSection
=
()
=>
wrapper
.
find
(
'
.qa-commit-based
'
);
const
find
MrTable
=
()
=>
findMrTableSection
().
find
(
MergeRequestTable
);
describe
(
'
template
'
,
()
=>
{
describe
(
'
template
'
,
()
=>
{
describe
(
'
without a group being selected
'
,
()
=>
{
describe
(
'
without a group being selected
'
,
()
=>
{
...
@@ -70,12 +74,18 @@ describe('ProductivityApp component', () => {
...
@@ -70,12 +74,18 @@ describe('ProductivityApp component', () => {
describe
(
'
with a group being selected
'
,
()
=>
{
describe
(
'
with a group being selected
'
,
()
=>
{
beforeEach
(()
=>
{
beforeEach
(()
=>
{
store
.
state
.
filters
.
groupNamespace
=
'
gitlab-org
'
;
wrapper
.
vm
.
$store
.
dispatch
(
'
filters/setGroupNamespace
'
,
'
gitlab-org
'
);
mock
.
onGet
(
wrapper
.
vm
.
$store
.
state
.
endpoint
).
replyOnce
(
200
);
});
});
describe
(
'
and
user has no access to the group
'
,
()
=>
{
describe
(
'
user has no access to the group
'
,
()
=>
{
beforeEach
(()
=>
{
beforeEach
(()
=>
{
store
.
state
.
charts
.
charts
[
chartKeys
.
main
].
errorCode
=
403
;
const
error
=
{
response
:
{
status
:
403
}
};
wrapper
.
vm
.
$store
.
dispatch
(
'
charts/receiveChartDataError
'
,
{
chartKey
:
chartKeys
.
main
,
error
,
});
wrapper
.
vm
.
$store
.
state
.
charts
.
charts
[
chartKeys
.
main
].
errorCode
=
403
;
});
});
it
(
'
renders the no access illustration
'
,
()
=>
{
it
(
'
renders the no access illustration
'
,
()
=>
{
...
@@ -86,350 +96,279 @@ describe('ProductivityApp component', () => {
...
@@ -86,350 +96,279 @@ describe('ProductivityApp component', () => {
});
});
});
});
describe
(
'
and
user has access to the group
'
,
()
=>
{
describe
(
'
user has access to the group
'
,
()
=>
{
beforeEach
(()
=>
{
beforeEach
(()
=>
{
store
.
state
.
charts
.
charts
[
chartKeys
.
main
].
errorCode
=
null
;
wrapper
.
vm
.
$
store
.
state
.
charts
.
charts
[
chartKeys
.
main
].
errorCode
=
null
;
});
});
describe
(
'
Time to merge chart
'
,
()
=>
{
describe
(
'
when the main chart is loading
'
,
()
=>
{
it
(
'
renders the title
'
,
()
=>
{
beforeEach
(
()
=>
{
expect
(
findTimeToMergeSection
().
text
()).
toContain
(
'
Time to merge
'
);
wrapper
.
vm
.
$store
.
dispatch
(
'
charts/requestChartData
'
,
chartKeys
.
main
);
});
});
describe
(
'
when chart is loading
'
,
()
=>
{
it
(
'
renders a metric chart component for the main chart
'
,
()
=>
{
beforeEach
(()
=>
{
expect
(
findMainMetricChart
().
exists
()).
toBe
(
true
);
store
.
state
.
charts
.
charts
[
chartKeys
.
main
].
isLoading
=
true
;
});
it
(
'
renders a loading indicator
'
,
()
=>
{
expect
(
findTimeToMergeSection
()
.
find
(
GlLoadingIcon
)
.
exists
(),
).
toBe
(
true
);
});
});
});
describe
(
'
when chart finished loading
'
,
()
=>
{
it
(
'
sets isLoading=true on the metric chart
'
,
()
=>
{
beforeEach
(()
=>
{
expect
(
findMainMetricChart
().
props
(
'
isLoading
'
)).
toBe
(
true
);
store
.
state
.
charts
.
charts
[
chartKeys
.
main
].
isLoading
=
false
;
});
});
describe
(
'
and the chart has data
'
,
()
=>
{
beforeEach
(()
=>
{
store
.
state
.
charts
.
charts
[
chartKeys
.
main
].
data
=
{
1
:
2
,
2
:
3
};
});
it
(
'
renders a column chart
'
,
()
=>
{
expect
(
findTimeToMergeSection
()
.
find
(
GlColumnChart
)
.
exists
(),
).
toBe
(
true
);
});
describe
(
'
when an item on the chart is clicked
'
,
()
=>
{
beforeEach
(()
=>
{
const
data
=
{
chart
:
null
,
params
:
{
data
:
{
value
:
[
0
,
1
],
},
},
};
findTimeToMergeSection
()
.
find
(
GlColumnChart
)
.
vm
.
$emit
(
'
chartItemClicked
'
,
data
);
});
it
(
'
dispatches chartItemClicked action
'
,
()
=>
{
expect
(
actionSpies
.
chartItemClicked
).
toHaveBeenCalledWith
({
chartKey
:
chartKeys
.
main
,
item
:
0
,
});
});
it
(
'
dispatches setMergeRequestsPage action
'
,
()
=>
{
expect
(
actionSpies
.
setMergeRequestsPage
).
toHaveBeenCalledWith
(
0
);
});
});
});
describe
(
"
and the chart doesn't have any data
"
,
()
=>
{
beforeEach
(()
=>
{
store
.
state
.
charts
.
charts
[
chartKeys
.
main
].
data
=
null
;
});
it
(
'
renders a "no data" message
'
,
()
=>
{
it
(
'
does not render any other charts
'
,
()
=>
{
expect
(
findTimeToMergeSection
().
text
()).
toContain
(
expect
(
findSecondaryChartsSection
().
exists
()).
toBe
(
false
);
'
There is no data available. Please change your selection.
'
,
);
});
});
});
});
});
describe
(
'
Time based histogram
'
,
()
=>
{
it
(
'
does not render the MR table
'
,
()
=>
{
beforeEach
(()
=>
{
expect
(
findMrTableSortSection
().
exists
()).
toBe
(
false
);
store
.
state
.
charts
.
charts
[
chartKeys
.
main
].
isLoading
=
false
;
expect
(
findMrTableSection
().
exists
()).
toBe
(
false
);
store
.
state
.
charts
.
charts
[
chartKeys
.
main
].
data
=
{
1
:
2
,
2
:
3
};
});
});
});
describe
(
'
when chart is loading
'
,
()
=>
{
describe
(
'
when the main chart finished loading
'
,
()
=>
{
describe
(
'
and has data
'
,
()
=>
{
beforeEach
(()
=>
{
beforeEach
(()
=>
{
store
.
state
.
charts
.
charts
[
chartKeys
.
timeBasedHistogram
].
isLoading
=
true
;
wrapper
.
vm
.
$store
.
dispatch
(
'
charts/receiveChartDataSuccess
'
,
{
chartKey
:
chartKeys
.
main
,
data
:
mainChartData
,
});
});
});
it
(
'
renders a loading indicator
'
,
()
=>
{
it
(
'
sets isLoading=false on the metric chart
'
,
()
=>
{
expect
(
expect
(
findMainMetricChart
().
props
(
'
isLoading
'
)).
toBe
(
false
);
findTimeBasedSection
()
.
find
(
GlLoadingIcon
)
.
exists
(),
).
toBe
(
true
);
});
});
});
describe
(
'
when chart finished loading
'
,
()
=>
{
it
(
'
passes non-empty chartData to the metric chart
'
,
()
=>
{
beforeEach
(()
=>
{
expect
(
findMainMetricChart
().
props
(
'
chartData
'
)).
not
.
toEqual
([]);
store
.
state
.
charts
.
charts
[
chartKeys
.
timeBasedHistogram
].
isLoading
=
false
;
});
});
describe
(
'
and the chart has data
'
,
()
=>
{
describe
(
'
when an item on the chart is clicked
'
,
()
=>
{
beforeEach
(()
=>
{
beforeEach
(()
=>
{
store
.
state
.
charts
.
charts
[
chartKeys
.
timeBasedHistogram
].
data
=
{
1
:
2
,
2
:
3
};
const
data
=
{
});
chart
:
null
,
params
:
{
data
:
{
value
:
[
0
,
1
],
},
},
};
it
(
'
renders a metric type dropdown
'
,
()
=>
{
findMainMetricChart
()
expect
(
.
find
(
GlColumnChart
)
findTimeBasedSection
()
.
vm
.
$emit
(
'
chartItemClicked
'
,
data
);
.
find
(
GlDropdown
)
.
exists
(),
).
toBe
(
true
);
});
});
it
(
'
should change the metric type
'
,
()
=>
{
it
(
'
dispatches chartItemClicked action
'
,
()
=>
{
findTimeBasedSection
()
expect
(
actionSpies
.
chartItemClicked
).
toHaveBeenCalledWith
({
.
findAll
(
GlDropdownItem
)
chartKey
:
chartKeys
.
main
,
.
at
(
0
)
item
:
0
,
.
vm
.
$emit
(
'
click
'
);
expect
(
actionSpies
.
setMetricType
).
toHaveBeenCalledWith
({
metricType
:
'
time_to_first_comment
'
,
chartKey
:
chartKeys
.
timeBasedHistogram
,
});
});
});
});
it
(
'
renders a column chart
'
,
()
=>
{
it
(
'
dispatches setMergeRequestsPage action
'
,
()
=>
{
expect
(
expect
(
actionSpies
.
setMergeRequestsPage
).
toHaveBeenCalledWith
(
0
);
findTimeBasedSection
()
.
find
(
GlColumnChart
)
.
exists
(),
).
toBe
(
true
);
});
});
});
});
describe
(
"
and the chart doesn't have any data
"
,
()
=>
{
describe
(
'
Time based histogram
'
,
()
=>
{
beforeEach
(
()
=>
{
it
(
'
renders a metric chart component
'
,
()
=>
{
store
.
state
.
charts
.
charts
[
chartKeys
.
timeBasedHistogram
].
data
=
null
;
expect
(
findTimeBasedMetricChart
().
exists
()).
toBe
(
true
)
;
});
});
it
(
'
renders a "no data" message
'
,
()
=>
{
describe
(
'
when chart finished loading
'
,
()
=>
{
expect
(
findTimeBasedSection
().
text
()).
toContain
(
describe
(
'
and the chart has data
'
,
()
=>
{
'
There is no data for the selected metric. Please change your selection.
'
,
beforeEach
(()
=>
{
);
wrapper
.
vm
.
$store
.
dispatch
(
'
charts/receiveChartDataSuccess
'
,
{
});
chartKey
:
chartKeys
.
timeBasedHistogram
,
});
data
:
{
1
:
2
,
2
:
3
},
});
});
});
});
describe
(
'
Commit based histogram
'
,
()
=>
{
it
(
'
sets isLoading=false on the metric chart
'
,
()
=>
{
beforeEach
(()
=>
{
expect
(
findTimeBasedMetricChart
().
props
(
'
isLoading
'
)).
toBe
(
false
);
store
.
state
.
charts
.
charts
[
chartKeys
.
main
].
isLoading
=
false
;
});
store
.
state
.
charts
.
charts
[
chartKeys
.
main
].
data
=
{
1
:
2
,
2
:
3
};
});
describe
(
'
when chart is loading
'
,
()
=>
{
it
(
'
passes non-empty chartData to the metric chart
'
,
()
=>
{
beforeEach
(()
=>
{
expect
(
findTimeBasedMetricChart
().
props
(
'
chartData
'
)).
not
.
toEqual
([]);
store
.
state
.
charts
.
charts
[
chartKeys
.
commitBasedHistogram
].
isLoading
=
true
;
});
});
it
(
'
renders a loading indicator
'
,
()
=>
{
it
(
'
should call setMetricType when `metricTypeChange` is emitted on the metric chart
'
,
()
=>
{
expect
(
findTimeBasedMetricChart
().
vm
.
$emit
(
'
metricTypeChange
'
,
'
time_to_merge
'
);
findCommitBasedSection
()
.
find
(
GlLoadingIcon
)
.
exists
(),
).
toBe
(
true
);
});
});
describe
(
'
when chart finished loading
'
,
()
=>
{
expect
(
actionSpies
.
setMetricType
).
toHaveBeenCalledWith
({
beforeEach
(()
=>
{
metricType
:
'
time_to_merge
'
,
store
.
state
.
charts
.
charts
[
chartKeys
.
commitBasedHistogram
].
isLoading
=
false
;
chartKey
:
chartKeys
.
timeBasedHistogram
,
});
});
});
});
});
});
describe
(
'
and the chart has data
'
,
()
=>
{
describe
(
'
Commit based histogram
'
,
()
=>
{
beforeEach
(
()
=>
{
it
(
'
renders a metric chart component
'
,
()
=>
{
store
.
state
.
charts
.
charts
[
chartKeys
.
commitBasedHistogram
].
data
=
{
1
:
2
,
2
:
3
}
;
expect
(
findCommitBasedMetricChart
().
exists
()).
toBe
(
true
)
;
});
});
it
(
'
renders a column chart
'
,
()
=>
{
describe
(
'
when chart finished loading
'
,
()
=>
{
expect
(
describe
(
'
and the chart has data
'
,
()
=>
{
findCommitBasedSection
()
beforeEach
(()
=>
{
.
find
(
GlColumnChart
)
wrapper
.
vm
.
$store
.
dispatch
(
'
charts/receiveChartDataSuccess
'
,
{
.
exists
(),
chartKey
:
chartKeys
.
commitBasedHistogram
,
).
toBe
(
true
);
data
:
{
1
:
2
,
2
:
3
},
});
});
});
describe
(
'
metric dropdown
'
,
()
=>
{
it
(
'
sets isLoading=false on the metric chart
'
,
()
=>
{
it
(
'
renders a metric type dropdown
'
,
()
=>
{
expect
(
findCommitBasedMetricChart
().
props
(
'
isLoading
'
)).
toBe
(
false
);
expect
(
});
findCommitBasedSection
()
.
find
(
GlDropdown
)
.
exists
(),
).
toBe
(
true
);
});
describe
(
'
when the user changes the metric
'
,
()
=>
{
it
(
'
passes non-empty chartData to the metric chart
'
,
()
=>
{
beforeEach
(()
=>
{
expect
(
findCommitBasedMetricChart
().
props
(
'
chartData
'
)).
not
.
toEqual
([]);
findCommitBasedSection
()
.
findAll
(
GlDropdownItem
)
.
at
(
0
)
.
vm
.
$emit
(
'
click
'
);
});
});
it
(
'
should dispatch setMetricType action
'
,
()
=>
{
describe
(
'
when the user changes the metric
'
,
()
=>
{
expect
(
actionSpies
.
setMetricType
).
toHaveBeenCalledWith
({
beforeEach
(()
=>
{
metricType
:
'
commits_count
'
,
findCommitBasedMetricChart
().
vm
.
$emit
(
'
metricTypeChange
'
,
'
loc_per_commit
'
);
chartKey
:
chartKeys
.
commitBasedHistogram
,
});
});
});
it
(
"
should update the chart's x axis label
"
,
()
=>
{
it
(
'
should call setMetricType when `metricTypeChange` is emitted on the metric chart
'
,
()
=>
{
const
columnChart
=
findCommitBasedSection
().
find
(
GlColumnChart
);
expect
(
actionSpies
.
setMetricType
).
toHaveBeenCalledWith
({
expect
(
columnChart
.
props
(
'
xAxisTitle
'
)).
toBe
(
'
Number of commits per MR
'
);
metricType
:
'
loc_per_commit
'
,
chartKey
:
chartKeys
.
commitBasedHistogram
,
});
});
it
(
"
should update the chart's x axis label
"
,
()
=>
{
const
columnChart
=
findCommitBasedMetricChart
().
find
(
GlColumnChart
);
expect
(
columnChart
.
props
(
'
xAxisTitle
'
)).
toBe
(
'
Number of commits per MR
'
);
});
});
});
});
});
});
});
});
});
describe
(
"
and the chart doesn't have any data
"
,
()
=>
{
describe
(
'
MR table
'
,
()
=>
{
beforeEach
(()
=>
{
describe
(
'
when table is loading
'
,
()
=>
{
store
.
state
.
charts
.
charts
[
chartKeys
.
commitBasedHistogram
].
data
=
null
;
beforeEach
(()
=>
{
});
wrapper
.
vm
.
$store
.
dispatch
(
'
table/requestMergeRequests
'
);
});
it
(
'
renders a "no data" message
'
,
()
=>
{
it
(
'
renders a loading indicator
'
,
()
=>
{
expect
(
findTimeBasedSection
().
text
()).
toContain
(
expect
(
'
There is no data for the selected metric. Please change your selection.
'
,
findMrTableSection
()
);
.
find
(
GlLoadingIcon
)
.
exists
(),
).
toBe
(
true
);
});
});
});
});
});
});
describe
(
'
MR table
'
,
()
=>
{
beforeEach
(()
=>
{
store
.
state
.
charts
.
charts
[
chartKeys
.
main
].
isLoading
=
false
;
store
.
state
.
charts
.
charts
[
chartKeys
.
main
].
data
=
{
1
:
2
,
2
:
3
};
});
describe
(
'
when table is loading
'
,
()
=>
{
describe
(
'
when table finished loading
'
,
()
=>
{
beforeEach
(()
=>
{
describe
(
'
and the table has data
'
,
()
=>
{
store
.
state
.
table
.
isLoadingTable
=
true
;
beforeEach
(()
=>
{
});
wrapper
.
vm
.
$store
.
dispatch
(
'
table/receiveMergeRequestsSuccess
'
,
{
headers
:
{},
data
:
[{
id
:
1
,
title
:
'
This is a test MR
'
}],
});
});
it
(
'
renders a loading indicator
'
,
()
=>
{
it
(
'
renders the MR table
'
,
()
=>
{
expect
(
expect
(
findMrTable
().
exists
()).
toBe
(
true
);
findMrTableSection
()
});
.
find
(
GlLoadingIcon
)
.
exists
(),
).
toBe
(
true
);
});
});
describe
(
'
when table finished loading
'
,
()
=>
{
it
(
'
doesn’t render a "no data" message
'
,
()
=>
{
beforeEach
(()
=>
{
expect
(
store
.
state
.
table
.
isLoadingTable
=
false
;
findMrTableSection
()
});
.
find
(
'
.js-no-data
'
)
.
exists
(),
).
toBe
(
false
);
});
describe
(
'
and the table has data
'
,
()
=>
{
it
(
'
should change the column metric
'
,
()
=>
{
beforeEach
(()
=>
{
findMrTable
().
vm
.
$emit
(
'
columnMetricChange
'
,
'
time_to_first_comment
'
);
store
.
state
.
table
.
mergeRequests
=
[{
id
:
1
,
title
:
'
This is a test MR
'
}];
expect
(
actionSpies
.
setColumnMetric
).
toHaveBeenCalledWith
(
});
'
time_to_first_comment
'
,
);
});
it
(
'
renders the MR table
'
,
()
=>
{
it
(
'
should change the page
'
,
()
=>
{
expect
(
findMrTable
().
exists
()).
toBe
(
true
);
const
page
=
2
;
});
findMrTable
().
vm
.
$emit
(
'
pageChange
'
,
page
);
expect
(
actionSpies
.
setMergeRequestsPage
).
toHaveBeenCalledWith
(
page
);
});
it
(
'
doesn’t render a "no data" message
'
,
()
=>
{
describe
(
'
sort controls
'
,
()
=>
{
expect
(
it
(
'
renders the sort dropdown and button
'
,
()
=>
{
findMrTableSection
()
expect
(
findSortFieldDropdown
().
exists
()).
toBe
(
true
);
.
find
(
'
.js-no-data
'
)
expect
(
findSortOrderToggle
().
exists
()).
toBe
(
true
);
.
exists
(),
});
).
toBe
(
false
);
});
it
(
'
should change the column metric
'
,
()
=>
{
it
(
'
should change the sort field
'
,
()
=>
{
findMrTable
().
vm
.
$emit
(
'
columnMetricChange
'
,
'
time_to_first_comment
'
);
findSortFieldDropdown
()
expect
(
actionSpies
.
setColumnMetric
).
toHaveBeenCalledWith
(
'
time_to_first_comment
'
);
.
findAll
(
GlDropdownItem
)
});
.
at
(
0
)
.
vm
.
$emit
(
'
click
'
);
it
(
'
should change the page
'
,
()
=>
{
expect
(
actionSpies
.
setSortField
).
toHaveBeenCalled
();
const
page
=
2
;
});
findMrTable
().
vm
.
$emit
(
'
pageChange
'
,
page
);
expect
(
actionSpies
.
setMergeRequestsPage
).
toHaveBeenCalledWith
(
page
);
});
describe
(
'
and there are merge requests available
'
,
()
=>
{
it
(
'
should toggle the sort order
'
,
()
=>
{
beforeEach
(()
=>
{
findSortOrderToggle
().
vm
.
$emit
(
'
click
'
);
store
.
state
.
table
.
mergeRequests
=
[{
id
:
1
}];
expect
(
actionSpies
.
toggleSortOrder
).
toHaveBeenCalled
();
});
});
});
});
describe
(
'
sort controls
'
,
()
=>
{
describe
(
"
and the table doesn't have any data
"
,
()
=>
{
it
(
'
renders the sort dropdown and button
'
,
()
=>
{
beforeEach
(()
=>
{
expect
(
findSortFieldDropdown
().
exists
()).
toBe
(
true
);
wrapper
.
vm
.
$store
.
dispatch
(
'
table/receiveMergeRequestsSuccess
'
,
{
expect
(
findSortOrderToggle
().
exists
()).
toBe
(
true
);
headers
:
{},
data
:
[],
});
});
});
it
(
'
should change the sort field
'
,
()
=>
{
it
(
'
renders a "no data" message
'
,
()
=>
{
findSortFieldDropdown
()
expect
(
.
findAll
(
GlDropdownItem
)
findMrTableSection
()
.
at
(
0
)
.
find
(
'
.js-no-data
'
)
.
vm
.
$emit
(
'
click
'
);
.
exists
(),
).
toBe
(
true
);
});
expect
(
actionSpies
.
setSortField
).
toHaveBeenCalled
();
it
(
'
doesn`t render the MR table
'
,
()
=>
{
expect
(
findMrTable
().
exists
()).
not
.
toBe
(
true
);
});
});
it
(
'
should toggle the sort order
'
,
()
=>
{
it
(
'
doesn`t render the sort dropdown and button
'
,
()
=>
{
findSortOrderToggle
().
vm
.
$emit
(
'
click
'
);
expect
(
findSortFieldDropdown
().
exists
()).
not
.
toBe
(
true
);
expect
(
actionSpies
.
toggleSortOrder
).
toHaveBeenCalled
(
);
expect
(
findSortOrderToggle
().
exists
()).
not
.
toBe
(
true
);
});
});
});
});
});
});
});
});
});
describe
(
"
and the table doesn't have any data
"
,
()
=>
{
describe
(
'
and has no data
'
,
()
=>
{
beforeEach
(()
=>
{
beforeEach
(()
=>
{
store
.
state
.
table
.
mergeRequests
=
[];
wrapper
.
vm
.
$store
.
dispatch
(
'
charts/receiveChartDataSuccess
'
,
{
chartKey
:
chartKeys
.
main
,
data
:
{},
});
});
});
it
(
'
renders a "no data" message
'
,
()
=>
{
it
(
'
sets isLoading=false on the metric chart
'
,
()
=>
{
expect
(
expect
(
findMainMetricChart
().
props
(
'
isLoading
'
)).
toBe
(
false
);
findMrTableSection
()
});
.
find
(
'
.js-no-data
'
)
.
exists
(),
).
toBe
(
true
);
});
it
(
'
doesn`t render the MR table
'
,
()
=>
{
it
(
'
passes an empty array as chartData to the metric chart
'
,
()
=>
{
expect
(
findMrTable
().
exists
()).
not
.
toBe
(
true
);
expect
(
findMainMetricChart
().
props
(
'
chartData
'
)).
toEqual
([]
);
});
});
it
(
'
doesn`t render the sort dropdown and button
'
,
()
=>
{
it
(
'
does not render any other charts
'
,
()
=>
{
expect
(
findSortFieldDropdown
().
exists
()).
not
.
toBe
(
true
);
expect
(
findSecondaryChartsSection
().
exists
()).
toBe
(
false
);
expect
(
findSortOrderToggle
().
exists
()).
not
.
toBe
(
true
);
});
});
it
(
'
does not render the MR table
'
,
()
=>
{
expect
(
findMrTableSortSection
().
exists
()).
toBe
(
false
);
expect
(
findMrTableSection
().
exists
()).
toBe
(
false
);
});
});
});
});
});
});
...
...
ee/spec/frontend/analytics/productivity_analytics/store/modules/charts/getters_spec.js
View file @
24281d62
...
@@ -178,6 +178,19 @@ describe('Productivity analytics chart getters', () => {
...
@@ -178,6 +178,19 @@ describe('Productivity analytics chart getters', () => {
});
});
});
});
describe
(
'
getSelectedMetric
'
,
()
=>
{
it
(
'
returns the currently selected metric for a given chartKey
'
,
()
=>
{
const
metricType
=
'
time_to_last_commit
'
;
state
.
charts
[
chartKeys
.
timeBasedHistogram
].
params
=
{
metricType
,
};
expect
(
getters
.
getSelectedMetric
(
state
)(
chartKeys
.
timeBasedHistogram
)).
toBe
(
'
time_to_last_commit
'
,
);
});
});
describe
(
'
hasNoAccessError
'
,
()
=>
{
describe
(
'
hasNoAccessError
'
,
()
=>
{
it
(
'
returns true if errorCode is set to 403
'
,
()
=>
{
it
(
'
returns true if errorCode is set to 403
'
,
()
=>
{
state
.
charts
[
chartKeys
.
main
].
errorCode
=
403
;
state
.
charts
[
chartKeys
.
main
].
errorCode
=
403
;
...
...
locale/gitlab.pot
View file @
24281d62
...
@@ -15728,9 +15728,6 @@ msgstr ""
...
@@ -15728,9 +15728,6 @@ msgstr ""
msgid "There is no data available. Please change your selection."
msgid "There is no data available. Please change your selection."
msgstr ""
msgstr ""
msgid "There is no data for the selected metric. Please change your selection."
msgstr ""
msgid "There was a problem communicating with your device."
msgid "There was a problem communicating with your device."
msgstr ""
msgstr ""
...
@@ -17981,7 +17978,7 @@ msgstr ""
...
@@ -17981,7 +17978,7 @@ msgstr ""
msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}"
msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}"
msgstr ""
msgstr ""
msgid "You can filter by
\"days to merge\"
by clicking on the columns in the chart."
msgid "You can filter by
'days to merge'
by clicking on the columns in the chart."
msgstr ""
msgstr ""
msgid "You can invite a new member to <strong>%{project_name}</strong> or invite another group."
msgid "You can invite a new member to <strong>%{project_name}</strong> or invite another group."
...
...
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