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 { ...@@ -83,6 +83,9 @@ export default {
alertId: { alertId: {
default: '', default: '',
}, },
isThreatMonitoringPage: {
default: false,
},
projectId: { projectId: {
default: '', default: '',
}, },
...@@ -364,7 +367,11 @@ export default { ...@@ -364,7 +367,11 @@ export default {
</alert-summary-row> </alert-summary-row>
<alert-details-table :alert="alert" :loading="loading" /> <alert-details-table :alert="alert" :loading="loading" />
</gl-tab> </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" /> <alert-metrics :dashboard-url="alert.metricsDashboardUrl" />
</gl-tab> </gl-tab>
<gl-tab :data-testid="$options.tabsConfig[2].id" :title="$options.tabsConfig[2].title"> <gl-tab :data-testid="$options.tabsConfig[2].id" :title="$options.tabsConfig[2].title">
......
...@@ -19,6 +19,10 @@ export default { ...@@ -19,6 +19,10 @@ 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: {
...@@ -62,6 +66,7 @@ export default { ...@@ -62,6 +66,7 @@ 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')"
......
...@@ -9,11 +9,10 @@ export const SEVERITY_LEVELS = { ...@@ -9,11 +9,10 @@ export const SEVERITY_LEVELS = {
UNKNOWN: s__('severity|Unknown'), UNKNOWN: s__('severity|Unknown'),
}; };
export const DEFAULT_PAGE = 'OPERATIONS';
/* eslint-disable @gitlab/require-i18n-strings */ /* eslint-disable @gitlab/require-i18n-strings */
export const PAGE_CONFIG = { export const PAGE_CONFIG = {
OPERATIONS: { OPERATIONS: {
TITLE: 'OPERATIONS',
// 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',
...@@ -26,4 +25,7 @@ export const PAGE_CONFIG = { ...@@ -26,4 +25,7 @@ export const PAGE_CONFIG = {
label: 'Status', label: 'Status',
}, },
}, },
THREAT_MONITORING: {
TITLE: 'THREAT_MONITORING',
},
}; };
...@@ -6,13 +6,13 @@ import createDefaultClient from '~/lib/graphql'; ...@@ -6,13 +6,13 @@ import createDefaultClient from '~/lib/graphql';
import AlertDetails from './components/alert_details.vue'; import AlertDetails from './components/alert_details.vue';
import sidebarStatusQuery from './graphql/queries/alert_sidebar_status.query.graphql'; import sidebarStatusQuery from './graphql/queries/alert_sidebar_status.query.graphql';
import createRouter from './router'; import createRouter from './router';
import { DEFAULT_PAGE, PAGE_CONFIG } from './constants'; import { PAGE_CONFIG } from './constants';
Vue.use(VueApollo); Vue.use(VueApollo);
export default (selector) => { export default (selector) => {
const domEl = document.querySelector(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 router = createRouter();
const resolvers = { const resolvers = {
...@@ -52,16 +52,19 @@ export default (selector) => { ...@@ -52,16 +52,19 @@ export default (selector) => {
const provide = { const provide = {
projectPath, projectPath,
alertId, alertId,
page,
projectIssuesPath, projectIssuesPath,
projectId, projectId,
}; };
if (page === DEFAULT_PAGE) { if (page === PAGE_CONFIG.OPERATIONS.TITLE) {
const { TRACK_ALERTS_DETAILS_VIEWS_OPTIONS, TRACK_ALERT_STATUS_UPDATE_OPTIONS } = PAGE_CONFIG[ const { TRACK_ALERTS_DETAILS_VIEWS_OPTIONS, TRACK_ALERT_STATUS_UPDATE_OPTIONS } = PAGE_CONFIG[
page page
]; ];
provide.trackAlertsDetailsViewsOptions = TRACK_ALERTS_DETAILS_VIEWS_OPTIONS; provide.trackAlertsDetailsViewsOptions = TRACK_ALERTS_DETAILS_VIEWS_OPTIONS;
provide.trackAlertStatusUpdateOptions = TRACK_ALERT_STATUS_UPDATE_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 // eslint-disable-next-line no-new
......
...@@ -20,7 +20,8 @@ module Projects::AlertManagementHelper ...@@ -20,7 +20,8 @@ module Projects::AlertManagementHelper
'alert-id' => alert_id, 'alert-id' => alert_id,
'project-path' => project.full_path, 'project-path' => project.full_path,
'project-id' => project.id, 'project-id' => project.id,
'project-issues-path' => project_issues_path(project) 'project-issues-path' => project_issues_path(project),
'page' => 'OPERATIONS'
} }
end end
......
import AlertDetails from '~/vue_shared/alert_details';
AlertDetails('#js-alert_details');
...@@ -12,6 +12,7 @@ import { ...@@ -12,6 +12,7 @@ import {
import produce from 'immer'; import produce from 'immer';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { convertToSnakeCase } from '~/lib/utils/text_utility'; 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 getAlertsQuery from '~/graphql_shared/queries/get_alerts.query.graphql';
import { DEFAULT_FILTERS, FIELDS, MESSAGES, PAGE_SIZE, STATUSES, DOMAIN } from './constants'; import { DEFAULT_FILTERS, FIELDS, MESSAGES, PAGE_SIZE, STATUSES, DOMAIN } from './constants';
import AlertFilters from './alert_filters.vue'; import AlertFilters from './alert_filters.vue';
...@@ -129,6 +130,9 @@ export default { ...@@ -129,6 +130,9 @@ export default {
handleStatusUpdate() { handleStatusUpdate() {
this.$apollo.queries.alerts.refetch(); this.$apollo.queries.alerts.refetch();
}, },
alertDetailsUrl({ iid }) {
return joinPaths(window.location.pathname, 'alerts', iid);
},
}, },
}; };
</script> </script>
...@@ -179,13 +183,14 @@ export default { ...@@ -179,13 +183,14 @@ export default {
</template> </template>
<template #cell(alertLabel)="{ item }"> <template #cell(alertLabel)="{ item }">
<div <gl-link
class="gl-word-break-all" class="gl-word-break-all gl-text-body!"
:title="`${item.iid} - ${item.title}`" :title="`${item.iid} - ${item.title}`"
:href="alertDetailsUrl(item)"
data-testid="threat-alerts-id" data-testid="threat-alerts-id"
> >
{{ item.title }} {{ item.title }}
</div> </gl-link>
</template> </template>
<template #cell(status)="{ item }"> <template #cell(status)="{ item }">
......
...@@ -5,12 +5,18 @@ module Projects ...@@ -5,12 +5,18 @@ module Projects
include SecurityAndCompliancePermissions include SecurityAndCompliancePermissions
before_action :authorize_read_threat_monitoring! before_action :authorize_read_threat_monitoring!
before_action do before_action do
push_frontend_feature_flag(:threat_monitoring_alerts, project) push_frontend_feature_flag(:threat_monitoring_alerts, project)
end end
feature_category :web_firewall feature_category :web_firewall
def alert_details
render_404 unless Feature.enabled?(:threat_monitoring_alerts, project)
@alert_id = params[:id]
end
def edit def edit
@environment = project.environments.find(params[:environment_id]) @environment = project.environments.find(params[:environment_id])
@policy_name = params[:id] @policy_name = params[:id]
......
...@@ -179,6 +179,7 @@ module EE ...@@ -179,6 +179,7 @@ module EE
projects/threat_monitoring#show projects/threat_monitoring#show
projects/threat_monitoring#new projects/threat_monitoring#new
projects/threat_monitoring#edit projects/threat_monitoring#edit
projects/threat_monitoring#alert_details
projects/audit_events#index projects/audit_events#index
] ]
end end
......
...@@ -14,6 +14,16 @@ module PolicyHelper ...@@ -14,6 +14,16 @@ module PolicyHelper
details.merge(edit_details) details.merge(edit_details)
end 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 private
def details(project) 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 ...@@ -40,6 +40,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resources :subscriptions, only: [:create, :destroy] resources :subscriptions, only: [:create, :destroy]
resource :threat_monitoring, only: [:show], controller: :threat_monitoring do resource :threat_monitoring, only: [:show], controller: :threat_monitoring do
get '/alerts/:id', action: 'alert_details'
resources :policies, only: [:new, :edit], controller: :threat_monitoring resources :policies, only: [:new, :edit], controller: :threat_monitoring
end end
......
...@@ -235,4 +235,68 @@ RSpec.describe Projects::ThreatMonitoringController do ...@@ -235,4 +235,68 @@ RSpec.describe Projects::ThreatMonitoringController do
end end
end 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 end
...@@ -134,6 +134,10 @@ describe('AlertsList component', () => { ...@@ -134,6 +134,10 @@ describe('AlertsList component', () => {
expect(wrapper.vm.sort).toBe('STATUS_ASC'); expect(wrapper.vm.sort).toBe('STATUS_ASC');
expect(findStatusColumnHeader().attributes('aria-sort')).toBe('ascending'); 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', () => { describe('empty state', () => {
......
...@@ -57,4 +57,22 @@ RSpec.describe PolicyHelper do ...@@ -57,4 +57,22 @@ RSpec.describe PolicyHelper do
end end
end 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 end
...@@ -224,6 +224,7 @@ RSpec.describe ProjectsHelper do ...@@ -224,6 +224,7 @@ RSpec.describe ProjectsHelper do
projects/threat_monitoring#show projects/threat_monitoring#show
projects/threat_monitoring#new projects/threat_monitoring#new
projects/threat_monitoring#edit projects/threat_monitoring#edit
projects/threat_monitoring#alert_details
projects/audit_events#index projects/audit_events#index
] ]
end end
......
...@@ -30214,6 +30214,9 @@ msgstr "" ...@@ -30214,6 +30214,9 @@ msgstr ""
msgid "Threat Monitoring" msgid "Threat Monitoring"
msgstr "" msgstr ""
msgid "ThreatMonitoring|Alert Details"
msgstr ""
msgid "ThreatMonitoring|Alerts" msgid "ThreatMonitoring|Alerts"
msgstr "" msgstr ""
......
...@@ -90,6 +90,7 @@ describe('AlertDetails', () => { ...@@ -90,6 +90,7 @@ describe('AlertDetails', () => {
const findEnvironmentName = () => wrapper.findByTestId('environmentName'); const findEnvironmentName = () => wrapper.findByTestId('environmentName');
const findEnvironmentPath = () => wrapper.findByTestId('environmentPath'); const findEnvironmentPath = () => wrapper.findByTestId('environmentPath');
const findDetailsTable = () => wrapper.find(AlertDetailsTable); const findDetailsTable = () => wrapper.find(AlertDetailsTable);
const findMetricsTab = () => wrapper.findByTestId('metrics');
describe('Alert details', () => { describe('Alert details', () => {
describe('when alert is null', () => { describe('when alert is null', () => {
...@@ -175,6 +176,15 @@ describe('AlertDetails', () => { ...@@ -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', () => { describe('Create incident from alert', () => {
it('should display "View incident" button that links the incident page when incident exists', () => { it('should display "View incident" button that links the incident page when incident exists', () => {
const issueIid = '3'; const issueIid = '3';
......
...@@ -3,6 +3,7 @@ import axios from 'axios'; ...@@ -3,6 +3,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import AlertSidebar from '~/vue_shared/alert_details/components/alert_sidebar.vue'; import AlertSidebar from '~/vue_shared/alert_details/components/alert_sidebar.vue';
import SidebarAssignees from '~/vue_shared/alert_details/components/sidebar/sidebar_assignees.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'; import mockAlerts from '../mocks/alerts.json';
const mockAlert = mockAlerts[0]; const mockAlert = mockAlerts[0];
...@@ -11,7 +12,12 @@ describe('Alert Details Sidebar', () => { ...@@ -11,7 +12,12 @@ describe('Alert Details Sidebar', () => {
let wrapper; let wrapper;
let mock; let mock;
function mountComponent({ mountMethod = shallowMount, stubs = {}, alert = {} } = {}) { function mountComponent({
mountMethod = shallowMount,
stubs = {},
alert = {},
provide = {},
} = {}) {
wrapper = mountMethod(AlertSidebar, { wrapper = mountMethod(AlertSidebar, {
data() { data() {
return { return {
...@@ -24,6 +30,7 @@ describe('Alert Details Sidebar', () => { ...@@ -24,6 +30,7 @@ describe('Alert Details Sidebar', () => {
provide: { provide: {
projectPath: 'projectPath', projectPath: 'projectPath',
projectId: '1', projectId: '1',
...provide,
}, },
stubs, stubs,
mocks: { mocks: {
...@@ -60,5 +67,29 @@ describe('Alert Details Sidebar', () => { ...@@ -60,5 +67,29 @@ describe('Alert Details Sidebar', () => {
}); });
expect(wrapper.find(SidebarAssignees).exists()).toBe(true); 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 ...@@ -113,7 +113,8 @@ RSpec.describe Projects::AlertManagementHelper do
'alert-id' => alert_id, 'alert-id' => alert_id,
'project-path' => project_path, 'project-path' => project_path,
'project-id' => project_id, 'project-id' => project_id,
'project-issues-path' => issues_path 'project-issues-path' => issues_path,
'page' => 'OPERATIONS'
) )
end end
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