Commit 74c0580b authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera

Merge branch '232580-state-count' into 'master'

Resolve "Add issue state counts to GraphQL"

Closes #235988

See merge request gitlab-org/gitlab!38278
parents 5dc74668 a2ae8863
...@@ -13,6 +13,7 @@ import { ...@@ -13,6 +13,7 @@ import {
GlPagination, GlPagination,
GlTabs, GlTabs,
GlTab, GlTab,
GlBadge,
} 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';
...@@ -20,7 +21,8 @@ import { convertToSnakeCase } from '~/lib/utils/text_utility'; ...@@ -20,7 +21,8 @@ import { convertToSnakeCase } from '~/lib/utils/text_utility';
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, DEFAULT_PAGE_SIZE, INCIDENT_SEARCH_DELAY, INCIDENT_STATE_TABS } from '../constants'; import getIncidentsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql';
import { I18N, DEFAULT_PAGE_SIZE, INCIDENT_SEARCH_DELAY, INCIDENT_STATUS_TABS } from '../constants';
const TH_TEST_ID = { 'data-testid': 'incident-management-created-at-sort' }; const TH_TEST_ID = { 'data-testid': 'incident-management-created-at-sort' };
const tdClass = const tdClass =
...@@ -39,7 +41,7 @@ const initialPaginationState = { ...@@ -39,7 +41,7 @@ const initialPaginationState = {
export default { export default {
i18n: I18N, i18n: I18N,
stateTabs: INCIDENT_STATE_TABS, statusTabs: INCIDENT_STATUS_TABS,
fields: [ fields: [
{ {
key: 'title', key: 'title',
...@@ -77,6 +79,7 @@ export default { ...@@ -77,6 +79,7 @@ export default {
GlTabs, GlTabs,
GlTab, GlTab,
PublishedCell: () => import('ee_component/incidents/components/published_cell.vue'), PublishedCell: () => import('ee_component/incidents/components/published_cell.vue'),
GlBadge,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -94,7 +97,7 @@ export default { ...@@ -94,7 +97,7 @@ export default {
variables() { variables() {
return { return {
searchTerm: this.searchTerm, searchTerm: this.searchTerm,
state: this.stateFilter, status: this.statusFilter,
projectPath: this.projectPath, projectPath: this.projectPath,
issueTypes: ['INCIDENT'], issueTypes: ['INCIDENT'],
sort: this.sort, sort: this.sort,
...@@ -114,6 +117,19 @@ export default { ...@@ -114,6 +117,19 @@ export default {
this.errored = true; this.errored = true;
}, },
}, },
incidentsCount: {
query: getIncidentsCountByStatus,
variables() {
return {
searchTerm: this.searchTerm,
projectPath: this.projectPath,
issueTypes: ['INCIDENT'],
};
},
update(data) {
return data.project?.issueStatusCounts;
},
},
}, },
data() { data() {
return { return {
...@@ -123,15 +139,16 @@ export default { ...@@ -123,15 +139,16 @@ export default {
searchTerm: '', searchTerm: '',
pagination: initialPaginationState, pagination: initialPaginationState,
incidents: {}, incidents: {},
stateFilter: '',
sort: 'created_desc', sort: 'created_desc',
sortBy: 'createdAt', sortBy: 'createdAt',
sortDesc: true, sortDesc: true,
statusFilter: '',
filteredByStatus: '',
}; };
}, },
computed: { computed: {
showErrorMsg() { showErrorMsg() {
return this.errored && !this.isErrorAlertDismissed && !this.searchTerm; return this.errored && !this.isErrorAlertDismissed && this.incidentsCount?.all === 0;
}, },
loading() { loading() {
return this.$apollo.queries.incidents.loading; return this.$apollo.queries.incidents.loading;
...@@ -139,6 +156,9 @@ export default { ...@@ -139,6 +156,9 @@ export default {
hasIncidents() { hasIncidents() {
return this.incidents?.list?.length; return this.incidents?.list?.length;
}, },
incidentsForCurrentTab() {
return this.incidentsCount?.[this.filteredByStatus.toLowerCase()] ?? 0;
},
showPaginationControls() { showPaginationControls() {
return Boolean( return Boolean(
this.incidents?.pageInfo?.hasNextPage || this.incidents?.pageInfo?.hasPreviousPage, this.incidents?.pageInfo?.hasNextPage || this.incidents?.pageInfo?.hasPreviousPage,
...@@ -149,7 +169,9 @@ export default { ...@@ -149,7 +169,9 @@ export default {
}, },
nextPage() { nextPage() {
const nextPage = this.pagination.currentPage + 1; const nextPage = this.pagination.currentPage + 1;
return this.incidents?.list?.length < DEFAULT_PAGE_SIZE ? null : nextPage; return nextPage > Math.ceil(this.incidentsForCurrentTab / DEFAULT_PAGE_SIZE)
? null
: nextPage;
}, },
tbodyTrClass() { tbodyTrClass() {
return { return {
...@@ -181,9 +203,10 @@ export default { ...@@ -181,9 +203,10 @@ export default {
this.searchTerm = trimmedInput; this.searchTerm = trimmedInput;
} }
}, INCIDENT_SEARCH_DELAY), }, INCIDENT_SEARCH_DELAY),
filterIncidentsByState(tabIndex) { filterIncidentsByStatus(tabIndex) {
const { filters } = this.$options.stateTabs[tabIndex]; const { filters, status } = this.$options.statusTabs[tabIndex];
this.stateFilter = filters; this.statusFilter = filters;
this.filteredByStatus = status;
}, },
hasAssignees(assignees) { hasAssignees(assignees) {
return Boolean(assignees.nodes?.length); return Boolean(assignees.nodes?.length);
...@@ -231,10 +254,13 @@ export default { ...@@ -231,10 +254,13 @@ export default {
<div <div
class="incident-management-list-header gl-display-flex gl-justify-content-space-between gl-border-b-solid gl-border-b-1 gl-border-gray-100" class="incident-management-list-header gl-display-flex gl-justify-content-space-between gl-border-b-solid gl-border-b-1 gl-border-gray-100"
> >
<gl-tabs content-class="gl-p-0" @input="filterIncidentsByState"> <gl-tabs content-class="gl-p-0" @input="filterIncidentsByStatus">
<gl-tab v-for="tab in $options.stateTabs" :key="tab.state" :data-testid="tab.state"> <gl-tab v-for="tab in $options.statusTabs" :key="tab.status" :data-testid="tab.status">
<template #title> <template #title>
<span>{{ tab.title }}</span> <span>{{ tab.title }}</span>
<gl-badge v-if="incidentsCount" pill size="sm" class="gl-tab-counter-badge">
{{ incidentsCount[tab.status.toLowerCase()] }}
</gl-badge>
</template> </template>
</gl-tab> </gl-tab>
</gl-tabs> </gl-tabs>
......
...@@ -9,20 +9,20 @@ export const I18N = { ...@@ -9,20 +9,20 @@ export const I18N = {
searchPlaceholder: __('Search results…'), searchPlaceholder: __('Search results…'),
}; };
export const INCIDENT_STATE_TABS = [ export const INCIDENT_STATUS_TABS = [
{ {
title: s__('IncidentManagement|Open'), title: s__('IncidentManagement|Open'),
state: 'OPENED', status: 'OPENED',
filters: 'opened', filters: 'opened',
}, },
{ {
title: s__('IncidentManagement|Closed'), title: s__('IncidentManagement|Closed'),
state: 'CLOSED', status: 'CLOSED',
filters: 'closed', filters: 'closed',
}, },
{ {
title: s__('IncidentManagement|All'), title: s__('IncidentManagement|All'),
state: 'ALL', status: 'ALL',
filters: 'all', filters: 'all',
}, },
]; ];
......
query getIncidentsCountByStatus($searchTerm: String, $projectPath: ID!, $issueTypes: [IssueType!]) {
project(fullPath: $projectPath) {
issueStatusCounts(search: $searchTerm, types: $issueTypes) {
all
opened
closed
}
}
}
...@@ -2,7 +2,7 @@ query getIncidents( ...@@ -2,7 +2,7 @@ query getIncidents(
$projectPath: ID! $projectPath: ID!
$issueTypes: [IssueType!] $issueTypes: [IssueType!]
$sort: IssueSort $sort: IssueSort
$state: IssuableState $status: IssuableState
$firstPageSize: Int $firstPageSize: Int
$lastPageSize: Int $lastPageSize: Int
$prevPageCursor: String = "" $prevPageCursor: String = ""
...@@ -12,9 +12,9 @@ query getIncidents( ...@@ -12,9 +12,9 @@ query getIncidents(
project(fullPath: $projectPath) { project(fullPath: $projectPath) {
issues( issues(
search: $searchTerm search: $searchTerm
state: $state
types: $issueTypes types: $issueTypes
sort: $sort sort: $sort
state: $status
first: $firstPageSize first: $firstPageSize
last: $lastPageSize last: $lastPageSize
after: $nextPageCursor after: $nextPageCursor
......
---
title: Add incident count badge to the incident list
merge_request: 38278
author:
type: changed
...@@ -7,11 +7,13 @@ import { ...@@ -7,11 +7,13 @@ import {
GlPagination, GlPagination,
GlSearchBoxByType, GlSearchBoxByType,
GlTab, GlTab,
GlTabs,
GlBadge,
} from '@gitlab/ui'; } 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';
import { I18N, INCIDENT_STATE_TABS } from '~/incidents/constants'; import { I18N, INCIDENT_STATUS_TABS } from '~/incidents/constants';
import mockIncidents from '../mocks/incidents.json'; import mockIncidents from '../mocks/incidents.json';
jest.mock('~/lib/utils/url_utility', () => ({ jest.mock('~/lib/utils/url_utility', () => ({
...@@ -24,6 +26,11 @@ describe('Incidents List', () => { ...@@ -24,6 +26,11 @@ describe('Incidents List', () => {
let wrapper; let wrapper;
const newIssuePath = 'namespace/project/-/issues/new'; const newIssuePath = 'namespace/project/-/issues/new';
const incidentTemplateName = 'incident'; const incidentTemplateName = 'incident';
const incidentsCount = {
opened: 14,
closed: 1,
all: 16,
};
const findTable = () => wrapper.find(GlTable); const findTable = () => wrapper.find(GlTable);
const findTableRows = () => wrapper.findAll('table tbody tr'); const findTableRows = () => wrapper.findAll('table tbody tr');
...@@ -38,8 +45,10 @@ describe('Incidents List', () => { ...@@ -38,8 +45,10 @@ describe('Incidents List', () => {
const findClosedIcon = () => wrapper.findAll("[data-testid='incident-closed']"); const findClosedIcon = () => wrapper.findAll("[data-testid='incident-closed']");
const findPagination = () => wrapper.find(GlPagination); const findPagination = () => wrapper.find(GlPagination);
const findStatusFilterTabs = () => wrapper.findAll(GlTab); const findStatusFilterTabs = () => wrapper.findAll(GlTab);
const findStatusFilterBadge = () => wrapper.findAll(GlBadge);
const findStatusTabs = () => wrapper.find(GlTabs);
function mountComponent({ data = { incidents: [] }, loading = false }) { function mountComponent({ data = { incidents: [], incidentsCount: {} }, loading = false }) {
wrapper = mount(IncidentsList, { wrapper = mount(IncidentsList, {
data() { data() {
return data; return data;
...@@ -83,7 +92,7 @@ describe('Incidents List', () => { ...@@ -83,7 +92,7 @@ describe('Incidents List', () => {
it('shows empty state', () => { it('shows empty state', () => {
mountComponent({ mountComponent({
data: { incidents: { list: [] } }, data: { incidents: { list: [] }, incidentsCount: {} },
loading: false, loading: false,
}); });
expect(findTable().text()).toContain(I18N.noIncidents); expect(findTable().text()).toContain(I18N.noIncidents);
...@@ -91,7 +100,7 @@ describe('Incidents List', () => { ...@@ -91,7 +100,7 @@ describe('Incidents List', () => {
it('shows error state', () => { it('shows error state', () => {
mountComponent({ mountComponent({
data: { incidents: { list: [] }, errored: true }, data: { incidents: { list: [] }, incidentsCount: { all: 0 }, errored: true },
loading: false, loading: false,
}); });
expect(findTable().text()).toContain(I18N.noIncidents); expect(findTable().text()).toContain(I18N.noIncidents);
...@@ -101,7 +110,7 @@ describe('Incidents List', () => { ...@@ -101,7 +110,7 @@ describe('Incidents List', () => {
describe('Incident Management list', () => { describe('Incident Management list', () => {
beforeEach(() => { beforeEach(() => {
mountComponent({ mountComponent({
data: { incidents: { list: mockIncidents } }, data: { incidents: { list: mockIncidents }, incidentsCount },
loading: false, loading: false,
}); });
}); });
...@@ -153,7 +162,7 @@ describe('Incidents List', () => { ...@@ -153,7 +162,7 @@ describe('Incidents List', () => {
describe('Create Incident', () => { describe('Create Incident', () => {
beforeEach(() => { beforeEach(() => {
mountComponent({ mountComponent({
data: { incidents: { list: [] } }, data: { incidents: { list: [] }, incidentsCount: {} },
loading: false, loading: false,
}); });
}); });
...@@ -178,6 +187,7 @@ describe('Incidents List', () => { ...@@ -178,6 +187,7 @@ describe('Incidents List', () => {
list: mockIncidents, list: mockIncidents,
pageInfo: { hasNextPage: true, hasPreviousPage: true }, pageInfo: { hasNextPage: true, hasPreviousPage: true },
}, },
incidentsCount,
errored: false, errored: false,
}, },
loading: false, loading: false,
...@@ -240,6 +250,7 @@ describe('Incidents List', () => { ...@@ -240,6 +250,7 @@ describe('Incidents List', () => {
list: [...mockIncidents, ...mockIncidents, ...mockIncidents], list: [...mockIncidents, ...mockIncidents, ...mockIncidents],
pageInfo: { hasNextPage: true, hasPreviousPage: true }, pageInfo: { hasNextPage: true, hasPreviousPage: true },
}, },
incidentsCount,
errored: false, errored: false,
}, },
loading: false, loading: false,
...@@ -252,6 +263,7 @@ describe('Incidents List', () => { ...@@ -252,6 +263,7 @@ describe('Incidents List', () => {
}); });
it('returns `null` when currentPage is already last page', () => { it('returns `null` when currentPage is already last page', () => {
findStatusTabs().vm.$emit('input', 1);
findPagination().vm.$emit('input', 1); findPagination().vm.$emit('input', 1);
return wrapper.vm.$nextTick(() => { return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.nextPage).toBeNull(); expect(wrapper.vm.nextPage).toBeNull();
...@@ -267,6 +279,7 @@ describe('Incidents List', () => { ...@@ -267,6 +279,7 @@ describe('Incidents List', () => {
list: mockIncidents, list: mockIncidents,
pageInfo: { hasNextPage: true, hasPreviousPage: true }, pageInfo: { hasNextPage: true, hasPreviousPage: true },
}, },
incidentsCount,
errored: false, errored: false,
}, },
loading: false, loading: false,
...@@ -286,10 +299,10 @@ describe('Incidents List', () => { ...@@ -286,10 +299,10 @@ describe('Incidents List', () => {
}); });
}); });
describe('State Filter Tabs', () => { describe('Status Filter Tabs', () => {
beforeEach(() => { beforeEach(() => {
mountComponent({ mountComponent({
data: { incidents: mockIncidents }, data: { incidents: mockIncidents, incidentsCount },
loading: false, loading: false,
stubs: { stubs: {
GlTab: true, GlTab: true,
...@@ -301,7 +314,18 @@ describe('Incidents List', () => { ...@@ -301,7 +314,18 @@ describe('Incidents List', () => {
const tabs = findStatusFilterTabs().wrappers; const tabs = findStatusFilterTabs().wrappers;
tabs.forEach((tab, i) => { tabs.forEach((tab, i) => {
expect(tab.attributes('data-testid')).toContain(INCIDENT_STATE_TABS[i].state); expect(tab.attributes('data-testid')).toContain(INCIDENT_STATUS_TABS[i].status);
});
});
it('should display filter tabs with alerts count badge for each status', () => {
const tabs = findStatusFilterTabs().wrappers;
const badges = findStatusFilterBadge();
tabs.forEach((tab, i) => {
const status = INCIDENT_STATUS_TABS[i].status.toLowerCase();
expect(tab.attributes('data-testid')).toContain(INCIDENT_STATUS_TABS[i].status);
expect(badges.at(i).text()).toContain(incidentsCount[status]);
}); });
}); });
}); });
...@@ -310,7 +334,7 @@ describe('Incidents List', () => { ...@@ -310,7 +334,7 @@ describe('Incidents List', () => {
describe('sorting the incident list by column', () => { describe('sorting the incident list by column', () => {
beforeEach(() => { beforeEach(() => {
mountComponent({ mountComponent({
data: { incidents: mockIncidents }, data: { incidents: mockIncidents, incidentsCount },
loading: false, loading: false,
}); });
}); });
......
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