diff --git a/CHANGELOG.md b/CHANGELOG.md index a438c2ef93..b253b64947 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ 1. [#1209](https://github.com/influxdata/chronograf/pull/1209): HipChat Kapacitor config now uses only the subdomain instead of asking for the entire HipChat URL. 1. [#1219](https://github.com/influxdata/chronograf/pull/1219): Update query for default cell in new dashboard 1. [#1206](https://github.com/influxdata/chronograf/issues/1206): Chronograf now proxies to kapacitors behind proxy using vhost correctly. + 1. [#1205](https://github.com/influxdata/chronograf/pull/1205): Allow initial source to be an enterprise source. ### Features 1. [#1112](https://github.com/influxdata/chronograf/pull/1112): Add ability to delete a dashboard diff --git a/ui/src/App.js b/ui/src/App.js index fe4d3da391..33d4931249 100644 --- a/ui/src/App.js +++ b/ui/src/App.js @@ -1,18 +1,16 @@ import React, {PropTypes} from 'react' import {connect} from 'react-redux' -import classnames from 'classnames' import SideNavContainer from 'src/side_nav' +import Notifications from 'shared/components/Notifications' import { publishNotification as publishNotificationAction, - dismissNotification as dismissNotificationAction, - dismissAllNotifications as dismissAllNotificationsAction, } from 'src/shared/actions/notifications' const { + func, node, shape, string, - func, } = PropTypes const App = React.createClass({ @@ -24,99 +22,34 @@ const App = React.createClass({ params: shape({ sourceID: string.isRequired, }).isRequired, - publishNotification: func.isRequired, - dismissNotification: func.isRequired, - dismissAllNotifications: func.isRequired, - notifications: shape({ - success: string, - error: string, - warning: string, - }), + notify: func.isRequired, }, - handleNotification({type, text}) { - const validTypes = ['error', 'success', 'warning'] - if (!validTypes.includes(type) || text === undefined) { - console.error("handleNotification must have a valid type and text") // eslint-disable-line no-console - } - this.props.publishNotification(type, text) - }, + handleAddFlashMessage({type, text}) { + const {notify} = this.props - handleDismissNotification(type) { - this.props.dismissNotification(type) - }, - - componentWillReceiveProps(nextProps) { - if (nextProps.location.pathname !== this.props.location.pathname) { - this.props.dismissAllNotifications() - } + notify(type, text) }, render() { - const {params: {sourceID}} = this.props + const {params: {sourceID}, location} = this.props return (
- {this.renderNotifications()} + {this.props.children && React.cloneElement(this.props.children, { - addFlashMessage: this.handleNotification, + addFlashMessage: this.handleAddFlashMessage, })}
) }, - - renderNotifications() { - const {success, error, warning} = this.props.notifications - if (!success && !error && !warning) { - return null - } - return ( -
- {this.renderNotification('success', success)} - {this.renderNotification('error', error)} - {this.renderNotification('warning', warning)} -
- ) - }, - - renderNotification(type, message) { - if (!message) { - return null - } - const cls = classnames('alert', { - 'alert-danger': type === 'error', - 'alert-success': type === 'success', - 'alert-warning': type === 'warning', - }) - return ( -
- {message}{this.renderDismiss(type)} -
- ) - }, - - renderDismiss(type) { - return ( - - ) - }, }) -function mapStateToProps(state) { - return { - notifications: state.notifications, - } -} - -export default connect(mapStateToProps, { - publishNotification: publishNotificationAction, - dismissNotification: dismissNotificationAction, - dismissAllNotifications: dismissAllNotificationsAction, +export default connect(null, { + notify: publishNotificationAction, })(App) diff --git a/ui/src/shared/actions/notifications.js b/ui/src/shared/actions/notifications.js index 52dfcf4e51..b3cfc87388 100644 --- a/ui/src/shared/actions/notifications.js +++ b/ui/src/shared/actions/notifications.js @@ -1,4 +1,10 @@ export function publishNotification(type, message) { + // this validator is purely for development purposes. It might make sense to move this to a middleware. + const validTypes = ['error', 'success', 'warning'] + if (!validTypes.includes(type) || message === undefined) { + console.error("handleNotification must have a valid type and text") // eslint-disable-line no-console + } + return { type: 'NOTIFICATION_RECEIVED', payload: { diff --git a/ui/src/shared/components/Notifications.js b/ui/src/shared/components/Notifications.js new file mode 100644 index 0000000000..180ace259d --- /dev/null +++ b/ui/src/shared/components/Notifications.js @@ -0,0 +1,98 @@ +import React, {Component, PropTypes} from 'react' +import classnames from 'classnames' +import {connect} from 'react-redux' +import {bindActionCreators} from 'redux' + +import { + publishNotification as publishNotificationAction, + dismissNotification as dismissNotificationAction, + dismissAllNotifications as dismissAllNotificationsAction, +} from 'src/shared/actions/notifications' + +class Notifications extends Component { + constructor(props) { + super(props) + + this.renderNotification = ::this.renderNotification + this.renderDismiss = ::this.renderDismiss + } + + componentWillReceiveProps(nextProps) { + if (nextProps.location.pathname !== this.props.location.pathname) { + this.props.dismissAllNotifications() + } + } + + renderNotification(type, message) { + if (!message) { + return null + } + const cls = classnames('alert', { + 'alert-danger': type === 'error', + 'alert-success': type === 'success', + 'alert-warning': type === 'warning', + }) + return ( +
+ {message}{this.renderDismiss(type)} +
+ ) + } + + renderDismiss(type) { + const {dismissNotification} = this.props + + return ( + + ) + } + + render() { + const {success, error, warning} = this.props.notifications + if (!success && !error && !warning) { + return null + } + + return ( +
+ {this.renderNotification('success', success)} + {this.renderNotification('error', error)} + {this.renderNotification('warning', warning)} +
+ ) + } +} + +const { + func, + shape, + string, +} = PropTypes + +Notifications.propTypes = { + location: shape({ + pathname: string, + }), + publishNotification: func.isRequired, + dismissNotification: func.isRequired, + dismissAllNotifications: func.isRequired, + notifications: shape({ + success: string, + error: string, + warning: string, + }), +} + +const mapStateToProps = ({notifications}) => ({ + notifications, +}) + +const mapDispatchToProps = (dispatch) => ({ + publishNotification: bindActionCreators(publishNotificationAction, dispatch), + dismissNotification: bindActionCreators(dismissNotificationAction, dispatch), + dismissAllNotifications: bindActionCreators(dismissAllNotificationsAction, dispatch), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(Notifications) diff --git a/ui/src/sources/components/SourceForm.js b/ui/src/sources/components/SourceForm.js index dbb899d342..4c3ae3f9bf 100644 --- a/ui/src/sources/components/SourceForm.js +++ b/ui/src/sources/components/SourceForm.js @@ -11,9 +11,6 @@ const { export const SourceForm = React.createClass({ propTypes: { - addFlashMessage: func.isRequired, - addSourceAction: func, - updateSourceAction: func, source: shape({}).isRequired, editMode: bool.isRequired, onInputChange: func.isRequired, @@ -56,80 +53,55 @@ export const SourceForm = React.createClass({ render() { const {source, editMode, onInputChange} = this.props - if (editMode && !source.id) { - return
- } - return ( -
-
-
-
-

- {editMode ? "Edit Source" : "Add a New Source"} -

-
-
-
-
-
-
-
-
-
-

Connection Details

-
+
+

Connection Details

+
-
-
- - this.sourceURL = r} className="form-control" id="connect-string" placeholder="http://localhost:8086" onChange={onInputChange} value={source.url || ''} onBlur={this.handleBlurSourceURL}> -
-
- - this.sourceName = r} className="form-control" id="name" placeholder="Influx 1" onChange={onInputChange} value={source.name || ''}> -
-
- - this.sourceUsername = r} className="form-control" id="username" onChange={onInputChange} value={source.username || ''}> -
-
- - this.sourcePassword = r} className="form-control" id="password" onChange={onInputChange} value={source.password || ''}> -
- {_.get(source, 'type', '').includes("enterprise") ? -
- - this.metaUrl = r} className="form-control" id="meta-url" placeholder="http://localhost:8091" onChange={onInputChange} value={source.metaUrl || ''}> -
: null} -
- - this.sourceTelegraf = r} className="form-control" id="telegraf" onChange={onInputChange} value={source.telegraf || 'telegraf'}> -
-
-
- this.sourceDefault = r} /> - -
-
- {_.get(source, 'url', '').startsWith("https") ? -
-
- this.sourceInsecureSkipVerify = r} /> - -
- -
: null} -
- -
-
-
-
-
+
+
+ + this.sourceURL = r} className="form-control" id="connect-string" placeholder="http://localhost:8086" onChange={onInputChange} value={source.url || ''} onBlur={this.handleBlurSourceURL}> +
+
+ + this.sourceName = r} className="form-control" id="name" placeholder="Influx 1" onChange={onInputChange} value={source.name || ''}> +
+
+ + this.sourceUsername = r} className="form-control" id="username" onChange={onInputChange} value={source.username || ''}> +
+
+ + this.sourcePassword = r} className="form-control" id="password" onChange={onInputChange} value={source.password || ''}> +
+ {_.get(source, 'type', '').includes("enterprise") ? +
+ + this.metaUrl = r} className="form-control" id="meta-url" placeholder="http://localhost:8091" onChange={onInputChange} value={source.metaUrl || ''}> +
: null} +
+ + this.sourceTelegraf = r} className="form-control" id="telegraf" onChange={onInputChange} value={source.telegraf || 'telegraf'}> +
+
+
+ this.sourceDefault = r} /> +
-
+ {_.get(source, 'url', '').startsWith("https") ? +
+
+ this.sourceInsecureSkipVerify = r} /> + +
+ +
: null} +
+ +
+
) }, diff --git a/ui/src/sources/containers/CreateSource.js b/ui/src/sources/containers/CreateSource.js index 9e7e409e5d..8de5b2699c 100644 --- a/ui/src/sources/containers/CreateSource.js +++ b/ui/src/sources/containers/CreateSource.js @@ -1,36 +1,45 @@ import React, {PropTypes} from 'react' import {withRouter} from 'react-router' -import {addSource as addSourceAction} from 'src/shared/actions/sources' -import {createSource} from 'shared/apis' import {connect} from 'react-redux' +import {bindActionCreators} from 'redux' + +import { + createSource as createSourceAJAX, + updateSource as updateSourceAJAX, +} from 'shared/apis' +import SourceForm from 'src/sources/components/SourceForm' +import Notifications from 'shared/components/Notifications' +import { + addSource as addSourceAction, + updateSource as updateSourceAction, +} from 'src/shared/actions/sources' +import {publishNotification} from 'src/shared/actions/notifications' + +const { + func, + shape, + string, +} = PropTypes export const CreateSource = React.createClass({ propTypes: { - router: PropTypes.shape({ - push: PropTypes.func.isRequired, + router: shape({ + push: func.isRequired, }).isRequired, - location: PropTypes.shape({ - query: PropTypes.shape({ - redirectPath: PropTypes.string, + location: shape({ + query: shape({ + redirectPath: string, }).isRequired, }).isRequired, - addSourceAction: PropTypes.func, + addSource: func, + updateSource: func, + notify: func, }, - handleNewSource(e) { - e.preventDefault() - const source = { - url: this.sourceURL.value.trim(), - name: this.sourceName.value, - username: this.sourceUser.value, - password: this.sourcePassword.value, - isDefault: true, - telegraf: this.sourceTelegraf.value, + getInitialState() { + return { + source: {}, } - createSource(source).then(({data: sourceFromServer}) => { - this.props.addSourceAction(sourceFromServer) - this.redirectToApp(sourceFromServer) - }) }, redirectToApp(source) { @@ -43,47 +52,77 @@ export const CreateSource = React.createClass({ return this.props.router.push(fixedPath) }, - render() { - return ( -
-
-
-
-
-
-

Welcome to Chronograf

-
-
-

Connect to a New Source

-
+ handleInputChange(e) { + const val = e.target.value + const name = e.target.name + this.setState((prevState) => { + const newSource = Object.assign({}, prevState.source, { + [name]: val, + }) + return Object.assign({}, prevState, {source: newSource}) + }) + }, -
-
-
- - this.sourceURL = r} className="form-control" id="connect-string" defaultValue="http://localhost:8086"> -
-
- - this.sourceName = r} className="form-control" id="name" defaultValue="Influx 1"> -
-
- - this.sourceUser = r} className="form-control" id="username"> -
-
- - this.sourcePassword = r} className="form-control" id="password" type="password"> -
-
-
- - this.sourceTelegraf = r} className="form-control" id="telegraf" type="text" defaultValue="telegraf"> -
-
- -
-
+ handleBlurSourceURL(newSource) { + if (this.state.editMode) { + return + } + + if (!newSource.url) { + return + } + + // if there is a type on source it has already been created + if (newSource.type) { + return + } + + createSourceAJAX(newSource).then(({data: sourceFromServer}) => { + this.props.addSource(sourceFromServer) + this.setState({source: sourceFromServer, error: null}) + }).catch(({data: error}) => { + this.setState({error: error.message}) + }) + }, + + handleSubmit(newSource) { + const {error} = this.state + const {notify, updateSource} = this.props + + if (error) { + return notify('error', error) + } + + updateSourceAJAX(newSource).then(({data: sourceFromServer}) => { + updateSource(sourceFromServer) + this.redirectToApp(sourceFromServer) + }).catch(() => { + notify('error', 'There was a problem updating the source. Check the settings') + }) + }, + + render() { + const {location} = this.props + const {source} = this.state + + return ( +
+ +
+
+
+
+
+
+

Welcome to Chronograf

+
+
@@ -94,8 +133,12 @@ export const CreateSource = React.createClass({ }, }) -function mapStateToProps(_) { - return {} +function mapDispatchToProps(dispatch) { + return { + addSource: bindActionCreators(addSourceAction, dispatch), + updateSource: bindActionCreators(updateSourceAction, dispatch), + notify: bindActionCreators(publishNotification, dispatch), + } } -export default connect(mapStateToProps, {addSourceAction})(withRouter(CreateSource)) +export default connect(null, mapDispatchToProps)(withRouter(CreateSource)) diff --git a/ui/src/sources/containers/SourcePage.js b/ui/src/sources/containers/SourcePage.js index 2a5b65b9b3..f9f259e6ec 100644 --- a/ui/src/sources/containers/SourcePage.js +++ b/ui/src/sources/containers/SourcePage.js @@ -78,7 +78,7 @@ export const SourcePage = React.createClass({ createSource(newSource).then(({data: sourceFromServer}) => { this.props.addSourceAction(sourceFromServer) - this.setState({source: sourceFromServer}) + this.setState({source: sourceFromServer, error: null}) }).catch(({data: error}) => { // dont want to flash this until they submit this.setState({error: error.message}) @@ -105,22 +105,40 @@ export const SourcePage = React.createClass({ render() { const {source, editMode} = this.state - const {addFlashMessage, router, location, params} = this.props + + if (editMode && !source.id) { + return
+ } return ( - +
+
+
+
+

+ {editMode ? "Edit Source" : "Add a New Source"} +

+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
) }, }) diff --git a/ui/src/style/unsorted.scss b/ui/src/style/unsorted.scss index e9de6eea6b..7dc087b49e 100644 --- a/ui/src/style/unsorted.scss +++ b/ui/src/style/unsorted.scss @@ -2,16 +2,6 @@ Unsorted ---------------------------------------------- */ -.select-source-page { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - overflow: auto; - @include custom-scrollbar($g2-kevlar, $c-pool); - @include gradient-v($g2-kevlar, $g0-obsidian); -} .text-right .btn { margin: 0 0 0 4px; }