Commit a2ae8863 authored by David O'Regan's avatar David O'Regan Committed by Nicolò Maria Mezzopera

Add status count badge

We now add the status count
badge for incident types
using the status count graphql query
parent e4695080
......@@ -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