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