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>
import {
GlAlert,
GlAvatar,
GlAvatarLink,
GlAvatarsInline,
GlIntersectionObserver,
GlLoadingIcon,
GlTable,
......@@ -39,6 +42,9 @@ export default {
AlertStatus,
AlertFilters,
GlAlert,
GlAvatar,
GlAvatarLink,
GlAvatarsInline,
GlIntersectionObserver,
GlLink,
GlLoadingIcon,
......@@ -150,6 +156,9 @@ export default {
handleStatusUpdate() {
this.$apollo.queries.alerts.refetch();
},
hasAssignees(assignees) {
return Boolean(assignees.nodes?.length);
},
alertDetailsUrl({ iid }) {
return joinPaths(window.location.pathname, 'alerts', iid);
},
......@@ -220,15 +229,45 @@ export default {
</template>
<template #cell(issue)="{ item }">
<gl-link
v-if="item.issue"
v-gl-tooltip
:title="item.issue.title"
data-testid="threat-alerts-issue"
:href="getIssueMeta(item).link"
>
#{{ item.issue.iid }} {{ getIssueMeta(item).state }}
</gl-link>
<div data-testid="threat-alerts-issue">
<gl-link
v-if="item.issue"
v-gl-tooltip
:title="item.issue.title"
:href="getIssueMeta(item).link"
>
#{{ item.issue.iid }} {{ getIssueMeta(item).state }}
</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 #cell(status)="{ item }">
......
......@@ -46,6 +46,11 @@ export const FIELDS = [
label: s__('ThreatMonitoring|Incident'),
thClass: 'gl-bg-white! gl-w-15p',
},
{
key: 'assignees',
label: __('Assignees'),
thClass: 'gl-bg-white! gl-w-10p gl-pointer-events-none',
},
{
key: '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 { createLocalVue, mount } from '@vue/test-utils';
import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import AlertFilters from 'ee/threat_monitoring/components/alerts/alert_filters.vue';
import AlertStatus from 'ee/threat_monitoring/components/alerts/alert_status.vue';
import AlertsList from 'ee/threat_monitoring/components/alerts/alerts_list.vue';
import { DEFAULT_FILTERS } from 'ee/threat_monitoring/components/alerts/constants';
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 { defaultQuerySpy, emptyQuerySpy, loadingQuerySpy } from '../../mocks/mock_apollo';
import { mockAlerts, mockPageInfo } from '../../mocks/mock_data';
......@@ -38,6 +38,8 @@ describe('AlertsList component', () => {
};
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 findErrorAlert = () => wrapper.findByTestId('threat-alerts-error');
const findStartedAtColumn = () => wrapper.findByTestId('threat-alerts-started-at');
......@@ -71,24 +73,22 @@ describe('AlertsList component', () => {
};
}
wrapper = extendedWrapper(
mount(AlertsList, {
propsData: defaultProps,
provide: {
documentationPath: '#',
projectPath: DEFAULT_PROJECT_PATH,
},
stubs: {
AlertStatus: true,
AlertFilters: true,
GlAlert: true,
GlLoadingIcon: true,
GlIntersectionObserver: true,
...stubs,
},
...apolloOptions,
}),
);
wrapper = mountExtended(AlertsList, {
propsData: defaultProps,
provide: {
documentationPath: '#',
projectPath: DEFAULT_PROJECT_PATH,
},
stubs: {
AlertStatus: true,
AlertFilters: true,
GlAlert: true,
GlLoadingIcon: true,
GlIntersectionObserver: true,
...stubs,
},
...apolloOptions,
});
};
afterEach(() => {
......@@ -126,12 +126,16 @@ describe('AlertsList component', () => {
expect(wrapper.vm.filters).toEqual(newFilters);
});
it('does show all columns', () => {
expect(findStartedAtColumn().exists()).toBe(true);
expect(findIdColumn().exists()).toBe(true);
expect(findEventCountColumn().exists()).toBe(true);
expect(findIssueColumn().exists()).toBe(true);
expect(findStatusColumn().exists()).toBe(true);
it.each`
column | findColumn
${'startedAt'} | ${findStartedAtColumn}
${'id'} | ${findIdColumn}
${'eventCount'} | ${findEventCountColumn}
${'issue'} | ${findIssueColumn}
${'assignee'} | ${findAssigneeColumn}
${'status'} | ${findStatusColumn}
`('does show the $column column', ({ findColumn }) => {
expect(findColumn().exists()).toBe(true);
});
it('does not show the empty state', () => {
......@@ -176,8 +180,8 @@ describe('AlertsList component', () => {
});
describe('issue column', () => {
it('only displays text when an issue is created', () => {
expect(wrapper.findAllByTestId('threat-alerts-issue').length).toBe(2);
it('displays a "-" for an alert without an issue', () => {
expect(findIssueColumnAt(3).text()).toBe('-');
});
it.each`
......@@ -186,7 +190,7 @@ describe('AlertsList component', () => {
${'when an issue is created and is closed'} | ${1} | ${'#6 (closed)'} | ${'/#/-/issues/incident/6'}
`('displays the correct text $description', ({ id, text, link }) => {
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', () => {
......@@ -199,10 +203,34 @@ describe('AlertsList component', () => {
});
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', () => {
......
......@@ -94,7 +94,16 @@ export const formattedMockNetworkPolicyStatisticsResponse = {
export const mockAlerts = [
{
iid: '01',
assignees: { nodes: [] },
assignees: {
nodes: [
{
name: 'Administrator',
username: 'root',
avatarUrl: '/test-avatar-url',
webUrl: 'https://gitlab:3443/root',
},
],
},
eventCount: '1',
issueIid: null,
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