Commit 74f244c0 authored by Martin Wortschack's avatar Martin Wortschack

Merge branch '35528-add-ui-event-tracking-for-container-registry' into 'master'

Resolve "Add UI event tracking for container registry"

Closes #35528

See merge request gitlab-org/gitlab!19772
parents e36c7247 d5be2c9a
......@@ -8,12 +8,13 @@ import {
GlModalDirective,
GlEmptyState,
} from '@gitlab/ui';
import createFlash from '../../flash';
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
import Icon from '../../vue_shared/components/icon.vue';
import createFlash from '~/flash';
import Tracking from '~/tracking';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
import TableRegistry from './table_registry.vue';
import { errorMessages, errorMessagesTypes } from '../constants';
import { __ } from '../../locale';
import { DELETE_REPO_ERROR_MESSAGE } from '../constants';
import { __ } from '~/locale';
export default {
name: 'CollapsibeContainerRegisty',
......@@ -30,6 +31,7 @@ export default {
GlTooltip: GlTooltipDirective,
GlModal: GlModalDirective,
},
mixins: [Tracking.mixin({})],
props: {
repo: {
type: Object,
......@@ -40,6 +42,10 @@ export default {
return {
isOpen: false,
modalId: `confirm-repo-deletion-modal-${this.repo.id}`,
tracking: {
category: document.body.dataset.page,
label: 'registry_repository_delete',
},
};
},
computed: {
......@@ -61,15 +67,13 @@ export default {
}
},
handleDeleteRepository() {
this.track('confirm_delete', {});
return this.deleteItem(this.repo)
.then(() => {
createFlash(__('This container registry has been scheduled for deletion.'), 'notice');
this.fetchRepos();
})
.catch(() => this.showError(errorMessagesTypes.DELETE_REPO));
},
showError(message) {
createFlash(errorMessages[message]);
.catch(() => createFlash(DELETE_REPO_ERROR_MESSAGE));
},
},
};
......@@ -97,10 +101,9 @@ export default {
v-gl-modal="modalId"
:title="s__('ContainerRegistry|Remove repository')"
:aria-label="s__('ContainerRegistry|Remove repository')"
data-track-event="click_button"
data-track-label="registry_repository_delete"
class="js-remove-repo btn-inverted"
variant="danger"
@click="track('click_button', {})"
>
<icon name="remove" />
</gl-button>
......@@ -124,7 +127,13 @@ export default {
class="mx-auto my-0"
/>
</div>
<gl-modal :modal-id="modalId" ok-variant="danger" @ok="handleDeleteRepository">
<gl-modal
ref="deleteModal"
:modal-id="modalId"
ok-variant="danger"
@ok="handleDeleteRepository"
@cancel="track('cancel_delete', {})"
>
<template v-slot:modal-title>{{ s__('ContainerRegistry|Remove repository') }}</template>
<p
v-html="
......
<script>
import { mapActions, mapGetters } from 'vuex';
import {
GlButton,
GlFormCheckbox,
GlTooltipDirective,
GlModal,
GlModalDirective,
} from '@gitlab/ui';
import { n__, s__, sprintf } from '../../locale';
import createFlash from '../../flash';
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
import TablePagination from '../../vue_shared/components/pagination/table_pagination.vue';
import Icon from '../../vue_shared/components/icon.vue';
import timeagoMixin from '../../vue_shared/mixins/timeago';
import { errorMessages, errorMessagesTypes } from '../constants';
import { numberToHumanSize } from '../../lib/utils/number_utils';
import { GlButton, GlFormCheckbox, GlTooltipDirective, GlModal } from '@gitlab/ui';
import Tracking from '~/tracking';
import { n__, s__, sprintf } from '~/locale';
import createFlash from '~/flash';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import Icon from '~/vue_shared/components/icon.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { FETCH_REGISTRY_ERROR_MESSAGE, DELETE_REGISTRY_ERROR_MESSAGE } from '../constants';
export default {
components: {
......@@ -27,7 +22,6 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
GlModal: GlModalDirective,
},
mixins: [timeagoMixin],
props: {
......@@ -65,12 +59,21 @@ export default {
this.itemsToBeDeleted.length === 0 ? 1 : this.itemsToBeDeleted.length,
);
},
},
mounted() {
this.$refs.deleteModal.$refs.modal.$on('hide', this.removeModalEvents);
isMultiDelete() {
return this.itemsToBeDeleted.length > 1;
},
tracking() {
return {
property: this.repo.name,
label: this.isMultiDelete ? 'bulk_registry_tag_delete' : 'registry_tag_delete',
};
},
},
methods: {
...mapActions(['fetchList', 'deleteItem', 'multiDeleteItems']),
track(action) {
Tracking.event(document.body.dataset.page, action, this.tracking);
},
setModalDescription(itemIndex = -1) {
if (itemIndex === -1) {
this.modalDescription = sprintf(
......@@ -92,17 +95,11 @@ export default {
formatSize(size) {
return numberToHumanSize(size);
},
removeModalEvents() {
this.$refs.deleteModal.$refs.modal.$off('ok');
},
deleteSingleItem(index) {
this.setModalDescription(index);
this.itemsToBeDeleted = [index];
this.$refs.deleteModal.$refs.modal.$once('ok', () => {
this.removeModalEvents();
this.handleSingleDelete(this.repo.list[index]);
});
this.track('click_button');
this.$refs.deleteModal.show();
},
deleteMultipleItems() {
this.itemsToBeDeleted = [...this.selectedItems];
......@@ -111,17 +108,14 @@ export default {
} else if (this.selectedItems.length > 1) {
this.setModalDescription();
}
this.$refs.deleteModal.$refs.modal.$once('ok', () => {
this.removeModalEvents();
this.handleMultipleDelete();
});
this.track('click_button');
this.$refs.deleteModal.show();
},
handleSingleDelete(itemToDelete) {
this.itemsToBeDeleted = [];
this.deleteItem(itemToDelete)
.then(() => this.fetchList({ repo: this.repo }))
.catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY));
.catch(() => createFlash(DELETE_REGISTRY_ERROR_MESSAGE));
},
handleMultipleDelete() {
const { itemsToBeDeleted } = this;
......@@ -134,19 +128,16 @@ export default {
items: itemsToBeDeleted.map(x => this.repo.list[x].tag),
})
.then(() => this.fetchList({ repo: this.repo }))
.catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY));
.catch(() => createFlash(DELETE_REGISTRY_ERROR_MESSAGE));
} else {
this.showError(errorMessagesTypes.DELETE_REGISTRY);
createFlash(DELETE_REGISTRY_ERROR_MESSAGE);
}
},
onPageChange(pageNumber) {
this.fetchList({ repo: this.repo, page: pageNumber }).catch(() =>
this.showError(errorMessagesTypes.FETCH_REGISTRY),
createFlash(FETCH_REGISTRY_ERROR_MESSAGE),
);
},
showError(message) {
createFlash(errorMessages[message]);
},
onSelectAllChange() {
if (this.selectAllChecked) {
this.deselectAll();
......@@ -179,6 +170,15 @@ export default {
canDeleteRow(item) {
return item && item.canDelete && !this.isDeleteDisabled;
},
onDeletionConfirmed() {
this.track('confirm_delete');
if (this.isMultiDelete) {
this.handleMultipleDelete();
} else {
const index = this.itemsToBeDeleted[0];
this.handleSingleDelete(this.repo.list[index]);
}
},
},
};
</script>
......@@ -202,12 +202,10 @@ export default {
<th>
<gl-button
v-if="canDeleteRepo"
ref="bulkDeleteButton"
v-gl-tooltip
v-gl-modal="modalId"
:disabled="!selectedItems || selectedItems.length === 0"
class="js-delete-registry float-right"
data-track-event="click_button"
data-track-label="bulk_registry_tag_delete"
class="float-right"
variant="danger"
:title="s__('ContainerRegistry|Remove selected tags')"
:aria-label="s__('ContainerRegistry|Remove selected tags')"
......@@ -259,11 +257,8 @@ export default {
<td class="content action-buttons">
<gl-button
v-if="canDeleteRow(item)"
v-gl-modal="modalId"
:title="s__('ContainerRegistry|Remove tag')"
:aria-label="s__('ContainerRegistry|Remove tag')"
data-track-event="click_button"
data-track-label="registry_tag_delete"
variant="danger"
class="js-delete-registry-row float-right btn-inverted btn-border-color btn-icon"
@click="deleteSingleItem(index)"
......@@ -282,7 +277,13 @@ export default {
class="js-registry-pagination"
/>
<gl-modal ref="deleteModal" :modal-id="modalId" ok-variant="danger">
<gl-modal
ref="deleteModal"
:modal-id="modalId"
ok-variant="danger"
@ok="onDeletionConfirmed"
@cancel="track('cancel_delete')"
>
<template v-slot:modal-title>{{ modalAction }}</template>
<template v-slot:modal-ok>{{ modalAction }}</template>
<p v-html="modalDescription"></p>
......
import { __ } from '../locale';
export const errorMessagesTypes = {
FETCH_REGISTRY: 'FETCH_REGISTRY',
FETCH_REPOS: 'FETCH_REPOS',
DELETE_REPO: 'DELETE_REPO',
DELETE_REGISTRY: 'DELETE_REGISTRY',
};
export const errorMessages = {
[errorMessagesTypes.FETCH_REGISTRY]: __('Something went wrong while fetching the registry list.'),
[errorMessagesTypes.FETCH_REPOS]: __('Something went wrong while fetching the projects.'),
[errorMessagesTypes.DELETE_REPO]: __('Something went wrong on our end.'),
[errorMessagesTypes.DELETE_REGISTRY]: __('Something went wrong on our end.'),
};
export const FETCH_REGISTRY_ERROR_MESSAGE = __(
'Something went wrong while fetching the registry list.',
);
export const FETCH_REPOS_ERROR_MESSAGE = __('Something went wrong while fetching the projects.');
export const DELETE_REPO_ERROR_MESSAGE = __('Something went wrong on our end.');
export const DELETE_REGISTRY_ERROR_MESSAGE = __('Something went wrong on our end.');
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import * as types from './mutation_types';
import { errorMessages, errorMessagesTypes } from '../constants';
import { FETCH_REPOS_ERROR_MESSAGE, FETCH_REGISTRY_ERROR_MESSAGE } from '../constants';
export const fetchRepos = ({ commit, state }) => {
commit(types.TOGGLE_MAIN_LOADING);
......@@ -14,7 +14,7 @@ export const fetchRepos = ({ commit, state }) => {
})
.catch(() => {
commit(types.TOGGLE_MAIN_LOADING);
createFlash(errorMessages[errorMessagesTypes.FETCH_REPOS]);
createFlash(FETCH_REPOS_ERROR_MESSAGE);
});
};
......@@ -30,7 +30,7 @@ export const fetchList = ({ commit }, { repo, page }) => {
})
.catch(() => {
commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
createFlash(errorMessages[errorMessagesTypes.FETCH_REGISTRY]);
createFlash(FETCH_REGISTRY_ERROR_MESSAGE);
});
};
......
import * as types from './mutation_types';
import { parseIntPagination, normalizeHeaders } from '../../lib/utils/common_utils';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
export default {
[types.SET_MAIN_ENDPOINT](state, endpoint) {
Object.assign(state, { endpoint });
state.endpoint = endpoint;
},
[types.SET_IS_DELETE_DISABLED](state, isDeleteDisabled) {
Object.assign(state, { isDeleteDisabled });
state.isDeleteDisabled = isDeleteDisabled;
},
[types.SET_REPOS_LIST](state, list) {
Object.assign(state, {
repos: list.map(el => ({
canDelete: Boolean(el.destroy_path),
destroyPath: el.destroy_path,
id: el.id,
isLoading: false,
list: [],
location: el.location,
name: el.path,
tagsPath: el.tags_path,
projectId: el.project_id,
})),
});
state.repos = list.map(el => ({
canDelete: Boolean(el.destroy_path),
destroyPath: el.destroy_path,
id: el.id,
isLoading: false,
list: [],
location: el.location,
name: el.path,
tagsPath: el.tags_path,
projectId: el.project_id,
}));
},
[types.TOGGLE_MAIN_LOADING](state) {
Object.assign(state, { isLoading: !state.isLoading });
state.isLoading = !state.isLoading;
},
[types.SET_REGISTRY_LIST](state, { repo, resp, headers }) {
......
---
title: Add event tracking to container registry
merge_request: 19772
author:
type: changed
import Vue from 'vue';
import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
import collapsibleComponent from '~/registry/components/collapsible_container.vue';
import { repoPropsData } from '../mock_data';
import createFlash from '~/flash';
import Tracking from '~/tracking';
import collapsibleComponent from '~/registry/components/collapsible_container.vue';
import * as getters from '~/registry/stores/getters';
import { repoPropsData } from '../mock_data';
jest.mock('~/flash.js');
......@@ -16,9 +17,10 @@ describe('collapsible registry container', () => {
let wrapper;
let store;
const findDeleteBtn = w => w.find('.js-remove-repo');
const findContainerImageTags = w => w.find('.container-image-tags');
const findToggleRepos = w => w.findAll('.js-toggle-repo');
const findDeleteBtn = (w = wrapper) => w.find('.js-remove-repo');
const findContainerImageTags = (w = wrapper) => w.find('.container-image-tags');
const findToggleRepos = (w = wrapper) => w.findAll('.js-toggle-repo');
const findDeleteModal = (w = wrapper) => w.find({ ref: 'deleteModal' });
const mountWithStore = config => mount(collapsibleComponent, { ...config, store, localVue });
......@@ -124,4 +126,45 @@ describe('collapsible registry container', () => {
expect(deleteBtn.exists()).toBe(false);
});
});
describe('tracking', () => {
const category = 'mock_page';
beforeEach(() => {
jest.spyOn(Tracking, 'event');
wrapper.vm.deleteItem = jest.fn().mockResolvedValue();
wrapper.vm.fetchRepos = jest.fn();
wrapper.setData({
tracking: {
...wrapper.vm.tracking,
category,
},
});
});
it('send an event when delete button is clicked', () => {
const deleteBtn = findDeleteBtn();
deleteBtn.trigger('click');
expect(Tracking.event).toHaveBeenCalledWith(category, 'click_button', {
label: 'registry_repository_delete',
category,
});
});
it('send an event when cancel is pressed on modal', () => {
const deleteModal = findDeleteModal();
deleteModal.vm.$emit('cancel');
expect(Tracking.event).toHaveBeenCalledWith(category, 'cancel_delete', {
label: 'registry_repository_delete',
category,
});
});
it('send an event when confirm is clicked on modal', () => {
const deleteModal = findDeleteModal();
deleteModal.vm.$emit('ok');
expect(Tracking.event).toHaveBeenCalledWith(category, 'confirm_delete', {
label: 'registry_repository_delete',
category,
});
});
});
});
import Vue from 'vue';
import Vuex from 'vuex';
import tableRegistry from '~/registry/components/table_registry.vue';
import { mount, createLocalVue } from '@vue/test-utils';
import createFlash from '~/flash';
import Tracking from '~/tracking';
import tableRegistry from '~/registry/components/table_registry.vue';
import { repoPropsData } from '../mock_data';
import * as getters from '~/registry/stores/getters';
jest.mock('~/flash');
const [firstImage, secondImage] = repoPropsData.list;
const localVue = createLocalVue();
......@@ -15,11 +19,12 @@ describe('table registry', () => {
let wrapper;
let store;
const findSelectAllCheckbox = w => w.find('.js-select-all-checkbox > input');
const findSelectCheckboxes = w => w.findAll('.js-select-checkbox > input');
const findDeleteButton = w => w.find('.js-delete-registry');
const findDeleteButtonsRow = w => w.findAll('.js-delete-registry-row');
const findPagination = w => w.find('.js-registry-pagination');
const findSelectAllCheckbox = (w = wrapper) => w.find('.js-select-all-checkbox > input');
const findSelectCheckboxes = (w = wrapper) => w.findAll('.js-select-checkbox > input');
const findDeleteButton = (w = wrapper) => w.find({ ref: 'bulkDeleteButton' });
const findDeleteButtonsRow = (w = wrapper) => w.findAll('.js-delete-registry-row');
const findPagination = (w = wrapper) => w.find('.js-registry-pagination');
const findDeleteModal = (w = wrapper) => w.find({ ref: 'deleteModal' });
const bulkDeletePath = 'path';
const mountWithStore = config => mount(tableRegistry, { ...config, store, localVue });
......@@ -139,7 +144,7 @@ describe('table registry', () => {
},
});
wrapper.vm.handleMultipleDelete();
expect(wrapper.vm.showError).toHaveBeenCalled();
expect(createFlash).toHaveBeenCalled();
});
});
......@@ -169,6 +174,27 @@ describe('table registry', () => {
});
});
describe('modal event handlers', () => {
beforeEach(() => {
wrapper.vm.handleSingleDelete = jest.fn();
wrapper.vm.handleMultipleDelete = jest.fn();
});
it('on ok when one item is selected should call singleDelete', () => {
wrapper.setData({ itemsToBeDeleted: [0] });
wrapper.vm.onDeletionConfirmed();
expect(wrapper.vm.handleSingleDelete).toHaveBeenCalledWith(repoPropsData.list[0]);
expect(wrapper.vm.handleMultipleDelete).not.toHaveBeenCalled();
});
it('on ok when multiple items are selected should call muultiDelete', () => {
wrapper.setData({ itemsToBeDeleted: [0, 1, 2] });
wrapper.vm.onDeletionConfirmed();
expect(wrapper.vm.handleMultipleDelete).toHaveBeenCalled();
expect(wrapper.vm.handleSingleDelete).not.toHaveBeenCalled();
});
});
describe('pagination', () => {
const repo = {
repoPropsData,
......@@ -265,4 +291,83 @@ describe('table registry', () => {
expect(deleteBtns.length).toBe(0);
});
});
describe('event tracking', () => {
const mockPageName = 'mock_page';
beforeEach(() => {
jest.spyOn(Tracking, 'event');
wrapper.vm.handleSingleDelete = jest.fn();
wrapper.vm.handleMultipleDelete = jest.fn();
document.body.dataset.page = mockPageName;
});
afterEach(() => {
document.body.dataset.page = null;
});
describe('single tag delete', () => {
beforeEach(() => {
wrapper.setData({ itemsToBeDeleted: [0] });
});
it('send an event when delete button is clicked', () => {
const deleteBtn = findDeleteButtonsRow();
deleteBtn.at(0).trigger('click');
expect(Tracking.event).toHaveBeenCalledWith(mockPageName, 'click_button', {
label: 'registry_tag_delete',
property: 'foo',
});
});
it('send an event when cancel is pressed on modal', () => {
const deleteModal = findDeleteModal();
deleteModal.vm.$emit('cancel');
expect(Tracking.event).toHaveBeenCalledWith(mockPageName, 'cancel_delete', {
label: 'registry_tag_delete',
property: 'foo',
});
});
it('send an event when confirm is clicked on modal', () => {
const deleteModal = findDeleteModal();
deleteModal.vm.$emit('ok');
expect(Tracking.event).toHaveBeenCalledWith(mockPageName, 'confirm_delete', {
label: 'registry_tag_delete',
property: 'foo',
});
});
});
describe('bulk tag delete', () => {
beforeEach(() => {
const items = [0, 1, 2];
wrapper.setData({ itemsToBeDeleted: items, selectedItems: items });
});
it('send an event when delete button is clicked', () => {
const deleteBtn = findDeleteButton();
deleteBtn.vm.$emit('click');
expect(Tracking.event).toHaveBeenCalledWith(mockPageName, 'click_button', {
label: 'bulk_registry_tag_delete',
property: 'foo',
});
});
it('send an event when cancel is pressed on modal', () => {
const deleteModal = findDeleteModal();
deleteModal.vm.$emit('cancel');
expect(Tracking.event).toHaveBeenCalledWith(mockPageName, 'cancel_delete', {
label: 'bulk_registry_tag_delete',
property: 'foo',
});
});
it('send an event when confirm is clicked on modal', () => {
const deleteModal = findDeleteModal();
deleteModal.vm.$emit('ok');
expect(Tracking.event).toHaveBeenCalledWith(mockPageName, 'confirm_delete', {
label: 'bulk_registry_tag_delete',
property: 'foo',
});
});
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment