Commit 957906d2 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '213881-alerts-list-pagination' into 'master'

Alerts list pagination

See merge request gitlab-org/gitlab!33073
parents d9f8f1d2 56d8064a
......@@ -11,6 +11,7 @@ import {
GlTabs,
GlTab,
GlBadge,
GlPagination,
} from '@gitlab/ui';
import createFlash from '~/flash';
import { s__ } from '~/locale';
......@@ -22,6 +23,7 @@ import getAlertsCountByStatus from '../graphql/queries/get_count_by_status.query
import {
ALERTS_STATUS_TABS,
ALERTS_SEVERITY_LABELS,
DEFAULT_PAGE_SIZE,
trackAlertListViewsOptions,
trackAlertStatusUpdateOptions,
} from '../constants';
......@@ -34,6 +36,14 @@ const bodyTrClass =
'gl-border-1 gl-border-t-solid gl-border-gray-100 gl-hover-bg-blue-50 gl-hover-cursor-pointer gl-hover-border-b-solid gl-hover-border-blue-200';
const findDefaultSortColumn = () => document.querySelector('.js-started-at');
const initialPaginationState = {
currentPage: 1,
prevPageCursor: '',
nextPageCursor: '',
firstPageSize: DEFAULT_PAGE_SIZE,
lastPageSize: null,
};
export default {
i18n: {
noAlertsMsg: s__(
......@@ -110,6 +120,7 @@ export default {
GlTabs,
GlTab,
GlBadge,
GlPagination,
},
props: {
projectPath: {
......@@ -142,10 +153,20 @@ export default {
projectPath: this.projectPath,
statuses: this.statusFilter,
sort: this.sort,
firstPageSize: this.pagination.firstPageSize,
lastPageSize: this.pagination.lastPageSize,
prevPageCursor: this.pagination.prevPageCursor,
nextPageCursor: this.pagination.nextPageCursor,
};
},
update(data) {
return data.project?.alertManagementAlerts?.nodes;
const { alertManagementAlerts: { nodes: list = [], pageInfo = {} } = {} } =
data.project || {};
return {
list,
pageInfo,
};
},
error() {
this.errored = true;
......@@ -169,7 +190,9 @@ export default {
isAlertDismissed: false,
isErrorAlertDismissed: false,
sort: 'STARTED_AT_ASC',
statusFilter: this.$options.statusTabs[4].filters,
statusFilter: [],
filteredByStatus: '',
pagination: initialPaginationState,
};
},
computed: {
......@@ -185,19 +208,34 @@ export default {
return this.$apollo.queries.alerts.loading;
},
hasAlerts() {
return this.alerts?.length;
return this.alerts?.list?.length;
},
tbodyTrClass() {
return !this.loading && this.hasAlerts ? bodyTrClass : '';
},
showPaginationControls() {
return Boolean(this.prevPage || this.nextPage);
},
alertsForCurrentTab() {
return this.alertsCount ? this.alertsCount[this.filteredByStatus.toLowerCase()] : 0;
},
prevPage() {
return Math.max(this.pagination.currentPage - 1, 0);
},
nextPage() {
const nextPage = this.pagination.currentPage + 1;
return nextPage > Math.ceil(this.alertsForCurrentTab / DEFAULT_PAGE_SIZE) ? null : nextPage;
},
},
mounted() {
findDefaultSortColumn().ariaSort = 'ascending';
this.trackPageViews();
},
methods: {
filterAlertsByStatus(tabIndex) {
this.statusFilter = this.$options.statusTabs[tabIndex].filters;
this.resetPagination();
const { filters, status } = this.$options.statusTabs[tabIndex];
this.statusFilter = filters;
this.filteredByStatus = status;
},
fetchSortedData({ sortBy, sortDesc }) {
const sortDirection = sortDesc ? 'DESC' : 'ASC';
......@@ -206,6 +244,7 @@ export default {
if (sortBy !== 'startedAt') {
findDefaultSortColumn().ariaSort = 'none';
}
this.resetPagination();
this.sort = `${sortColumn}_${sortDirection}`;
},
updateAlertStatus(status, iid) {
......@@ -222,6 +261,7 @@ export default {
this.trackStatusUpdate(status);
this.$apollo.queries.alerts.refetch();
this.$apollo.queries.alertsCount.refetch();
this.resetPagination();
})
.catch(() => {
createFlash(
......@@ -246,6 +286,28 @@ export default {
// TODO: Update to show list of assignee(s) after https://gitlab.com/gitlab-org/gitlab/-/issues/218405
return assignees?.length > 0 ? assignees[0]?.username : s__('AlertManagement|Unassigned');
},
handlePageChange(page) {
const { startCursor, endCursor } = this.alerts.pageInfo;
if (page > this.pagination.currentPage) {
this.pagination = {
...initialPaginationState,
nextPageCursor: endCursor,
currentPage: page,
};
} else {
this.pagination = {
lastPageSize: DEFAULT_PAGE_SIZE,
firstPageSize: null,
prevPageCursor: startCursor,
nextPageCursor: '',
currentPage: page,
};
}
},
resetPagination() {
this.pagination = initialPaginationState;
},
},
};
</script>
......@@ -275,7 +337,7 @@ export default {
</h4>
<gl-table
class="alert-management-table mt-3"
:items="alerts"
:items="alerts ? alerts.list : []"
:fields="$options.fields"
:show-empty="true"
:busy="loading"
......@@ -283,6 +345,7 @@ export default {
:tbody-tr-class="tbodyTrClass"
:no-local-sorting="true"
sort-icon-left
sort-by="startedAt"
@row-clicked="navigateToAlertDetails"
@sort-changed="fetchSortedData"
>
......@@ -350,6 +413,16 @@ export default {
<gl-loading-icon size="lg" color="dark" class="mt-3" />
</template>
</gl-table>
<gl-pagination
v-if="showPaginationControls"
:value="pagination.currentPage"
:prev-page="prevPage"
:next-page="nextPage"
align="center"
class="gl-pagination prepend-top-default"
@input="handlePageChange"
/>
</div>
<gl-empty-state
v-else
......
......@@ -63,3 +63,5 @@ export const trackAlertStatusUpdateOptions = {
action: 'update_alert_status',
label: 'Status',
};
export const DEFAULT_PAGE_SIZE = 10;
#import "../fragments/list_item.fragment.graphql"
query getAlerts($projectPath: ID!, $statuses: [AlertManagementStatus!], $sort: AlertManagementAlertSort ) {
project(fullPath: $projectPath) {
alertManagementAlerts(statuses: $statuses, sort: $sort) {
nodes {
...AlertListItem
}
query getAlerts(
$projectPath: ID!,
$statuses: [AlertManagementStatus!],
$sort: AlertManagementAlertSort,
$firstPageSize: Int,
$lastPageSize: Int,
$prevPageCursor: String = ""
$nextPageCursor: String = ""
) {
project(fullPath: $projectPath, ) {
alertManagementAlerts(
statuses: $statuses,
sort: $sort,
first: $firstPageSize
last: $lastPageSize,
after: $nextPageCursor,
before: $prevPageCursor
) {
nodes {
...AlertListItem
},
pageInfo {
hasNextPage
endCursor
hasPreviousPage
startCursor
}
}
}
}
}
---
title: Alerts list pagination
merge_request: 33073
author:
type: added
......@@ -7,8 +7,10 @@ import {
GlDropdown,
GlDropdownItem,
GlIcon,
GlTabs,
GlTab,
GlBadge,
GlPagination,
} from '@gitlab/ui';
import { visitUrl } from '~/lib/utils/url_utility';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
......@@ -39,19 +41,21 @@ describe('AlertManagementList', () => {
const findLoader = () => wrapper.find(GlLoadingIcon);
const findStatusDropdown = () => wrapper.find(GlDropdown);
const findStatusFilterTabs = () => wrapper.findAll(GlTab);
const findStatusTabs = () => wrapper.find(GlTabs);
const findStatusFilterBadge = () => wrapper.findAll(GlBadge);
const findDateFields = () => wrapper.findAll(TimeAgo);
const findFirstStatusOption = () => findStatusDropdown().find(GlDropdownItem);
const findAssignees = () => wrapper.findAll('[data-testid="assigneesField"]');
const findSeverityFields = () => wrapper.findAll('[data-testid="severityField"]');
const findSeverityColumnHeader = () => wrapper.findAll('th').at(0);
const findStartTimeColumnHeader = () => wrapper.findAll('th').at(1);
const findPagination = () => wrapper.find(GlPagination);
const alertsCount = {
acknowledged: 6,
all: 16,
open: 14,
resolved: 2,
triggered: 10,
acknowledged: 6,
resolved: 1,
all: 16,
};
function mountComponent({
......@@ -76,6 +80,7 @@ describe('AlertManagementList', () => {
mocks: {
$apollo: {
mutate: jest.fn(),
query: jest.fn(),
queries: {
alerts: {
loading,
......@@ -134,7 +139,7 @@ describe('AlertManagementList', () => {
it('loading state', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: null, alertsCount: null },
data: { alerts: {}, alertsCount: null },
loading: true,
});
expect(findAlertsTable().exists()).toBe(true);
......@@ -149,7 +154,7 @@ describe('AlertManagementList', () => {
it('error state', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: null, alertsCount: null, errored: true },
data: { alerts: { errors: ['error'] }, alertsCount: null, errored: true },
loading: false,
});
expect(findAlertsTable().exists()).toBe(true);
......@@ -166,7 +171,7 @@ describe('AlertManagementList', () => {
it('empty state', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: [], alertsCount: { all: 0 }, errored: false },
data: { alerts: { list: [], pageInfo: {} }, alertsCount: { all: 0 }, errored: false },
loading: false,
});
expect(findAlertsTable().exists()).toBe(true);
......@@ -183,7 +188,7 @@ describe('AlertManagementList', () => {
it('has data state', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: mockAlerts, alertsCount, errored: false },
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
loading: false,
});
expect(findLoader().exists()).toBe(false);
......@@ -199,7 +204,7 @@ describe('AlertManagementList', () => {
it('displays status dropdown', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: mockAlerts, alertsCount, errored: false },
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
loading: false,
});
expect(findStatusDropdown().exists()).toBe(true);
......@@ -208,7 +213,7 @@ describe('AlertManagementList', () => {
it('shows correct severity icons', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: mockAlerts, alertsCount, errored: false },
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
loading: false,
});
......@@ -225,7 +230,7 @@ describe('AlertManagementList', () => {
it('renders severity text', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: mockAlerts, alertsCount, errored: false },
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
loading: false,
});
......@@ -239,7 +244,7 @@ describe('AlertManagementList', () => {
it('renders Unassigned when no assignee(s) present', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: mockAlerts, alertsCount, errored: false },
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
loading: false,
});
......@@ -253,7 +258,7 @@ describe('AlertManagementList', () => {
it('renders username(s) when assignee(s) present', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: mockAlerts, alertsCount, errored: false },
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
loading: false,
});
......@@ -267,7 +272,7 @@ describe('AlertManagementList', () => {
it('navigates to the detail page when alert row is clicked', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: mockAlerts, alertsCount, errored: false },
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
loading: false,
});
......@@ -282,15 +287,17 @@ describe('AlertManagementList', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: {
alerts: [
{
iid: 1,
status: 'acknowledged',
startedAt: '2020-03-17T23:18:14.996Z',
endedAt: '2020-04-17T23:18:14.996Z',
severity: 'high',
},
],
alerts: {
list: [
{
iid: 1,
status: 'acknowledged',
startedAt: '2020-03-17T23:18:14.996Z',
endedAt: '2020-04-17T23:18:14.996Z',
severity: 'high',
},
],
},
alertsCount,
errored: false,
},
......@@ -326,27 +333,31 @@ describe('AlertManagementList', () => {
beforeEach(() => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: mockAlerts, errored: false, sort: 'STARTED_AT_ASC', alertsCount },
data: { alerts: { list: mockAlerts }, errored: false, sort: 'STARTED_AT_ASC', alertsCount },
loading: false,
stubs: { GlTable },
});
});
it('updates sort with new direction and column key', () => {
findSeverityColumnHeader().trigger('click');
expect(wrapper.vm.$data.sort).toEqual('SEVERITY_ASC');
expect(wrapper.vm.$data.sort).toBe('SEVERITY_ASC');
findSeverityColumnHeader().trigger('click');
expect(wrapper.vm.$data.sort).toEqual('SEVERITY_DESC');
expect(wrapper.vm.$data.sort).toBe('SEVERITY_DESC');
});
it('updates the `ariaSort` attribute so the sort icon appears in the proper column', () => {
expect(mockStartedAtCol.ariaSort).toEqual('ascending');
expect(findStartTimeColumnHeader().attributes('aria-sort')).toBe('ascending');
findSeverityColumnHeader().trigger('click');
expect(mockStartedAtCol.ariaSort).toEqual('none');
wrapper.vm.$nextTick(() => {
expect(findStartTimeColumnHeader().attributes('aria-sort')).toBe('none');
expect(findSeverityColumnHeader().attributes('aria-sort')).toBe('ascending');
});
});
});
......@@ -367,7 +378,7 @@ describe('AlertManagementList', () => {
beforeEach(() => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: mockAlerts, alertsCount, errored: false },
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
loading: false,
});
});
......@@ -403,7 +414,7 @@ describe('AlertManagementList', () => {
jest.spyOn(Tracking, 'event');
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: mockAlerts, alertsCount },
data: { alerts: { list: mockAlerts }, alertsCount },
loading: false,
});
});
......@@ -424,4 +435,64 @@ describe('AlertManagementList', () => {
});
});
});
describe('Pagination', () => {
beforeEach(() => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: { list: mockAlerts, pageInfo: {} }, alertsCount, errored: false },
loading: false,
});
});
it('does NOT show pagination control when list is smaller than default page size', () => {
findStatusTabs().vm.$emit('input', 3);
wrapper.vm.$nextTick(() => {
expect(findPagination().exists()).toBe(false);
});
});
it('shows pagination control when list is larger than default page size', () => {
findStatusTabs().vm.$emit('input', 0);
wrapper.vm.$nextTick(() => {
expect(findPagination().exists()).toBe(true);
});
});
describe('prevPage', () => {
it('returns prevPage number', () => {
findPagination().vm.$emit('input', 3);
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.prevPage).toBe(2);
});
});
it('returns 0 when it is the first page', () => {
findPagination().vm.$emit('input', 1);
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.prevPage).toBe(0);
});
});
});
describe('nextPage', () => {
it('returns nextPage number', () => {
findPagination().vm.$emit('input', 1);
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.nextPage).toBe(2);
});
});
it('returns `null` when currentPage is already last page', () => {
findStatusTabs().vm.$emit('input', 3);
findPagination().vm.$emit('input', 1);
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.nextPage).toBeNull();
});
});
});
});
});
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