diff --git a/client/app/assets/less/inc/alert.less b/client/app/assets/less/inc/alert.less
index 0c3d89f9d1..009d4e0cba 100755
--- a/client/app/assets/less/inc/alert.less
+++ b/client/app/assets/less/inc/alert.less
@@ -2,7 +2,7 @@
flex-grow: 1;
input {
- margin: -0.2em 0; //
+ margin: -0.2em 0;
width: 100%;
min-width: 170px;
}
diff --git a/client/app/components/proptypes.js b/client/app/components/proptypes.js
index 41151d388e..814d128b08 100644
--- a/client/app/components/proptypes.js
+++ b/client/app/components/proptypes.js
@@ -126,7 +126,7 @@ export const Alert = PropTypes.shape({
rearm: PropTypes.number,
state: PropTypes.oneOf(['ok', 'triggered', 'unknown']),
user: UserProfile,
- query: Query.isRequired,
+ query: Query,
options: PropTypes.shape({
column: PropTypes.string,
op: PropTypes.string,
diff --git a/client/app/pages/alert/Alert.jsx b/client/app/pages/alert/Alert.jsx
index 917a55ac6b..69721990d1 100644
--- a/client/app/pages/alert/Alert.jsx
+++ b/client/app/pages/alert/Alert.jsx
@@ -1,8 +1,6 @@
import React from 'react';
-import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
-import { head, includes, template as templateBuilder, trim } from 'lodash';
-import cx from 'classnames';
+import { head, includes, trim, template } from 'lodash';
import { $route } from '@/services/ng';
import { currentUser } from '@/services/auth';
@@ -11,91 +9,31 @@ import notification from '@/services/notification';
import { Alert as AlertService } from '@/services/alert';
import { Query as QueryService } from '@/services/query';
-import { HelpTrigger } from '@/components/HelpTrigger';
import LoadingState from '@/components/items-list/components/LoadingState';
-import { TimeAgo } from '@/components/TimeAgo';
+import AlertView from './AlertView';
+import AlertEdit from './AlertEdit';
+import AlertNew from './AlertNew';
-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';
-import Dropdown from 'antd/lib/dropdown';
-import Menu from 'antd/lib/menu';
-
-import Criteria from './components/Criteria';
-import NotificationTemplate from './components/NotificationTemplate';
-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';
import PromiseRejectionError from '@/lib/promise-rejection-error';
-
-const defaultNameBuilder = templateBuilder('<%= query.name %>: <%= options.column %> <%= options.op %> <%= options.value %>');
-const spinnerIcon = ;
-
-function isNewAlert() {
- return $route.current.params.alertId === 'new';
-}
-
-function HorizontalFormItem({ children, label, className, ...props }) {
- const labelCol = { span: 4 };
- const wrapperCol = { span: 16 };
- if (!label) {
- wrapperCol.offset = 4;
- }
-
- className = cx('alert-form-item', className);
-
- return (
-
- { children }
-
- );
-}
-
-HorizontalFormItem.propTypes = {
- children: PropTypes.node,
- label: PropTypes.string,
- className: PropTypes.string,
+const MODES = {
+ NEW: 0,
+ VIEW: 1,
+ EDIT: 2,
};
-HorizontalFormItem.defaultProps = {
- children: null,
- label: null,
- className: null,
-};
+const defaultNameBuilder = template('<%= query.name %>: <%= options.column %> <%= options.op %> <%= options.value %>');
-function AlertState({ state, lastTriggered }) {
- return (
-
-
Status: {state}
- {state === 'unknown' && (
-
- Alert condition has not been evaluated.
-
- )}
- {lastTriggered && (
-
- Last triggered
-
- )}
-
- );
+export function getDefaultName(alert) {
+ if (!alert.query) {
+ return 'New Alert';
+ }
+ return defaultNameBuilder(alert);
}
-AlertState.propTypes = {
- state: PropTypes.string.isRequired,
- lastTriggered: PropTypes.string,
-};
-
-AlertState.defaultProps = {
- lastTriggered: null,
-};
-
class AlertPage extends React.Component {
_isMounted = false;
@@ -103,39 +41,43 @@ class AlertPage extends React.Component {
alert: null,
queryResult: null,
pendingRearm: null,
- editMode: false,
canEdit: false,
- saving: false,
- canceling: false,
+ mode: null,
}
componentDidMount() {
this._isMounted = true;
+ const { mode } = $route.current.locals;
+ this.setState({ mode });
- if (isNewAlert()) {
+ if (mode === MODES.NEW) {
this.setState({
alert: new AlertService({
options: {
op: 'greater than',
value: 1,
},
- pendingRearm: 0,
}),
- editMode: true,
+ pendingRearm: 0,
canEdit: true,
});
} else {
const { alertId } = $route.current.params;
- const { editMode } = $route.current.locals;
AlertService.get({ id: alertId }).$promise.then((alert) => {
- const canEdit = currentUser.canEdit(alert);
if (this._isMounted) {
- this.setState({
- alert,
- pendingRearm: alert.rearm,
- editMode: editMode && canEdit,
- canEdit,
- });
+ const canEdit = currentUser.canEdit(alert);
+
+ // force view mode if can't edit
+ if (!canEdit) {
+ this.setState({ mode: MODES.VIEW });
+ notification.warn(
+ 'You cannot edit this alert',
+ 'You do not have sufficient permissions to edit this alert, and have been redirected to the view-only page.',
+ { duration: 0 },
+ );
+ }
+
+ this.setState({ alert, canEdit, pendingRearm: alert.rearm });
this.onQuerySelected(alert.query);
}
}).catch((err) => {
@@ -150,13 +92,20 @@ class AlertPage extends React.Component {
this._isMounted = false;
}
- getDefaultName = () => {
- const { alert } = this.state;
- if (!alert.query) {
- return 'New Alert';
- }
- return defaultNameBuilder(alert);
- }
+ save = () => {
+ const { alert, pendingRearm } = this.state;
+
+ alert.name = trim(alert.name) || getDefaultName(alert);
+ alert.rearm = pendingRearm || null;
+
+ return alert.$save().then(() => {
+ notification.success('Saved.');
+ navigateTo(`/alerts/${alert.id}`, true, false);
+ this.setState({ mode: MODES.VIEW });
+ }).catch(() => {
+ notification.error('Failed saving alert.');
+ });
+ };
onQuerySelected = (query) => {
this.setState(({ alert }) => ({
@@ -182,6 +131,13 @@ class AlertPage extends React.Component {
}
}
+ onNameChange = (name) => {
+ const { alert } = this.state;
+ this.setState({
+ alert: Object.assign(alert, { name }),
+ });
+ }
+
onRearmChange = (pendingRearm) => {
this.setState({ pendingRearm });
}
@@ -194,54 +150,13 @@ class AlertPage extends React.Component {
});
}
- setName = (name) => {
- const { alert } = this.state;
- this.setState({
- alert: Object.assign(alert, { name }),
- });
- }
-
- edit = () => {
- const { id } = this.state.alert;
- navigateTo(`/alerts/${id}/edit`, true);
- }
-
- save = () => {
- const { alert, pendingRearm } = this.state;
-
- alert.name = trim(alert.name) || this.getDefaultName();
- alert.rearm = pendingRearm || null;
-
- this.setState({ saving: true, alert });
-
- alert.$save().then(() => {
- if (isNewAlert()) {
- notification.success('Created new Alert.');
- } else {
- notification.success('Saved.');
- }
- navigateTo(`/alerts/${alert.id}`, true);
- }).catch(() => {
- notification.error('Failed saving alert.');
- if (this._isMounted) {
- this.setState({ saving: false });
- }
- });
- };
-
- cancel = () => {
- const { alert } = this.state;
- this.setState({ canceling: true });
- navigateTo(`/alerts/${alert.id}`, true);
- };
-
delete = () => {
const { alert } = this.state;
const doDelete = () => {
alert.$delete(() => {
notification.success('Alert deleted successfully.');
- navigateTo('/alerts', true);
+ navigateTo('/alerts');
}, () => {
notification.error('Failed deleting alert.');
});
@@ -258,150 +173,43 @@ class AlertPage extends React.Component {
});
}
+ edit = () => {
+ const { id } = this.state.alert;
+ navigateTo(`/alerts/${id}/edit`, true, false);
+ this.setState({ mode: MODES.EDIT });
+ }
+
+ cancel = () => {
+ const { id } = this.state.alert;
+ navigateTo(`/alerts/${id}`, true, false);
+ this.setState({ mode: MODES.VIEW });
+ }
+
render() {
const { alert } = this.state;
if (!alert) {
return ;
}
- const isNew = isNewAlert();
- const { query, name, options } = alert;
- const { queryResult, editMode, pendingRearm, canEdit, saving, canceling } = this.state;
+ const { queryResult, mode, canEdit, pendingRearm } = this.state;
+ const commonProps = {
+ alert,
+ queryResult,
+ pendingRearm,
+ delete: this.delete,
+ save: this.save,
+ onQuerySelected: this.onQuerySelected,
+ onRearmChange: this.onRearmChange,
+ onNameChange: this.onNameChange,
+ onCriteriaChange: this.setAlertOptions,
+ onNotificationTemplateChange: this.setAlertOptions,
+ };
return (
-
-
-
- {editMode && query ? (
- this.setName(e.target.value)} />
- ) : name || this.getDefaultName() }
-
-
- {editMode && (
- <>
- {!isNew && (
- <>
-
-
- >
- )}
- >
- )}
- {!editMode && canEdit && (
-
- )}
- {canEdit && !isNew && (
-
-
- this.delete()}>Delete Alert
-
-
- )}
- >
-
-
- )}
-
-
-
-
-
-
- {editMode && (
-
- Setup Instructions
-
- )}
-
- {!editMode && alert.id && (
-
-
Destinations{' '}
-
-
-
-
-
-
-
-
- )}
-
+ {mode === MODES.NEW &&
}
+ {mode === MODES.VIEW &&
}
+ {mode === MODES.EDIT &&
}
);
}
@@ -411,14 +219,20 @@ export default function init(ngModule) {
ngModule.component('alertPage', react2angular(AlertPage));
return routesToAngularRoutes([
+ {
+ path: '/alerts/new',
+ title: 'New Alert',
+ mode: MODES.NEW,
+ },
{
path: '/alerts/:alertId',
title: 'Alert',
- editMode: false,
- }, {
+ mode: MODES.VIEW,
+ },
+ {
path: '/alerts/:alertId/edit',
title: 'Alert',
- editMode: true,
+ mode: MODES.EDIT,
},
], {
template: '',
diff --git a/client/app/pages/alert/AlertEdit.jsx b/client/app/pages/alert/AlertEdit.jsx
new file mode 100644
index 0000000000..0caa1cc0f9
--- /dev/null
+++ b/client/app/pages/alert/AlertEdit.jsx
@@ -0,0 +1,147 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { HelpTrigger } from '@/components/HelpTrigger';
+import { Alert as AlertType } from '@/components/proptypes';
+
+import Form from 'antd/lib/form';
+import Button from 'antd/lib/button';
+import Icon from 'antd/lib/icon';
+import Dropdown from 'antd/lib/dropdown';
+import Menu from 'antd/lib/menu';
+
+import Title from './components/Title';
+import Criteria from './components/Criteria';
+import NotificationTemplate from './components/NotificationTemplate';
+import Rearm from './components/Rearm';
+import Query from './components/Query';
+
+import HorizontalFormItem from './components/HorizontalFormItem';
+
+const spinnerIcon = ;
+
+export default class AlertEdit extends React.Component {
+ _isMounted = false;
+
+ state = {
+ saving: false,
+ canceling: false,
+ }
+
+ componentDidMount() {
+ this._isMounted = true;
+ }
+
+ componentWillUnmount() {
+ this._isMounted = false;
+ }
+
+ save = () => {
+ this.setState({ saving: true });
+ this.props.save().catch(() => {
+ if (this._isMounted) {
+ this.setState({ saving: false });
+ }
+ });
+ }
+
+ cancel = () => {
+ this.setState({ canceling: true });
+ this.props.cancel();
+ };
+
+ render() {
+ const { alert, queryResult, pendingRearm, onNotificationTemplateChange } = this.props;
+ const { onQuerySelected, onNameChange, onRearmChange, onCriteriaChange } = this.props;
+ const { query, name, options } = alert;
+ const { saving, canceling } = this.state;
+
+ return (
+ <>
+
+
+
+
+
+ Delete Alert
+
+
+ )}
+ >
+
+
+
+
+
+
+
+ Setup Instructions
+
+
+
+ >
+ );
+ }
+}
+
+AlertEdit.propTypes = {
+ alert: AlertType.isRequired,
+ queryResult: PropTypes.object, // eslint-disable-line react/forbid-prop-types,
+ pendingRearm: PropTypes.number,
+ delete: PropTypes.func.isRequired,
+ save: PropTypes.func.isRequired,
+ cancel: PropTypes.func.isRequired,
+ onQuerySelected: PropTypes.func.isRequired,
+ onNameChange: PropTypes.func.isRequired,
+ onCriteriaChange: PropTypes.func.isRequired,
+ onRearmChange: PropTypes.func.isRequired,
+ onNotificationTemplateChange: PropTypes.func.isRequired,
+};
+
+AlertEdit.defaultProps = {
+ queryResult: null,
+ pendingRearm: null,
+};
diff --git a/client/app/pages/alert/AlertNew.jsx b/client/app/pages/alert/AlertNew.jsx
new file mode 100644
index 0000000000..26b18f9f66
--- /dev/null
+++ b/client/app/pages/alert/AlertNew.jsx
@@ -0,0 +1,109 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { HelpTrigger } from '@/components/HelpTrigger';
+import { Alert as AlertType } from '@/components/proptypes';
+
+import Form from 'antd/lib/form';
+import Button from 'antd/lib/button';
+
+import Title from './components/Title';
+import Criteria from './components/Criteria';
+import NotificationTemplate from './components/NotificationTemplate';
+import Rearm from './components/Rearm';
+import Query from './components/Query';
+import HorizontalFormItem from './components/HorizontalFormItem';
+
+export default class AlertNew extends React.Component {
+ state = {
+ saving: false,
+ };
+
+ save = () => {
+ this.setState({ saving: true });
+ this.props.save().catch(() => {
+ this.setState({ saving: false });
+ });
+ }
+
+ render() {
+ const { alert, queryResult, pendingRearm, onNotificationTemplateChange } = this.props;
+ const { onQuerySelected, onNameChange, onRearmChange, onCriteriaChange } = this.props;
+ const { query, name, options } = alert;
+ const { saving } = this.state;
+
+ return (
+ <>
+
+
+
+
+
+ Setup Instructions
+
+
+
+ >
+ );
+ }
+}
+
+AlertNew.propTypes = {
+ alert: AlertType.isRequired,
+ queryResult: PropTypes.object, // eslint-disable-line react/forbid-prop-types,
+ pendingRearm: PropTypes.number,
+ onQuerySelected: PropTypes.func.isRequired,
+ save: PropTypes.func.isRequired,
+ onNameChange: PropTypes.func.isRequired,
+ onRearmChange: PropTypes.func.isRequired,
+ onCriteriaChange: PropTypes.func.isRequired,
+ onNotificationTemplateChange: PropTypes.func.isRequired,
+};
+
+AlertNew.defaultProps = {
+ queryResult: null,
+ pendingRearm: null,
+};
diff --git a/client/app/pages/alert/AlertView.jsx b/client/app/pages/alert/AlertView.jsx
new file mode 100644
index 0000000000..22fb40343b
--- /dev/null
+++ b/client/app/pages/alert/AlertView.jsx
@@ -0,0 +1,135 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import cx from 'classnames';
+
+import { TimeAgo } from '@/components/TimeAgo';
+import { Alert as AlertType } from '@/components/proptypes';
+
+import Form from 'antd/lib/form';
+import Button from 'antd/lib/button';
+import Icon from 'antd/lib/icon';
+import Dropdown from 'antd/lib/dropdown';
+import Menu from 'antd/lib/menu';
+import Tooltip from 'antd/lib/tooltip';
+
+import Title from './components/Title';
+import Criteria from './components/Criteria';
+import Rearm from './components/Rearm';
+import Query from './components/Query';
+import AlertDestinations from './components/AlertDestinations';
+import HorizontalFormItem from './components/HorizontalFormItem';
+import { STATE_CLASS } from '../alerts/AlertsList';
+
+
+function AlertState({ state, lastTriggered }) {
+ return (
+
+
Status: {state}
+ {state === 'unknown' && (
+
+ Alert condition has not been evaluated.
+
+ )}
+ {lastTriggered && (
+
+ Last triggered
+
+ )}
+
+ );
+}
+
+AlertState.propTypes = {
+ state: PropTypes.string.isRequired,
+ lastTriggered: PropTypes.string,
+};
+
+AlertState.defaultProps = {
+ lastTriggered: null,
+};
+
+export default class AlertView extends React.Component {
+ render() {
+ const { alert, queryResult, canEdit, onEdit } = this.props;
+ const { query, name, options, rearm } = alert;
+
+ return (
+ <>
+
+
+
+
+
+ Delete Alert
+
+
+ )}
+ >
+
+
+
+
+
+
+
+
Destinations{' '}
+
+
+
+
+
+
+
+
+
+ >
+ );
+ }
+}
+
+AlertView.propTypes = {
+ alert: AlertType.isRequired,
+ queryResult: PropTypes.object, // eslint-disable-line react/forbid-prop-types,
+ canEdit: PropTypes.bool.isRequired,
+ delete: PropTypes.func.isRequired,
+ onEdit: PropTypes.func.isRequired,
+};
+
+AlertView.defaultProps = {
+ queryResult: null,
+};
diff --git a/client/app/pages/alert/components/Criteria.jsx b/client/app/pages/alert/components/Criteria.jsx
index 49baf7a939..ac1348156d 100644
--- a/client/app/pages/alert/components/Criteria.jsx
+++ b/client/app/pages/alert/components/Criteria.jsx
@@ -118,6 +118,11 @@ Criteria.propTypes = {
columnNames: PropTypes.arrayOf(PropTypes.string).isRequired,
resultValues: PropTypes.arrayOf(PropTypes.object).isRequired,
alertOptions: AlertOptionsType.isRequired,
- onChange: PropTypes.func.isRequired,
- editMode: PropTypes.bool.isRequired,
+ onChange: PropTypes.func,
+ editMode: PropTypes.bool,
+};
+
+Criteria.defaultProps = {
+ onChange: () => {},
+ editMode: false,
};
diff --git a/client/app/pages/alert/components/HorizontalFormItem.jsx b/client/app/pages/alert/components/HorizontalFormItem.jsx
new file mode 100644
index 0000000000..0ad5f809ff
--- /dev/null
+++ b/client/app/pages/alert/components/HorizontalFormItem.jsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import cx from 'classnames';
+import Form from 'antd/lib/form';
+
+export default function HorizontalFormItem({ children, label, className, ...props }) {
+ const labelCol = { span: 4 };
+ const wrapperCol = { span: 16 };
+ if (!label) {
+ wrapperCol.offset = 4;
+ }
+
+ className = cx('alert-form-item', className);
+
+ return (
+
+ { children }
+
+ );
+}
+
+HorizontalFormItem.propTypes = {
+ children: PropTypes.node,
+ label: PropTypes.string,
+ className: PropTypes.string,
+};
+
+HorizontalFormItem.defaultProps = {
+ children: null,
+ label: null,
+ className: null,
+};
diff --git a/client/app/pages/alert/components/Query.jsx b/client/app/pages/alert/components/Query.jsx
index a50662e1d9..1ad056e57c 100644
--- a/client/app/pages/alert/components/Query.jsx
+++ b/client/app/pages/alert/components/Query.jsx
@@ -3,13 +3,14 @@ import PropTypes from 'prop-types';
import { QuerySelector } from '@/components/QuerySelector';
import { SchedulePhrase } from '@/components/queries/SchedulePhrase';
+import { Query as QueryType } from '@/components/proptypes';
import Tooltip from 'antd/lib/tooltip';
import Icon from 'antd/lib/icon';
import './Query.less';
-export default function QueryFormItem({ query, onChange, editMode }) {
+export default function QueryFormItem({ query, queryResult, onChange, editMode }) {
const queryHint = query && query.schedule ? (
Scheduled to refresh
@@ -41,16 +42,25 @@ export default function QueryFormItem({ query, onChange, editMode }) {
{query && queryHint}
+ {query && !queryResult && (
+
+ Loading query data
+
+ )}
>
);
}
QueryFormItem.propTypes = {
- query: PropTypes.object, // eslint-disable-line react/forbid-prop-types
- onChange: PropTypes.func.isRequired,
- editMode: PropTypes.bool.isRequired,
+ query: QueryType,
+ queryResult: PropTypes.object, // eslint-disable-line react/forbid-prop-types
+ onChange: PropTypes.func,
+ editMode: PropTypes.bool,
};
QueryFormItem.defaultProps = {
query: null,
+ queryResult: null,
+ onChange: () => {},
+ editMode: false,
};
diff --git a/client/app/pages/alert/components/Title.jsx b/client/app/pages/alert/components/Title.jsx
new file mode 100644
index 0000000000..a11ccc97ef
--- /dev/null
+++ b/client/app/pages/alert/components/Title.jsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Input from 'antd/lib/input';
+import { getDefaultName } from '../Alert';
+
+import { Alert as AlertType } from '@/components/proptypes';
+
+
+export default function Title({ alert, editMode, name, onChange, children }) {
+ const defaultName = getDefaultName(alert);
+ return (
+
+
+
+ {editMode && alert.query ? (
+ onChange(e.target.value)} />
+ ) : name || defaultName }
+
+ { children }
+
+
+ );
+}
+
+Title.propTypes = {
+ alert: AlertType.isRequired,
+ name: PropTypes.string,
+ children: PropTypes.node,
+ onChange: PropTypes.func,
+ editMode: PropTypes.bool,
+};
+
+Title.defaultProps = {
+ name: null,
+ children: null,
+ onChange: null,
+ editMode: false,
+};