Commit f7272b77 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '229406-incident-pagination' into 'master'

Add incident pagination

See merge request gitlab-org/gitlab!37993
parents 9a5e78c4 23e6e69a
...@@ -10,13 +10,14 @@ import { ...@@ -10,13 +10,14 @@ import {
GlButton, GlButton,
GlSearchBoxByType, GlSearchBoxByType,
GlIcon, GlIcon,
GlPagination,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { mergeUrlParams, joinPaths, visitUrl } from '~/lib/utils/url_utility'; import { mergeUrlParams, joinPaths, visitUrl } from '~/lib/utils/url_utility';
import getIncidents from '../graphql/queries/get_incidents.query.graphql'; import getIncidents from '../graphql/queries/get_incidents.query.graphql';
import { I18N, INCIDENT_SEARCH_DELAY } from '../constants'; import { I18N, DEFAULT_PAGE_SIZE, INCIDENT_SEARCH_DELAY } from '../constants';
const tdClass = const tdClass =
'table-col gl-display-flex d-md-table-cell gl-align-items-center gl-white-space-nowrap'; 'table-col gl-display-flex d-md-table-cell gl-align-items-center gl-white-space-nowrap';
...@@ -24,6 +25,14 @@ const thClass = 'gl-hover-bg-blue-50'; ...@@ -24,6 +25,14 @@ const thClass = 'gl-hover-bg-blue-50';
const bodyTrClass = const bodyTrClass =
'gl-border-1 gl-border-t-solid gl-border-gray-100 gl-hover-cursor-pointer gl-hover-bg-blue-50 gl-hover-border-b-solid gl-hover-border-blue-200'; 'gl-border-1 gl-border-t-solid gl-border-gray-100 gl-hover-cursor-pointer gl-hover-bg-blue-50 gl-hover-border-b-solid gl-hover-border-blue-200';
const initialPaginationState = {
currentPage: 1,
prevPageCursor: '',
nextPageCursor: '',
firstPageSize: DEFAULT_PAGE_SIZE,
lastPageSize: null,
};
export default { export default {
i18n: I18N, i18n: I18N,
fields: [ fields: [
...@@ -57,6 +66,7 @@ export default { ...@@ -57,6 +66,7 @@ export default {
TimeAgoTooltip, TimeAgoTooltip,
GlSearchBoxByType, GlSearchBoxByType,
GlIcon, GlIcon,
GlPagination,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -70,9 +80,18 @@ export default { ...@@ -70,9 +80,18 @@ export default {
searchTerm: this.searchTerm, searchTerm: this.searchTerm,
projectPath: this.projectPath, projectPath: this.projectPath,
labelNames: ['incident'], labelNames: ['incident'],
firstPageSize: this.pagination.firstPageSize,
lastPageSize: this.pagination.lastPageSize,
prevPageCursor: this.pagination.prevPageCursor,
nextPageCursor: this.pagination.nextPageCursor,
};
},
update({ project: { issues: { nodes = [], pageInfo = {} } = {} } = {} }) {
return {
list: nodes,
pageInfo,
}; };
}, },
update: ({ project: { issues: { nodes = [] } = {} } = {} }) => nodes,
error() { error() {
this.errored = true; this.errored = true;
}, },
...@@ -84,6 +103,8 @@ export default { ...@@ -84,6 +103,8 @@ export default {
isErrorAlertDismissed: false, isErrorAlertDismissed: false,
redirecting: false, redirecting: false,
searchTerm: '', searchTerm: '',
pagination: initialPaginationState,
incidents: {},
}; };
}, },
computed: { computed: {
...@@ -94,7 +115,19 @@ export default { ...@@ -94,7 +115,19 @@ export default {
return this.$apollo.queries.incidents.loading; return this.$apollo.queries.incidents.loading;
}, },
hasIncidents() { hasIncidents() {
return this.incidents?.length; return this.incidents?.list?.length;
},
showPaginationControls() {
return Boolean(
this.incidents?.pageInfo?.hasNextPage || this.incidents?.pageInfo?.hasPreviousPage,
);
},
prevPage() {
return Math.max(this.pagination.currentPage - 1, 0);
},
nextPage() {
const nextPage = this.pagination.currentPage + 1;
return this.incidents?.list?.length < DEFAULT_PAGE_SIZE ? null : nextPage;
}, },
tbodyTrClass() { tbodyTrClass() {
return { return {
...@@ -119,6 +152,28 @@ export default { ...@@ -119,6 +152,28 @@ export default {
navigateToIncidentDetails({ iid }) { navigateToIncidentDetails({ iid }) {
return visitUrl(joinPaths(this.issuePath, iid)); return visitUrl(joinPaths(this.issuePath, iid));
}, },
handlePageChange(page) {
const { startCursor, endCursor } = this.incidents.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> </script>
...@@ -155,7 +210,7 @@ export default { ...@@ -155,7 +210,7 @@ export default {
{{ s__('IncidentManagement|Incidents') }} {{ s__('IncidentManagement|Incidents') }}
</h4> </h4>
<gl-table <gl-table
:items="incidents" :items="incidents.list || []"
:fields="$options.fields" :fields="$options.fields"
:show-empty="true" :show-empty="true"
:busy="loading" :busy="loading"
...@@ -219,5 +274,15 @@ export default { ...@@ -219,5 +274,15 @@ export default {
{{ $options.i18n.noIncidents }} {{ $options.i18n.noIncidents }}
</template> </template>
</gl-table> </gl-table>
<gl-pagination
v-if="showPaginationControls"
:value="pagination.currentPage"
:prev-page="prevPage"
:next-page="nextPage"
align="center"
class="gl-pagination gl-mt-3"
@input="handlePageChange"
/>
</div> </div>
</template> </template>
...@@ -9,3 +9,4 @@ export const I18N = { ...@@ -9,3 +9,4 @@ export const I18N = {
}; };
export const INCIDENT_SEARCH_DELAY = 300; export const INCIDENT_SEARCH_DELAY = 300;
export const DEFAULT_PAGE_SIZE = 10;
query getIncidents( query getIncidents(
$searchTerm: String
$projectPath: ID! $projectPath: ID!
$labelNames: [String] $labelNames: [String]
$state: IssuableState $state: IssuableState
$firstPageSize: Int
$lastPageSize: Int
$prevPageCursor: String = ""
$nextPageCursor: String = ""
$searchTerm: String
) { ) {
project(fullPath: $projectPath) { project(fullPath: $projectPath) {
issues(search: $searchTerm, state: $state, labelName: $labelNames) { issues(
search: $searchTerm
state: $state
labelName: $labelNames
first: $firstPageSize
last: $lastPageSize
after: $nextPageCursor
before: $prevPageCursor
) {
nodes { nodes {
iid iid
title title
...@@ -26,6 +38,12 @@ query getIncidents( ...@@ -26,6 +38,12 @@ query getIncidents(
} }
} }
} }
pageInfo {
hasNextPage
endCursor
hasPreviousPage
startCursor
}
} }
} }
} }
---
title: Add pagination to the incident list
merge_request: 37993
author:
type: changed
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { GlAlert, GlLoadingIcon, GlTable, GlAvatar, GlSearchBoxByType } from '@gitlab/ui'; import {
GlAlert,
GlLoadingIcon,
GlTable,
GlAvatar,
GlPagination,
GlSearchBoxByType,
} from '@gitlab/ui';
import { visitUrl, joinPaths } from '~/lib/utils/url_utility'; import { visitUrl, joinPaths } from '~/lib/utils/url_utility';
import IncidentsList from '~/incidents/components/incidents_list.vue'; import IncidentsList from '~/incidents/components/incidents_list.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
...@@ -26,6 +33,7 @@ describe('Incidents List', () => { ...@@ -26,6 +33,7 @@ describe('Incidents List', () => {
const findCreateIncidentBtn = () => wrapper.find('[data-testid="createIncidentBtn"]'); const findCreateIncidentBtn = () => wrapper.find('[data-testid="createIncidentBtn"]');
const findSearch = () => wrapper.find(GlSearchBoxByType); const findSearch = () => wrapper.find(GlSearchBoxByType);
const findClosedIcon = () => wrapper.findAll("[data-testid='incident-closed']"); const findClosedIcon = () => wrapper.findAll("[data-testid='incident-closed']");
const findPagination = () => wrapper.find(GlPagination);
function mountComponent({ data = { incidents: [] }, loading = false }) { function mountComponent({ data = { incidents: [] }, loading = false }) {
wrapper = mount(IncidentsList, { wrapper = mount(IncidentsList, {
...@@ -70,7 +78,7 @@ describe('Incidents List', () => { ...@@ -70,7 +78,7 @@ describe('Incidents List', () => {
it('shows empty state', () => { it('shows empty state', () => {
mountComponent({ mountComponent({
data: { incidents: [] }, data: { incidents: { list: [] } },
loading: false, loading: false,
}); });
expect(findTable().text()).toContain(I18N.noIncidents); expect(findTable().text()).toContain(I18N.noIncidents);
...@@ -78,7 +86,7 @@ describe('Incidents List', () => { ...@@ -78,7 +86,7 @@ describe('Incidents List', () => {
it('shows error state', () => { it('shows error state', () => {
mountComponent({ mountComponent({
data: { incidents: [], errored: true }, data: { incidents: { list: [] }, errored: true },
loading: false, loading: false,
}); });
expect(findTable().text()).toContain(I18N.noIncidents); expect(findTable().text()).toContain(I18N.noIncidents);
...@@ -88,7 +96,7 @@ describe('Incidents List', () => { ...@@ -88,7 +96,7 @@ describe('Incidents List', () => {
describe('Incident Management list', () => { describe('Incident Management list', () => {
beforeEach(() => { beforeEach(() => {
mountComponent({ mountComponent({
data: { incidents: mockIncidents }, data: { incidents: { list: mockIncidents } },
loading: false, loading: false,
}); });
}); });
...@@ -140,7 +148,7 @@ describe('Incidents List', () => { ...@@ -140,7 +148,7 @@ describe('Incidents List', () => {
describe('Create Incident', () => { describe('Create Incident', () => {
beforeEach(() => { beforeEach(() => {
mountComponent({ mountComponent({
data: { incidents: [] }, data: { incidents: { list: [] } },
loading: false, loading: false,
}); });
}); });
...@@ -157,24 +165,120 @@ describe('Incidents List', () => { ...@@ -157,24 +165,120 @@ describe('Incidents List', () => {
}); });
}); });
describe('Search', () => { describe('Pagination', () => {
beforeEach(() => { beforeEach(() => {
mountComponent({ mountComponent({
data: { incidents: mockIncidents }, data: {
incidents: {
list: mockIncidents,
pageInfo: { hasNextPage: true, hasPreviousPage: true },
},
errored: false,
},
loading: false, loading: false,
}); });
}); });
it('renders the search component for incidents', () => { it('should render pagination', () => {
expect(findSearch().exists()).toBe(true); expect(wrapper.find(GlPagination).exists()).toBe(true);
});
describe('prevPage', () => {
it('returns prevPage button', () => {
findPagination().vm.$emit('input', 3);
return wrapper.vm.$nextTick(() => {
expect(
findPagination()
.findAll('.page-item')
.at(0)
.text(),
).toBe('Prev');
});
});
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 button', () => {
findPagination().vm.$emit('input', 3);
return wrapper.vm.$nextTick(() => {
expect(
findPagination()
.findAll('.page-item')
.at(1)
.text(),
).toBe('Next');
});
});
it('returns nextPage number', () => {
mountComponent({
data: {
incidents: {
list: [...mockIncidents, ...mockIncidents, ...mockIncidents],
pageInfo: { hasNextPage: true, hasPreviousPage: true },
},
errored: false,
},
loading: false,
});
findPagination().vm.$emit('input', 1);
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.nextPage).toBe(2);
});
});
it('returns `null` when currentPage is already last page', () => {
findPagination().vm.$emit('input', 1);
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.nextPage).toBeNull();
});
});
}); });
it('sets the `searchTerm` graphql variable', () => { describe('Search', () => {
const SEARCH_TERM = 'Simple Incident'; beforeEach(() => {
mountComponent({
data: {
incidents: {
list: mockIncidents,
pageInfo: { hasNextPage: true, hasPreviousPage: true },
},
errored: false,
},
loading: false,
});
});
findSearch().vm.$emit('input', SEARCH_TERM); it('renders the search component for incidents', () => {
expect(findSearch().exists()).toBe(true);
});
it('sets the `searchTerm` graphql variable', () => {
const SEARCH_TERM = 'Simple Incident';
expect(wrapper.vm.$data.searchTerm).toBe(SEARCH_TERM); findSearch().vm.$emit('input', SEARCH_TERM);
expect(wrapper.vm.$data.searchTerm).toBe(SEARCH_TERM);
});
}); });
}); });
}); });
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