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 { ...@@ -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