Skip to content

Commit

Permalink
Alert redesign #2 - Destinations
Browse files Browse the repository at this point in the history
  • Loading branch information
ranbena committed Sep 23, 2019
1 parent f994dc2 commit bfae2d9
Show file tree
Hide file tree
Showing 8 changed files with 336 additions and 18 deletions.
Binary file modified client/app/assets/images/destinations/hangouts_chat.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
53 changes: 40 additions & 13 deletions client/app/components/EmailSettingsWarning.jsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,49 @@
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import { currentUser, clientConfig } from '@/services/auth';
import cx from 'classnames';
import { clientConfig, currentUser } from '@/services/auth';
import Tooltip from 'antd/lib/tooltip';
import Alert from 'antd/lib/alert';
import { HelpTrigger } from '@/components/HelpTrigger';

export function EmailSettingsWarning({ featureName }) {
return (clientConfig.mailSettingsMissing && currentUser.isAdmin) ? (
<p className="alert alert-danger">
{`It looks like your mail server isn't configured. Make sure to configure it for the ${featureName} to work.`}
</p>
) : null;
export default function EmailSettingsWarning({ featureName, className, mode, adminOnly }) {
if (!clientConfig.mailSettingsMissing) {
return null;
}

if (adminOnly && !currentUser.isAdmin) {
return null;
}

const message = (
<span>
Your mail server isn&apos;t configured correctly, and is needed for {featureName} to work.{' '}
<HelpTrigger type="MAIL_CONFIG" className="f-inherit" />
</span>
);

if (mode === 'icon') {
return (
<Tooltip title={message}>
<i className={cx('fa fa-exclamation-triangle', className)} />
</Tooltip>
);
}

return (
<Alert message={message} type="error" className={className} />
);
}

EmailSettingsWarning.propTypes = {
featureName: PropTypes.string.isRequired,
className: PropTypes.string,
mode: PropTypes.oneOf(['alert', 'icon']),
adminOnly: PropTypes.bool,
};

export default function init(ngModule) {
ngModule.component('emailSettingsWarning', react2angular(EmailSettingsWarning));
}

init.init = true;
EmailSettingsWarning.defaultProps = {
className: null,
mode: 'alert',
adminOnly: false,
};
4 changes: 4 additions & 0 deletions client/app/components/HelpTrigger.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ export const TYPES = {
'/user-guide/alerts/setting-up-an-alert',
'Guide: Setting Up a New Alert',
],
MAIL_CONFIG: [
'/open-source/setup/#Mail-Configuration',
'Guide: Mail Configuration',
],
};

export class HelpTrigger extends React.Component {
Expand Down
7 changes: 7 additions & 0 deletions client/app/components/proptypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@ export const UserProfile = PropTypes.shape({
isDisabled: PropTypes.bool,
});

export const Destination = PropTypes.shape({
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
});

export const AlertOptions = PropTypes.shape({
column: PropTypes.string,
op: PropTypes.oneOf(['greater than', 'less than', 'equals']),
Expand Down
14 changes: 11 additions & 3 deletions client/app/pages/alert/Alert.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { TimeAgo } from '@/components/TimeAgo';

import Form from 'antd/lib/form';
import Button from 'antd/lib/button';
import Tooltip from 'antd/lib/tooltip';
import Icon from 'antd/lib/icon';
import Modal from 'antd/lib/modal';
import Input from 'antd/lib/input';
Expand All @@ -26,6 +27,7 @@ import Menu from 'antd/lib/menu';
import Criteria from './components/Criteria';
import Rearm from './components/Rearm';
import Query from './components/Query';
import AlertDestinations from './components/AlertDestinations';
import { STATE_CLASS } from '../alerts/AlertsList';
import { routesToAngularRoutes } from '@/lib/utils';

Expand Down Expand Up @@ -354,10 +356,16 @@ class AlertPage extends React.Component {
</HelpTrigger>
)}
</div>
{!editMode && (
{!editMode && alert.id && (
<div className="col-md-4">
<h4>Destinations</h4>
<div><i className="fa fa-hand-o-right" /> In next PR</div>
<h4>Destinations{' '}
<Tooltip title="Open Alert Destinations page in a new tab.">
<a href="/destinations" target="_blank">
<i className="fa fa-external-link" />
</a>
</Tooltip>
</h4>
<AlertDestinations alertId={alert.id} />
</div>
)}
</div>
Expand Down
210 changes: 210 additions & 0 deletions client/app/pages/alert/components/AlertDestinations.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import React from 'react';
import PropTypes from 'prop-types';
import { without, find, isEmpty, includes, map } from 'lodash';

import SelectItemsDialog from '@/components/SelectItemsDialog';
import { Destination as DestinationType, UserProfile as UserType } from '@/components/proptypes';

import { Destination as DestinationService, IMG_ROOT } from '@/services/destination';
import { AlertSubscription } from '@/services/alert-subscription';
import { $q } from '@/services/ng';
import { clientConfig, currentUser } from '@/services/auth';
import notification from '@/services/notification';
import ListItemAddon from '@/components/groups/ListItemAddon';
import EmailSettingsWarning from '@/components/EmailSettingsWarning';

import Icon from 'antd/lib/icon';
import Tooltip from 'antd/lib/tooltip';
import Switch from 'antd/lib/switch';
import Button from 'antd/lib/button';

import './AlertDestinations.less';

const USER_EMAIL_DEST_ID = -1;

function normalizeSub(sub) {
if (!sub.destination) {
sub.destination = {
id: USER_EMAIL_DEST_ID,
name: sub.user.email,
icon: 'DEPRECATED',
type: 'email',
};
}
return sub;
}

function ListItem({ destination: { name, type }, user, unsubscribe }) {
const canUnsubscribe = currentUser.isAdmin || currentUser.id === user.id;

return (
<li className="destination-wrapper">
<img src={`${IMG_ROOT}/${type}.png`} className="destination-icon" alt={name} />
<span className="flex-fill">{name}</span>
{type === 'email' && <EmailSettingsWarning className="destination-warning" featureName="alert emails" mode="icon" />}
{canUnsubscribe && (
<Tooltip title="Remove" mouseEnterDelay={0.5}>
<Icon type="close" className="remove-button" onClick={unsubscribe} />
</Tooltip>
)}
</li>
);
}

ListItem.propTypes = {
destination: DestinationType.isRequired,
user: UserType.isRequired,
unsubscribe: PropTypes.func.isRequired,
};


export default class AlertDestinations extends React.Component {
static propTypes = {
alertId: PropTypes.number.isRequired,
}

state = {
dests: [],
subs: null,
}

componentDidMount() {
const { alertId } = this.props;
$q
.all([
DestinationService.query().$promise, // get all destinations
AlertSubscription.query({ alertId }).$promise, // get subcriptions per alert
])
.then(([dests, subs]) => {
subs = subs.map(normalizeSub);
this.setState({ dests, subs });
});
}

showAddAlertSubDialog = () => {
const { dests, subs } = this.state;

SelectItemsDialog.showModal({
dialogTitle: 'Add Existing Alert Destinations',
selectedItemsTitle: 'Pending Destinations',
inputPlaceholder: 'Search destinations...',
searchItems: (searchTerm) => {
searchTerm = searchTerm.toLowerCase();
const filtered = dests.filter(d => isEmpty(searchTerm) || includes(d.name.toLowerCase(), searchTerm));
return Promise.resolve(filtered);
},
renderItem: (item, { isSelected }) => {
const alreadyInGroup = !!find(subs, s => s.destination.id === item.id);

return {
content: (
<div className="destination-wrapper">
<img src={`${IMG_ROOT}/${item.type}.png`} className="destination-icon" alt={name} />
<span className="flex-fill">{item.name}</span>
<ListItemAddon isSelected={isSelected} alreadyInGroup={alreadyInGroup} />
</div>
),
isDisabled: alreadyInGroup,
className: isSelected || alreadyInGroup ? 'selected' : '',
};
},
renderStagedItem: item => ({
content: (
<div className="destination-wrapper">
<img src={`${IMG_ROOT}/${item.type}.png`} className="destination-icon" alt={name} />
<span className="flex-fill">{item.name}</span>
<ListItemAddon isStaged />
</div>
),
}),
save: (items) => {
const promises = map(items, item => this.subscribe(item));
return Promise.all(promises).then(() => {
notification.success('Subscribed.');
}).catch(() => {
notification.error('Failed saving subscription.');
});
},
});
}

onUserEmailToggle = (sub) => {
if (sub) {
this.unsubscribe(sub);
} else {
this.subscribe();
}
}

subscribe = (dest) => {
const { alertId } = this.props;

const sub = new AlertSubscription({ alert_id: alertId });
if (dest) {
sub.destination_id = dest.id;
}

return sub.$save(() => {
const { subs } = this.state;
this.setState({
subs: [...subs, normalizeSub(sub)],
});
});
}

unsubscribe = (sub) => {
sub.$delete(
() => {
// not showing subscribe notification cause it's redundant here
const { subs } = this.state;
this.setState({
subs: without(subs, sub),
});
},
() => {
notification.error('Failed unsubscribing.');
},
);
};

render() {
if (!this.props.alertId) {
return null;
}

const { subs } = this.state;
const currentUserEmailSub = find(subs, {
destination: { id: USER_EMAIL_DEST_ID },
user: { id: currentUser.id },
});
const filteredSubs = without(subs, currentUserEmailSub);
const { mailSettingsMissing } = clientConfig;

return (
<div className="alert-destinations">
<Tooltip title="Click to add an existing &quot;Alert Destination&quot;" mouseEnterDelay={0.5}>
<Button type="primary" size="small" className="add-button" onClick={this.showAddAlertSubDialog}>
<i className="fa fa-plus f-12 m-r-5" /> Add
</Button>
</Tooltip>
<ul>
<li className="destination-wrapper">
<i className="destination-icon fa fa-envelope" />
<span className="flex-fill">{ currentUser.email }</span>
<EmailSettingsWarning className="destination-warning" featureName="alert emails" mode="icon" />
{!mailSettingsMissing && (
<Switch
size="small"
className="toggle-button"
checked={!!currentUserEmailSub}
loading={!subs}
onChange={() => this.onUserEmailToggle(currentUserEmailSub)}
/>
)}
</li>
{filteredSubs.map(s => <ListItem key={s.id} unsubscribe={() => this.unsubscribe(s)} {...s} />)}
</ul>
</div>
);
}
}
62 changes: 62 additions & 0 deletions client/app/pages/alert/components/AlertDestinations.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
.alert-destinations {
ul {
list-style: none;
padding: 0;
margin-top: 15px;

li {
color: rgba(0, 0, 0, 0.85);
height: 46px;
border-bottom: 1px solid #e8e8e8;

.remove-button {
cursor: pointer;
height: 40px;
width: 40px;
display: flex;
align-items: center;
justify-content: center;
}

.toggle-button {
margin: 0 7px;
}

.destination-warning {
color: #f5222d;

&:last-child {
margin-right: 14px;
}
}
}
}

.add-button {
position: absolute;
right: 14px;
top: 9px;
}
}

.destination-wrapper {
padding-left: 8px;
display: flex;
align-items: center;
min-height: 38px;
width: 100%;

.destination-icon {
height: 25px;
width: 25px;
margin: 2px 5px 0 0;
filter: grayscale(1);

&.fa {
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
}
}
}
Loading

0 comments on commit bfae2d9

Please sign in to comment.