Commit 479e3a9f authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '321891-change-alert-status' into 'master'

Generalize alert details status

See merge request gitlab-org/gitlab!56800
parents 93c7c8db c3a0efaa
...@@ -21,7 +21,7 @@ import Tracking from '~/tracking'; ...@@ -21,7 +21,7 @@ import Tracking from '~/tracking';
import initUserPopovers from '~/user_popovers'; import initUserPopovers from '~/user_popovers';
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue'; import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { SEVERITY_LEVELS } from '../constants'; import { PAGE_CONFIG, SEVERITY_LEVELS } from '../constants';
import createIssueMutation from '../graphql/mutations/alert_issue_create.mutation.graphql'; import createIssueMutation from '../graphql/mutations/alert_issue_create.mutation.graphql';
import toggleSidebarStatusMutation from '../graphql/mutations/alert_sidebar_status.mutation.graphql'; import toggleSidebarStatusMutation from '../graphql/mutations/alert_sidebar_status.mutation.graphql';
import alertQuery from '../graphql/queries/alert_details.query.graphql'; import alertQuery from '../graphql/queries/alert_details.query.graphql';
...@@ -92,6 +92,9 @@ export default { ...@@ -92,6 +92,9 @@ export default {
projectIssuesPath: { projectIssuesPath: {
default: '', default: '',
}, },
statuses: {
default: PAGE_CONFIG.OPERATIONS.STATUSES,
},
trackAlertsDetailsViewsOptions: { trackAlertsDetailsViewsOptions: {
default: null, default: null,
}, },
...@@ -367,7 +370,7 @@ export default { ...@@ -367,7 +370,7 @@ export default {
> >
{{ alert.runbook }} {{ alert.runbook }}
</alert-summary-row> </alert-summary-row>
<alert-details-table :alert="alert" :loading="loading" /> <alert-details-table :alert="alert" :loading="loading" :statuses="statuses" />
</gl-tab> </gl-tab>
<gl-tab <gl-tab
v-if="!isThreatMonitoringPage" v-if="!isThreatMonitoringPage"
......
...@@ -19,10 +19,6 @@ export default { ...@@ -19,10 +19,6 @@ export default {
projectId: { projectId: {
default: '', default: '',
}, },
// TODO remove this limitation in https://gitlab.com/gitlab-org/gitlab/-/issues/296717
isThreatMonitoringPage: {
default: false,
},
}, },
props: { props: {
alert: { alert: {
...@@ -66,7 +62,6 @@ export default { ...@@ -66,7 +62,6 @@ export default {
@alert-error="$emit('alert-error', $event)" @alert-error="$emit('alert-error', $event)"
/> />
<sidebar-status <sidebar-status
v-if="!isThreatMonitoringPage"
:project-path="projectPath" :project-path="projectPath"
:alert="alert" :alert="alert"
@toggle-sidebar="$emit('toggle-sidebar')" @toggle-sidebar="$emit('toggle-sidebar')"
......
...@@ -3,6 +3,7 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; ...@@ -3,6 +3,7 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import updateAlertStatusMutation from '~/graphql_shared/mutations/alert_status_update.mutation.graphql'; import updateAlertStatusMutation from '~/graphql_shared/mutations/alert_status_update.mutation.graphql';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import { PAGE_CONFIG } from '../constants';
export default { export default {
i18n: { i18n: {
...@@ -11,11 +12,6 @@ export default { ...@@ -11,11 +12,6 @@ export default {
), ),
UPDATE_ALERT_STATUS_INSTRUCTION: s__('AlertManagement|Please try again.'), UPDATE_ALERT_STATUS_INSTRUCTION: s__('AlertManagement|Please try again.'),
}, },
statuses: {
TRIGGERED: s__('AlertManagement|Triggered'),
ACKNOWLEDGED: s__('AlertManagement|Acknowledged'),
RESOLVED: s__('AlertManagement|Resolved'),
},
components: { components: {
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
...@@ -42,6 +38,11 @@ export default { ...@@ -42,6 +38,11 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
statuses: {
type: Object,
required: false,
default: () => PAGE_CONFIG.OPERATIONS.STATUSES,
},
}, },
computed: { computed: {
dropdownClass() { dropdownClass() {
...@@ -57,13 +58,13 @@ export default { ...@@ -57,13 +58,13 @@ export default {
mutation: updateAlertStatusMutation, mutation: updateAlertStatusMutation,
variables: { variables: {
iid: this.alert.iid, iid: this.alert.iid,
status: status.toUpperCase(), status,
projectPath: this.projectPath, projectPath: this.projectPath,
}, },
}) })
.then((resp) => { .then((resp) => {
if (this.trackAlertStatusUpdateOptions) { if (this.trackAlertStatusUpdateOptions) {
this.trackStatusUpdate(status); this.trackStatusUpdate(this.statuses[status]);
} }
const errors = resp.data?.updateAlertStatus?.errors || []; const errors = resp.data?.updateAlertStatus?.errors || [];
...@@ -99,7 +100,7 @@ export default { ...@@ -99,7 +100,7 @@ export default {
<gl-dropdown <gl-dropdown
ref="dropdown" ref="dropdown"
right right
:text="$options.statuses[alert.status]" :text="statuses[alert.status]"
class="w-100" class="w-100"
toggle-class="dropdown-menu-toggle" toggle-class="dropdown-menu-toggle"
@keydown.esc.native="$emit('hide-dropdown')" @keydown.esc.native="$emit('hide-dropdown')"
...@@ -110,12 +111,12 @@ export default { ...@@ -110,12 +111,12 @@ export default {
</p> </p>
<div class="dropdown-content dropdown-body"> <div class="dropdown-content dropdown-body">
<gl-dropdown-item <gl-dropdown-item
v-for="(label, field) in $options.statuses" v-for="(label, field) in statuses"
:key="field" :key="field"
data-testid="statusDropdownItem" data-testid="statusDropdownItem"
:active="label.toUpperCase() === alert.status" :active="field === alert.status"
:active-class="'is-active'" :active-class="'is-active'"
@click="updateAlertStatus(label)" @click="updateAlertStatus(field)"
> >
{{ label }} {{ label }}
</gl-dropdown-item> </gl-dropdown-item>
......
<script> <script>
import { GlIcon, GlLoadingIcon, GlTooltip, GlSprintf } from '@gitlab/ui'; import { GlIcon, GlLoadingIcon, GlTooltip, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale'; import { PAGE_CONFIG } from '../../constants';
import AlertStatus from '../alert_status.vue'; import AlertStatus from '../alert_status.vue';
export default { export default {
statuses: {
TRIGGERED: s__('AlertManagement|Triggered'),
ACKNOWLEDGED: s__('AlertManagement|Acknowledged'),
RESOLVED: s__('AlertManagement|Resolved'),
},
components: { components: {
GlIcon, GlIcon,
GlLoadingIcon, GlLoadingIcon,
...@@ -16,6 +11,11 @@ export default { ...@@ -16,6 +11,11 @@ export default {
GlSprintf, GlSprintf,
AlertStatus, AlertStatus,
}, },
inject: {
statuses: {
default: PAGE_CONFIG.OPERATIONS.STATUSES,
},
},
props: { props: {
projectPath: { projectPath: {
type: String, type: String,
...@@ -94,6 +94,7 @@ export default { ...@@ -94,6 +94,7 @@ export default {
:project-path="projectPath" :project-path="projectPath"
:is-dropdown-showing="isDropdownShowing" :is-dropdown-showing="isDropdownShowing"
:is-sidebar="true" :is-sidebar="true"
:statuses="statuses"
@alert-error="$emit('alert-error', $event)" @alert-error="$emit('alert-error', $event)"
@hide-dropdown="hideDropdown" @hide-dropdown="hideDropdown"
@handle-updating="handleUpdating" @handle-updating="handleUpdating"
...@@ -103,14 +104,11 @@ export default { ...@@ -103,14 +104,11 @@ export default {
<p <p
v-else-if="!isDropdownShowing" v-else-if="!isDropdownShowing"
class="value gl-m-0" class="value gl-m-0"
:class="{ 'no-value': !$options.statuses[alert.status] }" :class="{ 'no-value': !statuses[alert.status] }"
> >
<span <span v-if="statuses[alert.status]" class="gl-text-gray-500" data-testid="status">
v-if="$options.statuses[alert.status]" {{ statuses[alert.status] }}
class="gl-text-gray-500" </span>
data-testid="status"
>{{ $options.statuses[alert.status] }}</span
>
<span v-else> <span v-else>
{{ s__('AlertManagement|None') }} {{ s__('AlertManagement|None') }}
</span> </span>
......
...@@ -13,6 +13,11 @@ export const SEVERITY_LEVELS = { ...@@ -13,6 +13,11 @@ export const SEVERITY_LEVELS = {
export const PAGE_CONFIG = { export const PAGE_CONFIG = {
OPERATIONS: { OPERATIONS: {
TITLE: 'OPERATIONS', TITLE: 'OPERATIONS',
STATUSES: {
TRIGGERED: s__('AlertManagement|Triggered'),
ACKNOWLEDGED: s__('AlertManagement|Acknowledged'),
RESOLVED: s__('AlertManagement|Resolved'),
},
// Tracks snowplow event when user views alert details // Tracks snowplow event when user views alert details
TRACK_ALERTS_DETAILS_VIEWS_OPTIONS: { TRACK_ALERTS_DETAILS_VIEWS_OPTIONS: {
category: 'Alert Management', category: 'Alert Management',
...@@ -27,5 +32,11 @@ export const PAGE_CONFIG = { ...@@ -27,5 +32,11 @@ export const PAGE_CONFIG = {
}, },
THREAT_MONITORING: { THREAT_MONITORING: {
TITLE: 'THREAT_MONITORING', TITLE: 'THREAT_MONITORING',
STATUSES: {
TRIGGERED: s__('ThreatMonitoring|Unreviewed'),
ACKNOWLEDGED: s__('ThreatMonitoring|In review'),
RESOLVED: s__('ThreatMonitoring|Resolved'),
IGNORED: s__('ThreatMonitoring|Dismissed'),
},
}, },
}; };
...@@ -55,6 +55,7 @@ export default (selector) => { ...@@ -55,6 +55,7 @@ export default (selector) => {
page, page,
projectIssuesPath, projectIssuesPath,
projectId, projectId,
statuses: PAGE_CONFIG[page].STATUSES,
}; };
if (page === PAGE_CONFIG.OPERATIONS.TITLE) { if (page === PAGE_CONFIG.OPERATIONS.TITLE) {
......
...@@ -7,6 +7,7 @@ import { ...@@ -7,6 +7,7 @@ import {
splitCamelCase, splitCamelCase,
} from '~/lib/utils/text_utility'; } from '~/lib/utils/text_utility';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { PAGE_CONFIG } from '~/vue_shared/alert_details/constants';
const thClass = 'gl-bg-transparent! gl-border-1! gl-border-b-solid! gl-border-gray-200!'; const thClass = 'gl-bg-transparent! gl-border-1! gl-border-b-solid! gl-border-gray-200!';
const tdClass = 'gl-border-gray-100! gl-p-5!'; const tdClass = 'gl-border-gray-100! gl-p-5!';
...@@ -42,6 +43,11 @@ export default { ...@@ -42,6 +43,11 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
statuses: {
type: Object,
required: false,
default: () => PAGE_CONFIG.OPERATIONS.STATUSES,
},
}, },
fields: [ fields: [
{ {
...@@ -71,6 +77,8 @@ export default { ...@@ -71,6 +77,8 @@ export default {
let value; let value;
if (fieldName === 'environment') { if (fieldName === 'environment') {
value = fieldValue?.name; value = fieldValue?.name;
} else if (fieldName === 'status') {
value = this.statuses[fieldValue] || fieldValue;
} else { } else {
value = fieldValue; value = fieldValue;
} }
......
---
title: Generalize alert details status
merge_request: 56800
author:
type: added
...@@ -8,7 +8,7 @@ import { joinPaths } from '~/lib/utils/url_utility'; ...@@ -8,7 +8,7 @@ import { joinPaths } from '~/lib/utils/url_utility';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import AlertDetails from '~/vue_shared/alert_details/components/alert_details.vue'; import AlertDetails from '~/vue_shared/alert_details/components/alert_details.vue';
import AlertSummaryRow from '~/vue_shared/alert_details/components/alert_summary_row.vue'; import AlertSummaryRow from '~/vue_shared/alert_details/components/alert_summary_row.vue';
import { SEVERITY_LEVELS } from '~/vue_shared/alert_details/constants'; import { PAGE_CONFIG, SEVERITY_LEVELS } from '~/vue_shared/alert_details/constants';
import createIssueMutation from '~/vue_shared/alert_details/graphql/mutations/alert_issue_create.mutation.graphql'; import createIssueMutation from '~/vue_shared/alert_details/graphql/mutations/alert_issue_create.mutation.graphql';
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue'; import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
import mockAlerts from './mocks/alerts.json'; import mockAlerts from './mocks/alerts.json';
...@@ -271,7 +271,13 @@ describe('AlertDetails', () => { ...@@ -271,7 +271,13 @@ describe('AlertDetails', () => {
}); });
it('should display a table of raw alert details data', () => { it('should display a table of raw alert details data', () => {
expect(findDetailsTable().exists()).toBe(true); const details = findDetailsTable();
expect(details.exists()).toBe(true);
expect(details.props()).toStrictEqual({
alert: mockAlert,
statuses: PAGE_CONFIG.OPERATIONS.STATUSES,
loading: false,
});
}); });
}); });
......
...@@ -12,6 +12,7 @@ describe('AlertManagementStatus', () => { ...@@ -12,6 +12,7 @@ describe('AlertManagementStatus', () => {
let wrapper; let wrapper;
const findStatusDropdown = () => wrapper.find(GlDropdown); const findStatusDropdown = () => wrapper.find(GlDropdown);
const findFirstStatusOption = () => findStatusDropdown().find(GlDropdownItem); const findFirstStatusOption = () => findStatusDropdown().find(GlDropdownItem);
const findAllStatusOptions = () => findStatusDropdown().findAll(GlDropdownItem);
const selectFirstStatusOption = () => { const selectFirstStatusOption = () => {
findFirstStatusOption().vm.$emit('click'); findFirstStatusOption().vm.$emit('click');
...@@ -131,6 +132,24 @@ describe('AlertManagementStatus', () => { ...@@ -131,6 +132,24 @@ describe('AlertManagementStatus', () => {
}); });
}); });
describe('Statuses', () => {
it('renders default translated statuses', () => {
mountComponent({});
expect(findAllStatusOptions().length).toBe(3);
expect(findFirstStatusOption().text()).toBe('Triggered');
});
it('renders translated statuses', () => {
const status = 'TEST';
const translatedStatus = 'Test';
mountComponent({
props: { alert: { ...mockAlert, status }, statuses: { [status]: translatedStatus } },
});
expect(findAllStatusOptions().length).toBe(1);
expect(findFirstStatusOption().text()).toBe(translatedStatus);
});
});
describe('Snowplow tracking', () => { describe('Snowplow tracking', () => {
beforeEach(() => { beforeEach(() => {
jest.spyOn(Tracking, 'event'); jest.spyOn(Tracking, 'event');
......
...@@ -76,20 +76,4 @@ describe('Alert Details Sidebar', () => { ...@@ -76,20 +76,4 @@ describe('Alert Details Sidebar', () => {
expect(wrapper.find(SidebarStatus).exists()).toBe(true); expect(wrapper.find(SidebarStatus).exists()).toBe(true);
}); });
}); });
describe('the sidebar renders for threat monitoring', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
mountComponent();
});
it('should not render side bar status dropdown', () => {
mountComponent({
mountMethod: mount,
alert: mockAlert,
provide: { isThreatMonitoringPage: true },
});
expect(wrapper.find(SidebarStatus).exists()).toBe(false);
});
});
}); });
import { GlDropdown, GlDropdownItem, GlLoadingIcon } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem, GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import updateAlertStatusMutation from '~/graphql_shared/mutations/alert_status_update.mutation.graphql'; import updateAlertStatusMutation from '~/graphql_shared/mutations/alert_status_update.mutation.graphql';
import AlertStatus from '~/vue_shared/alert_details/components/alert_status.vue';
import AlertSidebarStatus from '~/vue_shared/alert_details/components/sidebar/sidebar_status.vue'; import AlertSidebarStatus from '~/vue_shared/alert_details/components/sidebar/sidebar_status.vue';
import { PAGE_CONFIG } from '~/vue_shared/alert_details/constants';
import mockAlerts from '../mocks/alerts.json'; import mockAlerts from '../mocks/alerts.json';
const mockAlert = mockAlerts[0]; const mockAlert = mockAlerts[0];
...@@ -12,8 +14,16 @@ describe('Alert Details Sidebar Status', () => { ...@@ -12,8 +14,16 @@ describe('Alert Details Sidebar Status', () => {
const findStatusDropdownItem = () => wrapper.find(GlDropdownItem); const findStatusDropdownItem = () => wrapper.find(GlDropdownItem);
const findStatusLoadingIcon = () => wrapper.find(GlLoadingIcon); const findStatusLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findStatusDropdownHeader = () => wrapper.find('[data-testid="dropdown-header"]'); const findStatusDropdownHeader = () => wrapper.find('[data-testid="dropdown-header"]');
const findAlertStatus = () => wrapper.findComponent(AlertStatus);
const findStatus = () => wrapper.find('[data-testid="status"]');
function mountComponent({ data, sidebarCollapsed = true, loading = false, stubs = {} } = {}) { function mountComponent({
data,
sidebarCollapsed = true,
loading = false,
stubs = {},
provide = {},
} = {}) {
wrapper = mount(AlertSidebarStatus, { wrapper = mount(AlertSidebarStatus, {
propsData: { propsData: {
alert: { ...mockAlert }, alert: { ...mockAlert },
...@@ -32,6 +42,7 @@ describe('Alert Details Sidebar Status', () => { ...@@ -32,6 +42,7 @@ describe('Alert Details Sidebar Status', () => {
}, },
}, },
stubs, stubs,
provide,
}); });
} }
...@@ -96,8 +107,24 @@ describe('Alert Details Sidebar Status', () => { ...@@ -96,8 +107,24 @@ describe('Alert Details Sidebar Status', () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error())); jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error()));
findStatusDropdownItem().vm.$emit('click'); findStatusDropdownItem().vm.$emit('click');
expect(findStatusLoadingIcon().exists()).toBe(false); expect(findStatusLoadingIcon().exists()).toBe(false);
expect(wrapper.find('[data-testid="status"]').text()).toBe('Triggered'); expect(findStatus().text()).toBe('Triggered');
}); });
}); });
}); });
describe('Statuses', () => {
it('renders default translated statuses', () => {
mountComponent({});
expect(findAlertStatus().props('statuses')).toBe(PAGE_CONFIG.OPERATIONS.STATUSES);
expect(findStatus().text()).toBe('Triggered');
});
it('renders translated statuses', () => {
const status = 'TEST';
const statuses = { [status]: 'Test' };
mountComponent({ data: { alert: { ...mockAlert, status } }, provide: { statuses } });
expect(findAlertStatus().props('statuses')).toBe(statuses);
expect(findStatus().text()).toBe(statuses.TEST);
});
});
}); });
...@@ -75,45 +75,62 @@ describe('AlertDetails', () => { ...@@ -75,45 +75,62 @@ describe('AlertDetails', () => {
}); });
describe('with table data', () => { describe('with table data', () => {
beforeEach(mountComponent); describe('default', () => {
beforeEach(mountComponent);
it('renders a table', () => {
expect(findTableComponent().exists()).toBe(true); it('renders a table', () => {
}); expect(findTableComponent().exists()).toBe(true);
});
it('renders a cell based on alert data', () => {
expect(findTableComponent().text()).toContain('SyntaxError: Invalid or unexpected token'); it('renders a cell based on alert data', () => {
}); expect(findTableComponent().text()).toContain('SyntaxError: Invalid or unexpected token');
});
it('should show allowed alert fields', () => {
const fields = findTableKeys(); it('should show allowed alert fields', () => {
const fields = findTableKeys();
expect(findTableField(fields, 'Iid').exists()).toBe(true); ['Iid', 'Title', 'Severity', 'Status', 'Hosts', 'Environment'].forEach((field) => {
expect(findTableField(fields, 'Title').exists()).toBe(true); expect(findTableField(fields, field).exists()).toBe(true);
expect(findTableField(fields, 'Severity').exists()).toBe(true); });
expect(findTableField(fields, 'Status').exists()).toBe(true); });
expect(findTableField(fields, 'Hosts').exists()).toBe(true);
expect(findTableField(fields, 'Environment').exists()).toBe(true); it('should not show disallowed alert fields', () => {
const fields = findTableKeys();
['Typename', 'Todos', 'Notes', 'Assignees'].forEach((field) => {
expect(findTableField(fields, field).exists()).toBe(false);
});
});
}); });
it('should not show disallowed alert fields', () => { describe('environment', () => {
const fields = findTableKeys(); it('should display only the name for the environment', () => {
mountComponent();
expect(findTableFieldValueByKey('Environment').text()).toBe(environmentName);
});
expect(findTableField(fields, 'Typename').exists()).toBe(false); it('should not display the environment row if there is not data', () => {
expect(findTableField(fields, 'Todos').exists()).toBe(false); environmentData = { name: null, path: null };
expect(findTableField(fields, 'Notes').exists()).toBe(false); mountComponent();
expect(findTableField(fields, 'Assignees').exists()).toBe(false);
});
it('should display only the name for the environment', () => { expect(findTableFieldValueByKey('Environment').text()).toBeFalsy();
expect(findTableFieldValueByKey('Environment').text()).toBe(environmentName); });
}); });
it('should not display the environment row if there is not data', () => { describe('status', () => {
environmentData = { name: null, path: null }; it('should show the translated status for the default statuses', () => {
mountComponent(); mountComponent();
expect(findTableFieldValueByKey('Status').text()).toBe('Triggered');
expect(findTableFieldValueByKey('Environment').text()).toBeFalsy(); });
it('should show the translated status for provided statuses', () => {
const translatedStatus = 'Test';
mountComponent({ statuses: { TRIGGERED: translatedStatus } });
expect(findTableFieldValueByKey('Status').text()).toBe(translatedStatus);
});
it('should show the provided status if value is not defined in statuses', () => {
mountComponent({ statuses: {} });
expect(findTableFieldValueByKey('Status').text()).toBe('TRIGGERED');
});
}); });
}); });
}); });
......
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