diff --git a/CHANGELOG.md b/CHANGELOG.md index 854a4e229b..811b45ff3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## v1.3.10.0 [unreleased] ### Bug Fixes 1. [#2095](https://github.com/influxdata/chronograf/pull/2095): Improve the copy in the retention policy edit page +1. [#2122](https://github.com/influxdata/chronograf/pull/2122): Fix 'could not connect to source' bug on source creation with unsafe-ssl 1. [#2093](https://github.com/influxdata/chronograf/pull/2093): Fix when exporting `SHOW DATABASES` CSV has bad data 1. [#2098](https://github.com/influxdata/chronograf/pull/2098): Fix not-equal-to highlighting in Kapacitor Rule Builder diff --git a/ui/src/index.js b/ui/src/index.js index 8a8838dfc5..8783417b8a 100644 --- a/ui/src/index.js +++ b/ui/src/index.js @@ -26,7 +26,7 @@ import { TickscriptPage, } from 'src/kapacitor' import {AdminPage} from 'src/admin' -import {CreateSource, SourcePage, ManageSources} from 'src/sources' +import {SourcePage, ManageSources} from 'src/sources' import NotFound from 'shared/components/NotFound' import {getMe} from 'shared/apis' @@ -127,7 +127,7 @@ const Root = React.createClass({ diff --git a/ui/src/shared/constants/index.js b/ui/src/shared/constants/index.js index cb9afc9373..6f00398878 100644 --- a/ui/src/shared/constants/index.js +++ b/ui/src/shared/constants/index.js @@ -411,3 +411,14 @@ export const PAGE_HEADER_HEIGHT = 60 // TODO: get this dynamically to ensure lon export const PAGE_CONTAINER_MARGIN = 30 // TODO: get this dynamically to ensure longevity export const LAYOUT_MARGIN = 4 export const DASHBOARD_LAYOUT_ROW_HEIGHT = 83.5 + +export const DEFAULT_SOURCE = { + url: 'http://localhost:8086', + name: 'Influx 1', + username: '', + password: '', + default: true, + telegraf: 'telegraf', + insecureSkipVerify: false, + metaUrl: '', +} diff --git a/ui/src/sources/components/SourceForm.js b/ui/src/sources/components/SourceForm.js index 020b295a35..bebb855698 100644 --- a/ui/src/sources/components/SourceForm.js +++ b/ui/src/sources/components/SourceForm.js @@ -8,32 +8,8 @@ class SourceForm extends Component { super(props) } - handleSubmitForm = e => { - e.preventDefault() - const newSource = { - ...this.props.source, - url: this.sourceURL.value.trim(), - name: this.sourceName.value, - username: this.sourceUsername.value, - password: this.sourcePassword.value, - default: this.sourceDefault.checked, - telegraf: this.sourceTelegraf.value, - insecureSkipVerify: this.sourceInsecureSkipVerify - ? this.sourceInsecureSkipVerify.checked - : false, - metaUrl: this.metaUrl && this.metaUrl.value.trim(), - } - - this.props.onSubmit(newSource) - } - - handleUseDefaultValues = () => { - this.sourceURL.value = 'http://localhost:8086' - this.sourceName.value = 'My InfluxDB' - } - handleBlurSourceURL = () => { - const url = this.sourceURL.value.trim() + const url = this.props.source.url.trim() if (!url) { return @@ -41,32 +17,31 @@ class SourceForm extends Component { const newSource = { ...this.props.source, - url: this.sourceURL.value.trim(), + url, } this.props.onBlurSourceURL(newSource) } render() { - const {source, editMode, onInputChange} = this.props + const {source, editMode, onSubmit, onInputChange} = this.props return (

Connection Details


-
+
(this.sourceURL = r)} className="form-control" id="connect-string" placeholder="Address of InfluxDB" onChange={onInputChange} - value={source.url || ''} + value={source.url} onBlur={this.handleBlurSourceURL} required={true} /> @@ -76,12 +51,11 @@ class SourceForm extends Component { (this.sourceName = r)} className="form-control" id="name" placeholder="Name this source" onChange={onInputChange} - value={source.name || ''} + value={source.name} required={true} />
@@ -90,11 +64,10 @@ class SourceForm extends Component { (this.sourceUsername = r)} className="form-control" id="username" onChange={onInputChange} - value={source.username || ''} + value={source.username} />
@@ -102,11 +75,10 @@ class SourceForm extends Component { (this.sourcePassword = r)} className="form-control" id="password" onChange={onInputChange} - value={source.password || ''} + value={source.password} />
{_.get(source, 'type', '').includes('enterprise') @@ -115,12 +87,11 @@ class SourceForm extends Component { (this.metaUrl = r)} className="form-control" id="meta-url" placeholder="http://localhost:8091" onChange={onInputChange} - value={source.metaUrl || ''} + value={source.metaUrl} /> : null} @@ -129,11 +100,10 @@ class SourceForm extends Component { (this.sourceTelegraf = r)} className="form-control" id="telegraf" onChange={onInputChange} - value={source.telegraf || 'telegraf'} + value={source.telegraf} />
@@ -141,8 +111,9 @@ class SourceForm extends Component { (this.sourceDefault = r)} + name="default" + checked={source.default} + onChange={onInputChange} />
@@ -176,13 +148,6 @@ class SourceForm extends Component { {editMode ? 'Save Changes' : 'Add Source'}
- - Use Default Values - @@ -190,10 +155,19 @@ class SourceForm extends Component { } } -const {bool, func, shape} = PropTypes +const {bool, func, shape, string} = PropTypes SourceForm.propTypes = { - source: shape({}).isRequired, + source: shape({ + url: string.isRequired, + name: string.isRequired, + username: string.isRequired, + password: string.isRequired, + telegraf: string.isRequired, + insecureSkipVerify: bool.isRequired, + default: bool.isRequired, + metaUrl: string.isRequired, + }).isRequired, editMode: bool.isRequired, onInputChange: func.isRequired, onSubmit: func.isRequired, diff --git a/ui/src/sources/containers/CreateSource.js b/ui/src/sources/containers/CreateSource.js deleted file mode 100644 index e37d248aa3..0000000000 --- a/ui/src/sources/containers/CreateSource.js +++ /dev/null @@ -1,149 +0,0 @@ -import React, {PropTypes} from 'react' -import {withRouter} from 'react-router' -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 'shared/actions/sources' -import {publishNotification} from 'shared/actions/notifications' - -const {func, shape, string} = PropTypes - -export const CreateSource = React.createClass({ - propTypes: { - router: shape({ - push: func.isRequired, - }).isRequired, - location: shape({ - query: shape({ - redirectPath: string, - }).isRequired, - }).isRequired, - addSource: func, - updateSource: func, - notify: func, - }, - - getInitialState() { - return { - source: {}, - } - }, - - redirectToApp(source) { - const {redirectPath} = this.props.location.query - if (!redirectPath) { - return this.props.router.push(`/sources/${source.id}/hosts`) - } - - const fixedPath = redirectPath.replace( - /\/sources\/[^/]*/, - `/sources/${source.id}` - ) - return this.props.router.push(fixedPath) - }, - - 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}) - }) - }, - - 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 {source} = this.state - - return ( -
- -
-
-
-
-
-
-

Welcome to Chronograf

-
- -
-
-
-
-
-
- ) - }, -}) - -function mapDispatchToProps(dispatch) { - return { - addSource: bindActionCreators(addSourceAction, dispatch), - updateSource: bindActionCreators(updateSourceAction, dispatch), - notify: bindActionCreators(publishNotification, dispatch), - } -} - -export default connect(null, mapDispatchToProps)(withRouter(CreateSource)) diff --git a/ui/src/sources/containers/SourcePage.js b/ui/src/sources/containers/SourcePage.js index d407203812..4997c6a48e 100644 --- a/ui/src/sources/containers/SourcePage.js +++ b/ui/src/sources/containers/SourcePage.js @@ -1,67 +1,73 @@ -import React, {PropTypes} from 'react' +import React, {PropTypes, Component} from 'react' import {withRouter} from 'react-router' +import _ from 'lodash' import {getSource} from 'shared/apis' import {createSource, updateSource} from 'shared/apis' import { addSource as addSourceAction, updateSource as updateSourceAction, } from 'shared/actions/sources' +import {publishNotification} from 'shared/actions/notifications' import {connect} from 'react-redux' import SourceForm from 'src/sources/components/SourceForm' import FancyScrollbar from 'shared/components/FancyScrollbar' import SourceIndicator from 'shared/components/SourceIndicator' +import {DEFAULT_SOURCE} from 'shared/constants' +const initialPath = '/sources/new' -const {func, shape, string} = PropTypes +class SourcePage extends Component { + constructor(props) { + super(props) -export const SourcePage = React.createClass({ - propTypes: { - params: shape({ - id: string, - sourceID: string, - }), - router: shape({ - push: func.isRequired, - }).isRequired, - location: shape({ - query: shape({ - redirectPath: string, - }).isRequired, - }).isRequired, - addFlashMessage: func.isRequired, - addSourceAction: func, - updateSourceAction: func, - }, - - getInitialState() { - return { - source: {}, - editMode: this.props.params.id !== undefined, - error: '', + this.state = { + isLoading: true, + source: DEFAULT_SOURCE, + editMode: props.params.id !== undefined, + isInitialSource: props.router.location.pathname === initialPath, } - }, + } componentDidMount() { - if (!this.state.editMode) { - return + const {editMode} = this.state + const {params} = this.props + + if (!editMode) { + return this.setState({isLoading: false}) } - getSource(this.props.params.id).then(({data: source}) => { - this.setState({source}) - }) - }, - handleInputChange(e) { - const val = e.target.value - const name = e.target.name - this.setState(prevState => { - const newSource = Object.assign({}, prevState.source, { - [name]: val, + getSource(params.id) + .then(({data: source}) => { + this.setState({ + source: {...DEFAULT_SOURCE, ...source}, + isLoading: false, + }) }) - return Object.assign({}, prevState, {source: newSource}) - }) - }, + .catch(error => { + this.handleError('Could not connect to source', error) + this.setState({isLoading: false}) + }) + } - handleBlurSourceURL(newSource) { + handleInputChange = e => { + let val = e.target.value + const name = e.target.name + + if (e.target.type === 'checkbox') { + val = e.target.checked + } + + this.setState(prevState => { + const source = { + ...prevState.source, + [name]: val, + } + + return {...prevState, source} + }) + } + + handleBlurSourceURL = newSource => { if (this.state.editMode) { return } @@ -78,57 +84,108 @@ export const SourcePage = React.createClass({ createSource(newSource) .then(({data: sourceFromServer}) => { this.props.addSourceAction(sourceFromServer) - this.setState({source: sourceFromServer, error: null}) - }) - .catch(({data: error}) => { - // dont want to flash this until they submit - this.setState({error: error.message}) - }) - }, - - handleSubmit(newSource) { - const {router, params, addFlashMessage} = this.props - const {error} = this.state - - if (error) { - return addFlashMessage({type: 'error', text: error}) - } - - updateSource(newSource) - .then(({data: sourceFromServer}) => { - this.props.updateSourceAction(sourceFromServer) - router.push(`/sources/${params.sourceID}/manage-sources`) - addFlashMessage({type: 'success', text: 'The source info saved'}) - }) - .catch(() => { - addFlashMessage({ - type: 'error', - text: 'There was a problem updating the source. Check the settings', + this.setState({ + source: {...DEFAULT_SOURCE, ...sourceFromServer}, + isCreated: true, }) }) - }, + .catch(err => { + // dont want to flash this until they submit + const error = this.parseError(err) + console.error('Error on source creation: ', error) + }) + } + + handleSubmit = e => { + e.preventDefault() + const {notify} = this.props + const {isCreated, source, editMode} = this.state + const isNewSource = !editMode + + if (!isCreated && isNewSource) { + return createSource(source) + .then(({data: sourceFromServer}) => { + this.props.addSourceAction(sourceFromServer) + this._redirect(sourceFromServer) + }) + .catch(error => { + this.handleError('Unable to create source', error) + }) + } + + updateSource(source) + .then(({data: sourceFromServer}) => { + this.props.updateSourceAction(sourceFromServer) + this._redirect(sourceFromServer) + notify('success', `New source ${source.name} added`) + }) + .catch(error => { + this.handleError('Unable to update source', error) + }) + } + + handleError = (bannerText, err) => { + const {notify} = this.props + const error = this.parseError(err) + console.error('Error: ', error) + notify('error', `${bannerText}: ${error}`) + } + + _redirect = source => { + const {isInitialSource} = this.state + const {params, router} = this.props + + if (isInitialSource) { + return this._redirectToApp(source) + } + + router.push(`/sources/${params.sourceID}/manage-sources`) + } + + _redirectToApp = source => { + const {location, router} = this.props + const {redirectPath} = location.query + + if (!redirectPath) { + return router.push(`/sources/${source.id}/hosts`) + } + + const fixedPath = redirectPath.replace( + /\/sources\/[^/]*/, + `/sources/${source.id}` + ) + return router.push(fixedPath) + } + + parseError = error => { + return _.get(error, ['data', 'message'], error) + } render() { - const {source, editMode} = this.state + const {isLoading, source, editMode, isInitialSource} = this.state - if (editMode && !source.id) { + if (isLoading) { return
} return ( -
-
-
-
-

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

-
-
- -
-
-
+
+ {isInitialSource + ? null + :
+
+
+
+

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

+
+
+ +
+
+
+
}
@@ -148,13 +205,33 @@ export const SourcePage = React.createClass({
) - }, -}) - -function mapStateToProps(_) { - return {} + } } -export default connect(mapStateToProps, {addSourceAction, updateSourceAction})( - withRouter(SourcePage) -) +const {func, shape, string} = PropTypes + +SourcePage.propTypes = { + params: shape({ + id: string, + sourceID: string, + }), + router: shape({ + push: func.isRequired, + }).isRequired, + location: shape({ + query: shape({ + redirectPath: string, + }).isRequired, + }).isRequired, + notify: func, + addSourceAction: func, + updateSourceAction: func, +} + +const mapStateToProps = () => ({}) + +export default connect(mapStateToProps, { + notify: publishNotification, + addSourceAction, + updateSourceAction, +})(withRouter(SourcePage)) diff --git a/ui/src/sources/index.js b/ui/src/sources/index.js index 9cb7df89eb..416cd7a817 100644 --- a/ui/src/sources/index.js +++ b/ui/src/sources/index.js @@ -1,4 +1,3 @@ -import CreateSource from './containers/CreateSource' import SourcePage from './containers/SourcePage' import ManageSources from './containers/ManageSources' -export {CreateSource, SourcePage, ManageSources} +export {SourcePage, ManageSources} diff --git a/ui/src/style/layout/page-header.scss b/ui/src/style/layout/page-header.scss index 4477b6c291..9b2d89cc3b 100644 --- a/ui/src/style/layout/page-header.scss +++ b/ui/src/style/layout/page-header.scss @@ -23,6 +23,7 @@ $page-header-weight: 400 !important; max-width: 100%; } .page-header__container { + position: relative; width: 100%; display: flex; align-items: center; @@ -31,6 +32,26 @@ $page-header-weight: 400 !important; width: 100%; max-width: ($page-wrapper-max-width - $page-wrapper-padding - $page-wrapper-padding); } +.page-header__container.page-header__source-page { + justify-content: center; +} +.page-header__col-md-8 { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; +} +@media screen and (min-width: 992px) { + /* + NOTE: + Breakpoint and % width are based on the bootstrap grid + If the source form column sizing is ever changed, this + will have to be manually updated + */ + .page-header__col-md-8 { + width: 66.66667%; + } +} .page-header__left, .page-header__right { display: flex; diff --git a/ui/src/style/layout/page.scss b/ui/src/style/layout/page.scss index 51680d3cec..935a685ea7 100644 --- a/ui/src/style/layout/page.scss +++ b/ui/src/style/layout/page.scss @@ -22,6 +22,11 @@ width: 100%; height: calc(100% - #{$chronograf-page-header-height}) !important; @include gradient-v($g2-kevlar,$g0-obsidian); + + &:only-child { + top: 0; + height: 100%; + } } .container-fluid { padding: ($chronograf-page-header-height / 2) $page-wrapper-padding; @@ -36,7 +41,7 @@ .page-contents.presentation-mode { top: 0; height: 100% !important; - + .container-fluid {padding: 8px !important;} .template-control--manage {display: none;} -} \ No newline at end of file +}