Commit 1b420996 authored by Scott Hampton's avatar Scott Hampton

Merge branch '296713-threat-alert-assignee' into 'master'

Add assignee column to threat alert list

See merge request gitlab-org/gitlab!60340
parents f1a40767 c35157b7
<script> <script>
import { import {
GlAlert, GlAlert,
GlAvatar,
GlAvatarLink,
GlAvatarsInline,
GlIntersectionObserver, GlIntersectionObserver,
GlLoadingIcon, GlLoadingIcon,
GlTable, GlTable,
...@@ -39,6 +42,9 @@ export default { ...@@ -39,6 +42,9 @@ export default {
AlertStatus, AlertStatus,
AlertFilters, AlertFilters,
GlAlert, GlAlert,
GlAvatar,
GlAvatarLink,
GlAvatarsInline,
GlIntersectionObserver, GlIntersectionObserver,
GlLink, GlLink,
GlLoadingIcon, GlLoadingIcon,
...@@ -150,6 +156,9 @@ export default { ...@@ -150,6 +156,9 @@ export default {
handleStatusUpdate() { handleStatusUpdate() {
this.$apollo.queries.alerts.refetch(); this.$apollo.queries.alerts.refetch();
}, },
hasAssignees(assignees) {
return Boolean(assignees.nodes?.length);
},
alertDetailsUrl({ iid }) { alertDetailsUrl({ iid }) {
return joinPaths(window.location.pathname, 'alerts', iid); return joinPaths(window.location.pathname, 'alerts', iid);
}, },
...@@ -220,15 +229,45 @@ export default { ...@@ -220,15 +229,45 @@ export default {
</template> </template>
<template #cell(issue)="{ item }"> <template #cell(issue)="{ item }">
<gl-link <div data-testid="threat-alerts-issue">
v-if="item.issue" <gl-link
v-gl-tooltip v-if="item.issue"
:title="item.issue.title" v-gl-tooltip
data-testid="threat-alerts-issue" :title="item.issue.title"
:href="getIssueMeta(item).link" :href="getIssueMeta(item).link"
> >
#{{ item.issue.iid }} {{ getIssueMeta(item).state }} #{{ item.issue.iid }} {{ getIssueMeta(item).state }}
</gl-link> </gl-link>
<span v-else>-</span>
</div>
</template>
<template #cell(assignees)="{ item }">
<div class="gl-display-flex" data-testid="threat-alerts-assignee">
<gl-avatars-inline
v-if="hasAssignees(item.assignees)"
data-testid="assigneesField"
:avatars="item.assignees.nodes"
:collapsed="true"
:max-visible="4"
:avatar-size="24"
badge-tooltip-prop="name"
:badge-tooltip-max-chars="100"
>
<template #avatar="{ avatar }">
<gl-avatar-link
:key="avatar.username"
v-gl-tooltip
target="_blank"
:href="avatar.webUrl"
:title="avatar.name"
>
<gl-avatar :src="avatar.avatarUrl" :label="avatar.name" :size="24" />
</gl-avatar-link>
</template>
</gl-avatars-inline>
<span v-else class="gl-ml-3">-</span>
</div>
</template> </template>
<template #cell(status)="{ item }"> <template #cell(status)="{ item }">
......
...@@ -46,6 +46,11 @@ export const FIELDS = [ ...@@ -46,6 +46,11 @@ export const FIELDS = [
label: s__('ThreatMonitoring|Incident'), label: s__('ThreatMonitoring|Incident'),
thClass: 'gl-bg-white! gl-w-15p', thClass: 'gl-bg-white! gl-w-15p',
}, },
{
key: 'assignees',
label: __('Assignees'),
thClass: 'gl-bg-white! gl-w-10p gl-pointer-events-none',
},
{ {
key: 'status', key: 'status',
label: s__('ThreatMonitoring|Status'), label: s__('ThreatMonitoring|Status'),
......
---
title: Add assignee column to threat alert list
merge_request: 60340
author:
type: added
import { GlIntersectionObserver, GlSkeletonLoading } from '@gitlab/ui'; import { GlIntersectionObserver, GlSkeletonLoading } from '@gitlab/ui';
import { createLocalVue, mount } from '@vue/test-utils'; import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import AlertFilters from 'ee/threat_monitoring/components/alerts/alert_filters.vue'; import AlertFilters from 'ee/threat_monitoring/components/alerts/alert_filters.vue';
import AlertStatus from 'ee/threat_monitoring/components/alerts/alert_status.vue'; import AlertStatus from 'ee/threat_monitoring/components/alerts/alert_status.vue';
import AlertsList from 'ee/threat_monitoring/components/alerts/alerts_list.vue'; import AlertsList from 'ee/threat_monitoring/components/alerts/alerts_list.vue';
import { DEFAULT_FILTERS } from 'ee/threat_monitoring/components/alerts/constants'; import { DEFAULT_FILTERS } from 'ee/threat_monitoring/components/alerts/constants';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import getAlertsQuery from '~/graphql_shared/queries/get_alerts.query.graphql'; import getAlertsQuery from '~/graphql_shared/queries/get_alerts.query.graphql';
import { defaultQuerySpy, emptyQuerySpy, loadingQuerySpy } from '../../mocks/mock_apollo'; import { defaultQuerySpy, emptyQuerySpy, loadingQuerySpy } from '../../mocks/mock_apollo';
import { mockAlerts, mockPageInfo } from '../../mocks/mock_data'; import { mockAlerts, mockPageInfo } from '../../mocks/mock_data';
...@@ -38,6 +38,8 @@ describe('AlertsList component', () => { ...@@ -38,6 +38,8 @@ describe('AlertsList component', () => {
}; };
const findAlertFilters = () => wrapper.findComponent(AlertFilters); const findAlertFilters = () => wrapper.findComponent(AlertFilters);
const findAssigneeColumn = () => wrapper.findByTestId('threat-alerts-assignee');
const findAssigneeColumnAt = (id) => wrapper.findAllByTestId('threat-alerts-assignee').at(id);
const findUnconfiguredAlert = () => wrapper.findByTestId('threat-alerts-unconfigured'); const findUnconfiguredAlert = () => wrapper.findByTestId('threat-alerts-unconfigured');
const findErrorAlert = () => wrapper.findByTestId('threat-alerts-error'); const findErrorAlert = () => wrapper.findByTestId('threat-alerts-error');
const findStartedAtColumn = () => wrapper.findByTestId('threat-alerts-started-at'); const findStartedAtColumn = () => wrapper.findByTestId('threat-alerts-started-at');
...@@ -71,24 +73,22 @@ describe('AlertsList component', () => { ...@@ -71,24 +73,22 @@ describe('AlertsList component', () => {
}; };
} }
wrapper = extendedWrapper( wrapper = mountExtended(AlertsList, {
mount(AlertsList, { propsData: defaultProps,
propsData: defaultProps, provide: {
provide: { documentationPath: '#',
documentationPath: '#', projectPath: DEFAULT_PROJECT_PATH,
projectPath: DEFAULT_PROJECT_PATH, },
}, stubs: {
stubs: { AlertStatus: true,
AlertStatus: true, AlertFilters: true,
AlertFilters: true, GlAlert: true,
GlAlert: true, GlLoadingIcon: true,
GlLoadingIcon: true, GlIntersectionObserver: true,
GlIntersectionObserver: true, ...stubs,
...stubs, },
}, ...apolloOptions,
...apolloOptions, });
}),
);
}; };
afterEach(() => { afterEach(() => {
...@@ -126,12 +126,16 @@ describe('AlertsList component', () => { ...@@ -126,12 +126,16 @@ describe('AlertsList component', () => {
expect(wrapper.vm.filters).toEqual(newFilters); expect(wrapper.vm.filters).toEqual(newFilters);
}); });
it('does show all columns', () => { it.each`
expect(findStartedAtColumn().exists()).toBe(true); column | findColumn
expect(findIdColumn().exists()).toBe(true); ${'startedAt'} | ${findStartedAtColumn}
expect(findEventCountColumn().exists()).toBe(true); ${'id'} | ${findIdColumn}
expect(findIssueColumn().exists()).toBe(true); ${'eventCount'} | ${findEventCountColumn}
expect(findStatusColumn().exists()).toBe(true); ${'issue'} | ${findIssueColumn}
${'assignee'} | ${findAssigneeColumn}
${'status'} | ${findStatusColumn}
`('does show the $column column', ({ findColumn }) => {
expect(findColumn().exists()).toBe(true);
}); });
it('does not show the empty state', () => { it('does not show the empty state', () => {
...@@ -176,8 +180,8 @@ describe('AlertsList component', () => { ...@@ -176,8 +180,8 @@ describe('AlertsList component', () => {
}); });
describe('issue column', () => { describe('issue column', () => {
it('only displays text when an issue is created', () => { it('displays a "-" for an alert without an issue', () => {
expect(wrapper.findAllByTestId('threat-alerts-issue').length).toBe(2); expect(findIssueColumnAt(3).text()).toBe('-');
}); });
it.each` it.each`
...@@ -186,7 +190,7 @@ describe('AlertsList component', () => { ...@@ -186,7 +190,7 @@ describe('AlertsList component', () => {
${'when an issue is created and is closed'} | ${1} | ${'#6 (closed)'} | ${'/#/-/issues/incident/6'} ${'when an issue is created and is closed'} | ${1} | ${'#6 (closed)'} | ${'/#/-/issues/incident/6'}
`('displays the correct text $description', ({ id, text, link }) => { `('displays the correct text $description', ({ id, text, link }) => {
expect(findIssueColumnAt(id).text()).toBe(text); expect(findIssueColumnAt(id).text()).toBe(text);
expect(findIssueColumnAt(id).attributes('href')).toBe(link); expect(findIssueColumnAt(id).find('a').attributes('href')).toBe(link);
}); });
describe('gon.relative_url_root', () => { describe('gon.relative_url_root', () => {
...@@ -199,10 +203,34 @@ describe('AlertsList component', () => { ...@@ -199,10 +203,34 @@ describe('AlertsList component', () => {
}); });
it('creates the correct href when the gon.relative_url_root is set', () => { it('creates the correct href when the gon.relative_url_root is set', () => {
expect(findIssueColumnAt(0).attributes('href')).toBe('/test/#/-/issues/incident/5'); expect(findIssueColumnAt(0).find('a').attributes('href')).toBe(
'/test/#/-/issues/incident/5',
);
}); });
}); });
}); });
describe('assignee column', () => {
it('displays an avatar for an alert with an assignee', () => {
const index = 0;
const { name, avatarUrl, webUrl } = mockAlerts[index].assignees.nodes[0];
expect(findAssigneeColumnAt(index).find('a')).toBeDefined();
expect(findAssigneeColumnAt(index).find('a').attributes()).toMatchObject({
href: webUrl,
target: '_blank',
title: name,
});
expect(findAssigneeColumnAt(index).find('img')).toBeDefined();
expect(findAssigneeColumnAt(index).find('img').attributes()).toMatchObject({
src: avatarUrl,
label: name,
});
});
it('displays a "-" for an unassigned alert', () => {
expect(findAssigneeColumnAt(1).text()).toBe('-');
});
});
}); });
describe('empty state', () => { describe('empty state', () => {
......
...@@ -94,7 +94,16 @@ export const formattedMockNetworkPolicyStatisticsResponse = { ...@@ -94,7 +94,16 @@ export const formattedMockNetworkPolicyStatisticsResponse = {
export const mockAlerts = [ export const mockAlerts = [
{ {
iid: '01', iid: '01',
assignees: { nodes: [] }, assignees: {
nodes: [
{
name: 'Administrator',
username: 'root',
avatarUrl: '/test-avatar-url',
webUrl: 'https://gitlab:3443/root',
},
],
},
eventCount: '1', eventCount: '1',
issueIid: null, issueIid: null,
issue: { iid: '5', state: 'opened', title: 'Issue 01' }, issue: { iid: '5', state: 'opened', title: 'Issue 01' },
......
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