diff --git a/ui/src/logs/actions/index.ts b/ui/src/logs/actions/index.ts index fa8ac6a897..05d603adbd 100644 --- a/ui/src/logs/actions/index.ts +++ b/ui/src/logs/actions/index.ts @@ -450,8 +450,7 @@ export const populateNamespacesAsync = ( export const getSourceAndPopulateNamespacesAsync = (sourceID: string) => async ( dispatch ): Promise => { - const response = await getSource(sourceID) - const source = response.data + const source = await getSource(sourceID) const proxyLink = getDeep(source, 'links.proxy', null) diff --git a/ui/src/shared/apis/index.ts b/ui/src/shared/apis/index.ts index 6828c694c0..eb51aefeb3 100644 --- a/ui/src/shared/apis/index.ts +++ b/ui/src/shared/apis/index.ts @@ -9,29 +9,51 @@ export function getSources() { }) } -export function getSource(id) { - return AJAX({ - url: null, - resource: 'sources', - id, - }) +export const getSource = async (id: string): Promise => { + try { + const {data: source} = await AJAX({ + url: null, + resource: 'sources', + id, + }) + + return source + } catch (error) { + throw error + } } -export function createSource(attributes) { - return AJAX({ - url: null, - resource: 'sources', - method: 'POST', - data: attributes, - }) +export const createSource = async ( + attributes: Partial +): Promise => { + try { + const {data: source} = await AJAX({ + url: null, + resource: 'sources', + method: 'POST', + data: attributes, + }) + + return source + } catch (error) { + throw error + } } -export function updateSource(newSource) { - return AJAX({ - url: newSource.links.self, - method: 'PATCH', - data: newSource, - }) +export const updateSource = async ( + newSource: Partial +): Promise => { + try { + const {data: source} = await AJAX({ + url: newSource.links.self, + method: 'PATCH', + data: newSource, + }) + + return source + } catch (error) { + throw error + } } export function deleteSource(source) { diff --git a/ui/src/shared/copy/notifications.ts b/ui/src/shared/copy/notifications.ts index 4e99e9a747..73d4cd765e 100644 --- a/ui/src/shared/copy/notifications.ts +++ b/ui/src/shared/copy/notifications.ts @@ -155,8 +155,10 @@ export const notifyUnableToRetrieveSources = () => 'Unable to retrieve sources.' export const notifyUnableToConnectSource = sourceName => `Unable to connect to source ${sourceName}.` -export const notifyErrorConnectingToSource = errorMessage => - `Unable to connect to InfluxDB source: ${errorMessage}` +export const notifyErrorConnectingToSource = errorMessage => ({ + ...defaultErrorNotification, + message: `Unable to connect to InfluxDB source: ${errorMessage}`, +}) // Multitenancy User Notifications // ---------------------------------------------------------------------------- diff --git a/ui/src/sources/components/SourceForm.js b/ui/src/sources/components/SourceForm.js deleted file mode 100644 index 6e092e609e..0000000000 --- a/ui/src/sources/components/SourceForm.js +++ /dev/null @@ -1,208 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import classnames from 'classnames' -import {connect} from 'react-redux' -import _ from 'lodash' - -import {insecureSkipVerifyText} from 'shared/copy/tooltipText' - -import {SUPERADMIN_ROLE} from 'src/auth/Authorized' - -export const SourceForm = ({ - source, - editMode, - onSubmit, - onInputChange, - onBlurSourceURL, - isUsingAuth, - gotoPurgatory, - isInitialSource, - me, -}) => ( -
- {isUsingAuth && isInitialSource ? ( -
- {me.role === SUPERADMIN_ROLE ? ( -

- {me.currentOrganization.name} has no connections -

- ) : ( -

- {me.currentOrganization.name} has no connections - available to {me.role}s -

- )} -
Add a Connection below:
-
- ) : null} - -
-
- - -
-
- - -
-
- - -
-
- - -
- {_.get(source, 'type', '').includes('enterprise') ? ( -
- - -
- ) : null} -
- - -
-
- - -
-
-
- - -
-
- {_.get(source, 'url', '').startsWith('https') ? ( -
-
- - -
- -
- ) : null} -
- - -
- {isUsingAuth ? ( - - ) : null} -
-
-
-) - -const {bool, func, shape, string} = PropTypes - -SourceForm.propTypes = { - 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, - onBlurSourceURL: func.isRequired, - me: shape({ - role: string, - currentOrganization: shape({ - id: string.isRequired, - name: string.isRequired, - }), - }), - isUsingAuth: bool, - isInitialSource: bool, - gotoPurgatory: func, -} - -const mapStateToProps = ({auth: {isUsingAuth, me}}) => ({isUsingAuth, me}) - -export default connect(mapStateToProps)(SourceForm) diff --git a/ui/src/sources/components/SourceForm.tsx b/ui/src/sources/components/SourceForm.tsx new file mode 100644 index 0000000000..39071b10f8 --- /dev/null +++ b/ui/src/sources/components/SourceForm.tsx @@ -0,0 +1,224 @@ +import React, {PureComponent, FocusEvent, MouseEvent, ChangeEvent} from 'react' +import classnames from 'classnames' +import {connect} from 'react-redux' +import _ from 'lodash' + +import {insecureSkipVerifyText} from 'src/shared/copy/tooltipText' + +import {SUPERADMIN_ROLE} from 'src/auth/Authorized' +import {Source, Me} from 'src/types' + +interface Props { + me: Me + source: Partial + editMode: boolean + isUsingAuth: boolean + gotoPurgatory: () => void + isInitialSource: boolean + onSubmit: (e: MouseEvent) => void + onInputChange: (e: ChangeEvent) => void + onBlurSourceURL: (e: FocusEvent) => void +} + +export class SourceForm extends PureComponent { + public render() { + const { + source, + onSubmit, + isUsingAuth, + onInputChange, + gotoPurgatory, + onBlurSourceURL, + isInitialSource, + } = this.props + return ( +
+ {isUsingAuth && isInitialSource && this.authIndicatior} +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ {this.isEnterprise && ( +
+ + +
+ )} +
+ + +
+
+ + +
+
+
+ + +
+
+ {this.isHTTPS && ( +
+
+ + +
+ +
+ )} +
+ + +
+ {isUsingAuth && ( + + )} +
+
+
+ ) + } + + private get authIndicatior(): JSX.Element { + const {me} = this.props + return ( +
+ {me.role.name === SUPERADMIN_ROLE ? ( +

+ {me.currentOrganization.name} has no connections +

+ ) : ( +

+ {me.currentOrganization.name} has no connections + available to {me.role}s +

+ )} +
Add a Connection below:
+
+ ) + } + + private get submitText(): string { + const {editMode} = this.props + if (editMode) { + return 'Save Changes' + } + + return 'Add Connection' + } + + private get submitIconClass(): string { + const {editMode} = this.props + return `icon ${editMode ? 'checkmark' : 'plus'}` + } + + private get submitClass(): string { + const {editMode} = this.props + return classnames('btn btn-block', { + 'btn-primary': editMode, + 'btn-success': !editMode, + }) + } + + private get isEnterprise(): boolean { + const {source} = this.props + return _.get(source, 'type', '').includes('enterprise') + } + + private get isHTTPS(): boolean { + const {source} = this.props + return _.get(source, 'url', '').startsWith('https') + } +} + +const mapStateToProps = ({auth: {isUsingAuth, me}}) => ({isUsingAuth, me}) + +export default connect(mapStateToProps)(SourceForm) diff --git a/ui/src/sources/containers/SourcePage.js b/ui/src/sources/containers/SourcePage.tsx similarity index 53% rename from ui/src/sources/containers/SourcePage.js rename to ui/src/sources/containers/SourcePage.tsx index 7d44ace13b..359d030bbe 100644 --- a/ui/src/sources/containers/SourcePage.js +++ b/ui/src/sources/containers/SourcePage.tsx @@ -1,47 +1,67 @@ -import React, {Component} from 'react' -import PropTypes from 'prop-types' -import {withRouter} from 'react-router' +import React, {PureComponent, MouseEvent, ChangeEvent} from 'react' +import {withRouter, WithRouterProps} from 'react-router' import _ from 'lodash' -import {getSource} from 'shared/apis' -import {createSource, updateSource} from 'shared/apis' +import {getSource} from 'src/shared/apis' +import {createSource, updateSource} from 'src/shared/apis' import { addSource as addSourceAction, updateSource as updateSourceAction, -} from 'shared/actions/sources' -import {notify as notifyAction} from 'shared/actions/notifications' + AddSource, + UpdateSource, +} from 'src/shared/actions/sources' +import { + notify as notifyAction, + PubishNotification, +} from 'src/shared/actions/notifications' import {connect} from 'react-redux' -import {bindActionCreators} from 'redux' -import Notifications from 'shared/components/Notifications' +import Notifications from 'src/shared/components/Notifications' 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' +import FancyScrollbar from 'src/shared/components/FancyScrollbar' +import SourceIndicator from 'src/shared/components/SourceIndicator' +import {DEFAULT_SOURCE} from 'src/shared/constants' +import {Source} from 'src/types' + +const INITIAL_PATH = '/sources/new' import { - notifyErrorConnectingToSource, - notifySourceCreationSucceeded, - notifySourceCreationFailed, notifySourceUdpated, notifySourceUdpateFailed, -} from 'shared/copy/notifications' + notifySourceCreationFailed, + notifyErrorConnectingToSource, + notifySourceCreationSucceeded, +} from 'src/shared/copy/notifications' import {ErrorHandling} from 'src/shared/decorators/errors' +interface Props extends WithRouterProps { + notify: PubishNotification + addSource: AddSource + updateSource: UpdateSource +} + +interface State { + isCreated: boolean + isLoading: boolean + source: Partial + editMode: boolean + isInitialSource: boolean +} + @ErrorHandling -class SourcePage extends Component { +class SourcePage extends PureComponent { constructor(props) { super(props) this.state = { isLoading: true, + isCreated: false, source: DEFAULT_SOURCE, editMode: props.params.id !== undefined, - isInitialSource: props.router.location.pathname === initialPath, + isInitialSource: props.router.location.pathname === INITIAL_PATH, } } - componentDidMount() { + public async componentDidMount() { const {editMode} = this.state const {params, notify} = this.props @@ -49,156 +69,19 @@ class SourcePage extends Component { return this.setState({isLoading: false}) } - getSource(params.id) - .then(({data: source}) => { - this.setState({ - source: {...DEFAULT_SOURCE, ...source}, - isLoading: false, - }) + try { + const source = await getSource(params.id) + this.setState({ + source: {...DEFAULT_SOURCE, ...source}, + isLoading: false, }) - .catch(error => { - notify(notifyErrorConnectingToSource(this._parseError(error))) - this.setState({isLoading: false}) - }) - } - - handleInputChange = e => { - let val = e.target.value - const name = e.target.name - - if (e.target.type === 'checkbox') { - val = e.target.checked + } catch (error) { + notify(notifyErrorConnectingToSource(this.parseError(error))) + this.setState({isLoading: false}) } - - this.setState(prevState => { - const source = { - ...prevState.source, - [name]: val, - } - - return {...prevState, source} - }) } - handleBlurSourceURL = () => { - const {source, editMode} = this.state - if (editMode) { - this.setState(this._normalizeSource) - return - } - - if (!source.url) { - return - } - - this.setState(this._normalizeSource, this._createSourceOnBlur) - } - - handleSubmit = e => { - e.preventDefault() - const {isCreated, editMode} = this.state - const isNewSource = !editMode - - if (!isCreated && isNewSource) { - return this.setState(this._normalizeSource, this._createSource) - } - - this.setState(this._normalizeSource, this._updateSource) - } - - gotoPurgatory = () => { - const {router} = this.props - router.push('/purgatory') - } - - _normalizeSource({source}) { - const url = source.url.trim() - if (source.url.startsWith('http')) { - return {source: {...source, url}} - } - return {source: {...source, url: `http://${url}`}} - } - - _createSourceOnBlur = () => { - const {source} = this.state - // if there is a type on source it has already been created - if (source.type) { - return - } - createSource(source) - .then(({data: sourceFromServer}) => { - this.props.addSource(sourceFromServer) - 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 creating InfluxDB connection: ', error) - }) - } - - _createSource = () => { - const {source} = this.state - const {notify} = this.props - createSource(source) - .then(({data: sourceFromServer}) => { - this.props.addSource(sourceFromServer) - this._redirect(sourceFromServer) - notify(notifySourceCreationSucceeded(source.name)) - }) - .catch(error => { - notify(notifySourceCreationFailed(source.name, this._parseError(error))) - }) - } - - _updateSource = () => { - const {source} = this.state - const {notify} = this.props - updateSource(source) - .then(({data: sourceFromServer}) => { - this.props.updateSource(sourceFromServer) - this._redirect(sourceFromServer) - notify(notifySourceUdpated(source.name)) - }) - .catch(error => { - notify(notifySourceUdpateFailed(source.name, this._parseError(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() { + public render() { const {isLoading, source, editMode, isInitialSource} = this.state if (isLoading) { @@ -248,31 +131,147 @@ class SourcePage extends Component { ) } + + private handleSubmit = (e: MouseEvent): void => { + e.preventDefault() + const {isCreated, editMode} = this.state + const isNewSource = !editMode + + if (!isCreated && isNewSource) { + return this.setState(this.normalizeSource, this.createSource) + } + + this.setState(this.normalizeSource, this.updateSource) + } + + private gotoPurgatory = (): void => { + const {router} = this.props + router.push('/purgatory') + } + + private normalizeSource({source}) { + const url = source.url.trim() + if (source.url.startsWith('http')) { + return {source: {...source, url}} + } + return {source: {...source, url: `http://${url}`}} + } + + private createSourceOnBlur = async () => { + const {source} = this.state + // if there is a type on source it has already been created + if (source.type) { + return + } + + try { + const sourceFromServer = await createSource(source) + this.props.addSource(sourceFromServer) + 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 creating InfluxDB connection: ', error) + } + } + + private createSource = async () => { + const {source} = this.state + const {notify} = this.props + try { + const sourceFromServer = await createSource(source) + this.props.addSource(sourceFromServer) + this.redirect(sourceFromServer) + notify(notifySourceCreationSucceeded(source.name)) + } catch (err) { + // dont want to flash this until they submit + notify(notifySourceCreationFailed(source.name, this.parseError(err))) + } + } + + private updateSource = async () => { + const {source} = this.state + const {notify} = this.props + try { + const sourceFromServer = await updateSource(source) + this.props.updateSource(sourceFromServer) + this.redirect(sourceFromServer) + notify(notifySourceUdpated(source.name)) + } catch (error) { + notify(notifySourceUdpateFailed(source.name, this.parseError(error))) + } + } + + private redirect = source => { + const {isInitialSource} = this.state + const {params, router} = this.props + + if (isInitialSource) { + return this.redirectToApp(source) + } + + router.push(`/sources/${params.sourceID}/manage-sources`) + } + + private parseError = (error): string => { + return _.get(error, ['data', 'message'], error) + } + + private 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) + } + + private handleInputChange = (e: ChangeEvent) => { + let val = e.target.value + const name = e.target.name + + if (e.target.type === 'checkbox') { + val = e.target.checked as any + } + + this.setState(prevState => { + const source = { + ...prevState.source, + [name]: val, + } + + return {...prevState, source} + }) + } + + private handleBlurSourceURL = () => { + const {source, editMode} = this.state + if (editMode) { + this.setState(this.normalizeSource) + return + } + + if (!source.url) { + return + } + + this.setState(this.normalizeSource, this.createSourceOnBlur) + } } -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.isRequired, - addSource: func.isRequired, - updateSource: func.isRequired, +const mdtp = { + notify: notifyAction, + addSource: addSourceAction, + updateSource: updateSourceAction, } -const mapDispatchToProps = dispatch => ({ - notify: bindActionCreators(notifyAction, dispatch), - addSource: bindActionCreators(addSourceAction, dispatch), - updateSource: bindActionCreators(updateSourceAction, dispatch), -}) -export default connect(null, mapDispatchToProps)(withRouter(SourcePage)) +export default connect(null, mdtp)(withRouter(SourcePage)) diff --git a/ui/src/types/index.ts b/ui/src/types/index.ts index 60da481c9f..00fa6fc97b 100644 --- a/ui/src/types/index.ts +++ b/ui/src/types/index.ts @@ -25,7 +25,7 @@ import { TagValues, } from './query' import {AlertRule, Kapacitor, Task, RuleValues} from './kapacitor' -import {Source, SourceLinks} from './sources' +import {NewSource, Source, SourceLinks} from './sources' import {DropdownAction, DropdownItem, Constructable} from './shared' import { Notification, @@ -70,6 +70,7 @@ export { TagValues, AlertRule, Kapacitor, + NewSource, Source, SourceLinks, DropdownAction, diff --git a/ui/src/types/sources.ts b/ui/src/types/sources.ts index e912ae0472..2ba7fb6c40 100644 --- a/ui/src/types/sources.ts +++ b/ui/src/types/sources.ts @@ -1,5 +1,7 @@ import {Kapacitor, Service} from './' +export type NewSource = Pick> + export interface Source { id: string name: string diff --git a/ui/test/sources/components/SourceForm.test.js b/ui/test/sources/components/SourceForm.test.tsx similarity index 95% rename from ui/test/sources/components/SourceForm.test.js rename to ui/test/sources/components/SourceForm.test.tsx index 8375706d61..7e9e21f0fb 100644 --- a/ui/test/sources/components/SourceForm.test.js +++ b/ui/test/sources/components/SourceForm.test.tsx @@ -2,6 +2,7 @@ import React from 'react' import {shallow} from 'enzyme' import {SourceForm} from 'src/sources/components/SourceForm' +import {me} from 'test/resources' const setup = (override = {}) => { const noop = () => {} @@ -23,7 +24,7 @@ const setup = (override = {}) => { isUsingAuth: false, gotoPurgatory: noop, isInitialSource: false, - me: {}, + me, ...override, }