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