Introduce ability to add an IFQL connection

pull/10616/head
Andrew Watkins 2018-05-18 16:00:04 -07:00
parent f32af7bb09
commit e3c75858e5
14 changed files with 328 additions and 25 deletions

View File

@ -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<HTMLFormElement>) => void
onInputChange: (e: ChangeEvent<HTMLInputElement>) => void
}
class IFQLForm extends PureComponent<Props> {
public render() {
const {service, onSubmit, onInputChange} = this.props
return (
<div className="ifql-overlay">
<div className="template-variable-manager--header">
<div className="page-header__left">
<h1 className="page-header__title">Connect to IFQL</h1>
</div>
<div className="page-header__right" />
</div>
<div className="template-variable-manager--body">
<form onSubmit={onSubmit} style={{display: 'inline-block'}}>
<Input
name="url"
label="IFQL URL"
value={this.url}
placeholder={this.url}
onChange={onInputChange}
/>
<Input
name="name"
label="Name"
value={service.name}
placeholder={service.name}
onChange={onInputChange}
maxLength={33}
/>
<div className="form-group form-group-submit col-xs-12 text-center">
<button
className="btn btn-success"
type="submit"
data-test="submit-button"
>
{this.buttonText}
</button>
</div>
</form>
</div>
</div>
)
}
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

View File

@ -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<Props, State> {
constructor(props) {
super(props)
this.state = {
service: this.defaultService,
}
}
public render() {
return (
<IFQLForm
service={this.state.service}
onSubmit={this.handleSubmit}
onInputChange={this.handleInputChange}
exists={false}
/>
)
}
private get defaultService(): NewService {
return {
name: 'IFQL',
url: this.url,
username: '',
insecureSkipVerify: false,
type: 'ifql',
active: true,
}
}
private handleInputChange = (e: ChangeEvent<HTMLInputElement>): 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)

View File

@ -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<Props & WithRouterProps> {
@ -22,17 +29,40 @@ export class CheckServices extends PureComponent<Props & WithRouterProps> {
}
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(
<OverlayContext.Consumer>
{({onDismissOverlay}) => (
<IFQLOverlay onDismiss={onDismissOverlay} source={source} />
)}
</OverlayContext.Consumer>,
{}
)
}
}
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))

View File

@ -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<Props, State> {
export class IFQLPage extends PureComponent<
Props & RouteComponentProps<any, any>,
State
> {
constructor(props) {
super(props)
this.state = {
@ -430,12 +441,12 @@ export class IFQLPage extends PureComponent<Props, State> {
}
}
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)

View File

@ -5,9 +5,10 @@ interface Props {
label: string
value: string
placeholder: string
onChange: (e: ChangeEvent<HTMLInputElement>) => void
maxLength?: number
inputType?: string
customClass?: string
onChange: (e: ChangeEvent<HTMLInputElement>) => void
}
const KapacitorFormInput: SFC<Props> = ({
@ -18,8 +19,9 @@ const KapacitorFormInput: SFC<Props> = ({
onChange,
maxLength,
inputType,
customClass,
}) => (
<div className="form-group">
<div className={`form-group ${customClass}`}>
<label htmlFor={name}>{label}</label>
<input
className="form-control"
@ -37,6 +39,7 @@ const KapacitorFormInput: SFC<Props> = ({
KapacitorFormInput.defaultProps = {
inputType: '',
customClass: 'col-sm-6',
}
export default KapacitorFormInput

View File

@ -169,6 +169,7 @@ export class KapacitorPage extends PureComponent<Props, State> {
return (
<KapacitorForm
hash={hash}
notify={notify}
source={source}
exists={exists}
kapacitor={kapacitor}
@ -176,7 +177,6 @@ export class KapacitorPage extends PureComponent<Props, State> {
onChangeUrl={this.handleChangeUrl}
onReset={this.handleResetToDefaults}
onInputChange={this.handleInputChange}
notify={notify}
onCheckboxChange={this.handleCheckboxChange}
/>
)

View File

@ -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

View File

@ -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<void>
export const fetchServicesAsync = (source: Source) => async (
dispatch
): Promise<void> => {
@ -110,3 +112,21 @@ export const fetchServicesAsync = (source: Source) => async (
dispatch(notify(couldNotGetServices))
}
}
export type CreateServiceAsync = (
source: Source,
service: NewService
) => (dispatch) => Promise<void>
export const createServiceAsync = (
source: Source,
service: NewService
) => async (dispatch): Promise<void> => {
try {
const s = await createServiceAJAX(source, service)
dispatch(addService(s))
} catch (err) {
console.error(err.data)
throw err.data
}
}

View File

@ -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<Service[]> => {
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<Service> => {
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)

View File

@ -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,
})

View File

@ -0,0 +1,4 @@
.ifql-overlay {
max-width: 500px;
margin: 0 auto;
}

View File

@ -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';

View File

@ -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,
}

View File

@ -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