Commit feb6f8d8 authored by Illya Klymov's avatar Illya Klymov

Merge branch '223846-alert-assignee-avatar' into 'master'

Alert Management assignee avatar

Closes #223846

See merge request gitlab-org/gitlab!40275
parents 125fba58 82e56d7d
......@@ -4,6 +4,9 @@ import {
GlLoadingIcon,
GlTable,
GlAlert,
GlAvatarsInline,
GlAvatarLink,
GlAvatar,
GlIcon,
GlLink,
GlTabs,
......@@ -12,6 +15,7 @@ import {
GlPagination,
GlSearchBoxByType,
GlSprintf,
GlTooltipDirective,
} from '@gitlab/ui';
import { debounce, trim } from 'lodash';
import { __, s__ } from '~/locale';
......@@ -57,6 +61,7 @@ export default {
"AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear.",
),
searchPlaceholder: __('Search or filter results...'),
unassigned: __('Unassigned'),
},
fields: [
{
......@@ -113,6 +118,9 @@ export default {
GlLoadingIcon,
GlTable,
GlAlert,
GlAvatarsInline,
GlAvatarLink,
GlAvatar,
TimeAgo,
GlIcon,
GlLink,
......@@ -124,6 +132,9 @@ export default {
GlSprintf,
AlertStatus,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
projectPath: {
type: String,
......@@ -267,11 +278,8 @@ export default {
const { category, action, label } = trackAlertStatusUpdateOptions;
Tracking.event(category, action, { label, property: status });
},
getAssignees(assignees) {
// TODO: Update to show list of assignee(s) after https://gitlab.com/gitlab-org/gitlab/-/issues/218405
return assignees.nodes?.length > 0
? assignees.nodes[0]?.username
: s__('AlertManagement|Unassigned');
hasAssignees(assignees) {
return Boolean(assignees.nodes?.length);
},
getIssueLink(item) {
return joinPaths('/', this.projectPath, '-', 'issues', item.issueIid);
......@@ -418,8 +426,32 @@ export default {
</template>
<template #cell(assignees)="{ item }">
<div class="gl-max-w-full text-truncate" data-testid="assigneesField">
{{ getAssignees(item.assignees) }}
<div data-testid="assigneesField">
<template v-if="hasAssignees(item.assignees)">
<gl-avatars-inline
: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>
</template>
<template v-else>
{{ $options.i18n.unassigned }}
</template>
</div>
</template>
......
......@@ -12,7 +12,7 @@ import {
} from '@gitlab/ui';
import { debounce } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import { s__, __ } from '~/locale';
import { s__ } from '~/locale';
import alertSetAssignees from '../../graphql/mutations/alert_set_assignees.mutation.graphql';
import SidebarAssignee from './sidebar_assignee.vue';
......@@ -82,8 +82,11 @@ export default {
userName() {
return this.alert?.assignees?.nodes[0]?.username;
},
assignedUser() {
return this.userName || __('None');
userFullName() {
return this.alert?.assignees?.nodes[0]?.name;
},
userImg() {
return this.alert?.assignees?.nodes[0]?.avatarUrl;
},
sortedUsers() {
return this.users
......@@ -184,15 +187,15 @@ export default {
</script>
<template>
<div class="block alert-status">
<div ref="status" class="sidebar-collapsed-icon" @click="$emit('toggle-sidebar')">
<div class="block alert-assignees ">
<div ref="assignees" class="sidebar-collapsed-icon" @click="$emit('toggle-sidebar')">
<gl-icon name="user" :size="14" />
<gl-loading-icon v-if="isUpdating" />
</div>
<gl-tooltip :target="() => $refs.status" boundary="viewport" placement="left">
<gl-tooltip :target="() => $refs.assignees" boundary="viewport" placement="left">
<gl-sprintf :message="$options.i18n.ASSIGNEES_BLOCK">
<template #assignees>
{{ assignedUser }}
{{ userName }}
</template>
</gl-sprintf>
</gl-tooltip>
......@@ -215,7 +218,7 @@ export default {
<div class="dropdown dropdown-menu-selectable" :class="dropdownClass">
<gl-deprecated-dropdown
ref="dropdown"
:text="assignedUser"
:text="userName"
class="w-100"
toggle-class="dropdown-menu-toggle"
variant="outline-default"
......@@ -272,14 +275,28 @@ export default {
</div>
<gl-loading-icon v-if="isUpdating" :inline="true" />
<p v-else-if="!isDropdownShowing" class="value gl-m-0" :class="{ 'no-value': !userName }">
<span v-if="userName" class="gl-text-gray-500" data-testid="assigned-users">{{
assignedUser
}}</span>
<span v-else class="gl-display-flex gl-align-items-center">
<div v-else-if="!isDropdownShowing" class="value gl-m-0" :class="{ 'no-value': !userName }">
<div v-if="userName" class="gl-display-inline-flex gl-mt-2" data-testid="assigned-users">
<span class="gl-relative mr-2">
<img
:alt="userName"
:src="userImg"
:width="32"
class="avatar avatar-inline gl-m-0 s32"
data-qa-selector="avatar_image"
/>
</span>
<span class="gl-display-flex gl-flex-direction-column gl-overflow-hidden">
<strong class="dropdown-menu-user-full-name">
{{ userFullName }}
</strong>
<span class="dropdown-menu-user-username">{{ userName }}</span>
</span>
</div>
<span v-else class="gl-display-flex gl-align-items-center gl-line-height-normal">
{{ __('None') }} -
<gl-button
class="gl-pl-2"
class="gl-ml-2"
href="#"
variant="link"
data-testid="unassigned-users"
......@@ -288,7 +305,7 @@ export default {
{{ __('assign yourself') }}
</gl-button>
</span>
</p>
</div>
</div>
</div>
</template>
......@@ -8,7 +8,10 @@ fragment AlertListItem on AlertManagementAlert {
issueIid
assignees {
nodes {
name
username
avatarUrl
webUrl
}
}
}
......@@ -10,6 +10,9 @@ mutation alertSetAssignees($projectPath: ID!, $assigneeUsernames: [String!]!, $i
assignees {
nodes {
username
name
avatarUrl
webUrl
}
}
notes {
......
---
title: Add Alert Management assignee avatar for list and details view
merge_request: 40275
author:
type: changed
......@@ -2252,9 +2252,6 @@ msgstr ""
msgid "AlertManagement|Triggered"
msgstr ""
msgid "AlertManagement|Unassigned"
msgstr ""
msgid "AlertManagement|Unknown"
msgstr ""
......
......@@ -11,6 +11,7 @@ import {
GlBadge,
GlPagination,
GlSearchBoxByType,
GlAvatar,
} from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import { visitUrl } from '~/lib/utils/url_utility';
......@@ -268,18 +269,22 @@ describe('AlertManagementTable', () => {
).toBe('Unassigned');
});
it('renders username(s) when assignee(s) present', () => {
it('renders user avatar when assignee present', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
loading: false,
});
expect(
findAssignees()
.at(1)
.text(),
).toBe(mockAlerts[1].assignees.nodes[0].username);
const avatar = findAssignees()
.at(1)
.find(GlAvatar);
const { src, label } = avatar.attributes();
const { name, avatarUrl } = mockAlerts[1].assignees.nodes[0];
expect(avatar.exists()).toBe(true);
expect(label).toBe(name);
expect(src).toBe(avatarUrl);
});
it('navigates to the detail page when alert row is clicked', () => {
......
......@@ -56,6 +56,9 @@ describe('Alert Details Sidebar Assignees', () => {
mock.restore();
});
const findAssigned = () => wrapper.find('[data-testid="assigned-users"]');
const findUnassigned = () => wrapper.find('[data-testid="unassigned-users"]');
describe('updating the alert status', () => {
const mockUpdatedMutationResult = {
data: {
......@@ -100,28 +103,26 @@ describe('Alert Details Sidebar Assignees', () => {
});
});
it('renders a unassigned option', () => {
it('renders a unassigned option', async () => {
wrapper.setData({ isDropdownSearching: false });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find(GlDeprecatedDropdownItem).text()).toBe('Unassigned');
});
await wrapper.vm.$nextTick();
expect(wrapper.find(GlDeprecatedDropdownItem).text()).toBe('Unassigned');
});
it('calls `$apollo.mutate` with `AlertSetAssignees` mutation and variables containing `iid`, `assigneeUsernames`, & `projectPath`', () => {
it('calls `$apollo.mutate` with `AlertSetAssignees` mutation and variables containing `iid`, `assigneeUsernames`, & `projectPath`', async () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult);
wrapper.setData({ isDropdownSearching: false });
return wrapper.vm.$nextTick().then(() => {
wrapper.find(SidebarAssignee).vm.$emit('update-alert-assignees', 'root');
await wrapper.vm.$nextTick();
wrapper.find(SidebarAssignee).vm.$emit('update-alert-assignees', 'root');
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: AlertSetAssignees,
variables: {
iid: '1527542',
assigneeUsernames: ['root'],
projectPath: 'projectPath',
},
});
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: AlertSetAssignees,
variables: {
iid: '1527542',
assigneeUsernames: ['root'],
projectPath: 'projectPath',
},
});
});
......@@ -151,7 +152,34 @@ describe('Alert Details Sidebar Assignees', () => {
it('stops updating and cancels loading when the request fails', () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error()));
wrapper.vm.updateAlertAssignees('root');
expect(wrapper.find('[data-testid="unassigned-users"]').text()).toBe('assign yourself');
expect(findUnassigned().text()).toBe('assign yourself');
});
it('shows a user avatar, username and full name when a user is set', () => {
mountComponent({
data: { alert: mockAlerts[1] },
sidebarCollapsed: false,
loading: false,
stubs: {
SidebarAssignee,
},
});
expect(
findAssigned()
.find('img')
.attributes('src'),
).toBe('/url');
expect(
findAssigned()
.find('.dropdown-menu-user-full-name')
.text(),
).toBe('root');
expect(
findAssigned()
.find('.dropdown-menu-user-username')
.text(),
).toBe('root');
});
});
});
......@@ -20,7 +20,7 @@
"startedAt": "2020-04-17T23:18:14.996Z",
"endedAt": "2020-04-17T23:18:14.996Z",
"status": "ACKNOWLEDGED",
"assignees": { "nodes": [{ "username": "root" }] },
"assignees": { "nodes": [{ "username": "root", "avatarUrl": "/url", "name": "root" }] },
"issueIid": "1",
"notes": {
"nodes": [
......@@ -49,7 +49,7 @@
"startedAt": "2020-04-17T23:18:14.996Z",
"endedAt": "2020-04-17T23:18:14.996Z",
"status": "RESOLVED",
"assignees": { "nodes": [{ "username": "root" }] },
"assignees": { "nodes": [{ "username": "root", "avatarUrl": "/url", "name": "root" }] },
"notes": {
"nodes": [
{
......
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