diff --git a/ui/src/ifql/components/IFQLForm.tsx b/ui/src/ifql/components/IFQLForm.tsx new file mode 100644 index 0000000000..1042b007dc --- /dev/null +++ b/ui/src/ifql/components/IFQLForm.tsx @@ -0,0 +1,80 @@ +import React, {ChangeEvent, PureComponent} from 'react' + +import Input from 'src/kapacitor/components/KapacitorFormInput' + +import {NewService} from 'src/types' + +interface Props { + service: NewService + exists: boolean + onSubmit: (e: ChangeEvent) => void + onInputChange: (e: ChangeEvent) => void +} + +class IFQLForm extends PureComponent { + public render() { + const {service, onSubmit, onInputChange} = this.props + + return ( +
+
+
+

Connect to IFQL

+
+
+
+
+
+ + +
+ +
+
+
+
+ ) + } + + private get buttonText(): string { + const {exists} = this.props + + if (exists) { + return 'Update' + } + + return 'Connect' + } + + private get url(): string { + const { + service: {url}, + } = this.props + if (url) { + return url + } + + return '' + } +} + +export default IFQLForm diff --git a/ui/src/ifql/components/IFQLOverlay.tsx b/ui/src/ifql/components/IFQLOverlay.tsx new file mode 100644 index 0000000000..6c35875749 --- /dev/null +++ b/ui/src/ifql/components/IFQLOverlay.tsx @@ -0,0 +1,95 @@ +import React, {PureComponent, ChangeEvent} from 'react' +import {connect} from 'react-redux' + +import IFQLForm from 'src/ifql/components/IFQLForm' + +import {Source, NewService, Notification} from 'src/types' + +import {notify as notifyAction} from 'src/shared/actions/notifications' +import { + createServiceAsync, + CreateServiceAsync, +} from 'src/shared/actions/services' +import {ifqlCreated, ifqlNotCreated} from 'src/shared/copy/notifications' + +interface Props { + source: Source + onDismiss: () => void + notify: (message: Notification) => void + createService: CreateServiceAsync +} + +interface State { + service: NewService +} + +const port = 8093 + +class IFQLOverlay extends PureComponent { + constructor(props) { + super(props) + this.state = { + service: this.defaultService, + } + } + + public render() { + return ( + + ) + } + + private get defaultService(): NewService { + return { + name: 'IFQL', + url: this.url, + username: '', + insecureSkipVerify: false, + type: 'ifql', + active: true, + } + } + + private handleInputChange = (e: ChangeEvent): void => { + const {value, name} = e.target + const update = {[name]: value} + + this.setState({service: {...this.state.service, ...update}}) + } + + private handleSubmit = async e => { + e.preventDefault() + const {notify, source, onDismiss, createService} = this.props + + const {service} = this.state + + try { + await createService(source, service) + } catch (error) { + notify(ifqlNotCreated(error.message)) + return + } + + notify(ifqlCreated) + onDismiss() + } + + private get url(): string { + const parser = document.createElement('a') + parser.href = this.props.source.url + + return `${parser.protocol}//${parser.hostname}:${port}` + } +} + +const mdtp = { + notify: notifyAction, + createService: createServiceAsync, +} + +export default connect(null, mdtp)(IFQLOverlay) diff --git a/ui/src/ifql/containers/CheckServices.tsx b/ui/src/ifql/containers/CheckServices.tsx index 4db734238c..d452a25637 100644 --- a/ui/src/ifql/containers/CheckServices.tsx +++ b/ui/src/ifql/containers/CheckServices.tsx @@ -1,14 +1,21 @@ -import {PureComponent, ReactChildren} from 'react' +import React, {PureComponent, ReactChildren} from 'react' import {connect} from 'react-redux' import {withRouter, WithRouterProps} from 'react-router' -import {Source} from 'src/types' -import * as actions from 'src/shared/actions/services' +import IFQLOverlay from 'src/ifql/components/IFQLOverlay' +import {OverlayContext} from 'src/shared/components/OverlayTechnology' +import {Source, Service} from 'src/types' +import * as a from 'src/shared/actions/overlayTechnology' +import * as b from 'src/shared/actions/services' + +const actions = {...a, ...b} interface Props { sources: Source[] + services: Service[] children: ReactChildren - fetchServicesAsync: actions.FetchServicesAsync + showOverlay: a.ShowOverlay + fetchServicesAsync: b.FetchServicesAsync } export class CheckServices extends PureComponent { @@ -22,17 +29,40 @@ export class CheckServices extends PureComponent { } await this.props.fetchServicesAsync(source) + + if (!this.props.services.length) { + this.overlay() + } } public render() { return this.props.children } + + private overlay() { + const {showOverlay, services, sources, params} = this.props + const source = sources.find(s => s.id === params.sourceID) + + if (services.length) { + return + } + + showOverlay( + + {({onDismissOverlay}) => ( + + )} + , + {} + ) + } } const mdtp = { fetchServicesAsync: actions.fetchServicesAsync, + showOverlay: actions.showOverlay, } -const mstp = ({sources}) => ({sources}) +const mstp = ({sources, services}) => ({sources, services}) export default connect(mstp, mdtp)(withRouter(CheckServices)) diff --git a/ui/src/ifql/containers/IFQLPage.tsx b/ui/src/ifql/containers/IFQLPage.tsx index 6bcc52473f..7adbba3dd2 100644 --- a/ui/src/ifql/containers/IFQLPage.tsx +++ b/ui/src/ifql/containers/IFQLPage.tsx @@ -1,5 +1,5 @@ import React, {PureComponent} from 'react' -import {bindActionCreators} from 'redux' +import {RouteComponentProps} from 'react-router' import {connect} from 'react-redux' import _ from 'lodash' @@ -7,7 +7,7 @@ import {ErrorHandling} from 'src/shared/decorators/errors' import CheckServices from 'src/ifql/containers/CheckServices' import TimeMachine from 'src/ifql/components/TimeMachine' import KeyboardShortcuts from 'src/shared/components/KeyboardShortcuts' -import {InputArg, Handlers, DeleteFuncNodeArgs, Func} from 'src/types/ifql' + import {notify as notifyAction} from 'src/shared/actions/notifications' import {analyzeSuccess} from 'src/shared/copy/notifications' @@ -16,9 +16,16 @@ import {getSuggestions, getAST, getTimeSeries} from 'src/ifql/apis' import {builder, argTypes} from 'src/ifql/constants' import {funcNames} from 'src/ifql/constants' -import {Notification} from 'src/types' -import {Suggestion, FlatBody, Links} from 'src/types/ifql' -import {Service} from 'src/types' +import {Source, Service, Notification} from 'src/types' +import { + Suggestion, + FlatBody, + Links, + InputArg, + Handlers, + DeleteFuncNodeArgs, + Func, +} from 'src/types/ifql' interface Status { type: string @@ -28,6 +35,7 @@ interface Status { interface Props { links: Links services: Service[] + sources: Source[] notify: (message: Notification) => void } @@ -47,7 +55,10 @@ interface State { export const IFQLContext = React.createContext() @ErrorHandling -export class IFQLPage extends PureComponent { +export class IFQLPage extends PureComponent< + Props & RouteComponentProps, + State +> { constructor(props) { super(props) this.state = { @@ -430,12 +441,12 @@ export class IFQLPage extends PureComponent { } } -const mapStateToProps = ({links, services}) => { - return {links: links.ifql, services} +const mapStateToProps = ({links, services, sources}) => { + return {links: links.ifql, services, sources} } -const mapDispatchToProps = dispatch => ({ - notify: bindActionCreators(notifyAction, dispatch), -}) +const mapDispatchToProps = { + notify: notifyAction, +} export default connect(mapStateToProps, mapDispatchToProps)(IFQLPage) diff --git a/ui/src/kapacitor/components/KapacitorFormInput.tsx b/ui/src/kapacitor/components/KapacitorFormInput.tsx index fe5439a832..29619a1b26 100644 --- a/ui/src/kapacitor/components/KapacitorFormInput.tsx +++ b/ui/src/kapacitor/components/KapacitorFormInput.tsx @@ -5,9 +5,10 @@ interface Props { label: string value: string placeholder: string - onChange: (e: ChangeEvent) => void maxLength?: number inputType?: string + customClass?: string + onChange: (e: ChangeEvent) => void } const KapacitorFormInput: SFC = ({ @@ -18,8 +19,9 @@ const KapacitorFormInput: SFC = ({ onChange, maxLength, inputType, + customClass, }) => ( -
+
= ({ KapacitorFormInput.defaultProps = { inputType: '', + customClass: 'col-sm-6', } export default KapacitorFormInput diff --git a/ui/src/kapacitor/containers/KapacitorPage.tsx b/ui/src/kapacitor/containers/KapacitorPage.tsx index a2f73e7ccb..3084965a43 100644 --- a/ui/src/kapacitor/containers/KapacitorPage.tsx +++ b/ui/src/kapacitor/containers/KapacitorPage.tsx @@ -169,6 +169,7 @@ export class KapacitorPage extends PureComponent { return ( { onChangeUrl={this.handleChangeUrl} onReset={this.handleResetToDefaults} onInputChange={this.handleInputChange} - notify={notify} onCheckboxChange={this.handleCheckboxChange} /> ) diff --git a/ui/src/shared/actions/overlayTechnology.ts b/ui/src/shared/actions/overlayTechnology.ts index 69fb9eac0e..ec19ed4424 100644 --- a/ui/src/shared/actions/overlayTechnology.ts +++ b/ui/src/shared/actions/overlayTechnology.ts @@ -8,6 +8,19 @@ interface Options { transitionTime?: number } +export type ShowOverlay = ( + OverlayNode: OverlayNodeType, + options: Options +) => ActionOverlayNode + +export interface ActionOverlayNode { + type: 'SHOW_OVERLAY' + payload: { + OverlayNode + options + } +} + export const showOverlay = ( OverlayNode: OverlayNodeType, options: Options diff --git a/ui/src/shared/actions/services.ts b/ui/src/shared/actions/services.ts index 059ac09380..1c56e22f02 100644 --- a/ui/src/shared/actions/services.ts +++ b/ui/src/shared/actions/services.ts @@ -1,5 +1,8 @@ -import {Source, Service} from 'src/types' -import {getServices as getServicesAJAX} from 'src/shared/apis' +import {Source, Service, NewService} from 'src/types' +import { + getServices as getServicesAJAX, + createService as createServiceAJAX, +} from 'src/shared/apis' import {notify} from './notifications' import {couldNotGetServices} from 'src/shared/copy/notifications' @@ -99,7 +102,6 @@ export const setActiveService = ( }) export type FetchServicesAsync = (source: Source) => (dispatch) => Promise - export const fetchServicesAsync = (source: Source) => async ( dispatch ): Promise => { @@ -110,3 +112,21 @@ export const fetchServicesAsync = (source: Source) => async ( dispatch(notify(couldNotGetServices)) } } + +export type CreateServiceAsync = ( + source: Source, + service: NewService +) => (dispatch) => Promise + +export const createServiceAsync = ( + source: Source, + service: NewService +) => async (dispatch): Promise => { + try { + const s = await createServiceAJAX(source, service) + dispatch(addService(s)) + } catch (err) { + console.error(err.data) + throw err.data + } +} diff --git a/ui/src/shared/apis/index.ts b/ui/src/shared/apis/index.ts index a74772153f..d81f9fd7a0 100644 --- a/ui/src/shared/apis/index.ts +++ b/ui/src/shared/apis/index.ts @@ -1,6 +1,6 @@ import AJAX from 'src/utils/ajax' import {AlertTypes} from 'src/kapacitor/constants' -import {Kapacitor, Service} from 'src/types' +import {Kapacitor, Source, Service, NewService} from 'src/types' export function getSources() { return AJAX({ @@ -311,6 +311,31 @@ export const getServices = async (url: string): Promise => { method: 'GET', }) + return data.services + } catch (error) { + console.error(error) + throw error + } +} + +export const createService = async ( + source: Source, + { + url, + name = 'My IFQLD', + type, + username, + password, + insecureSkipVerify, + }: NewService +): Promise => { + try { + const {data} = await AJAX({ + url: source.links.services, + method: 'POST', + data: {url, name, type, username, password, insecureSkipVerify}, + }) + return data } catch (error) { console.error(error) diff --git a/ui/src/shared/copy/notifications.js b/ui/src/shared/copy/notifications.ts similarity index 98% rename from ui/src/shared/copy/notifications.js rename to ui/src/shared/copy/notifications.ts index 99f237b3f9..16c912a9b7 100644 --- a/ui/src/shared/copy/notifications.js +++ b/ui/src/shared/copy/notifications.ts @@ -1,7 +1,7 @@ // All copy for notifications should be stored here for easy editing // and ensuring stylistic consistency -import {FIVE_SECONDS, TEN_SECONDS, INFINITE} from 'shared/constants/index' +import {FIVE_SECONDS, TEN_SECONDS, INFINITE} from 'src/shared/constants/index' const defaultErrorNotification = { type: 'error', @@ -620,3 +620,13 @@ export const couldNotGetServices = { ...defaultErrorNotification, message: 'We could not get services', } + +export const ifqlCreated = { + ...defaultSuccessNotification, + message: 'IFQL Connection Created. Script your heart out!', +} + +export const ifqlNotCreated = (message: string) => ({ + ...defaultErrorNotification, + message, +}) diff --git a/ui/src/style/components/time-machine/ifql-overlay.scss b/ui/src/style/components/time-machine/ifql-overlay.scss new file mode 100644 index 0000000000..0da27b6bba --- /dev/null +++ b/ui/src/style/components/time-machine/ifql-overlay.scss @@ -0,0 +1,4 @@ +.ifql-overlay { + max-width: 500px; + margin: 0 auto; +} \ No newline at end of file diff --git a/ui/src/style/pages/time-machine.scss b/ui/src/style/pages/time-machine.scss index 2b6c49f5b7..b379f22c81 100644 --- a/ui/src/style/pages/time-machine.scss +++ b/ui/src/style/pages/time-machine.scss @@ -3,6 +3,7 @@ ---------------------------------------------------------------------------- */ +@import '../components/time-machine/ifql-overlay'; @import '../components/time-machine/ifql-editor'; @import '../components/time-machine/ifql-builder'; @import '../components/time-machine/ifql-explorer'; diff --git a/ui/src/types/index.ts b/ui/src/types/index.ts index 91e87246bf..6804f009dc 100644 --- a/ui/src/types/index.ts +++ b/ui/src/types/index.ts @@ -1,4 +1,4 @@ -import {Service} from './services' +import {Service, NewService} from './services' import {AuthLinks, Organization, Role, User, Me} from './auth' import {Template, Cell, CellQuery, Legend, Axes} from './dashboard' import { @@ -55,4 +55,5 @@ export { NotificationFunc, Axes, Service, + NewService, } diff --git a/ui/src/types/services.ts b/ui/src/types/services.ts index 74e41c6745..19115df1e5 100644 --- a/ui/src/types/services.ts +++ b/ui/src/types/services.ts @@ -1,3 +1,13 @@ +export interface NewService { + url: string + name: string + type: string + username?: string + password?: string + active: boolean + insecureSkipVerify: boolean +} + export interface Service { id?: string url: string