From be404a466949ad7a22fd4366df078c10edf0bacb Mon Sep 17 00:00:00 2001 From: Ran Byron Date: Mon, 7 Oct 2019 11:45:59 +0300 Subject: [PATCH] Separated Alert to View / Edit / New page components (#4183) * Separated Alert to View / Edit / New page components * Moved query result loader into * Added missing Query prop * No reload for edit / view / new state changes * Added warn and tooltip when non-editable * Rebased --- client/app/assets/less/inc/alert.less | 2 +- client/app/components/proptypes.js | 2 +- client/app/pages/alert/Alert.jsx | 370 +++++------------- client/app/pages/alert/AlertEdit.jsx | 147 +++++++ client/app/pages/alert/AlertNew.jsx | 109 ++++++ client/app/pages/alert/AlertView.jsx | 135 +++++++ .../app/pages/alert/components/Criteria.jsx | 9 +- .../alert/components/HorizontalFormItem.jsx | 32 ++ client/app/pages/alert/components/Query.jsx | 18 +- client/app/pages/alert/components/Title.jsx | 38 ++ 10 files changed, 576 insertions(+), 286 deletions(-) create mode 100644 client/app/pages/alert/AlertEdit.jsx create mode 100644 client/app/pages/alert/AlertNew.jsx create mode 100644 client/app/pages/alert/AlertView.jsx create mode 100644 client/app/pages/alert/components/HorizontalFormItem.jsx create mode 100644 client/app/pages/alert/components/Title.jsx 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 - - - )} - > - - - )} - -
-
-
-
-
- {isNew && ( -
- Start by selecting the query that you would like to monitor using the search bar. -
- Keep in mind that Alerts do not work with queries that use parameters. -
- )} - {!editMode && ( - - - - )} - - - - {query && !queryResult && ( - - Loading query data - - )} - {queryResult && options && ( - <> - - - - {editMode ? ( - <> - - - - - this.setAlertOptions({ custom_subject: subject })} - body={options.custom_body} - setBody={body => this.setAlertOptions({ custom_body: body })} - /> - - - ) : ( - - -
- Set to {options.custom_subject || options.custom_body ? 'custom' : 'default'} notification template. -
- )} - - )} - {isNew && ( - - - - )} -
- {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 ( + <> + + <Button className="m-r-5" onClick={() => this.cancel()}> + {canceling ? spinnerIcon : <i className="fa fa-times m-r-5" />} + Cancel + </Button> + <Button type="primary" onClick={() => this.save()}> + {saving ? spinnerIcon : <i className="fa fa-check m-r-5" />} + Save Changes + </Button> + <Dropdown + className="m-l-5" + trigger={['click']} + placement="bottomRight" + overlay={( + <Menu> + <Menu.Item> + <a onClick={this.props.delete}>Delete Alert</a> + </Menu.Item> + </Menu> + )} + > + <Button><Icon type="ellipsis" rotate={90} /></Button> + </Dropdown> + +
+
+
+ + + + {queryResult && options && ( + <> + + + + + + + + onNotificationTemplateChange({ custom_subject: subject })} + body={options.custom_body} + setBody={body => onNotificationTemplateChange({ custom_body: body })} + /> + + + )} +
+ + 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 ( + <> + + <div className="row bg-white tiled p-20"> + <div className="d-flex"> + <Form className="flex-fill"> + <div className="m-b-30"> + Start by selecting the query that you would like to monitor using the search bar. + <br /> + Keep in mind that Alerts do not work with queries that use parameters. + </div> + <HorizontalFormItem label="Query"> + <Query query={query} queryResult={queryResult} onChange={onQuerySelected} editMode /> + </HorizontalFormItem> + {queryResult && options && ( + <> + <HorizontalFormItem label="Trigger when" className="alert-criteria"> + <Criteria + columnNames={queryResult.getColumnNames()} + resultValues={queryResult.getData()} + alertOptions={options} + onChange={onCriteriaChange} + editMode + /> + </HorizontalFormItem> + <HorizontalFormItem label="When triggered, send notification"> + <Rearm value={pendingRearm || 0} onChange={onRearmChange} editMode /> + </HorizontalFormItem> + <HorizontalFormItem label="Template"> + <NotificationTemplate + alert={alert} + query={query} + columnNames={queryResult.getColumnNames()} + resultValues={queryResult.getData()} + subject={options.custom_subject} + setSubject={subject => onNotificationTemplateChange({ custom_subject: subject })} + body={options.custom_body} + setBody={body => onNotificationTemplateChange({ custom_body: body })} + /> + </HorizontalFormItem> + </> + )} + <HorizontalFormItem> + <Button type="primary" onClick={this.save} disabled={!query} className="btn-create-alert"> + {saving && <i className="fa fa-spinner fa-pulse m-r-5" />} + Create Alert + </Button> + </HorizontalFormItem> + </Form> + <HelpTrigger className="f-13" type="ALERT_SETUP"> + Setup Instructions <i className="fa fa-question-circle" /> + </HelpTrigger> + </div> + </div> + </> + ); + } +} + +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 ( + <div className="alert-state"> + <span className={`alert-state-indicator label ${STATE_CLASS[state]}`}>Status: {state}</span> + {state === 'unknown' && ( + <div className="ant-form-explain"> + Alert condition has not been evaluated. + </div> + )} + {lastTriggered && ( + <div className="ant-form-explain"> + Last triggered <span className="alert-last-triggered"><TimeAgo date={lastTriggered} /></span> + </div> + )} + </div> + ); +} + +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 ( + <> + <Title name={name} alert={alert}> + <Tooltip title={canEdit ? '' : 'You do not have sufficient permissions to edit this alert'}> + <Button type="default" onClick={canEdit ? onEdit : null} className={cx({ disabled: !canEdit })}><i className="fa fa-edit m-r-5" />Edit</Button> + <Dropdown + className={cx('m-l-5', { disabled: !canEdit })} + trigger={[canEdit ? 'click' : undefined]} + placement="bottomRight" + overlay={( + <Menu> + <Menu.Item> + <a onClick={this.props.delete}>Delete Alert</a> + </Menu.Item> + </Menu> + )} + > + <Button><Icon type="ellipsis" rotate={90} /></Button> + </Dropdown> + </Tooltip> + +
+
+
+ + + + + + + {query && !queryResult && ( + + Loading query data + + )} + {queryResult && options && ( + <> + + + + + +
+ Set to {options.custom_subject || options.custom_body ? 'custom' : 'default'} notification template. +
+ + )} +
+
+
+

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, +};