Commit b1b639c9 authored by Sarah Yasonik's avatar Sarah Yasonik Committed by Paul Slaughter

Adjust incident list column widths and truncation

- Also add escalation status to incident list
  Adds a column to the incident list at Monitor > 
  Incidents showing the escalation status.
- https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80645

Changelog: changed
parent 1a7d4e2c
...@@ -24,9 +24,11 @@ import { ...@@ -24,9 +24,11 @@ import {
} from '~/vue_shared/components/paginated_table_with_search_and_tabs/constants'; } from '~/vue_shared/components/paginated_table_with_search_and_tabs/constants';
import PaginatedTableWithSearchAndTabs from '~/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue'; import PaginatedTableWithSearchAndTabs from '~/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import { import {
I18N, I18N,
INCIDENT_STATUS_TABS, INCIDENT_STATUS_TABS,
ESCALATION_STATUSES,
TH_CREATED_AT_TEST_ID, TH_CREATED_AT_TEST_ID,
TH_INCIDENT_SLA_TEST_ID, TH_INCIDENT_SLA_TEST_ID,
TH_SEVERITY_TEST_ID, TH_SEVERITY_TEST_ID,
...@@ -38,7 +40,7 @@ import { ...@@ -38,7 +40,7 @@ import {
import getIncidentsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql'; import getIncidentsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql';
import getIncidents from '../graphql/queries/get_incidents.query.graphql'; import getIncidents from '../graphql/queries/get_incidents.query.graphql';
const MAX_VISIBLE_ASSIGNEES = 4; const MAX_VISIBLE_ASSIGNEES = 3;
export default { export default {
trackIncidentCreateNewOptions, trackIncidentCreateNewOptions,
...@@ -49,7 +51,7 @@ export default { ...@@ -49,7 +51,7 @@ export default {
{ {
key: 'severity', key: 'severity',
label: s__('IncidentManagement|Severity'), label: s__('IncidentManagement|Severity'),
thClass: `${thClass} w-15p`, thClass: `${thClass} gl-w-15p`,
tdClass: `${tdClass} sortable-cell`, tdClass: `${tdClass} sortable-cell`,
actualSortKey: 'SEVERITY', actualSortKey: 'SEVERITY',
sortable: true, sortable: true,
...@@ -61,6 +63,12 @@ export default { ...@@ -61,6 +63,12 @@ export default {
thClass: `gl-pointer-events-none`, thClass: `gl-pointer-events-none`,
tdClass, tdClass,
}, },
{
key: 'escalationStatus',
label: s__('IncidentManagement|Status'),
thClass: `${thClass} gl-w-eighth gl-pointer-events-none`,
tdClass,
},
{ {
key: 'createdAt', key: 'createdAt',
label: s__('IncidentManagement|Date created'), label: s__('IncidentManagement|Date created'),
...@@ -73,7 +81,7 @@ export default { ...@@ -73,7 +81,7 @@ export default {
{ {
key: 'incidentSla', key: 'incidentSla',
label: s__('IncidentManagement|Time to SLA'), label: s__('IncidentManagement|Time to SLA'),
thClass: `gl-text-right gl-w-eighth`, thClass: `gl-text-right gl-w-10p`,
tdClass: `${tdClass} gl-text-right`, tdClass: `${tdClass} gl-text-right`,
thAttr: TH_INCIDENT_SLA_TEST_ID, thAttr: TH_INCIDENT_SLA_TEST_ID,
actualSortKey: 'SLA_DUE_AT', actualSortKey: 'SLA_DUE_AT',
...@@ -83,13 +91,13 @@ export default { ...@@ -83,13 +91,13 @@ export default {
{ {
key: 'assignees', key: 'assignees',
label: s__('IncidentManagement|Assignees'), label: s__('IncidentManagement|Assignees'),
thClass: 'gl-pointer-events-none w-15p', thClass: 'gl-pointer-events-none gl-w-15',
tdClass, tdClass,
}, },
{ {
key: 'published', key: 'published',
label: s__('IncidentManagement|Published'), label: s__('IncidentManagement|Published'),
thClass: `${thClass} w-15p`, thClass: `${thClass} gl-w-15`,
tdClass: `${tdClass} sortable-cell`, tdClass: `${tdClass} sortable-cell`,
actualSortKey: 'PUBLISHED', actualSortKey: 'PUBLISHED',
sortable: true, sortable: true,
...@@ -112,6 +120,7 @@ export default { ...@@ -112,6 +120,7 @@ export default {
GlEmptyState, GlEmptyState,
SeverityToken, SeverityToken,
PaginatedTableWithSearchAndTabs, PaginatedTableWithSearchAndTabs,
TooltipOnTruncate,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -129,6 +138,7 @@ export default { ...@@ -129,6 +138,7 @@ export default {
'assigneeUsernameQuery', 'assigneeUsernameQuery',
'slaFeatureAvailable', 'slaFeatureAvailable',
'canCreateIncident', 'canCreateIncident',
'incidentEscalationsAvailable',
], ],
apollo: { apollo: {
incidents: { incidents: {
...@@ -222,6 +232,7 @@ export default { ...@@ -222,6 +232,7 @@ export default {
const isHidden = { const isHidden = {
published: !this.publishedAvailable, published: !this.publishedAvailable,
incidentSla: !this.slaFeatureAvailable, incidentSla: !this.slaFeatureAvailable,
escalationStatus: !this.incidentEscalationsAvailable,
}; };
return this.$options.fields.filter(({ key }) => !isHidden[key]); return this.$options.fields.filter(({ key }) => !isHidden[key]);
...@@ -283,6 +294,9 @@ export default { ...@@ -283,6 +294,9 @@ export default {
getSeverity(severity) { getSeverity(severity) {
return INCIDENT_SEVERITY[severity]; return INCIDENT_SEVERITY[severity];
}, },
getEscalationStatus(escalationStatus) {
return ESCALATION_STATUSES[escalationStatus] || this.$options.i18n.noEscalationStatus;
},
pageChanged(pagination) { pageChanged(pagination) {
this.pagination = pagination; this.pagination = pagination;
}, },
...@@ -370,7 +384,12 @@ export default { ...@@ -370,7 +384,12 @@ export default {
<template #cell(title)="{ item }"> <template #cell(title)="{ item }">
<div :class="{ 'gl-display-flex gl-align-items-center': item.state === 'closed' }"> <div :class="{ 'gl-display-flex gl-align-items-center': item.state === 'closed' }">
<div class="gl-max-w-full text-truncate" :title="item.title">{{ item.title }}</div> <tooltip-on-truncate
:title="item.title"
class="gl-max-w-full gl-text-truncate gl-display-block"
>
{{ item.title }}
</tooltip-on-truncate>
<gl-icon <gl-icon
v-if="item.state === 'closed'" v-if="item.state === 'closed'"
name="issue-close" name="issue-close"
...@@ -381,8 +400,21 @@ export default { ...@@ -381,8 +400,21 @@ export default {
</div> </div>
</template> </template>
<template v-if="incidentEscalationsAvailable" #cell(escalationStatus)="{ item }">
<tooltip-on-truncate
:title="getEscalationStatus(item.escalationStatus)"
data-testid="incident-escalation-status"
class="gl-display-block gl-text-truncate"
>
{{ getEscalationStatus(item.escalationStatus) }}
</tooltip-on-truncate>
</template>
<template #cell(createdAt)="{ item }"> <template #cell(createdAt)="{ item }">
<time-ago-tooltip :time="item.createdAt" /> <time-ago-tooltip
:time="item.createdAt"
class="gl-display-block gl-max-w-full gl-text-truncate"
/>
</template> </template>
<template v-if="slaFeatureAvailable" #cell(incidentSla)="{ item }"> <template v-if="slaFeatureAvailable" #cell(incidentSla)="{ item }">
...@@ -392,6 +424,7 @@ export default { ...@@ -392,6 +424,7 @@ export default {
:project-path="projectPath" :project-path="projectPath"
:sla-due-at="item.slaDueAt" :sla-due-at="item.slaDueAt"
data-testid="incident-sla" data-testid="incident-sla"
class="gl-display-block gl-max-w-full gl-text-truncate"
/> />
</template> </template>
...@@ -432,6 +465,7 @@ export default { ...@@ -432,6 +465,7 @@ export default {
:un-published="$options.i18n.unPublished" :un-published="$options.i18n.unPublished"
/> />
</template> </template>
<template #table-busy> <template #table-busy>
<gl-loading-icon size="lg" color="dark" class="mt-3" /> <gl-loading-icon size="lg" color="dark" class="mt-3" />
</template> </template>
......
...@@ -7,6 +7,7 @@ export const I18N = { ...@@ -7,6 +7,7 @@ export const I18N = {
unassigned: s__('IncidentManagement|Unassigned'), unassigned: s__('IncidentManagement|Unassigned'),
createIncidentBtnLabel: s__('IncidentManagement|Create incident'), createIncidentBtnLabel: s__('IncidentManagement|Create incident'),
unPublished: s__('IncidentManagement|Unpublished'), unPublished: s__('IncidentManagement|Unpublished'),
noEscalationStatus: s__('IncidentManagement|None'),
emptyState: { emptyState: {
title: s__('IncidentManagement|Display your incidents in a dedicated view'), title: s__('IncidentManagement|Display your incidents in a dedicated view'),
emptyClosedTabTitle: s__('IncidentManagement|There are no closed incidents'), emptyClosedTabTitle: s__('IncidentManagement|There are no closed incidents'),
...@@ -37,6 +38,12 @@ export const INCIDENT_STATUS_TABS = [ ...@@ -37,6 +38,12 @@ export const INCIDENT_STATUS_TABS = [
}, },
]; ];
export const ESCALATION_STATUSES = {
TRIGGERED: s__('AlertManagement|Triggered'),
ACKNOWLEDGED: s__('AlertManagement|Acknowledged'),
RESOLVED: s__('AlertManagement|Resolved'),
};
export const DEFAULT_PAGE_SIZE = 20; export const DEFAULT_PAGE_SIZE = 20;
export const TH_CREATED_AT_TEST_ID = { 'data-testid': 'incident-management-created-at-sort' }; export const TH_CREATED_AT_TEST_ID = { 'data-testid': 'incident-management-created-at-sort' };
export const TH_SEVERITY_TEST_ID = { 'data-testid': 'incident-management-severity-sort' }; export const TH_SEVERITY_TEST_ID = { 'data-testid': 'incident-management-severity-sort' };
......
# eslint-disable-next-line @graphql-eslint/require-id-when-available # eslint-disable-next-line @graphql-eslint/require-id-when-available
fragment IncidentFields on Issue { fragment IncidentFields on Issue {
severity severity
escalationStatus
} }
...@@ -46,6 +46,7 @@ export default () => { ...@@ -46,6 +46,7 @@ export default () => {
assigneeUsernameQuery, assigneeUsernameQuery,
slaFeatureAvailable: parseBoolean(slaFeatureAvailable), slaFeatureAvailable: parseBoolean(slaFeatureAvailable),
canCreateIncident: parseBoolean(canCreateIncident), canCreateIncident: parseBoolean(canCreateIncident),
incidentEscalationsAvailable: parseBoolean(gon?.features?.incidentEscalations),
}, },
apolloProvider, apolloProvider,
render(createElement) { render(createElement) {
......
<script> <script>
import { GlIcon } from '@gitlab/ui'; import { GlIcon } from '@gitlab/ui';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
export default { export default {
components: { components: {
GlIcon, GlIcon,
TooltipOnTruncate,
}, },
props: { props: {
severity: { severity: {
...@@ -30,13 +32,15 @@ export default { ...@@ -30,13 +32,15 @@ export default {
<template> <template>
<div <div
class="incident-severity gl-display-inline-flex gl-align-items-center gl-justify-content-between" class="incident-severity gl-display-inline-flex gl-align-items-center gl-justify-content-between gl-max-w-full"
> >
<gl-icon <gl-icon
:size="iconSize" :size="iconSize"
:name="`severity-${severity.icon}`" :name="`severity-${severity.icon}`"
:class="[`icon-${severity.icon}`, { 'gl-mr-3': !iconOnly }]" :class="[`icon-${severity.icon}`, { 'gl-mr-3': !iconOnly }]"
/> />
<span v-if="!iconOnly">{{ severity.label }}</span> <tooltip-on-truncate v-if="!iconOnly" :title="severity.label" class="gl-text-truncate">{{
severity.label
}}</tooltip-on-truncate>
</div> </div>
</template> </template>
...@@ -6,6 +6,9 @@ class Projects::IncidentsController < Projects::ApplicationController ...@@ -6,6 +6,9 @@ class Projects::IncidentsController < Projects::ApplicationController
before_action :authorize_read_issue! before_action :authorize_read_issue!
before_action :load_incident, only: [:show] before_action :load_incident, only: [:show]
before_action do
push_frontend_feature_flag(:incident_escalations, @project)
end
feature_category :incident_management feature_category :incident_management
......
# eslint-disable-next-line @graphql-eslint/require-id-when-available # eslint-disable-next-line @graphql-eslint/require-id-when-available
fragment IncidentFields on Issue { fragment IncidentFields on Issue {
severity severity
escalationStatus
statusPagePublishedIncident statusPagePublishedIncident
slaDueAt slaDueAt
} }
...@@ -59,12 +59,6 @@ export default { ...@@ -59,12 +59,6 @@ export default {
hasNoTimeRemaining() { hasNoTimeRemaining() {
return this.remainingTime === 0; return this.remainingTime === 0;
}, },
isMissedSLA() {
return this.hasNoTimeRemaining && !this.isClosed;
},
isAchievedSLA() {
return this.hasNoTimeRemaining && this.isClosed;
},
isClosed() { isClosed() {
return this.issueState === 'closed'; return this.issueState === 'closed';
}, },
...@@ -74,14 +68,18 @@ export default { ...@@ -74,14 +68,18 @@ export default {
shouldShow() { shouldShow() {
return isValidSlaDueAt(this.slaDueAt); return isValidSlaDueAt(this.slaDueAt);
}, },
slaText() { hasNoTimeRemainingText() {
if (this.isMissedSLA) { if (this.isClosed) {
return this.$options.i18n.missedSLAText;
}
if (this.isAchievedSLA) {
return this.$options.i18n.achievedSLAText; return this.$options.i18n.achievedSLAText;
} }
return this.$options.i18n.missedSLAText;
},
slaText() {
if (this.hasNoTimeRemaining) {
return this.hasNoTimeRemainingText;
}
const remainingDuration = formatTime(this.remainingTime); const remainingDuration = formatTime(this.remainingTime);
// remove the seconds portion of the string // remove the seconds portion of the string
...@@ -89,7 +87,7 @@ export default { ...@@ -89,7 +87,7 @@ export default {
}, },
slaTitle() { slaTitle() {
if (this.hasNoTimeRemaining) { if (this.hasNoTimeRemaining) {
return ''; return this.hasNoTimeRemainingText;
} }
const minutes = Math.floor(this.remainingTime / 1000 / 60) % 60; const minutes = Math.floor(this.remainingTime / 1000 / 60) % 60;
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Incident Management index', :js do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user) }
let_it_be(:incident) { create(:incident, project: project) }
before_all do
project.add_developer(developer)
end
before do
sign_in(developer)
end
context 'when a developer displays the incident list' do
it 'has expected columns' do
visit project_incidents_path(project)
wait_for_requests
table = page.find('.gl-table')
expect(table).to have_content('Severity')
expect(table).to have_content('Incident')
expect(table).to have_content('Status')
expect(table).to have_content('Date created')
expect(table).to have_content('Assignees')
expect(table).not_to have_content('Time to SLA')
expect(table).not_to have_content('Published')
end
context 'with SLA feature available' do
before do
stub_licensed_features(incident_sla: true)
end
it 'includes the SLA column' do
visit project_incidents_path(project)
wait_for_requests
expect(page.find('.gl-table')).to have_content('Time to SLA')
end
end
context 'with Status Page feature available' do
before do
stub_licensed_features(status_page: true)
end
it 'includes the Published column' do
visit project_incidents_path(project)
wait_for_requests
expect(page.find('.gl-table')).to have_content('Published')
end
end
end
end
...@@ -82,7 +82,7 @@ exports[`ee/BoardContentSidebar incident sidebar matches the snapshot 1`] = ` ...@@ -82,7 +82,7 @@ exports[`ee/BoardContentSidebar incident sidebar matches the snapshot 1`] = `
class="sidebar-collapsed-icon" class="sidebar-collapsed-icon"
> >
<div <div
class="incident-severity gl-display-inline-flex gl-align-items-center gl-justify-content-between" class="incident-severity gl-display-inline-flex gl-align-items-center gl-justify-content-between gl-max-w-full"
> >
<svg <svg
aria-hidden="true" aria-hidden="true"
...@@ -229,7 +229,7 @@ exports[`ee/BoardContentSidebar incident sidebar matches the snapshot 1`] = ` ...@@ -229,7 +229,7 @@ exports[`ee/BoardContentSidebar incident sidebar matches the snapshot 1`] = `
class="gl-new-dropdown-item-text-primary" class="gl-new-dropdown-item-text-primary"
> >
<div <div
class="incident-severity gl-display-inline-flex gl-align-items-center gl-justify-content-between" class="incident-severity gl-display-inline-flex gl-align-items-center gl-justify-content-between gl-max-w-full"
> >
<svg <svg
aria-hidden="true" aria-hidden="true"
...@@ -242,7 +242,9 @@ exports[`ee/BoardContentSidebar incident sidebar matches the snapshot 1`] = ` ...@@ -242,7 +242,9 @@ exports[`ee/BoardContentSidebar incident sidebar matches the snapshot 1`] = `
/> />
</svg> </svg>
<span> <span
class="gl-min-w-0 gl-text-truncate"
>
Critical - S1 Critical - S1
</span> </span>
</div> </div>
...@@ -286,7 +288,7 @@ exports[`ee/BoardContentSidebar incident sidebar matches the snapshot 1`] = ` ...@@ -286,7 +288,7 @@ exports[`ee/BoardContentSidebar incident sidebar matches the snapshot 1`] = `
class="gl-new-dropdown-item-text-primary" class="gl-new-dropdown-item-text-primary"
> >
<div <div
class="incident-severity gl-display-inline-flex gl-align-items-center gl-justify-content-between" class="incident-severity gl-display-inline-flex gl-align-items-center gl-justify-content-between gl-max-w-full"
> >
<svg <svg
aria-hidden="true" aria-hidden="true"
...@@ -299,7 +301,9 @@ exports[`ee/BoardContentSidebar incident sidebar matches the snapshot 1`] = ` ...@@ -299,7 +301,9 @@ exports[`ee/BoardContentSidebar incident sidebar matches the snapshot 1`] = `
/> />
</svg> </svg>
<span> <span
class="gl-min-w-0 gl-text-truncate"
>
High - S2 High - S2
</span> </span>
</div> </div>
...@@ -343,7 +347,7 @@ exports[`ee/BoardContentSidebar incident sidebar matches the snapshot 1`] = ` ...@@ -343,7 +347,7 @@ exports[`ee/BoardContentSidebar incident sidebar matches the snapshot 1`] = `
class="gl-new-dropdown-item-text-primary" class="gl-new-dropdown-item-text-primary"
> >
<div <div
class="incident-severity gl-display-inline-flex gl-align-items-center gl-justify-content-between" class="incident-severity gl-display-inline-flex gl-align-items-center gl-justify-content-between gl-max-w-full"
> >
<svg <svg
aria-hidden="true" aria-hidden="true"
...@@ -356,7 +360,9 @@ exports[`ee/BoardContentSidebar incident sidebar matches the snapshot 1`] = ` ...@@ -356,7 +360,9 @@ exports[`ee/BoardContentSidebar incident sidebar matches the snapshot 1`] = `
/> />
</svg> </svg>
<span> <span
class="gl-min-w-0 gl-text-truncate"
>
Medium - S3 Medium - S3
</span> </span>
</div> </div>
...@@ -400,7 +406,7 @@ exports[`ee/BoardContentSidebar incident sidebar matches the snapshot 1`] = ` ...@@ -400,7 +406,7 @@ exports[`ee/BoardContentSidebar incident sidebar matches the snapshot 1`] = `
class="gl-new-dropdown-item-text-primary" class="gl-new-dropdown-item-text-primary"
> >
<div <div
class="incident-severity gl-display-inline-flex gl-align-items-center gl-justify-content-between" class="incident-severity gl-display-inline-flex gl-align-items-center gl-justify-content-between gl-max-w-full"
> >
<svg <svg
aria-hidden="true" aria-hidden="true"
...@@ -413,7 +419,9 @@ exports[`ee/BoardContentSidebar incident sidebar matches the snapshot 1`] = ` ...@@ -413,7 +419,9 @@ exports[`ee/BoardContentSidebar incident sidebar matches the snapshot 1`] = `
/> />
</svg> </svg>
<span> <span
class="gl-min-w-0 gl-text-truncate"
>
Low - S4 Low - S4
</span> </span>
</div> </div>
...@@ -457,7 +465,7 @@ exports[`ee/BoardContentSidebar incident sidebar matches the snapshot 1`] = ` ...@@ -457,7 +465,7 @@ exports[`ee/BoardContentSidebar incident sidebar matches the snapshot 1`] = `
class="gl-new-dropdown-item-text-primary" class="gl-new-dropdown-item-text-primary"
> >
<div <div
class="incident-severity gl-display-inline-flex gl-align-items-center gl-justify-content-between" class="incident-severity gl-display-inline-flex gl-align-items-center gl-justify-content-between gl-max-w-full"
> >
<svg <svg
aria-hidden="true" aria-hidden="true"
...@@ -470,7 +478,9 @@ exports[`ee/BoardContentSidebar incident sidebar matches the snapshot 1`] = ` ...@@ -470,7 +478,9 @@ exports[`ee/BoardContentSidebar incident sidebar matches the snapshot 1`] = `
/> />
</svg> </svg>
<span> <span
class="gl-min-w-0 gl-text-truncate"
>
Unknown Unknown
</span> </span>
</div> </div>
...@@ -490,7 +500,7 @@ exports[`ee/BoardContentSidebar incident sidebar matches the snapshot 1`] = ` ...@@ -490,7 +500,7 @@ exports[`ee/BoardContentSidebar incident sidebar matches the snapshot 1`] = `
</div> </div>
<div <div
class="incident-severity gl-display-inline-flex gl-align-items-center gl-justify-content-between" class="incident-severity gl-display-inline-flex gl-align-items-center gl-justify-content-between gl-max-w-full"
> >
<svg <svg
aria-hidden="true" aria-hidden="true"
...@@ -503,7 +513,9 @@ exports[`ee/BoardContentSidebar incident sidebar matches the snapshot 1`] = ` ...@@ -503,7 +513,9 @@ exports[`ee/BoardContentSidebar incident sidebar matches the snapshot 1`] = `
/> />
</svg> </svg>
<span> <span
class="gl-min-w-0 gl-text-truncate"
>
Unknown Unknown
</span> </span>
</div> </div>
......
...@@ -16,6 +16,7 @@ const defaultProvide = { ...@@ -16,6 +16,7 @@ const defaultProvide = {
assigneeUsernameQuery: '', assigneeUsernameQuery: '',
slaFeatureAvailable: true, slaFeatureAvailable: true,
canCreateIncident: true, canCreateIncident: true,
incidentEscalationsAvailable: true,
}; };
describe('Incidents Service Level Agreement', () => { describe('Incidents Service Level Agreement', () => {
......
...@@ -92,7 +92,7 @@ describe('Service Level Agreement', () => { ...@@ -92,7 +92,7 @@ describe('Service Level Agreement', () => {
${5} | ${7} | ${'5 hours, 7 minutes remaining'} ${5} | ${7} | ${'5 hours, 7 minutes remaining'}
${5} | ${0} | ${'5 hours, 0 minutes remaining'} ${5} | ${0} | ${'5 hours, 0 minutes remaining'}
${0} | ${7} | ${'7 minutes remaining'} ${0} | ${7} | ${'7 minutes remaining'}
${0} | ${0} | ${''} ${0} | ${0} | ${'Missed SLA'}
`( `(
'returns the correct message for: hours: "$hours", minutes: "$minutes"', 'returns the correct message for: hours: "$hours", minutes: "$minutes"',
({ hours, minutes, expectedMessage }) => { ({ hours, minutes, expectedMessage }) => {
......
...@@ -19262,6 +19262,9 @@ msgstr "" ...@@ -19262,6 +19262,9 @@ msgstr ""
msgid "IncidentManagement|No incidents to display." msgid "IncidentManagement|No incidents to display."
msgstr "" msgstr ""
msgid "IncidentManagement|None"
msgstr ""
msgid "IncidentManagement|Open" msgid "IncidentManagement|Open"
msgstr "" msgstr ""
...@@ -19280,6 +19283,9 @@ msgstr "" ...@@ -19280,6 +19283,9 @@ msgstr ""
msgid "IncidentManagement|Severity" msgid "IncidentManagement|Severity"
msgstr "" msgstr ""
msgid "IncidentManagement|Status"
msgstr ""
msgid "IncidentManagement|There are no closed incidents" msgid "IncidentManagement|There are no closed incidents"
msgstr "" msgstr ""
......
...@@ -43,6 +43,7 @@ RSpec.describe Projects::IncidentsController do ...@@ -43,6 +43,7 @@ RSpec.describe Projects::IncidentsController do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index) expect(response).to render_template(:index)
expect(Gon.features).to include('incidentEscalations' => true)
end end
context 'when user is unauthorized' do context 'when user is unauthorized' do
......
...@@ -34,5 +34,28 @@ RSpec.describe 'Incident Management index', :js do ...@@ -34,5 +34,28 @@ RSpec.describe 'Incident Management index', :js do
it 'alert page title' do it 'alert page title' do
expect(page).to have_content('Incidents') expect(page).to have_content('Incidents')
end end
it 'has expected columns' do
table = page.find('.gl-table')
expect(table).to have_content('Severity')
expect(table).to have_content('Incident')
expect(table).to have_content('Status')
expect(table).to have_content('Date created')
expect(table).to have_content('Assignees')
end
context 'when :incident_escalations feature is disabled' do
before do
stub_feature_flags(incident_escalations: false)
end
it 'does not include the Status columns' do
visit project_incidents_path(project)
wait_for_requests
expect(page.find('.gl-table')).not_to have_content('Status')
end
end
end end
end end
...@@ -48,6 +48,7 @@ describe('Incidents List', () => { ...@@ -48,6 +48,7 @@ describe('Incidents List', () => {
const findClosedIcon = () => wrapper.findAll("[data-testid='incident-closed']"); const findClosedIcon = () => wrapper.findAll("[data-testid='incident-closed']");
const findEmptyState = () => wrapper.find(GlEmptyState); const findEmptyState = () => wrapper.find(GlEmptyState);
const findSeverity = () => wrapper.findAll(SeverityToken); const findSeverity = () => wrapper.findAll(SeverityToken);
const findEscalationStatus = () => wrapper.findAll('[data-testid="incident-escalation-status"]');
function mountComponent({ data = {}, loading = false, provide = {} } = {}) { function mountComponent({ data = {}, loading = false, provide = {} } = {}) {
wrapper = mount(IncidentsList, { wrapper = mount(IncidentsList, {
...@@ -80,6 +81,7 @@ describe('Incidents List', () => { ...@@ -80,6 +81,7 @@ describe('Incidents List', () => {
assigneeUsernameQuery: '', assigneeUsernameQuery: '',
slaFeatureAvailable: true, slaFeatureAvailable: true,
canCreateIncident: true, canCreateIncident: true,
incidentEscalationsAvailable: true,
...provide, ...provide,
}, },
stubs: { stubs: {
...@@ -184,6 +186,34 @@ describe('Incidents List', () => { ...@@ -184,6 +186,34 @@ describe('Incidents List', () => {
expect(findSeverity().length).toBe(mockIncidents.length); expect(findSeverity().length).toBe(mockIncidents.length);
}); });
describe('Escalation status', () => {
it('renders escalation status per row', () => {
expect(findEscalationStatus().length).toBe(mockIncidents.length);
const actualStatuses = findEscalationStatus().wrappers.map((status) => status.text());
expect(actualStatuses).toEqual([
'Triggered',
'Acknowledged',
'Resolved',
I18N.noEscalationStatus,
]);
});
describe('when feature is disabled', () => {
beforeEach(() => {
mountComponent({
data: { incidents: { list: mockIncidents }, incidentsCount },
provide: { incidentEscalationsAvailable: false },
loading: false,
});
});
it('is absent if feature flag is disabled', () => {
expect(findEscalationStatus().length).toBe(0);
});
});
});
it('contains a link to the incident details page', async () => { it('contains a link to the incident details page', async () => {
findTableRows().at(0).trigger('click'); findTableRows().at(0).trigger('click');
expect(visitUrl).toHaveBeenCalledWith( expect(visitUrl).toHaveBeenCalledWith(
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
"assignees": {}, "assignees": {},
"state": "opened", "state": "opened",
"severity": "CRITICAL", "severity": "CRITICAL",
"escalationStatus": "TRIGGERED",
"slaDueAt": "2020-06-04T12:46:08Z" "slaDueAt": "2020-06-04T12:46:08Z"
}, },
{ {
...@@ -26,6 +27,7 @@ ...@@ -26,6 +27,7 @@
}, },
"state": "opened", "state": "opened",
"severity": "HIGH", "severity": "HIGH",
"escalationStatus": "ACKNOWLEDGED",
"slaDueAt": null "slaDueAt": null
}, },
{ {
...@@ -35,7 +37,8 @@ ...@@ -35,7 +37,8 @@
"createdAt": "2020-05-19T08:53:55Z", "createdAt": "2020-05-19T08:53:55Z",
"assignees": {}, "assignees": {},
"state": "closed", "state": "closed",
"severity": "LOW" "severity": "LOW",
"escalationStatus": "RESOLVED"
}, },
{ {
"id": 4, "id": 4,
...@@ -44,6 +47,7 @@ ...@@ -44,6 +47,7 @@
"createdAt": "2020-05-18T17:13:35Z", "createdAt": "2020-05-18T17:13:35Z",
"assignees": {}, "assignees": {},
"state": "closed", "state": "closed",
"severity": "MEDIUM" "severity": "MEDIUM",
"escalationStatus": null
} }
] ]
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