Commit 84bc1f0b authored by Alexander Turinske's avatar Alexander Turinske Committed by Savas Vedova

Create threat monitoring alert details page

- create threat monitoring alert details route
- use shared alert_details code
- update page attribute for alert management
- temporarily disable threat monitoring alert details page
  alert status update
- add tests
parent 89c2ce6d
......@@ -83,6 +83,9 @@ export default {
alertId: {
default: '',
},
isThreatMonitoringPage: {
default: false,
},
projectId: {
default: '',
},
......@@ -364,7 +367,11 @@ export default {
</alert-summary-row>
<alert-details-table :alert="alert" :loading="loading" />
</gl-tab>
<gl-tab :data-testid="$options.tabsConfig[1].id" :title="$options.tabsConfig[1].title">
<gl-tab
v-if="isThreatMonitoringPage"
:data-testid="$options.tabsConfig[1].id"
:title="$options.tabsConfig[1].title"
>
<alert-metrics :dashboard-url="alert.metricsDashboardUrl" />
</gl-tab>
<gl-tab :data-testid="$options.tabsConfig[2].id" :title="$options.tabsConfig[2].title">
......
......@@ -19,6 +19,10 @@ export default {
projectId: {
default: '',
},
// TODO remove this limitation in https://gitlab.com/gitlab-org/gitlab/-/issues/296717
isThreatMonitoringPage: {
default: false,
},
},
props: {
alert: {
......@@ -62,6 +66,7 @@ export default {
@alert-error="$emit('alert-error', $event)"
/>
<sidebar-status
v-if="!isThreatMonitoringPage"
:project-path="projectPath"
:alert="alert"
@toggle-sidebar="$emit('toggle-sidebar')"
......
......@@ -9,11 +9,10 @@ export const SEVERITY_LEVELS = {
UNKNOWN: s__('severity|Unknown'),
};
export const DEFAULT_PAGE = 'OPERATIONS';
/* eslint-disable @gitlab/require-i18n-strings */
export const PAGE_CONFIG = {
OPERATIONS: {
TITLE: 'OPERATIONS',
// Tracks snowplow event when user views alert details
TRACK_ALERTS_DETAILS_VIEWS_OPTIONS: {
category: 'Alert Management',
......@@ -26,4 +25,7 @@ export const PAGE_CONFIG = {
label: 'Status',
},
},
THREAT_MONITORING: {
TITLE: 'THREAT_MONITORING',
},
};
......@@ -6,13 +6,13 @@ import createDefaultClient from '~/lib/graphql';
import AlertDetails from './components/alert_details.vue';
import sidebarStatusQuery from './graphql/queries/alert_sidebar_status.query.graphql';
import createRouter from './router';
import { DEFAULT_PAGE, PAGE_CONFIG } from './constants';
import { PAGE_CONFIG } from './constants';
Vue.use(VueApollo);
export default (selector) => {
const domEl = document.querySelector(selector);
const { alertId, projectPath, projectIssuesPath, projectId, page = DEFAULT_PAGE } = domEl.dataset;
const { alertId, projectPath, projectIssuesPath, projectId, page } = domEl.dataset;
const router = createRouter();
const resolvers = {
......@@ -52,16 +52,19 @@ export default (selector) => {
const provide = {
projectPath,
alertId,
page,
projectIssuesPath,
projectId,
};
if (page === DEFAULT_PAGE) {
if (page === PAGE_CONFIG.OPERATIONS.TITLE) {
const { TRACK_ALERTS_DETAILS_VIEWS_OPTIONS, TRACK_ALERT_STATUS_UPDATE_OPTIONS } = PAGE_CONFIG[
page
];
provide.trackAlertsDetailsViewsOptions = TRACK_ALERTS_DETAILS_VIEWS_OPTIONS;
provide.trackAlertStatusUpdateOptions = TRACK_ALERT_STATUS_UPDATE_OPTIONS;
} else if (page === PAGE_CONFIG.THREAT_MONITORING.TITLE) {
provide.isThreatMonitoringPage = true;
}
// eslint-disable-next-line no-new
......
......@@ -20,7 +20,8 @@ module Projects::AlertManagementHelper
'alert-id' => alert_id,
'project-path' => project.full_path,
'project-id' => project.id,
'project-issues-path' => project_issues_path(project)
'project-issues-path' => project_issues_path(project),
'page' => 'OPERATIONS'
}
end
......
import AlertDetails from '~/vue_shared/alert_details';
AlertDetails('#js-alert_details');
......@@ -12,6 +12,7 @@ import {
import produce from 'immer';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { convertToSnakeCase } from '~/lib/utils/text_utility';
import { joinPaths } from '~/lib/utils/url_utility';
import getAlertsQuery from '~/graphql_shared/queries/get_alerts.query.graphql';
import { DEFAULT_FILTERS, FIELDS, MESSAGES, PAGE_SIZE, STATUSES, DOMAIN } from './constants';
import AlertFilters from './alert_filters.vue';
......@@ -129,6 +130,9 @@ export default {
handleStatusUpdate() {
this.$apollo.queries.alerts.refetch();
},
alertDetailsUrl({ iid }) {
return joinPaths(window.location.pathname, 'alerts', iid);
},
},
};
</script>
......@@ -179,13 +183,14 @@ export default {
</template>
<template #cell(alertLabel)="{ item }">
<div
class="gl-word-break-all"
<gl-link
class="gl-word-break-all gl-text-body!"
:title="`${item.iid} - ${item.title}`"
:href="alertDetailsUrl(item)"
data-testid="threat-alerts-id"
>
{{ item.title }}
</div>
</gl-link>
</template>
<template #cell(status)="{ item }">
......
......@@ -5,12 +5,18 @@ module Projects
include SecurityAndCompliancePermissions
before_action :authorize_read_threat_monitoring!
before_action do
push_frontend_feature_flag(:threat_monitoring_alerts, project)
end
feature_category :web_firewall
def alert_details
render_404 unless Feature.enabled?(:threat_monitoring_alerts, project)
@alert_id = params[:id]
end
def edit
@environment = project.environments.find(params[:environment_id])
@policy_name = params[:id]
......
......@@ -179,6 +179,7 @@ module EE
projects/threat_monitoring#show
projects/threat_monitoring#new
projects/threat_monitoring#edit
projects/threat_monitoring#alert_details
projects/audit_events#index
]
end
......
......@@ -14,6 +14,16 @@ module PolicyHelper
details.merge(edit_details)
end
def threat_monitoring_alert_details_data(project, alert_id)
{
'alert-id' => alert_id,
'project-path' => project.full_path,
'project-id' => project.id,
'project-issues-path' => project_issues_path(project),
'page' => 'THREAT_MONITORING'
}
end
private
def details(project)
......
- add_to_breadcrumbs s_('ThreatMonitoring|Threat Monitoring'), project_threat_monitoring_path(@project)
- page_title s_('ThreatMonitoring|Alert Details')
#js-alert_details{ data: threat_monitoring_alert_details_data(@project, @alert_id) }
---
title: Add route for threat monitoring alert details
merge_request: 51417
author:
type: added
......@@ -40,6 +40,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resources :subscriptions, only: [:create, :destroy]
resource :threat_monitoring, only: [:show], controller: :threat_monitoring do
get '/alerts/:id', action: 'alert_details'
resources :policies, only: [:new, :edit], controller: :threat_monitoring
end
......
......@@ -235,4 +235,68 @@ RSpec.describe Projects::ThreatMonitoringController do
end
end
end
describe 'GET threat monitoring alerts' do
subject { get :alert_details, params: { namespace_id: project.namespace, project_id: project, id: '5' } }
context 'with authorized user' do
before do
project.add_developer(user)
sign_in(user)
end
context 'when feature is available' do
before do
stub_licensed_features(threat_monitoring: true)
end
it 'renders the show template' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:alert_details)
end
end
context 'when feature is not available' do
before do
stub_licensed_features(threat_monitoring: true)
stub_feature_flags(threat_monitoring_alerts: false)
end
it 'returns 404' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'with unauthorized user' do
before do
sign_in(user)
end
context 'when feature is available' do
before do
stub_licensed_features(threat_monitoring: true)
end
it 'returns 404' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'with anonymous user' do
it 'returns 302' do
subject
expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to(new_user_session_path)
end
end
end
end
......@@ -134,6 +134,10 @@ describe('AlertsList component', () => {
expect(wrapper.vm.sort).toBe('STATUS_ASC');
expect(findStatusColumnHeader().attributes('aria-sort')).toBe('ascending');
});
it('navigates to the alert details page on title click', () => {
expect(findIdColumn().attributes('href')).toBe('/alerts/01');
});
});
describe('empty state', () => {
......
......@@ -57,4 +57,22 @@ RSpec.describe PolicyHelper do
end
end
end
describe '#policy_alert_details' do
let(:alert) { build(:alert_management_alert, project: project) }
context 'when a new alert is created' do
subject { helper.threat_monitoring_alert_details_data(project, alert.id) }
it 'returns expected policy data' do
expect(subject).to match({
'alert-id' => alert.id,
'project-path' => project.full_path,
'project-id' => project.id,
'project-issues-path' => project_issues_path(project),
'page' => 'THREAT_MONITORING'
})
end
end
end
end
......@@ -224,6 +224,7 @@ RSpec.describe ProjectsHelper do
projects/threat_monitoring#show
projects/threat_monitoring#new
projects/threat_monitoring#edit
projects/threat_monitoring#alert_details
projects/audit_events#index
]
end
......
......@@ -30214,6 +30214,9 @@ msgstr ""
msgid "Threat Monitoring"
msgstr ""
msgid "ThreatMonitoring|Alert Details"
msgstr ""
msgid "ThreatMonitoring|Alerts"
msgstr ""
......
......@@ -90,6 +90,7 @@ describe('AlertDetails', () => {
const findEnvironmentName = () => wrapper.findByTestId('environmentName');
const findEnvironmentPath = () => wrapper.findByTestId('environmentPath');
const findDetailsTable = () => wrapper.find(AlertDetailsTable);
const findMetricsTab = () => wrapper.findByTestId('metrics');
describe('Alert details', () => {
describe('when alert is null', () => {
......@@ -175,6 +176,15 @@ describe('AlertDetails', () => {
});
});
describe('Threat Monitoring details', () => {
it('should not render the metrics tab', () => {
mountComponent({
data: { alert: mockAlert, provide: { isThreatMonitoringPage: true } },
});
expect(findMetricsTab().exists()).toBe(false);
});
});
describe('Create incident from alert', () => {
it('should display "View incident" button that links the incident page when incident exists', () => {
const issueIid = '3';
......
......@@ -3,6 +3,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import AlertSidebar from '~/vue_shared/alert_details/components/alert_sidebar.vue';
import SidebarAssignees from '~/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue';
import SidebarStatus from '~/vue_shared/alert_details/components/sidebar/sidebar_status.vue';
import mockAlerts from '../mocks/alerts.json';
const mockAlert = mockAlerts[0];
......@@ -11,7 +12,12 @@ describe('Alert Details Sidebar', () => {
let wrapper;
let mock;
function mountComponent({ mountMethod = shallowMount, stubs = {}, alert = {} } = {}) {
function mountComponent({
mountMethod = shallowMount,
stubs = {},
alert = {},
provide = {},
} = {}) {
wrapper = mountMethod(AlertSidebar, {
data() {
return {
......@@ -24,6 +30,7 @@ describe('Alert Details Sidebar', () => {
provide: {
projectPath: 'projectPath',
projectId: '1',
...provide,
},
stubs,
mocks: {
......@@ -60,5 +67,29 @@ describe('Alert Details Sidebar', () => {
});
expect(wrapper.find(SidebarAssignees).exists()).toBe(true);
});
it('should render side bar status dropdown', () => {
mountComponent({
mountMethod: mount,
alert: mockAlert,
});
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);
});
});
});
......@@ -113,7 +113,8 @@ RSpec.describe Projects::AlertManagementHelper do
'alert-id' => alert_id,
'project-path' => project_path,
'project-id' => project_id,
'project-issues-path' => issues_path
'project-issues-path' => issues_path,
'page' => 'OPERATIONS'
)
end
end
......
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