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
2c023838
Commit
2c023838
authored
Apr 24, 2020
by
Martin Wortschack
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add filter bar component
- Implements a filter bar component and wires the store to the app.
parent
d58353db
Changes
8
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
290 additions
and
17 deletions
+290
-17
app/assets/stylesheets/utilities.scss
app/assets/stylesheets/utilities.scss
+1
-0
ee/app/assets/javascripts/analytics/code_review_analytics/code_review_analytics_bundle.js
...ics/code_review_analytics/code_review_analytics_bundle.js
+9
-1
ee/app/assets/javascripts/analytics/code_review_analytics/components/app.vue
...cripts/analytics/code_review_analytics/components/app.vue
+16
-5
ee/app/assets/javascripts/analytics/code_review_analytics/components/filter_bar.vue
...analytics/code_review_analytics/components/filter_bar.vue
+71
-0
ee/app/views/projects/analytics/code_reviews/index.html.haml
ee/app/views/projects/analytics/code_reviews/index.html.haml
+1
-1
ee/spec/frontend/analytics/code_review_analytics/components/app_spec.js
...nd/analytics/code_review_analytics/components/app_spec.js
+86
-10
ee/spec/frontend/analytics/code_review_analytics/components/filter_bar_spec.js
...ytics/code_review_analytics/components/filter_bar_spec.js
+103
-0
locale/gitlab.pot
locale/gitlab.pot
+3
-0
No files found.
app/assets/stylesheets/utilities.scss
View file @
2c023838
...
...
@@ -50,6 +50,7 @@
.border-color-default
{
border-color
:
$border-color
;
}
.border-bottom-color-default
{
border-bottom-color
:
$border-color
;
}
.border-radius-default
{
border-radius
:
$border-radius-default
;
}
.border-radius-small
{
border-radius
:
$border-radius-small
;
}
.box-shadow-default
{
box-shadow
:
0
2px
4px
0
$black-transparent
;
}
.gl-children-ml-sm-3
>
*
{
...
...
ee/app/assets/javascripts/analytics/code_review_analytics/code_review_analytics_bundle.js
View file @
2c023838
...
...
@@ -4,7 +4,13 @@ import CodeAnalyticsApp from './components/app.vue';
export
default
()
=>
{
const
container
=
document
.
getElementById
(
'
js-code-review-analytics
'
);
const
{
projectId
,
newMergeRequestUrl
,
emptyStateSvgPath
}
=
container
.
dataset
;
const
{
projectId
,
newMergeRequestUrl
,
emptyStateSvgPath
,
milestonePath
,
labelsPath
,
}
=
container
.
dataset
;
if
(
!
container
)
return
;
...
...
@@ -18,6 +24,8 @@ export default () => {
projectId
:
Number
(
projectId
),
newMergeRequestUrl
,
emptyStateSvgPath
,
milestonePath
,
labelsPath
,
},
});
},
...
...
ee/app/assets/javascripts/analytics/code_review_analytics/components/app.vue
View file @
2c023838
<
script
>
import
{
mapState
,
mapActions
}
from
'
vuex
'
;
import
{
GlBadge
,
GlLoadingIcon
,
GlEmptyState
,
GlPagination
}
from
'
@gitlab/ui
'
;
import
glFeatureFlagsMixin
from
'
~/vue_shared/mixins/gl_feature_flags_mixin
'
;
import
{
GlBadge
,
GlLoadingIcon
,
GlEmptyState
,
GlPagination
}
from
'
@gitlab/ui
'
;
import
MergeRequestTable
from
'
./merge_request_table.vue
'
;
import
FilterBar
from
'
./filter_bar.vue
'
;
import
FilteredSearchCodeReviewAnalytics
from
'
../filtered_search_code_review_analytics
'
;
export
default
{
...
...
@@ -11,6 +12,7 @@ export default {
GlLoadingIcon
,
GlPagination
,
GlEmptyState
,
FilterBar
,
MergeRequestTable
,
},
mixins
:
[
glFeatureFlagsMixin
()],
...
...
@@ -27,6 +29,14 @@ export default {
type
:
String
,
required
:
true
,
},
milestonePath
:
{
type
:
String
,
required
:
true
,
},
labelsPath
:
{
type
:
String
,
required
:
true
,
},
},
computed
:
{
...
mapState
(
'
mergeRequests
'
,
{
...
...
@@ -52,12 +62,16 @@ export default {
if
(
!
this
.
codeReviewAnalyticsHasNewSearch
)
{
this
.
filterManager
=
new
FilteredSearchCodeReviewAnalytics
();
this
.
filterManager
.
setup
();
}
else
{
this
.
setMilestonesEndpoint
(
this
.
milestonePath
);
this
.
setLabelsEndpoint
(
this
.
labelsPath
);
}
this
.
setProjectId
(
this
.
projectId
);
this
.
fetchMergeRequests
();
},
methods
:
{
...
mapActions
(
'
filters
'
,
[
'
setMilestonesEndpoint
'
,
'
setLabelsEndpoint
'
]),
...
mapActions
(
'
mergeRequests
'
,
[
'
setProjectId
'
,
'
fetchMergeRequests
'
,
'
setPage
'
]),
},
};
...
...
@@ -65,10 +79,7 @@ export default {
<
template
>
<div>
<div
v-if=
"codeReviewAnalyticsHasNewSearch"
class=
"bg-secondary-50 p-3 border-top border-bottom"
></div>
<filter-bar
v-if=
"codeReviewAnalyticsHasNewSearch"
/>
<div
class=
"mt-2"
>
<gl-loading-icon
v-show=
"isLoading"
size=
"md"
class=
"mt-3"
/>
<template
v-if=
"!isLoading"
>
...
...
ee/app/assets/javascripts/analytics/code_review_analytics/components/filter_bar.vue
0 → 100644
View file @
2c023838
<
script
>
import
{
mapState
,
mapActions
}
from
'
vuex
'
;
import
{
GlFilteredSearch
,
GlFilteredSearchToken
}
from
'
@gitlab/ui
'
;
import
{
__
}
from
'
~/locale
'
;
export
default
{
components
:
{
GlFilteredSearch
,
},
data
()
{
return
{
searchTerms
:
[],
};
},
computed
:
{
...
mapState
(
'
filters
'
,
{
milestonePath
:
'
milestonePath
'
,
labelsPath
:
'
labelsPath
'
,
milestones
:
state
=>
state
.
milestones
.
data
,
}),
tokens
()
{
return
[
{
icon
:
'
clock
'
,
title
:
__
(
'
Milestone
'
),
type
:
'
milestone
'
,
token
:
GlFilteredSearchToken
,
options
:
this
.
milestones
,
unique
:
true
,
},
];
},
},
created
()
{
this
.
fetchMilestones
();
},
methods
:
{
...
mapActions
(
'
filters
'
,
[
'
fetchMilestones
'
,
'
setFilters
'
]),
filteredSearchSubmit
(
filters
)
{
const
result
=
filters
.
reduce
((
acc
,
item
)
=>
{
const
{
type
,
value
:
{
data
},
}
=
item
;
if
(
!
acc
[
type
])
{
acc
[
type
]
=
[];
}
acc
[
type
].
push
(
data
);
return
acc
;
},
{});
this
.
setFilters
({
label_name
:
result
.
label
,
milestone_title
:
result
.
milestone
});
},
},
};
</
script
>
<
template
>
<div
class=
"bg-secondary-50 p-3 border-top border-bottom"
>
<gl-filtered-search
:v-model=
"searchTerms"
:placeholder=
"__('Filter results')"
:clear-button-title=
"__('Clear')"
:close-button-title=
"__('Close')"
:available-tokens=
"tokens"
@
submit=
"filteredSearchSubmit"
/>
</div>
</
template
>
ee/app/views/projects/analytics/code_reviews/index.html.haml
View file @
2c023838
...
...
@@ -7,4 +7,4 @@
%span
.text-secondary
=
_
(
'Review time is defined as the time it takes from first comment until merged.'
)
-
if
Feature
.
disabled?
(
:code_review_analytics_has_new_search
)
=
render
'shared/issuable/search_bar'
,
type: :issues_analytics
,
show_sorting_dropdown:
false
#js-code-review-analytics
{
data:
{
project_id:
@project
.
id
,
new_merge_request_url:
namespace_project_new_merge_request_path
(
@project
.
namespace
),
empty_state_svg_path:
image_path
(
'illustrations/merge_requests.svg'
)
}
}
#js-code-review-analytics
{
data:
{
project_id:
@project
.
id
,
new_merge_request_url:
namespace_project_new_merge_request_path
(
@project
.
namespace
),
empty_state_svg_path:
image_path
(
'illustrations/merge_requests.svg'
)
,
milestone_path:
project_milestones_path
(
@project
),
labels_path:
project_labels_path
(
@project
)
}
}
ee/spec/frontend/analytics/code_review_analytics/components/app_spec.js
View file @
2c023838
...
...
@@ -3,11 +3,15 @@ import Vuex from 'vuex';
import
{
GlLoadingIcon
,
GlEmptyState
,
GlBadge
,
GlPagination
}
from
'
@gitlab/ui
'
;
import
CodeReviewAnalyticsApp
from
'
ee/analytics/code_review_analytics/components/app.vue
'
;
import
MergeRequestTable
from
'
ee/analytics/code_review_analytics/components/merge_request_table.vue
'
;
import
createState
from
'
ee/analytics/code_review_analytics/store/modules/merge_requests/state
'
;
import
FilterBar
from
'
ee/analytics/code_review_analytics/components/filter_bar.vue
'
;
import
createMergeRequestsState
from
'
ee/analytics/code_review_analytics/store/modules/merge_requests/state
'
;
import
createFiltersState
from
'
ee/analytics/code_review_analytics/store/modules/filters/state
'
;
import
{
TEST_HOST
}
from
'
helpers/test_constants
'
;
const
mockFilterManagerSetup
=
jest
.
fn
();
jest
.
mock
(
'
ee/analytics/code_review_analytics/filtered_search_code_review_analytics
'
,
()
=>
jest
.
fn
().
mockImplementation
(()
=>
({
setup
:
jest
.
fn
()
,
setup
:
mockFilterManagerSetup
,
})),
);
...
...
@@ -20,6 +24,8 @@ describe('CodeReviewAnalyticsApp component', () => {
let
setPage
;
let
fetchMergeRequests
;
let
setMilestonesEndpoint
;
let
setLabelsEndpoint
;
const
pageInfo
=
{
page
:
1
,
...
...
@@ -33,8 +39,8 @@ describe('CodeReviewAnalyticsApp component', () => {
mergeRequests
:
{
namespaced
:
true
,
state
:
{
...
createState
(),
...
initialState
,
...
create
MergeRequests
State
(),
...
initialState
.
mergeRequests
,
},
actions
:
{
setProjectId
:
jest
.
fn
(),
...
...
@@ -46,10 +52,21 @@ describe('CodeReviewAnalyticsApp component', () => {
...
getters
,
},
},
filters
:
{
namespaced
:
true
,
state
:
{
...
createFiltersState
(),
...
initialState
.
filters
,
},
actions
:
{
setMilestonesEndpoint
,
setLabelsEndpoint
,
},
},
},
});
const
createComponent
=
store
=>
const
createComponent
=
(
store
,
codeReviewAnalyticsHasNewSearch
=
false
)
=>
shallowMount
(
CodeReviewAnalyticsApp
,
{
localVue
,
store
,
...
...
@@ -57,10 +74,12 @@ describe('CodeReviewAnalyticsApp component', () => {
projectId
:
1
,
newMergeRequestUrl
:
'
new_merge_request
'
,
emptyStateSvgPath
:
'
svg
'
,
milestonePath
:
`
${
TEST_HOST
}
/milestones`
,
labelsPath
:
`
${
TEST_HOST
}
/labels`
,
},
provide
:
{
glFeatures
:
{
codeReviewAnalyticsHasNewSearch
:
false
,
codeReviewAnalyticsHasNewSearch
,
},
},
});
...
...
@@ -68,12 +87,15 @@ describe('CodeReviewAnalyticsApp component', () => {
beforeEach
(()
=>
{
setPage
=
jest
.
fn
();
fetchMergeRequests
=
jest
.
fn
();
setMilestonesEndpoint
=
jest
.
fn
();
setLabelsEndpoint
=
jest
.
fn
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
const
findFilterBar
=
()
=>
wrapper
.
find
(
FilterBar
);
const
findEmptyState
=
()
=>
wrapper
.
find
(
GlEmptyState
);
const
findLoadingIcon
=
()
=>
wrapper
.
find
(
GlLoadingIcon
);
const
findBadge
=
()
=>
wrapper
.
find
(
GlBadge
);
...
...
@@ -81,9 +103,57 @@ describe('CodeReviewAnalyticsApp component', () => {
const
findPagination
=
()
=>
wrapper
.
find
(
GlPagination
);
describe
(
'
template
'
,
()
=>
{
describe
(
'
when "codeReviewAnalyticsHasNewSearch" is disabled
'
,
()
=>
{
beforeEach
(()
=>
{
vuexStore
=
createStore
();
wrapper
=
createComponent
(
vuexStore
);
});
it
(
'
does not render the filter bar component
'
,
()
=>
{
expect
(
findFilterBar
().
exists
()).
toBe
(
false
);
});
it
(
"
calls the filterManager's setup method
"
,
()
=>
{
expect
(
mockFilterManagerSetup
).
toHaveBeenCalled
();
});
it
(
'
does not call setMilestonesEndpoint action
'
,
()
=>
{
expect
(
setMilestonesEndpoint
).
not
.
toHaveBeenCalled
();
});
it
(
'
does not call setLabelsEndpoint action
'
,
()
=>
{
expect
(
setLabelsEndpoint
).
not
.
toHaveBeenCalled
();
});
});
describe
(
'
when "codeReviewAnalyticsHasNewSearch" is enabled
'
,
()
=>
{
describe
(
'
when the feature is enabled
'
,
()
=>
{
beforeEach
(()
=>
{
vuexStore
=
createStore
();
wrapper
=
createComponent
(
vuexStore
,
true
);
});
it
(
'
renders the filter bar component
'
,
()
=>
{
expect
(
findFilterBar
().
exists
()).
toBe
(
true
);
});
it
(
"
does not call the filterManager's setup method
"
,
()
=>
{
expect
(
mockFilterManagerSetup
).
not
.
toHaveBeenCalled
();
});
it
(
'
calls setMilestonesEndpoint action
'
,
()
=>
{
expect
(
setMilestonesEndpoint
).
toHaveBeenCalled
();
});
it
(
'
calls setLabelsEndpoint action
'
,
()
=>
{
expect
(
setLabelsEndpoint
).
toHaveBeenCalled
();
});
});
});
describe
(
'
while loading
'
,
()
=>
{
beforeEach
(()
=>
{
vuexStore
=
createStore
({
isLoading
:
true
});
vuexStore
=
createStore
({
mergeRequests
:
{
isLoading
:
true
}
});
wrapper
=
createComponent
(
vuexStore
);
});
...
...
@@ -108,7 +178,7 @@ describe('CodeReviewAnalyticsApp component', () => {
describe
(
'
and there are no merge requests
'
,
()
=>
{
beforeEach
(()
=>
{
vuexStore
=
createStore
(
{
isLoading
:
false
,
pageInfo
:
{
page
:
0
,
perPage
:
0
,
total
:
0
}
},
{
mergeRequests
:
{
isLoading
:
false
,
pageInfo
:
{
page
:
0
,
perPage
:
0
,
total
:
0
}
}
},
{
showMrCount
:
()
=>
true
},
);
wrapper
=
createComponent
(
vuexStore
);
...
...
@@ -137,7 +207,10 @@ describe('CodeReviewAnalyticsApp component', () => {
describe
(
'
and there are merge requests
'
,
()
=>
{
beforeEach
(()
=>
{
vuexStore
=
createStore
({
isLoading
:
false
,
pageInfo
},
{
showMrCount
:
()
=>
true
});
vuexStore
=
createStore
(
{
mergeRequests
:
{
isLoading
:
false
,
pageInfo
}
},
{
showMrCount
:
()
=>
true
},
);
wrapper
=
createComponent
(
vuexStore
);
});
...
...
@@ -167,7 +240,10 @@ describe('CodeReviewAnalyticsApp component', () => {
describe
(
'
changing the page
'
,
()
=>
{
beforeEach
(()
=>
{
vuexStore
=
createStore
({
isLoading
:
false
,
pageInfo
},
{
showMrCount
:
()
=>
true
});
vuexStore
=
createStore
(
{
mergeRequests
:
{
isLoading
:
false
,
pageInfo
}
},
{
showMrCount
:
()
=>
true
},
);
wrapper
=
createComponent
(
vuexStore
);
wrapper
.
vm
.
currentPage
=
2
;
});
...
...
ee/spec/frontend/analytics/code_review_analytics/components/filter_bar_spec.js
0 → 100644
View file @
2c023838
import
{
createLocalVue
,
shallowMount
}
from
'
@vue/test-utils
'
;
import
Vuex
from
'
vuex
'
;
import
{
GlFilteredSearch
}
from
'
@gitlab/ui
'
;
import
FilterBar
from
'
ee/analytics/code_review_analytics/components/filter_bar.vue
'
;
import
createFiltersState
from
'
ee/analytics/code_review_analytics/store/modules/filters/state
'
;
import
{
mockMilestones
}
from
'
../mock_data
'
;
const
localVue
=
createLocalVue
();
localVue
.
use
(
Vuex
);
const
milestoneTokenType
=
'
milestone
'
;
describe
(
'
FilteredSearchBar
'
,
()
=>
{
let
wrapper
;
let
vuexStore
;
let
setFiltersMock
;
const
createStore
=
(
initialState
=
{})
=>
{
setFiltersMock
=
jest
.
fn
();
return
new
Vuex
.
Store
({
modules
:
{
filters
:
{
namespaced
:
true
,
state
:
{
...
createFiltersState
(),
...
initialState
,
},
actions
:
{
fetchMilestones
:
jest
.
fn
(),
setFilters
:
setFiltersMock
,
},
},
},
});
};
const
createComponent
=
store
=>
shallowMount
(
FilterBar
,
{
localVue
,
store
,
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
const
findFilteredSearch
=
()
=>
wrapper
.
find
(
GlFilteredSearch
);
const
getSearchToken
=
type
=>
findFilteredSearch
()
.
props
(
'
availableTokens
'
)
.
filter
(
token
=>
token
.
type
===
type
)[
0
];
it
(
'
renders GlFilteredSearch component
'
,
()
=>
{
vuexStore
=
createStore
();
wrapper
=
createComponent
(
vuexStore
);
expect
(
findFilteredSearch
().
exists
()).
toBe
(
true
);
});
describe
(
'
when the state has data
'
,
()
=>
{
beforeEach
(()
=>
{
vuexStore
=
createStore
({
milestones
:
{
data
:
mockMilestones
}
});
wrapper
=
createComponent
(
vuexStore
);
});
it
(
'
displays the milestone token
'
,
()
=>
{
const
tokens
=
findFilteredSearch
().
props
(
'
availableTokens
'
);
expect
(
tokens
).
toHaveLength
(
1
);
expect
(
tokens
[
0
].
type
).
toBe
(
milestoneTokenType
);
});
it
(
'
displays options in the milestone token
'
,
()
=>
{
const
{
options
}
=
getSearchToken
(
milestoneTokenType
);
expect
(
options
).
toHaveLength
(
mockMilestones
.
length
);
});
});
describe
(
'
when the user interacts
'
,
()
=>
{
beforeEach
(()
=>
{
vuexStore
=
createStore
({
milestones
:
{
data
:
mockMilestones
}
});
wrapper
=
createComponent
(
vuexStore
);
});
it
(
'
clicks on the search button, setFilters is dispatched
'
,
()
=>
{
findFilteredSearch
().
vm
.
$emit
(
'
submit
'
,
[
{
type
:
'
milestone
'
,
value
:
{
data
:
'
my-milestone
'
,
operator
:
'
=
'
}
},
]);
expect
(
setFiltersMock
).
toHaveBeenCalledWith
(
expect
.
anything
(),
{
label_name
:
undefined
,
milestone_title
:
[
'
my-milestone
'
],
},
undefined
,
);
});
});
});
locale/gitlab.pot
View file @
2c023838
...
...
@@ -9227,6 +9227,9 @@ msgstr ""
msgid "Filter projects"
msgstr ""
msgid "Filter results"
msgstr ""
msgid "Filter results by group"
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