Improve direct user paging (#1471)
# What this PR does Direct user paging feature improvements ## Which issue(s) this PR fixes https://github.com/grafana/oncall/issues/1358 ## Checklist - [ ] Tests updated - [ ] Documentation added - [ ] `CHANGELOG.md` updated
This commit is contained in:
parent
15f6898426
commit
82a9f8a5e7
12 changed files with 278 additions and 104 deletions
|
|
@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
### Added
|
||||
|
||||
- Enable web overrides for Terraform-based schedules
|
||||
- Direct user paging improvements ([1358](https://github.com/grafana/oncall/issues/1358))
|
||||
|
||||
## v1.1.36 (2023-03-09)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
import { FormItem, FormItemType } from 'components/GForm/GForm.types';
|
||||
|
||||
export const manualAlertFormConfig: { name: string; fields: FormItem[] } = {
|
||||
name: 'Manual Alert Group',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: FormItemType.Input,
|
||||
label: 'Title',
|
||||
validation: { required: true },
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
type: FormItemType.TextArea,
|
||||
label: 'Description',
|
||||
validation: { required: true },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -5,13 +5,14 @@ import cn from 'classnames/bind';
|
|||
|
||||
import Block from 'components/GBlock/Block';
|
||||
import GForm from 'components/GForm/GForm';
|
||||
import { FormItem, FormItemType } from 'components/GForm/GForm.types';
|
||||
import Text from 'components/Text/Text';
|
||||
import EscalationVariants from 'containers/EscalationVariants/EscalationVariants';
|
||||
import { prepareForUpdate } from 'containers/EscalationVariants/EscalationVariants.helpers';
|
||||
import { Alert } from 'models/alertgroup/alertgroup.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
||||
import { manualAlertFormConfig } from './ManualAlertGroup.config';
|
||||
|
||||
import styles from './ManualAlertGroup.module.css';
|
||||
|
||||
interface ManualAlertGroupProps {
|
||||
|
|
@ -21,23 +22,6 @@ interface ManualAlertGroupProps {
|
|||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
const manualAlertFormConfig: { name: string; fields: FormItem[] } = {
|
||||
name: 'Manual Alert Group',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: FormItemType.Input,
|
||||
label: 'Title',
|
||||
validation: { required: true },
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
type: FormItemType.TextArea,
|
||||
label: 'Describe what is going on',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const ManualAlertGroup: FC<ManualAlertGroupProps> = (props) => {
|
||||
const store = useStore();
|
||||
const [userResponders, setUserResponders] = useState([]);
|
||||
|
|
|
|||
|
|
@ -13,12 +13,13 @@ interface PluginLinkProps {
|
|||
wrap?: boolean;
|
||||
children: any;
|
||||
query?: Record<string, any>;
|
||||
target?: string;
|
||||
}
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
const PluginLink: FC<PluginLinkProps> = (props) => {
|
||||
const { children, query, disabled, className, wrap = true } = props;
|
||||
const { children, query, disabled, className, wrap = true, target } = props;
|
||||
|
||||
const newPath = useMemo(() => getPathFromQueryParams(query), [query]);
|
||||
|
||||
|
|
@ -35,6 +36,7 @@ const PluginLink: FC<PluginLinkProps> = (props) => {
|
|||
|
||||
return (
|
||||
<Link
|
||||
target={target}
|
||||
onClick={handleClick}
|
||||
className={cx('root', className, { 'no-wrap': !wrap, root_disabled: disabled })}
|
||||
to={newPath}
|
||||
|
|
|
|||
|
|
@ -21,9 +21,17 @@
|
|||
overflow: scroll;
|
||||
}
|
||||
|
||||
.schedule-table {
|
||||
.table {
|
||||
height: 120px;
|
||||
overflow: auto;
|
||||
|
||||
& tr:hover {
|
||||
background: var(--background-secondary) !important;
|
||||
}
|
||||
|
||||
& tbody tr:nth-child(odd) {
|
||||
background: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.responders-filters {
|
||||
|
|
@ -32,6 +40,8 @@
|
|||
|
||||
.responder-item {
|
||||
cursor: pointer;
|
||||
width: 280px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.body {
|
||||
|
|
@ -43,12 +53,12 @@
|
|||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
|
||||
& > li .trash-button {
|
||||
& > li .hover-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& > li:hover .trash-button {
|
||||
display: block;
|
||||
& > li:hover .hover-button {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
& > li {
|
||||
|
|
@ -79,3 +89,17 @@
|
|||
background: #299c46;
|
||||
}
|
||||
}
|
||||
|
||||
.radio-buttons {
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
.select {
|
||||
width: 150px !important;
|
||||
}
|
||||
|
||||
.responder-name {
|
||||
max-width: 250px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
import React, { useState, useCallback } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { ToolbarButton, ButtonGroup, HorizontalGroup, Icon, Select, IconButton, Label } from '@grafana/ui';
|
||||
import { HorizontalGroup, Icon, Select, IconButton, Label, Tooltip, Button } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import dayjs from 'dayjs';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import Avatar from 'components/Avatar/Avatar';
|
||||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
import Text from 'components/Text/Text';
|
||||
import UserWarning from 'containers/UserWarningModal/UserWarning';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { getTzOffsetString } from 'models/timezone/timezone.helpers';
|
||||
import { User } from 'models/user/user.types';
|
||||
import { UserActions } from 'utils/authorization';
|
||||
|
||||
|
|
@ -24,7 +23,7 @@ const cx = cn.bind(styles);
|
|||
export interface EscalationVariantsProps {
|
||||
onUpdateEscalationVariants: (data: any) => void;
|
||||
value: { scheduleResponders; userResponders };
|
||||
variant?: 'default' | 'primary';
|
||||
variant?: 'secondary' | 'primary';
|
||||
hideSelected?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -124,29 +123,17 @@ const EscalationVariants = observer(
|
|||
</>
|
||||
)}
|
||||
<div className={cx('assign-responders-button')}>
|
||||
<ButtonGroup>
|
||||
<WithPermissionControlTooltip userAction={UserActions.AlertGroupsWrite}>
|
||||
<ToolbarButton
|
||||
icon="users-alt"
|
||||
variant={variant}
|
||||
onClick={() => {
|
||||
setShowEscalationVariants(true);
|
||||
}}
|
||||
>
|
||||
Add responders
|
||||
</ToolbarButton>
|
||||
</WithPermissionControlTooltip>
|
||||
<WithPermissionControlTooltip userAction={UserActions.AlertGroupsWrite}>
|
||||
<ToolbarButton
|
||||
isOpen={false}
|
||||
narrow
|
||||
variant={variant}
|
||||
onClick={() => {
|
||||
setShowEscalationVariants(true);
|
||||
}}
|
||||
/>
|
||||
</WithPermissionControlTooltip>
|
||||
</ButtonGroup>
|
||||
<WithPermissionControlTooltip userAction={UserActions.AlertGroupsWrite}>
|
||||
<Button
|
||||
icon="users-alt"
|
||||
variant={variant}
|
||||
onClick={() => {
|
||||
setShowEscalationVariants(true);
|
||||
}}
|
||||
>
|
||||
Add responders
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
</div>
|
||||
{showEscalationVariants && (
|
||||
<EscalationVariantsPopup
|
||||
|
|
@ -170,7 +157,17 @@ const EscalationVariants = observer(
|
|||
onUserSelect={(user: User) => {
|
||||
onUpdateEscalationVariants({
|
||||
...value,
|
||||
userResponders: [...value.userResponders, { type: ResponderType.User, data: user, important: false }],
|
||||
userResponders: [
|
||||
...value.userResponders,
|
||||
{
|
||||
type: ResponderType.User,
|
||||
data: user,
|
||||
important:
|
||||
user.notification_chain_verbal.important && !user.notification_chain_verbal.default
|
||||
? true
|
||||
: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
|
@ -188,20 +185,72 @@ const UserResponder = ({ important, data, onImportantChange, handleDelete }) =>
|
|||
<div className={cx('timeline-icon-background', { 'timeline-icon-background--green': true })}>
|
||||
<Avatar size="big" src={data?.avatar} />
|
||||
</div>
|
||||
<Text>
|
||||
{data?.username} ({getTzOffsetString(dayjs().tz(data?.timezone))})
|
||||
</Text>
|
||||
<Select
|
||||
isSearchable={false}
|
||||
value={Number(important)}
|
||||
options={[
|
||||
{ value: 1, label: 'Important' },
|
||||
{ value: 0, label: 'Default' },
|
||||
]}
|
||||
onChange={onImportantChange}
|
||||
<Text className={cx('responder-name')}>{data?.username}</Text>
|
||||
{data.notification_chain_verbal.default || data.notification_chain_verbal.important ? (
|
||||
<HorizontalGroup>
|
||||
<Text type="secondary">by</Text>
|
||||
<Select
|
||||
className={cx('select')}
|
||||
isSearchable={false}
|
||||
value={Number(important)}
|
||||
options={[
|
||||
{
|
||||
value: 0,
|
||||
label: 'Default',
|
||||
description: 'Use "Default notifications" from user\'s personal settings',
|
||||
},
|
||||
{
|
||||
value: 1,
|
||||
label: 'Important',
|
||||
description: 'Use "Important notifications" from user\'s personal settings',
|
||||
},
|
||||
]}
|
||||
// @ts-ignore
|
||||
isOptionDisabled={({ value }) =>
|
||||
(value === 0 && !data.notification_chain_verbal.default) ||
|
||||
(value === 1 && !data.notification_chain_verbal.important)
|
||||
}
|
||||
getOptionLabel={({ value, label }) => {
|
||||
return (
|
||||
<Text
|
||||
type={
|
||||
(value === 0 && !data.notification_chain_verbal.default) ||
|
||||
(value === 1 && !data.notification_chain_verbal.important)
|
||||
? 'disabled'
|
||||
: 'primary'
|
||||
}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
);
|
||||
}}
|
||||
onChange={onImportantChange}
|
||||
/>
|
||||
<Text type="secondary">notification chain</Text>
|
||||
</HorizontalGroup>
|
||||
) : (
|
||||
<HorizontalGroup>
|
||||
<Tooltip content="User doesn't have configured notification chains">
|
||||
<Icon name="exclamation-triangle" style={{ color: 'var(--error-text-color)' }} />
|
||||
</Tooltip>
|
||||
</HorizontalGroup>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup>
|
||||
<PluginLink className={cx('hover-button')} target="_blank" query={{ page: 'users', id: data.pk }}>
|
||||
<IconButton
|
||||
tooltip="Open user profile in new tab"
|
||||
style={{ color: 'var(--always-gray)' }}
|
||||
name="external-link-alt"
|
||||
/>
|
||||
</PluginLink>
|
||||
<IconButton
|
||||
tooltip="Remove responder"
|
||||
className={cx('hover-button')}
|
||||
name="trash-alt"
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
<IconButton className={cx('trash-button')} name="trash-alt" onClick={handleDelete} />
|
||||
</HorizontalGroup>
|
||||
</li>
|
||||
);
|
||||
|
|
@ -215,18 +264,39 @@ const ScheduleResponder = ({ important, data, onImportantChange, handleDelete })
|
|||
<div className={cx('timeline-icon-background')}>
|
||||
<Icon size="lg" name="calendar-alt" />
|
||||
</div>
|
||||
<Text>{data.name}</Text>
|
||||
<Text className={cx('responder-name')}>{data.name}</Text>
|
||||
<Text type="secondary">by</Text>
|
||||
<Select
|
||||
className={cx('select')}
|
||||
isSearchable={false}
|
||||
value={Number(important)}
|
||||
options={[
|
||||
{ value: 1, label: 'Important' },
|
||||
{ value: 0, label: 'Default' },
|
||||
{ value: 0, label: 'Default', description: 'Use "Default notifications" from users personal settings' },
|
||||
{
|
||||
value: 1,
|
||||
label: 'Important',
|
||||
description: 'Use "Important notifications" from users personal settings',
|
||||
},
|
||||
]}
|
||||
onChange={onImportantChange}
|
||||
/>
|
||||
<Text type="secondary">notification policies</Text>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup>
|
||||
<PluginLink className={cx('hover-button')} target="_blank" query={{ page: 'schedules', id: data.id }}>
|
||||
<IconButton
|
||||
tooltip="Open schedule in new tab"
|
||||
style={{ color: 'var(--always-gray)' }}
|
||||
name="external-link-alt"
|
||||
/>
|
||||
</PluginLink>
|
||||
<IconButton
|
||||
className={cx('hover-button')}
|
||||
tooltip="Remove responder"
|
||||
name="trash-alt"
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
<IconButton className={cx('trash-button')} name="trash-alt" onClick={handleDelete} />
|
||||
</HorizontalGroup>
|
||||
</li>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
import { Icon, RadioButtonGroup } from '@grafana/ui';
|
||||
import { Icon, Input, RadioButtonGroup } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import GTable from 'components/GTable/GTable';
|
||||
import SearchInput from 'components/SearchInput/SearchInput';
|
||||
import Text from 'components/Text/Text';
|
||||
import { EscalationVariantsProps } from 'containers/EscalationVariants/EscalationVariants';
|
||||
import styles from 'containers/EscalationVariants/EscalationVariants.module.scss';
|
||||
|
|
@ -40,6 +39,14 @@ const EscalationVariantsPopup = observer((props: EscalationVariantsPopupProps) =
|
|||
const [usersSearchTerm, setUsersSearchTerm] = useState('');
|
||||
const [schedulesSearchTerm, setSchedulesSearchTerm] = useState('');
|
||||
|
||||
const handleSetSchedulesSearchTerm = useCallback((e) => {
|
||||
setSchedulesSearchTerm(e.target.value);
|
||||
}, []);
|
||||
|
||||
const handleSetUsersSearchTerm = useCallback((e) => {
|
||||
setUsersSearchTerm(e.target.value);
|
||||
}, []);
|
||||
|
||||
const handleOptionChange = useCallback((option: string) => {
|
||||
setActiveOption(option);
|
||||
}, []);
|
||||
|
|
@ -142,42 +149,51 @@ const EscalationVariantsPopup = observer((props: EscalationVariantsPopupProps) =
|
|||
{ value: 'schedules', label: 'Schedules' },
|
||||
{ value: 'users', label: 'Users' },
|
||||
]}
|
||||
className={cx('radio-buttons')}
|
||||
value={activeOption}
|
||||
onChange={handleOptionChange}
|
||||
fullWidth
|
||||
/>
|
||||
{activeOption === 'schedules' && (
|
||||
<>
|
||||
<SearchInput
|
||||
<Input
|
||||
prefix={<Icon name="search" />}
|
||||
key="schedules search"
|
||||
className={cx('responders-filters')}
|
||||
value={schedulesSearchTerm}
|
||||
onChange={setSchedulesSearchTerm}
|
||||
placeholder="Search schedules..."
|
||||
// @ts-ignore
|
||||
width={'unset'}
|
||||
onChange={handleSetSchedulesSearchTerm}
|
||||
/>
|
||||
<GTable
|
||||
emptyText={store.scheduleStore.getSearchResult()?.results ? 'No schedules found' : 'Loading...'}
|
||||
rowKey="id"
|
||||
columns={scheduleColumns}
|
||||
data={store.scheduleStore.getSearchResult()?.results}
|
||||
className={cx('schedule-table')}
|
||||
className={cx('table')}
|
||||
showHeader={false}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{activeOption === 'users' && (
|
||||
<>
|
||||
<SearchInput
|
||||
<Input
|
||||
prefix={<Icon name="search" />}
|
||||
key="users search"
|
||||
// @ts-ignore
|
||||
width={'unset'}
|
||||
className={cx('responders-filters')}
|
||||
placeholder="Search users..."
|
||||
value={usersSearchTerm}
|
||||
onChange={setUsersSearchTerm}
|
||||
onChange={handleSetUsersSearchTerm}
|
||||
/>
|
||||
<GTable
|
||||
emptyText={store.userStore.getSearchResult()?.results ? 'No users found' : 'Loading...'}
|
||||
rowKey="id"
|
||||
columns={userColumns}
|
||||
data={store.userStore.getSearchResult()?.results}
|
||||
className={cx('schedule-table')}
|
||||
className={cx('table')}
|
||||
showHeader={false}
|
||||
/>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -20,3 +20,7 @@
|
|||
border-radius: 50%;
|
||||
background: #6ccf8e;
|
||||
}
|
||||
|
||||
.modal {
|
||||
width: 650px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,11 +86,11 @@ const UserWarning: FC<UserWarningProps> = (props) => {
|
|||
);
|
||||
|
||||
return (
|
||||
<Modal isOpen title="Add responder" onDismiss={onHide}>
|
||||
<Modal isOpen title="Add responder" onDismiss={onHide} className={cx('modal')}>
|
||||
<VerticalGroup className={cx('user-warning')}>
|
||||
{showUserHasNoNotificationPolicyWarning && (
|
||||
<HorizontalGroup>
|
||||
<Icon name="exclamation-triangle" style={{ color: 'orange' }} />
|
||||
<Icon name="exclamation-triangle" style={{ color: 'var(--error-text-color)' }} />
|
||||
<Text>
|
||||
<Text strong>{user.username}</Text> has no notification policy
|
||||
</Text>
|
||||
|
|
@ -129,7 +129,12 @@ const UserWarning: FC<UserWarningProps> = (props) => {
|
|||
</ul>
|
||||
)}
|
||||
<Text>
|
||||
Are you sure you want to select <Text strong>{user.username}</Text>?
|
||||
<HorizontalGroup>
|
||||
<Icon name="question-circle" />
|
||||
<Text>
|
||||
Are you sure you want to select <Text strong>{user.username}</Text>?
|
||||
</Text>
|
||||
</HorizontalGroup>
|
||||
</Text>
|
||||
<HorizontalGroup justify="flex-end">
|
||||
<Button variant="secondary" onClick={onHide}>
|
||||
|
|
|
|||
|
|
@ -181,9 +181,17 @@
|
|||
& > li {
|
||||
padding: 8px 12px;
|
||||
width: 100%;
|
||||
|
||||
& .hover-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
& > li:hover {
|
||||
background: var(--background-secondary);
|
||||
|
||||
& .hover-button {
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -373,7 +373,7 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
|
|||
|
||||
<HorizontalGroup>
|
||||
<EscalationVariants
|
||||
variant="default"
|
||||
variant="secondary"
|
||||
hideSelected
|
||||
value={prepareForEdit(incident.paged_users)}
|
||||
onUpdateEscalationVariants={this.handleAddResponders}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
|
||||
import { HorizontalGroup, IconButton } from '@grafana/ui';
|
||||
import { HorizontalGroup, Icon, IconButton, Tooltip } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import Avatar from 'components/Avatar/Avatar';
|
||||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
import Text from 'components/Text/Text';
|
||||
import WithConfirm from 'components/WithConfirm/WithConfirm';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { Alert } from 'models/alertgroup/alertgroup.types';
|
||||
import { User } from 'models/user/user.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { UserActions } from 'utils/authorization';
|
||||
|
||||
import styles from './../Incident.module.scss';
|
||||
|
|
@ -20,7 +23,7 @@ interface PagedUsersProps {
|
|||
onRemove: (id: User['pk']) => void;
|
||||
}
|
||||
|
||||
const PagedUsers = (props: PagedUsersProps) => {
|
||||
const PagedUsers = observer((props: PagedUsersProps) => {
|
||||
const { pagedUsers, onRemove } = props;
|
||||
|
||||
const getPagedUserRemoveHandler = useCallback((id: User['pk']) => {
|
||||
|
|
@ -29,6 +32,17 @@ const PagedUsers = (props: PagedUsersProps) => {
|
|||
};
|
||||
}, []);
|
||||
|
||||
const { userStore } = useStore();
|
||||
|
||||
useEffect(() => {
|
||||
pagedUsers &&
|
||||
pagedUsers.forEach((user) => {
|
||||
if (!userStore.items[user.pk]) {
|
||||
userStore.updateItem(user.pk);
|
||||
}
|
||||
});
|
||||
}, [pagedUsers]);
|
||||
|
||||
if (!pagedUsers || !pagedUsers.length) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -39,31 +53,58 @@ const PagedUsers = (props: PagedUsersProps) => {
|
|||
Current responders
|
||||
</Text.Title>
|
||||
<ul className={cx('paged-users-list')}>
|
||||
{pagedUsers.map((pagedUser) => (
|
||||
<li key={pagedUser.pk}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup>
|
||||
<Avatar size="big" src={pagedUser.avatar} />
|
||||
<Text strong>{pagedUser.username}</Text>
|
||||
{pagedUsers.map((pagedUser) => {
|
||||
const storeUser = userStore.items[pagedUser.pk];
|
||||
|
||||
return (
|
||||
<li key={pagedUser.pk}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup>
|
||||
<Avatar size="big" src={pagedUser.avatar} />
|
||||
<Text strong>{pagedUser.username}</Text>
|
||||
{Boolean(
|
||||
storeUser &&
|
||||
!storeUser.notification_chain_verbal.default &&
|
||||
!storeUser.notification_chain_verbal.important
|
||||
) && (
|
||||
<Tooltip content="User doesn't have configured notification chains">
|
||||
<Icon name="exclamation-triangle" style={{ color: 'var(--error-text-color)' }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup>
|
||||
<PluginLink
|
||||
className={cx('hover-button')}
|
||||
target="_blank"
|
||||
query={{ page: 'users', id: pagedUser.pk }}
|
||||
>
|
||||
<IconButton
|
||||
tooltip="Open user profile in new tab"
|
||||
style={{ color: 'var(--always-gray)' }}
|
||||
name="external-link-alt"
|
||||
/>
|
||||
</PluginLink>
|
||||
<WithPermissionControlTooltip userAction={UserActions.AlertGroupsWrite}>
|
||||
<WithConfirm
|
||||
title={`Are you sure to remove "${pagedUser.username}" from responders?`}
|
||||
confirmText="Remove"
|
||||
>
|
||||
<IconButton
|
||||
className={cx('hover-button')}
|
||||
onClick={getPagedUserRemoveHandler(pagedUser.pk)}
|
||||
tooltip="Remove from responders"
|
||||
name="trash-alt"
|
||||
/>
|
||||
</WithConfirm>
|
||||
</WithPermissionControlTooltip>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
<WithPermissionControlTooltip userAction={UserActions.AlertGroupsWrite}>
|
||||
<WithConfirm
|
||||
title={`Are you sure to remove "${pagedUser.username}" from responders?`}
|
||||
confirmText="Remove"
|
||||
>
|
||||
<IconButton
|
||||
onClick={getPagedUserRemoveHandler(pagedUser.pk)}
|
||||
tooltip="Remove from responders"
|
||||
name="trash-alt"
|
||||
/>
|
||||
</WithConfirm>
|
||||
</WithPermissionControlTooltip>
|
||||
</HorizontalGroup>
|
||||
</li>
|
||||
))}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default PagedUsers;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue