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
c20c01c5
Commit
c20c01c5
authored
Nov 18, 2020
by
Chad Woolley
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add snowplow analytics for projects/groups dropdowns
See
https://gitlab.com/gitlab-org/gitlab/-/issues/275968
parent
cdd241d2
Changes
13
Hide whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
103 additions
and
22 deletions
+103
-22
app/assets/javascripts/frequent_items/components/app.vue
app/assets/javascripts/frequent_items/components/app.vue
+0
-2
app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
...ts/frequent_items/components/frequent_items_list_item.vue
+11
-1
app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue
...frequent_items/components/frequent_items_search_input.vue
+9
-2
app/assets/javascripts/frequent_items/index.js
app/assets/javascripts/frequent_items/index.js
+5
-0
app/assets/javascripts/frequent_items/store/index.js
app/assets/javascripts/frequent_items/store/index.js
+4
-3
app/assets/javascripts/frequent_items/store/state.js
app/assets/javascripts/frequent_items/store/state.js
+2
-1
app/views/layouts/nav/groups_dropdown/_show.html.haml
app/views/layouts/nav/groups_dropdown/_show.html.haml
+2
-2
app/views/layouts/nav/projects_dropdown/_show.html.haml
app/views/layouts/nav/projects_dropdown/_show.html.haml
+3
-3
spec/frontend/frequent_items/components/app_spec.js
spec/frontend/frequent_items/components/app_spec.js
+2
-1
spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
...requent_items/components/frequent_items_list_item_spec.js
+26
-1
spec/frontend/frequent_items/components/frequent_items_list_spec.js
...end/frequent_items/components/frequent_items_list_spec.js
+2
-0
spec/frontend/frequent_items/components/frequent_items_search_input_spec.js
...uent_items/components/frequent_items_search_input_spec.js
+37
-5
spec/frontend/frequent_items/mock_data.js
spec/frontend/frequent_items/mock_data.js
+0
-1
No files found.
app/assets/javascripts/frequent_items/components/app.vue
View file @
c20c01c5
...
...
@@ -3,7 +3,6 @@ import { mapState, mapActions, mapGetters } from 'vuex';
import
{
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
AccessorUtilities
from
'
~/lib/utils/accessor
'
;
import
eventHub
from
'
../event_hub
'
;
import
store
from
'
../store
'
;
import
{
FREQUENT_ITEMS
,
STORAGE_KEY
}
from
'
../constants
'
;
import
{
isMobile
,
updateExistingFrequentItem
,
sanitizeItem
}
from
'
../utils
'
;
import
FrequentItemsSearchInput
from
'
./frequent_items_search_input.vue
'
;
...
...
@@ -11,7 +10,6 @@ import FrequentItemsList from './frequent_items_list.vue';
import
frequentItemsMixin
from
'
./frequent_items_mixin
'
;
export
default
{
store
,
components
:
{
FrequentItemsSearchInput
,
FrequentItemsList
,
...
...
app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
View file @
c20c01c5
<
script
>
/* eslint-disable vue/require-default-prop, vue/no-v-html */
import
{
mapState
}
from
'
vuex
'
;
import
Identicon
from
'
~/vue_shared/components/identicon.vue
'
;
import
highlight
from
'
~/lib/utils/highlight
'
;
import
{
truncateNamespace
}
from
'
~/lib/utils/text_utility
'
;
import
Tracking
from
'
~/tracking
'
;
const
trackingMixin
=
Tracking
.
mixin
();
export
default
{
components
:
{
Identicon
,
},
mixins
:
[
trackingMixin
],
props
:
{
matcher
:
{
type
:
String
,
...
...
@@ -37,6 +42,7 @@ export default {
},
},
computed
:
{
...
mapState
([
'
dropdownType
'
]),
truncatedNamespace
()
{
return
truncateNamespace
(
this
.
namespace
);
},
...
...
@@ -49,7 +55,11 @@ export default {
<
template
>
<li
class=
"frequent-items-list-item-container"
>
<a
:href=
"webUrl"
class=
"clearfix"
>
<a
:href=
"webUrl"
class=
"clearfix"
@
click=
"track('click_link',
{ label: `${dropdownType}_dropdown_frequent_items_list_item` })"
>
<div
ref=
"frequentItemsItemAvatarContainer"
class=
"frequent-items-item-avatar-container avatar-container rect-avatar s32"
...
...
app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue
View file @
c20c01c5
<
script
>
import
{
debounce
}
from
'
lodash
'
;
import
{
mapActions
}
from
'
vuex
'
;
import
{
mapActions
,
mapState
}
from
'
vuex
'
;
import
{
GlIcon
}
from
'
@gitlab/ui
'
;
import
eventHub
from
'
../event_hub
'
;
import
frequentItemsMixin
from
'
./frequent_items_mixin
'
;
import
Tracking
from
'
~/tracking
'
;
const
trackingMixin
=
Tracking
.
mixin
();
export
default
{
components
:
{
GlIcon
,
},
mixins
:
[
frequentItemsMixin
],
mixins
:
[
frequentItemsMixin
,
trackingMixin
],
data
()
{
return
{
searchQuery
:
''
,
};
},
computed
:
{
...
mapState
([
'
dropdownType
'
]),
translations
()
{
return
this
.
getTranslations
([
'
searchInputPlaceholder
'
]);
},
},
watch
:
{
searchQuery
:
debounce
(
function
debounceSearchQuery
()
{
this
.
track
(
'
type_search_query
'
,
{
label
:
`
${
this
.
dropdownType
}
_dropdown_frequent_items_search_input`
,
});
this
.
setSearchQuery
(
this
.
searchQuery
);
},
500
),
},
...
...
app/assets/javascripts/frequent_items/index.js
View file @
c20c01c5
...
...
@@ -2,6 +2,7 @@ import $ from 'jquery';
import
Vue
from
'
vue
'
;
import
Translate
from
'
~/vue_shared/translate
'
;
import
eventHub
from
'
./event_hub
'
;
import
{
createStore
}
from
'
~/frequent_items/store
'
;
Vue
.
use
(
Translate
);
...
...
@@ -28,11 +29,15 @@ export default function initFrequentItemDropdowns() {
return
;
}
const
dropdownType
=
namespace
;
const
store
=
createStore
({
dropdownType
});
import
(
'
./components/app.vue
'
)
.
then
(({
default
:
FrequentItems
})
=>
{
// eslint-disable-next-line no-new
new
Vue
({
el
,
store
,
data
()
{
const
{
dataset
}
=
this
.
$options
.
el
;
const
item
=
{
...
...
app/assets/javascripts/frequent_items/store/index.js
View file @
c20c01c5
...
...
@@ -7,10 +7,11 @@ import state from './state';
Vue
.
use
(
Vuex
);
export
default
()
=>
new
Vuex
.
Store
({
export
const
createStore
=
(
initState
=
{})
=>
{
return
new
Vuex
.
Store
({
actions
,
getters
,
mutations
,
state
:
state
(),
state
:
state
(
initState
),
});
};
app/assets/javascripts/frequent_items/store/state.js
View file @
c20c01c5
export
default
()
=>
({
export
default
(
{
dropdownType
=
''
}
=
{}
)
=>
({
namespace
:
''
,
dropdownType
,
storageKey
:
''
,
searchQuery
:
''
,
isLoadingItems
:
false
,
...
...
app/views/layouts/nav/groups_dropdown/_show.html.haml
View file @
c20c01c5
...
...
@@ -3,10 +3,10 @@
.frequent-items-dropdown-sidebar.qa-groups-dropdown-sidebar
%ul
=
nav_link
(
path:
'dashboard/groups#index'
)
do
=
link_to
dashboard_groups_path
,
class:
'qa-your-groups-link'
do
=
link_to
dashboard_groups_path
,
class:
'qa-your-groups-link'
,
data:
{
track_label:
"groups_dropdown_your_groups"
,
track_event:
"click_link"
}
do
=
_
(
'Your groups'
)
=
nav_link
(
path:
'groups#explore'
)
do
=
link_to
explore_groups_path
do
=
link_to
explore_groups_path
,
data:
{
track_label:
"groups_dropdown_explore_groups"
,
track_event:
"click_link"
}
do
=
_
(
'Explore groups'
)
.frequent-items-dropdown-content
#js-groups-dropdown
{
data:
{
user_name:
current_user
.
username
,
group:
group_meta
}
}
app/views/layouts/nav/projects_dropdown/_show.html.haml
View file @
c20c01c5
...
...
@@ -3,13 +3,13 @@
.frequent-items-dropdown-sidebar.qa-projects-dropdown-sidebar
%ul
=
nav_link
(
path:
'dashboard/projects#index'
)
do
=
link_to
dashboard_projects_path
,
class:
'qa-your-projects-link'
do
=
link_to
dashboard_projects_path
,
class:
'qa-your-projects-link'
,
data:
{
track_label:
"projects_dropdown_your_projects"
,
track_event:
"click_link"
}
do
=
_
(
'Your projects'
)
=
nav_link
(
path:
'projects#starred'
)
do
=
link_to
starred_dashboard_projects_path
do
=
link_to
starred_dashboard_projects_path
,
data:
{
track_label:
"projects_dropdown_starred_projects"
,
track_event:
"click_link"
}
do
=
_
(
'Starred projects'
)
=
nav_link
(
path:
'projects#trending'
)
do
=
link_to
explore_root_path
do
=
link_to
explore_root_path
,
data:
{
track_label:
"projects_dropdown_explore_projects"
,
track_event:
"click_link"
}
do
=
_
(
'Explore projects'
)
.frequent-items-dropdown-content
#js-projects-dropdown
{
data:
{
user_name:
current_user
.
username
,
project:
project_meta
}
}
spec/frontend/frequent_items/components/app_spec.js
View file @
c20c01c5
...
...
@@ -6,10 +6,10 @@ import waitForPromises from 'helpers/wait_for_promises';
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
appComponent
from
'
~/frequent_items/components/app.vue
'
;
import
eventHub
from
'
~/frequent_items/event_hub
'
;
import
store
from
'
~/frequent_items/store
'
;
import
{
FREQUENT_ITEMS
,
HOUR_IN_MS
}
from
'
~/frequent_items/constants
'
;
import
{
getTopFrequentItems
}
from
'
~/frequent_items/utils
'
;
import
{
currentSession
,
mockFrequentProjects
,
mockSearchedProjects
}
from
'
../mock_data
'
;
import
{
createStore
}
from
'
~/frequent_items/store
'
;
useLocalStorageSpy
();
...
...
@@ -18,6 +18,7 @@ const createComponentWithStore = (namespace = 'projects') => {
session
=
currentSession
[
namespace
];
gon
.
api_version
=
session
.
apiVersion
;
const
Component
=
Vue
.
extend
(
appComponent
);
const
store
=
createStore
();
return
mountComponentWithStore
(
Component
,
{
store
,
...
...
spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
View file @
c20c01c5
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
mockTracking
,
unmockTracking
}
from
'
helpers/tracking_helper
'
;
import
{
trimText
}
from
'
helpers/text_helper
'
;
import
frequentItemsListItemComponent
from
'
~/frequent_items/components/frequent_items_list_item.vue
'
;
import
{
mockProject
}
from
'
../mock_data
'
;
// can also use 'mockGroup', but not useful to test here
import
{
createStore
}
from
'
~/frequent_items/store
'
;
import
{
mockProject
}
from
'
../mock_data
'
;
describe
(
'
FrequentItemsListItemComponent
'
,
()
=>
{
let
wrapper
;
let
trackingSpy
;
let
store
=
createStore
();
const
findTitle
=
()
=>
wrapper
.
find
({
ref
:
'
frequentItemsItemTitle
'
});
const
findAvatar
=
()
=>
wrapper
.
find
({
ref
:
'
frequentItemsItemAvatar
'
});
...
...
@@ -18,6 +22,7 @@ describe('FrequentItemsListItemComponent', () => {
const
createComponent
=
(
props
=
{})
=>
{
wrapper
=
shallowMount
(
frequentItemsListItemComponent
,
{
store
,
propsData
:
{
itemId
:
mockProject
.
id
,
itemName
:
mockProject
.
name
,
...
...
@@ -29,7 +34,14 @@ describe('FrequentItemsListItemComponent', () => {
});
};
beforeEach
(()
=>
{
store
=
createStore
({
dropdownType
:
'
project
'
});
trackingSpy
=
mockTracking
(
'
_category_
'
,
document
,
jest
.
spyOn
);
trackingSpy
.
mockImplementation
(()
=>
{});
});
afterEach
(()
=>
{
unmockTracking
();
wrapper
.
destroy
();
wrapper
=
null
;
});
...
...
@@ -97,5 +109,18 @@ describe('FrequentItemsListItemComponent', () => {
`
(
'
should render $expected $name
'
,
({
selector
,
expected
})
=>
{
expect
(
selector
()).
toHaveLength
(
expected
);
});
it
(
'
tracks when item link is clicked
'
,
()
=>
{
const
link
=
wrapper
.
find
(
'
a
'
);
// NOTE: this listener is required to prevent the click from going through and causing:
// `Error: Not implemented: navigation ...`
link
.
element
.
addEventListener
(
'
click
'
,
e
=>
{
e
.
preventDefault
();
});
link
.
trigger
(
'
click
'
);
expect
(
trackingSpy
).
toHaveBeenCalledWith
(
undefined
,
'
click_link
'
,
{
label
:
'
project_dropdown_frequent_items_list_item
'
,
});
});
});
});
spec/frontend/frequent_items/components/frequent_items_list_spec.js
View file @
c20c01c5
import
{
mount
}
from
'
@vue/test-utils
'
;
import
{
createStore
}
from
'
~/frequent_items/store
'
;
import
frequentItemsListComponent
from
'
~/frequent_items/components/frequent_items_list.vue
'
;
import
frequentItemsListItemComponent
from
'
~/frequent_items/components/frequent_items_list_item.vue
'
;
import
{
mockFrequentProjects
}
from
'
../mock_data
'
;
...
...
@@ -8,6 +9,7 @@ describe('FrequentItemsListComponent', () => {
const
createComponent
=
(
props
=
{})
=>
{
wrapper
=
mount
(
frequentItemsListComponent
,
{
store
:
createStore
(),
propsData
:
{
namespace
:
'
projects
'
,
items
:
mockFrequentProjects
,
...
...
spec/frontend/frequent_items/components/frequent_items_search_input_spec.js
View file @
c20c01c5
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
mockTracking
,
unmockTracking
}
from
'
helpers/tracking_helper
'
;
import
searchComponent
from
'
~/frequent_items/components/frequent_items_search_input.vue
'
;
import
{
createStore
}
from
'
~/frequent_items/store
'
;
import
eventHub
from
'
~/frequent_items/event_hub
'
;
const
createComponent
=
(
namespace
=
'
projects
'
)
=>
shallowMount
(
searchComponent
,
{
propsData
:
{
namespace
},
});
describe
(
'
FrequentItemsSearchInputComponent
'
,
()
=>
{
let
wrapper
;
let
trackingSpy
;
let
vm
;
let
store
;
const
createComponent
=
(
namespace
=
'
projects
'
)
=>
shallowMount
(
searchComponent
,
{
store
,
propsData
:
{
namespace
},
});
beforeEach
(()
=>
{
store
=
createStore
({
dropdownType
:
'
project
'
});
jest
.
spyOn
(
store
,
'
dispatch
'
).
mockImplementation
(()
=>
{});
trackingSpy
=
mockTracking
(
'
_category_
'
,
document
,
jest
.
spyOn
);
trackingSpy
.
mockImplementation
(()
=>
{});
wrapper
=
createComponent
();
({
vm
}
=
wrapper
);
});
afterEach
(()
=>
{
unmockTracking
();
vm
.
$destroy
();
});
...
...
@@ -76,4 +88,24 @@ describe('FrequentItemsSearchInputComponent', () => {
);
});
});
describe
(
'
tracking
'
,
()
=>
{
it
(
'
tracks when search query is entered
'
,
async
()
=>
{
expect
(
trackingSpy
).
not
.
toHaveBeenCalled
();
expect
(
store
.
dispatch
).
not
.
toHaveBeenCalled
();
const
value
=
'
my project
'
;
const
input
=
wrapper
.
find
(
'
input
'
);
input
.
setValue
(
value
);
input
.
trigger
(
'
input
'
);
await
wrapper
.
vm
.
$nextTick
();
expect
(
trackingSpy
).
toHaveBeenCalledWith
(
undefined
,
'
type_search_query
'
,
{
label
:
'
project_dropdown_frequent_items_search_input
'
,
});
expect
(
store
.
dispatch
).
toHaveBeenCalledWith
(
'
setSearchQuery
'
,
value
);
});
});
});
spec/frontend/frequent_items/mock_data.js
View file @
c20c01c5
...
...
@@ -30,7 +30,6 @@ export const currentSession = {
};
export
const
mockNamespace
=
'
projects
'
;
export
const
mockStorageKey
=
'
test-user/frequent-projects
'
;
export
const
mockGroup
=
{
...
...
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