Commit 72d1779d authored by David O'Regan's avatar David O'Regan Committed by Natalia Tepluhina

Add incident state columns

We add incident state
columns to allow users
to filter via state
parent cb4db18b
...@@ -11,13 +11,15 @@ import { ...@@ -11,13 +11,15 @@ import {
GlSearchBoxByType, GlSearchBoxByType,
GlIcon, GlIcon,
GlPagination, GlPagination,
GlTabs,
GlTab,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { debounce } from 'lodash'; import { debounce, trim } 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, DEFAULT_PAGE_SIZE, INCIDENT_SEARCH_DELAY } from '../constants'; import { I18N, DEFAULT_PAGE_SIZE, INCIDENT_SEARCH_DELAY, INCIDENT_STATE_TABS } 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';
...@@ -35,6 +37,7 @@ const initialPaginationState = { ...@@ -35,6 +37,7 @@ const initialPaginationState = {
export default { export default {
i18n: I18N, i18n: I18N,
stateTabs: INCIDENT_STATE_TABS,
fields: [ fields: [
{ {
key: 'title', key: 'title',
...@@ -67,6 +70,8 @@ export default { ...@@ -67,6 +70,8 @@ export default {
GlSearchBoxByType, GlSearchBoxByType,
GlIcon, GlIcon,
GlPagination, GlPagination,
GlTabs,
GlTab,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -78,6 +83,7 @@ export default { ...@@ -78,6 +83,7 @@ export default {
variables() { variables() {
return { return {
searchTerm: this.searchTerm, searchTerm: this.searchTerm,
state: this.stateFilter,
projectPath: this.projectPath, projectPath: this.projectPath,
labelNames: ['incident'], labelNames: ['incident'],
firstPageSize: this.pagination.firstPageSize, firstPageSize: this.pagination.firstPageSize,
...@@ -105,6 +111,7 @@ export default { ...@@ -105,6 +111,7 @@ export default {
searchTerm: '', searchTerm: '',
pagination: initialPaginationState, pagination: initialPaginationState,
incidents: {}, incidents: {},
stateFilter: '',
}; };
}, },
computed: { computed: {
...@@ -138,14 +145,17 @@ export default { ...@@ -138,14 +145,17 @@ export default {
return mergeUrlParams({ issuable_template: this.incidentTemplateName }, this.newIssuePath); return mergeUrlParams({ issuable_template: this.incidentTemplateName }, this.newIssuePath);
}, },
}, },
watch: { methods: {
searchTerm: debounce(function debounceSearch(input) { onInputChange: debounce(function debounceSearch(input) {
if (input !== this.searchTerm) { const trimmedInput = trim(input);
this.searchTerm = input; if (trimmedInput !== this.searchTerm) {
this.searchTerm = trimmedInput;
} }
}, INCIDENT_SEARCH_DELAY), }, INCIDENT_SEARCH_DELAY),
}, filterIncidentsByState(tabIndex) {
methods: { const { filters } = this.$options.stateTabs[tabIndex];
this.stateFilter = filters;
},
hasAssignees(assignees) { hasAssignees(assignees) {
return Boolean(assignees.nodes?.length); return Boolean(assignees.nodes?.length);
}, },
...@@ -183,9 +193,17 @@ export default { ...@@ -183,9 +193,17 @@ export default {
{{ $options.i18n.errorMsg }} {{ $options.i18n.errorMsg }}
</gl-alert> </gl-alert>
<div class="gl-display-flex gl-justify-content-end"> <div class="incident-management-list-header gl-display-flex gl-justify-content-space-between">
<gl-tabs content-class="gl-p-0" @input="filterIncidentsByState">
<gl-tab v-for="tab in $options.stateTabs" :key="tab.state" :data-testid="tab.state">
<template #title>
<span>{{ tab.title }}</span>
</template>
</gl-tab>
</gl-tabs>
<gl-button <gl-button
class="gl-mt-3 gl-mb-3 create-incident-button" class="gl-my-3 create-incident-button"
data-testid="createIncidentBtn" data-testid="createIncidentBtn"
:loading="redirecting" :loading="redirecting"
:disabled="redirecting" :disabled="redirecting"
...@@ -200,9 +218,10 @@ export default { ...@@ -200,9 +218,10 @@ export default {
<div class="gl-bg-gray-10 gl-p-5 gl-border-b-solid gl-border-b-1 gl-border-gray-100"> <div class="gl-bg-gray-10 gl-p-5 gl-border-b-solid gl-border-b-1 gl-border-gray-100">
<gl-search-box-by-type <gl-search-box-by-type
v-model.trim="searchTerm" :value="searchTerm"
class="gl-bg-white" class="gl-bg-white"
:placeholder="$options.i18n.searchPlaceholder" :placeholder="$options.i18n.searchPlaceholder"
@input="onInputChange"
/> />
</div> </div>
...@@ -221,7 +240,7 @@ export default { ...@@ -221,7 +240,7 @@ export default {
@row-clicked="navigateToIncidentDetails" @row-clicked="navigateToIncidentDetails"
> >
<template #cell(title)="{ item }"> <template #cell(title)="{ item }">
<div class="gl-display-flex gl-justify-content-center"> <div class="gl-display-sm-flex gl-align-items-center">
<div class="gl-max-w-full text-truncate" :title="item.title">{{ item.title }}</div> <div class="gl-max-w-full text-truncate" :title="item.title">{{ item.title }}</div>
<gl-icon <gl-icon
v-if="item.state === 'closed'" v-if="item.state === 'closed'"
......
...@@ -8,5 +8,23 @@ export const I18N = { ...@@ -8,5 +8,23 @@ export const I18N = {
searchPlaceholder: __('Search or filter results...'), searchPlaceholder: __('Search or filter results...'),
}; };
export const INCIDENT_STATE_TABS = [
{
title: s__('IncidentManagement|Open'),
state: 'OPENED',
filters: 'opened',
},
{
title: s__('IncidentManagement|Closed'),
state: 'CLOSED',
filters: 'closed',
},
{
title: s__('IncidentManagement|All incidents'),
state: 'ALL',
filters: 'all',
},
];
export const INCIDENT_SEARCH_DELAY = 300; export const INCIDENT_SEARCH_DELAY = 300;
export const DEFAULT_PAGE_SIZE = 10; export const DEFAULT_PAGE_SIZE = 10;
...@@ -90,6 +90,10 @@ ...@@ -90,6 +90,10 @@
} }
@include media-breakpoint-down(xs) { @include media-breakpoint-down(xs) {
.incident-management-list-header {
flex-direction: column-reverse;
};
.create-incident-button { .create-incident-button {
@include gl-w-full; @include gl-w-full;
} }
......
...@@ -8,5 +8,6 @@ module Types ...@@ -8,5 +8,6 @@ module Types
value 'opened' value 'opened'
value 'closed' value 'closed'
value 'locked' value 'locked'
value 'all'
end end
end end
---
title: Add incident state columns
merge_request: 37889
author:
type: other
...@@ -5856,6 +5856,7 @@ type InstanceSecurityDashboard { ...@@ -5856,6 +5856,7 @@ type InstanceSecurityDashboard {
State of a GitLab issue or merge request State of a GitLab issue or merge request
""" """
enum IssuableState { enum IssuableState {
all
closed closed
locked locked
opened opened
...@@ -6557,6 +6558,7 @@ enum IssueSort { ...@@ -6557,6 +6558,7 @@ enum IssueSort {
State of a GitLab issue State of a GitLab issue
""" """
enum IssueState { enum IssueState {
all
closed closed
locked locked
opened opened
...@@ -7982,6 +7984,7 @@ type MergeRequestSetWipPayload { ...@@ -7982,6 +7984,7 @@ type MergeRequestSetWipPayload {
State of a GitLab merge request State of a GitLab merge request
""" """
enum MergeRequestState { enum MergeRequestState {
all
closed closed
locked locked
merged merged
......
...@@ -16168,6 +16168,12 @@ ...@@ -16168,6 +16168,12 @@
"description": null, "description": null,
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
},
{
"name": "all",
"description": null,
"isDeprecated": false,
"deprecationReason": null
} }
], ],
"possibleTypes": null "possibleTypes": null
...@@ -18090,6 +18096,12 @@ ...@@ -18090,6 +18096,12 @@
"description": null, "description": null,
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
},
{
"name": "all",
"description": null,
"isDeprecated": false,
"deprecationReason": null
} }
], ],
"possibleTypes": null "possibleTypes": null
...@@ -22376,6 +22388,12 @@ ...@@ -22376,6 +22388,12 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "all",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "merged", "name": "merged",
"description": null, "description": null,
...@@ -12710,9 +12710,15 @@ msgstr "" ...@@ -12710,9 +12710,15 @@ msgstr ""
msgid "Incident Management Limits" msgid "Incident Management Limits"
msgstr "" msgstr ""
msgid "IncidentManagement|All incidents"
msgstr ""
msgid "IncidentManagement|Assignees" msgid "IncidentManagement|Assignees"
msgstr "" msgstr ""
msgid "IncidentManagement|Closed"
msgstr ""
msgid "IncidentManagement|Create incident" msgid "IncidentManagement|Create incident"
msgstr "" msgstr ""
...@@ -12728,6 +12734,9 @@ msgstr "" ...@@ -12728,6 +12734,9 @@ msgstr ""
msgid "IncidentManagement|No incidents to display." msgid "IncidentManagement|No incidents to display."
msgstr "" msgstr ""
msgid "IncidentManagement|Open"
msgstr ""
msgid "IncidentManagement|There was an error displaying the incidents." msgid "IncidentManagement|There was an error displaying the incidents."
msgstr "" msgstr ""
......
...@@ -6,11 +6,12 @@ import { ...@@ -6,11 +6,12 @@ import {
GlAvatar, GlAvatar,
GlPagination, GlPagination,
GlSearchBoxByType, GlSearchBoxByType,
GlTab,
} 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 } from '~/incidents/constants'; import { I18N, INCIDENT_STATE_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', () => ({
...@@ -34,6 +35,7 @@ describe('Incidents List', () => { ...@@ -34,6 +35,7 @@ describe('Incidents List', () => {
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); const findPagination = () => wrapper.find(GlPagination);
const findStatusFilterTabs = () => wrapper.findAll(GlTab);
function mountComponent({ data = { incidents: [] }, loading = false }) { function mountComponent({ data = { incidents: [] }, loading = false }) {
wrapper = mount(IncidentsList, { wrapper = mount(IncidentsList, {
...@@ -280,5 +282,25 @@ describe('Incidents List', () => { ...@@ -280,5 +282,25 @@ describe('Incidents List', () => {
expect(wrapper.vm.$data.searchTerm).toBe(SEARCH_TERM); expect(wrapper.vm.$data.searchTerm).toBe(SEARCH_TERM);
}); });
}); });
describe('State Filter Tabs', () => {
beforeEach(() => {
mountComponent({
data: { incidents: mockIncidents },
loading: false,
stubs: {
GlTab: true,
},
});
});
it('should display filter tabs', () => {
const tabs = findStatusFilterTabs().wrappers;
tabs.forEach((tab, i) => {
expect(tab.attributes('data-testid')).toContain(INCIDENT_STATE_TABS[i].state);
});
});
});
}); });
}); });
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