Merge pull request #498 from influxdata/cherry/flux-connection-page

feature(chronograf) flux connections page (#4026)
pull/10616/head
Andrew Watkins 2018-07-27 13:57:12 -07:00 committed by GitHub
commit 09879fa221
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1307 additions and 267 deletions

View File

@ -1,12 +1,14 @@
package server
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"github.com/influxdata/platform/chronograf"
"github.com/influxdata/platform/flux"
)
type postServiceRequest struct {
@ -121,6 +123,15 @@ func (s *Service) NewService(w http.ResponseWriter, r *http.Request) {
return
}
if req.Type != nil && req.URL != nil && *req.Type == "flux" {
err := pingFlux(ctx, *req.URL, req.InsecureSkipVerify)
if err != nil {
msg := fmt.Sprintf("Unable to reach flux %s: %v", *req.URL, err)
Error(w, http.StatusGatewayTimeout, msg, s.Logger)
return
}
}
srv := chronograf.Server{
SrcID: srcID,
Name: *req.Name,
@ -241,7 +252,7 @@ func (p *patchServiceRequest) Valid() error {
if p.URL != nil {
url, err := url.ParseRequestURI(*p.URL)
if err != nil {
return fmt.Errorf("invalid source URI: %v", err)
return fmt.Errorf("invalid service URI: %v", err)
}
if len(url.Scheme) == 0 {
return fmt.Errorf("Invalid URL; no URL scheme defined")
@ -309,6 +320,15 @@ func (s *Service) UpdateService(w http.ResponseWriter, r *http.Request) {
srv.Metadata = *req.Metadata
}
if srv.Type == "flux" {
err := pingFlux(ctx, srv.URL, srv.InsecureSkipVerify)
if err != nil {
msg := fmt.Sprintf("Unable to reach flux %s: %v", srv.URL, err)
Error(w, http.StatusGatewayTimeout, msg, s.Logger)
return
}
}
if err := s.Store.Servers(ctx).Update(ctx, srv); err != nil {
msg := fmt.Sprintf("Error updating service ID %d", id)
Error(w, http.StatusInternalServerError, msg, s.Logger)
@ -318,3 +338,15 @@ func (s *Service) UpdateService(w http.ResponseWriter, r *http.Request) {
res := newService(srv)
encodeJSON(w, http.StatusOK, res, s.Logger)
}
func pingFlux(ctx context.Context, address string, insecureSkipVerify bool) error {
url, err := url.ParseRequestURI(address)
if err != nil {
return fmt.Errorf("invalid service URI: %v", err)
}
client := &flux.Client{
URL: url,
InsecureSkipVerify: insecureSkipVerify,
}
return client.Ping(ctx)
}

View File

@ -1936,6 +1936,417 @@
}
}
},
"/sources/{id}/services": {
"get": {
"tags": ["sources", "services"],
"parameters": [
{
"name": "id",
"in": "path",
"type": "string",
"description": "ID of the source",
"required": true
}
],
"summary": "Retrieve list of services for a source",
"responses": {
"200": {
"description": "An array of services",
"schema": {
"$ref": "#/definitions/Services"
}
},
"default": {
"description": "Unexpected internal server error",
"schema": {
"$ref": "#/definitions/Error"
}
}
}
},
"post": {
"tags": ["sources", "services"],
"parameters": [
{
"name": "id",
"in": "path",
"type": "string",
"description": "ID of the source",
"required": true
},
{
"name": "service",
"in": "body",
"description": "Configuration options for the service",
"schema": {
"$ref": "#/definitions/Service"
}
}
],
"summary": "Create a new service",
"responses": {
"200": {
"description": "Returns the newly created service",
"schema": {
"$ref": "#/definitions/Service"
}
},
"504": {
"description": "Gateway timeout happens when the server cannot connect to the service",
"schema": {
"$ref": "#/definitions/Error"
}
},
"default": {
"description": "Unexpected internal server error",
"schema": {
"$ref": "#/definitions/Error"
}
}
}
}
},
"/sources/{id}/services/{srv_id}": {
"get": {
"tags": ["sources", "services"],
"parameters": [
{
"name": "id",
"in": "path",
"type": "string",
"description": "ID of the source",
"required": true
},
{
"name": "srv_id",
"in": "path",
"type": "string",
"description": "ID of the service",
"required": true
}
],
"summary": "Retrieve a service",
"description": "Retrieve a single service by id",
"responses": {
"200": {
"description": "Service connection information",
"schema": {
"$ref": "#/definitions/Service"
}
},
"404": {
"description": "Unknown data source or service id",
"schema": {
"$ref": "#/definitions/Error"
}
},
"default": {
"description": "Unexpected internal server error",
"schema": {
"$ref": "#/definitions/Error"
}
}
}
},
"patch": {
"tags": ["sources", "services"],
"summary": "Update service configuration",
"parameters": [
{
"name": "id",
"in": "path",
"type": "string",
"description": "ID of the source",
"required": true
},
{
"name": "srv_id",
"in": "path",
"type": "string",
"description": "ID of a service backend",
"required": true
},
{
"name": "service",
"in": "body",
"description": "service configuration",
"schema": {
"$ref": "#/definitions/Service"
},
"required": true
}
],
"responses": {
"200": {
"description": "Service configuration was changed",
"schema": {
"$ref": "#/definitions/Service"
}
},
"504": {
"description": "Gateway timeout happens when the server cannot connect to the service",
"schema": {
"$ref": "#/definitions/Error"
}
},
"422": {
"description": "Unprocessable entity happens when the service ID provided does not exist",
"schema": {
"$ref": "#/definitions/Error"
}
},
"default": {
"description": "A processing or an unexpected error.",
"schema": {
"$ref": "#/definitions/Error"
}
}
}
},
"delete": {
"tags": ["sources", "services"],
"parameters": [
{
"name": "id",
"in": "path",
"type": "string",
"description": "ID of the source",
"required": true
},
{
"name": "srv_id",
"in": "path",
"type": "string",
"description": "ID of the service",
"required": true
}
],
"summary": "Remove Service backend",
"description":
"This specific service will be removed.",
"responses": {
"204": {
"description": "service has been removed."
},
"404": {
"description": "Unknown Data source or Service id",
"schema": {
"$ref": "#/definitions/Error"
}
},
"default": {
"description": "Unexpected internal server error",
"schema": {
"$ref": "#/definitions/Error"
}
}
}
}
},
"/sources/{id}/services/{srv_id}/proxy": {
"get": {
"tags": ["sources", "services", "proxy"],
"description":
"GET to `path` of Service. The response and status code from Service is directly returned.",
"parameters": [
{
"name": "id",
"in": "path",
"type": "string",
"description": "ID of the source",
"required": true
},
{
"name": "srv_id",
"in": "path",
"type": "string",
"description": "ID of the service backend.",
"required": true
},
{
"name": "path",
"in": "query",
"type": "string",
"description":
"The Service API path to use in the proxy redirect",
"required": true
}
],
"responses": {
"204": {
"description": "Service returned no content"
},
"404": {
"description": "Data source or Service ID does not exist.",
"schema": {
"$ref": "#/definitions/Error"
}
},
"default": {
"description": "Response directly from the service",
"schema": {
"$ref": "#/definitions/ServiceProxyResponse"
}
}
}
},
"delete": {
"tags": ["sources", "services", "proxy"],
"description":
"DELETE to `path` of Service. The response and status code from the service is directly returned.",
"parameters": [
{
"name": "id",
"in": "path",
"type": "string",
"description": "ID of the source",
"required": true
},
{
"name": "srv_id",
"in": "path",
"type": "string",
"description": "ID of the Service backend.",
"required": true
},
{
"name": "path",
"in": "query",
"type": "string",
"description":
"The Service API path to use in the proxy redirect",
"required": true
}
],
"responses": {
"204": {
"description": "Service returned no content"
},
"404": {
"description": "Data source or Service ID does not exist.",
"schema": {
"$ref": "#/definitions/Error"
}
},
"default": {
"description": "Response directly from the service",
"schema": {
"$ref": "#/definitions/ServiceProxyResponse"
}
}
}
},
"patch": {
"tags": ["sources", "services", "proxy"],
"description":
"PATCH body directly to configured service. The response and status code from Service is directly returned.",
"parameters": [
{
"name": "id",
"in": "path",
"type": "string",
"description": "ID of the source",
"required": true
},
{
"name": "srv_id",
"in": "path",
"type": "string",
"description": "ID of the Service backend.",
"required": true
},
{
"name": "path",
"in": "query",
"type": "string",
"description":
"The Service API path to use in the proxy redirect",
"required": true
},
{
"name": "query",
"in": "body",
"description": "Service body",
"schema": {
"$ref": "#/definitions/ServiceProxy"
},
"required": true
}
],
"responses": {
"204": {
"description": "Service returned no content"
},
"404": {
"description": "Data source or Service ID does not exist.",
"schema": {
"$ref": "#/definitions/Error"
}
},
"default": {
"description": "Response directly from Service",
"schema": {
"$ref": "#/definitions/ServiceProxyResponse"
}
}
}
},
"post": {
"tags": ["sources", "services", "proxy"],
"description":
"POST body directly to configured Service. The response and status code from Service is directly returned.",
"parameters": [
{
"name": "id",
"in": "path",
"type": "string",
"description": "ID of the source",
"required": true
},
{
"name": "srv_id",
"in": "path",
"type": "string",
"description": "ID of the Service backend.",
"required": true
},
{
"name": "path",
"in": "query",
"type": "string",
"description":
"The Service API path to use in the proxy redirect",
"required": true
},
{
"name": "query",
"in": "body",
"description": "Service body",
"schema": {
"$ref": "#/definitions/ServiceProxy"
},
"required": true
}
],
"responses": {
"204": {
"description": "Service returned no content"
},
"404": {
"description": "Service ID does not exist.",
"schema": {
"$ref": "#/definitions/Error"
}
},
"default": {
"description": "Response directly from Service",
"schema": {
"$ref": "#/definitions/ServiceProxyResponse"
}
}
}
}
},
"/mappings": {
"get": {
"tags": ["layouts", "mappings"],
@ -3397,6 +3808,111 @@
"description": "Entire response from the kapacitor backend.",
"type": "object"
},
"Services": {
"type": "object",
"required": ["services"],
"properties": {
"services": {
"type": "array",
"items": {
"$ref": "#/definitions/Service"
}
}
}
},
"Service": {
"type": "object",
"required": ["name", "url"],
"example": {
"id": "1",
"sourceID": "1",
"url": "http://localhost:8093",
"insecureSkipVerify": false,
"type": "flux",
"metadata": {
"active": true
},
"links": {
"proxy": "/chronograf/v1/sources/1/services/1/proxy",
"self": "/chronograf/v1/sources/1/services/1",
"source": "/chronograf/v1/sources/1"
}
},
"properties": {
"id": {
"type": "string",
"description": "Unique identifier representing a service.",
"readOnly": true
},
"sourceID": {
"type": "string",
"description": "Unique identifier of the source associated with this service"
},
"name": {
"type": "string",
"description": "User facing name of the service."
},
"username": {
"type": "string",
"description": "Credentials for using this service"
},
"url": {
"type": "string",
"format": "url",
"description":
"URL for the service backend (e.g. http://localhost:8093)"
},
"insecureSkipVerify": {
"type": "boolean",
"description":
"True means any certificate presented by the service is accepted. Typically used for self-signed certs. Probably should only be used for testing."
},
"type": {
"type": "string",
"description": "Indicates what kind of service this is (e.g. flux service)"
},
"metadata": {
"type": "object",
"properties": {
"active": {
"type": "boolean",
"description": "Indicates whether the service is the current service being used for a source"
}
}
},
"links": {
"type": "object",
"properties": {
"self": {
"type": "string",
"description": "Self link mapping to this resource",
"format": "url"
},
"proxy": {
"type": "string",
"description":
"URL location of proxy endpoint for this service",
"format": "url"
},
"source": {
"type": "string",
"description":
"URL location of the source this service is associated with",
"format": "url"
}
}
}
}
},
"ServiceProxy": {
"description":
"Entirely used as the body for the request to the service backend.",
"type": "object"
},
"ServiceProxyResponse": {
"description": "Entire response from the service backend.",
"type": "object"
},
"Rules": {
"type": "object",
"required": ["rules"],

View File

@ -1,4 +1,5 @@
import {kapacitor, queryConfig} from 'mocks/dummy'
import {service} from 'test/fixtures'
export const getKapacitor = jest.fn(() => Promise.resolve(kapacitor))
export const getActiveKapacitor = jest.fn(() => Promise.resolve(kapacitor))
@ -8,3 +9,6 @@ export const pingKapacitor = jest.fn(() => Promise.resolve())
export const getQueryConfigAndStatus = jest.fn(() =>
Promise.resolve({data: queryConfig})
)
export const getService = jest.fn(() => {
Promise.resolve(service)
})

View File

@ -3,22 +3,20 @@ import React, {SFC} from 'react'
import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader'
interface Props {
onShowOverlay: () => void
overlay: JSX.Element
onGoToNewService: () => void
}
const EmptyFluxPage: SFC<Props> = ({onShowOverlay, overlay}) => (
const EmptyFluxPage: SFC<Props> = ({onGoToNewService}) => (
<div className="page">
<PageHeader titleText="Flux Editor" fullWidth={true} />
<div className="page-contents">
<div className="flux-empty">
<p>You do not have a configured Flux source</p>
<button className="btn btn-primary btn-md" onClick={onShowOverlay}>
<button className="btn btn-primary btn-md" onClick={onGoToNewService}>
Connect to Flux
</button>
</div>
</div>
{overlay}
</div>
)

View File

@ -1,14 +1,21 @@
import React, {PureComponent, ChangeEvent, FormEvent} from 'react'
import _ from 'lodash'
import FluxForm from 'src/flux/components/FluxForm'
import {Service, Notification} from 'src/types'
import {fluxUpdated, fluxNotUpdated} from 'src/shared/copy/notifications'
import {
fluxUpdated,
fluxNotUpdated,
notifyFluxNameAlreadyTaken,
} from 'src/shared/copy/notifications'
import {UpdateServiceAsync} from 'src/shared/actions/services'
import {FluxFormMode} from 'src/flux/constants/connection'
interface Props {
service: Service
onDismiss: () => void
services: Service[]
onDismiss?: () => void
updateService: UpdateServiceAsync
notify: (message: Notification) => void
}
@ -18,7 +25,14 @@ interface State {
}
class FluxEdit extends PureComponent<Props, State> {
constructor(props) {
public static getDerivedStateFromProps(nextProps: Props, prevState: State) {
if (_.isEmpty(prevState.service) && !_.isEmpty(nextProps.service)) {
return {service: nextProps.service}
}
return null
}
constructor(props: Props) {
super(props)
this.state = {
service: this.props.service,
@ -31,7 +45,7 @@ class FluxEdit extends PureComponent<Props, State> {
service={this.state.service}
onSubmit={this.handleSubmit}
onInputChange={this.handleInputChange}
mode="edit"
mode={FluxFormMode.EDIT}
/>
)
}
@ -47,8 +61,20 @@ class FluxEdit extends PureComponent<Props, State> {
e: FormEvent<HTMLFormElement>
): Promise<void> => {
e.preventDefault()
const {notify, onDismiss, updateService} = this.props
const {notify, onDismiss, updateService, services} = this.props
const {service} = this.state
service.name = service.name.trim()
let isNameTaken = false
services.forEach(s => {
if (s.name === service.name && s.id !== service.id) {
isNameTaken = true
}
})
if (isNameTaken) {
notify(notifyFluxNameAlreadyTaken(service.name))
return
}
try {
await updateService(service)
@ -58,7 +84,9 @@ class FluxEdit extends PureComponent<Props, State> {
}
notify(fluxUpdated)
onDismiss()
if (onDismiss) {
onDismiss()
}
}
}

View File

@ -1,12 +1,14 @@
import React, {ChangeEvent, PureComponent} from 'react'
import _ from 'lodash'
import Input from 'src/kapacitor/components/KapacitorFormInput'
import {NewService} from 'src/types'
import {FluxFormMode} from 'src/flux/constants/connection'
interface Props {
service: NewService
mode: string
mode: FluxFormMode
onSubmit: (e: ChangeEvent<HTMLFormElement>) => void
onInputChange: (e: ChangeEvent<HTMLInputElement>) => void
}
@ -14,60 +16,58 @@ interface Props {
class FluxForm extends PureComponent<Props> {
public render() {
const {service, onSubmit, onInputChange} = this.props
const name = _.get(service, 'name', '')
return (
<div className="template-variable-manager--body">
<form onSubmit={onSubmit} style={{display: 'inline-block'}}>
<Input
name="url"
label="Flux URL"
value={this.url}
placeholder={this.url}
onChange={onInputChange}
customClass="col-sm-6"
/>
<Input
name="name"
label="Name"
value={service.name}
placeholder={service.name}
onChange={onInputChange}
maxLength={33}
customClass="col-sm-6"
/>
<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>
<form onSubmit={onSubmit}>
<Input
name="url"
label="Flux URL"
value={this.url}
placeholder={this.url}
onChange={onInputChange}
customClass="col-sm-6"
/>
<Input
name="name"
label="Name"
value={name}
placeholder={name}
onChange={onInputChange}
maxLength={33}
customClass="col-sm-6"
/>
<div className="form-group form-group-submit col-xs-12 text-center">
{this.saveButton}
</div>
</form>
)
}
private get buttonText(): string {
private get saveButton(): JSX.Element {
const {mode} = this.props
if (mode === 'edit') {
return 'Update'
let text = 'Connect'
if (mode === FluxFormMode.EDIT) {
text = 'Save Changes'
}
return 'Connect'
return (
<button
className="btn btn-success"
type="submit"
data-test="submit-button"
>
<span className="icon checkmark" />
{text}
</button>
)
}
private get url(): string {
const {
service: {url},
} = this.props
if (url) {
return url
}
return ''
const {service} = this.props
return _.get(service, 'url', '')
}
}

View File

@ -1,26 +1,18 @@
import React, {PureComponent} from 'react'
import FluxOverlay from 'src/flux/components/FluxOverlay'
import OverlayTechnology from 'src/reusable_ui/components/overlays/OverlayTechnology'
import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader'
import {Service} from 'src/types'
interface Props {
service: Service
services: Service[]
onGoToEditFlux: (service: Service) => void
}
interface State {
isOverlayVisible: boolean
}
class FluxHeader extends PureComponent<Props, State> {
class FluxHeader extends PureComponent<Props> {
constructor(props: Props) {
super(props)
this.state = {
isOverlayVisible: false,
}
}
public render() {
@ -31,19 +23,14 @@ class FluxHeader extends PureComponent<Props, State> {
fullWidth={true}
optionsComponents={this.optionsComponents}
/>
{this.overlay}
</>
)
}
private handleToggleOverlay = (): void => {
this.setState({isOverlayVisible: !this.state.isOverlayVisible})
}
private get optionsComponents(): JSX.Element {
return (
<button
onClick={this.handleToggleOverlay}
onClick={this.handleGoToEditFlux}
className="btn btn-sm btn-default"
>
Edit Connection
@ -51,19 +38,9 @@ class FluxHeader extends PureComponent<Props, State> {
)
}
private get overlay(): JSX.Element {
const {service} = this.props
const {isOverlayVisible} = this.state
return (
<OverlayTechnology visible={isOverlayVisible}>
<FluxOverlay
mode="edit"
service={service}
onDismiss={this.handleToggleOverlay}
/>
</OverlayTechnology>
)
private handleGoToEditFlux = () => {
const {service, onGoToEditFlux} = this.props
onGoToEditFlux(service)
}
}

View File

@ -2,14 +2,26 @@ import React, {PureComponent, ChangeEvent, FormEvent} from 'react'
import FluxForm from 'src/flux/components/FluxForm'
import {NewService, Source, Notification} from 'src/types'
import {fluxCreated, fluxNotCreated} from 'src/shared/copy/notifications'
import {CreateServiceAsync} from 'src/shared/actions/services'
import {NewService, Source, Service, Notification} from 'src/types'
import {
fluxCreated,
fluxNotCreated,
notifyFluxNameAlreadyTaken,
} from 'src/shared/copy/notifications'
import {
CreateServiceAsync,
SetActiveServiceAsync,
} from 'src/shared/actions/services'
import {FluxFormMode} from 'src/flux/constants/connection'
import {getDeep} from 'src/utils/wrappers'
interface Props {
source: Source
onDismiss: () => void
services: Service[]
setActiveFlux?: SetActiveServiceAsync
onDismiss?: () => void
createService: CreateServiceAsync
router?: {push: (url: string) => void}
notify: (message: Notification) => void
}
@ -33,7 +45,7 @@ class FluxNew extends PureComponent<Props, State> {
service={this.state.service}
onSubmit={this.handleSubmit}
onInputChange={this.handleInputChange}
mode="new"
mode={FluxFormMode.NEW}
/>
)
}
@ -49,19 +61,42 @@ class FluxNew extends PureComponent<Props, State> {
e: FormEvent<HTMLFormElement>
): Promise<void> => {
e.preventDefault()
const {notify, source, onDismiss, createService} = this.props
const {
notify,
router,
source,
services,
onDismiss,
setActiveFlux,
createService,
} = this.props
const {service} = this.state
service.name = service.name.trim()
const isNameTaken = services.some(s => s.name === service.name)
if (isNameTaken) {
notify(notifyFluxNameAlreadyTaken(service.name))
return
}
try {
await createService(source, service)
const active = this.activeService
const s = await createService(source, service)
if (setActiveFlux) {
await setActiveFlux(source, s, active)
}
if (router) {
router.push(`/sources/${source.id}/flux/${s.id}/edit`)
}
} catch (error) {
notify(fluxNotCreated(error.message))
return
}
notify(fluxCreated)
onDismiss()
if (onDismiss) {
onDismiss()
}
}
private get defaultService(): NewService {
@ -71,10 +106,20 @@ class FluxNew extends PureComponent<Props, State> {
username: '',
insecureSkipVerify: false,
type: 'flux',
active: true,
metadata: {
active: true,
},
}
}
private get activeService(): Service {
const {services} = this.props
const activeService = services.find(s => {
return getDeep<boolean>(s, 'metadata.active', false)
})
return activeService || services[0]
}
private get url(): string {
const parser = document.createElement('a')
parser.href = this.props.source.url

View File

@ -1,86 +0,0 @@
import React, {PureComponent} from 'react'
import {connect} from 'react-redux'
import FluxNew from 'src/flux/components/FluxNew'
import FluxEdit from 'src/flux/components/FluxEdit'
import {Service, Source, Notification} from 'src/types'
import {notify as notifyAction} from 'src/shared/actions/notifications'
import {
updateServiceAsync,
UpdateServiceAsync,
createServiceAsync,
CreateServiceAsync,
} from 'src/shared/actions/services'
interface Props {
mode: string
source?: Source
service?: Service
onDismiss: () => void
notify: (message: Notification) => void
createService: CreateServiceAsync
updateService: UpdateServiceAsync
}
class FluxOverlay extends PureComponent<Props> {
public render() {
return (
<div className="flux-overlay">
<div className="template-variable-manager--header">
<div className="page-header--left">
<h1 className="page-header--title">Connect to Flux</h1>
</div>
<div className="page-header--right">
<span
className="page-header__dismiss"
onClick={this.props.onDismiss}
/>
</div>
</div>
{this.form}
</div>
)
}
private get form(): JSX.Element {
const {
mode,
source,
service,
notify,
onDismiss,
createService,
updateService,
} = this.props
if (mode === 'new') {
return (
<FluxNew
source={source}
notify={notify}
onDismiss={onDismiss}
createService={createService}
/>
)
}
return (
<FluxEdit
notify={notify}
service={service}
onDismiss={onDismiss}
updateService={updateService}
/>
)
}
}
const mdtp = {
notify: notifyAction,
createService: createServiceAsync,
updateService: updateServiceAsync,
}
export default connect(null, mdtp)(FluxOverlay)

View File

@ -0,0 +1,7 @@
export enum FluxFormMode {
NEW = 'new',
EDIT = 'edit',
}
export const FLUX_CONNECTION_TOOLTIP =
'<p>Flux Connections are<br/>scoped per InfluxDB Connection.<br/>Only one can be active at a time.</p>'

View File

@ -1,11 +1,10 @@
import React, {PureComponent, ReactChildren} from 'react'
import {connect} from 'react-redux'
import {WithRouterProps} from 'react-router'
import {WithRouterProps, withRouter} from 'react-router'
import {FluxPage} from 'src/flux'
import EmptyFluxPage from 'src/flux/components/EmptyFluxPage'
import FluxOverlay from 'src/flux/components/FluxOverlay'
import OverlayTechnology from 'src/reusable_ui/components/overlays/OverlayTechnology'
import {Source, Service, Notification} from 'src/types'
import {Links} from 'src/types/flux'
import {notify as notifyAction} from 'src/shared/actions/notifications'
@ -21,27 +20,16 @@ interface Props {
sources: Source[]
services: Service[]
children: ReactChildren
fetchServicesAsync: actions.FetchServicesAsync
fetchServicesAsync: actions.FetchFluxServicesForSourceAsync
notify: (message: Notification) => void
updateScript: UpdateScript
script: string
links: Links
}
interface State {
isOverlayShown: boolean
}
export class CheckServices extends PureComponent<
Props & WithRouterProps,
State
> {
export class CheckServices extends PureComponent<Props & WithRouterProps> {
constructor(props: Props & WithRouterProps) {
super(props)
this.state = {
isOverlayShown: false,
}
}
public async componentDidMount() {
@ -54,22 +42,13 @@ export class CheckServices extends PureComponent<
}
await this.props.fetchServicesAsync(source)
if (!this.props.services.length) {
this.setState({isOverlayShown: true})
}
}
public render() {
const {services, notify, updateScript, links, script} = this.props
if (!this.props.services.length) {
return (
<EmptyFluxPage
onShowOverlay={this.handleShowOverlay}
overlay={this.renderOverlay}
/>
)
return <EmptyFluxPage onGoToNewService={this.handleGoToNewFlux} />
}
return (
@ -81,43 +60,35 @@ export class CheckServices extends PureComponent<
script={script}
notify={notify}
updateScript={updateScript}
onGoToEditFlux={this.handleGoToEditFlux}
/>
{this.renderOverlay}
</NotificationContext.Provider>
)
}
private handleGoToNewFlux = () => {
const {router} = this.props
const addFluxResource = `/sources/${this.source.id}/flux/new`
router.push(addFluxResource)
}
private handleGoToEditFlux = (service: Service) => {
const {router} = this.props
const editFluxResource = `/sources/${this.source.id}/flux/${
service.id
}/edit`
router.push(editFluxResource)
}
private get source(): Source {
const {params, sources} = this.props
return sources.find(s => s.id === params.sourceID)
}
private get renderOverlay(): JSX.Element {
const {isOverlayShown} = this.state
return (
<OverlayTechnology visible={isOverlayShown}>
<FluxOverlay
mode="new"
source={this.source}
onDismiss={this.handleDismissOverlay}
/>
</OverlayTechnology>
)
}
private handleShowOverlay = (): void => {
this.setState({isOverlayShown: true})
}
private handleDismissOverlay = (): void => {
this.setState({isOverlayShown: false})
}
}
const mdtp = {
fetchServicesAsync: actions.fetchServicesAsync,
fetchServicesAsync: actions.fetchFluxServicesForSourceAsync,
updateScript: updateScriptAction,
notify: notifyAction,
}
@ -131,4 +102,4 @@ const mstp = ({sources, services, links, script}) => {
}
}
export default connect(mstp, mdtp)(CheckServices)
export default withRouter<Props>(connect(mstp, mdtp)(CheckServices))

View File

@ -0,0 +1,170 @@
import React, {PureComponent} from 'react'
import {connect} from 'react-redux'
import {withRouter} from 'react-router'
import FluxNew from 'src/flux/components/FluxNew'
import FluxEdit from 'src/flux/components/FluxEdit'
import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {getService} from 'src/shared/apis'
import {FluxFormMode} from 'src/flux/constants/connection'
import {
updateServiceAsync,
createServiceAsync,
CreateServiceAsync,
fetchFluxServicesForSourceAsync,
setActiveServiceAsync,
} from 'src/shared/actions/services'
import {couldNotGetFluxService} from 'src/shared/copy/notifications'
import {notify as notifyAction} from 'src/shared/actions/notifications'
import {Service, Source, Notification} from 'src/types'
interface Props {
source: Source
services: Service[]
params: {id: string; sourceID: string}
router: {push: (url: string) => void}
notify: (message: Notification) => void
createService: CreateServiceAsync
updateService: typeof updateServiceAsync
setActiveFlux: typeof setActiveServiceAsync
fetchServicesForSource: typeof fetchFluxServicesForSourceAsync
}
interface State {
service: Service
formMode: FluxFormMode
}
@ErrorHandling
export class FluxConnectionPage extends PureComponent<Props, State> {
public static getDerivedStateFromProps(nextProps: Props, prevState: State) {
const {
params: {id},
services,
} = nextProps
if (prevState.formMode === FluxFormMode.NEW && id) {
const service = services.find(s => {
return s.id === id
})
return {
...prevState,
service,
formMode: FluxFormMode.EDIT,
}
}
return null
}
constructor(props) {
super(props)
this.state = {
service: null,
formMode: FluxFormMode.NEW,
}
}
public async componentDidMount() {
const {
source,
notify,
params: {id},
fetchServicesForSource,
} = this.props
let service: Service
let formMode: FluxFormMode
if (id) {
try {
service = await getService(source.links.services, id)
formMode = FluxFormMode.EDIT
this.setState({service, formMode})
} catch (err) {
console.error('Could not get Service', err)
notify(couldNotGetFluxService(id))
}
} else {
formMode = FluxFormMode.NEW
this.setState({formMode})
}
await fetchServicesForSource(source)
}
public render() {
return (
<div className="page">
<PageHeader titleText={this.pageTitle} />
<FancyScrollbar className="page-contents">
<div className="container-fluid">
<div className="row">
<div className="col-md-8 col-md-offset-2">
<div className="panel">
<div className="panel-body">{this.form}</div>
</div>
</div>
</div>
</div>
</FancyScrollbar>
</div>
)
}
private get form() {
const {
source,
notify,
createService,
updateService,
setActiveFlux,
services,
router,
} = this.props
const {service, formMode} = this.state
if (formMode === FluxFormMode.NEW) {
return (
<FluxNew
source={source}
services={services}
notify={notify}
router={router}
setActiveFlux={setActiveFlux}
createService={createService}
/>
)
}
return (
<FluxEdit
notify={notify}
service={service}
services={services}
updateService={updateService}
/>
)
}
private get pageTitle() {
const {formMode} = this.state
if (formMode === FluxFormMode.NEW) {
return 'Add Flux Connection'
}
return 'Edit Flux Connection'
}
}
const mdtp = {
notify: notifyAction,
createService: createServiceAsync,
updateService: updateServiceAsync,
setActiveFlux: setActiveServiceAsync,
fetchServicesForSource: fetchFluxServicesForSourceAsync,
}
const mstp = ({services}) => ({services})
export default connect(mstp, mdtp)(withRouter(FluxConnectionPage))

View File

@ -16,6 +16,7 @@ import {UpdateScript} from 'src/flux/actions'
import {bodyNodes} from 'src/flux/helpers'
import {getSuggestions, getAST, getTimeSeries} from 'src/flux/apis'
import {builder, argTypes, emptyAST} from 'src/flux/constants'
import {getDeep} from 'src/utils/wrappers'
import {Source, Service, Notification, FluxTable} from 'src/types'
import {
@ -41,6 +42,7 @@ interface Props {
notify: (message: Notification) => void
script: string
updateScript: UpdateScript
onGoToEditFlux: (service: Service) => void
}
interface Body extends FlatBody {
@ -123,17 +125,27 @@ export class FluxPage extends PureComponent<Props, State> {
}
private get header(): JSX.Element {
const {services} = this.props
const {services, onGoToEditFlux} = this.props
if (!services.length) {
return null
}
return <FluxHeader service={this.service} />
return (
<FluxHeader
service={this.service}
services={services}
onGoToEditFlux={onGoToEditFlux}
/>
)
}
private get service(): Service {
return this.props.services[0]
const {services} = this.props
const activeService = services.find(s => {
return getDeep<boolean>(s, 'metadata.active', false)
})
return activeService || services[0]
}
private get getContext(): Context {

View File

@ -1,4 +1,5 @@
import FluxPage from 'src/flux/containers/FluxPage'
import CheckServices from 'src/flux/containers/CheckServices'
import FluxConnectionPage from 'src/flux/containers/FluxConnectionPage'
export {FluxPage, CheckServices}
export {FluxPage, CheckServices, FluxConnectionPage}

View File

@ -36,7 +36,7 @@ import {
} from 'src/kapacitor'
import {AdminChronografPage, AdminInfluxDBPage} from 'src/admin'
import {SourcePage, ManageSources} from 'src/sources'
import {CheckServices} from 'src/flux'
import {CheckServices, FluxConnectionPage} from 'src/flux'
import NotFound from 'src/shared/components/NotFound'
import {getLinksAsync} from 'src/shared/actions/links'
@ -152,6 +152,8 @@ class Root extends PureComponent<{}, State> {
path="kapacitors/:id/edit:hash"
component={KapacitorPage}
/>
<Route path="flux/new" component={FluxConnectionPage} />
<Route path="flux/:id/edit" component={FluxConnectionPage} />
<Route
path="admin-chronograf/:tab"
component={AdminChronografPage}

View File

@ -127,7 +127,7 @@ export class KapacitorPage extends PureComponent<Props, State> {
const isNew = !params.id
if (isNew && isNameTaken) {
notify(notifyKapacitorNameAlreadyTaken)
notify(notifyKapacitorNameAlreadyTaken(kapacitor.name))
return
}

View File

@ -3,6 +3,7 @@ import {
updateService as updateServiceAJAX,
getServices as getServicesAJAX,
createService as createServiceAJAX,
deleteService as deleteServiceAJAX,
} from 'src/shared/apis'
import {notify} from './notifications'
import {couldNotGetServices} from 'src/shared/copy/notifications'
@ -102,13 +103,62 @@ export const setActiveService = (
},
})
export type FetchServicesAsync = (source: Source) => (dispatch) => Promise<void>
export const fetchServicesAsync = (source: Source) => async (
export type SetActiveServiceAsync = (
source: Source,
activeService: Service,
prevActiveService: Service
) => (dispatch) => Promise<void>
export const setActiveServiceAsync = (
source: Source,
activeService: Service,
prevActiveService: Service
) => async (dispatch): Promise<void> => {
try {
activeService = {...activeService, metadata: {active: true}}
await updateServiceAJAX(activeService)
if (prevActiveService) {
prevActiveService = {...prevActiveService, metadata: {active: false}}
await updateServiceAJAX(prevActiveService)
}
dispatch(setActiveService(source, activeService))
} catch (err) {
console.error(err.data)
}
}
export type FetchAllFluxServicesAsync = (
sources: Source[]
) => (dispatch) => Promise<void>
export const fetchAllFluxServicesAsync: FetchAllFluxServicesAsync = sources => async (
dispatch
): Promise<void> => {
const allServices: Service[] = []
sources.forEach(async source => {
try {
const services = await getServicesAJAX(source.links.services)
const fluxServices = services.filter(s => s.type === 'flux')
allServices.push(...fluxServices)
} catch (err) {
dispatch(notify(couldNotGetServices))
}
})
dispatch(loadServices(allServices))
}
export type FetchFluxServicesForSourceAsync = (
source: Source
) => (dispatch) => Promise<void>
export const fetchFluxServicesForSourceAsync: FetchFluxServicesForSourceAsync = source => async (
dispatch
): Promise<void> => {
try {
const services = await getServicesAJAX(source.links.services)
dispatch(loadServices(services))
const fluxServices = services.filter(s => s.type === 'flux')
dispatch(loadServices(fluxServices))
} catch (err) {
dispatch(notify(couldNotGetServices))
}
@ -117,15 +167,17 @@ export const fetchServicesAsync = (source: Source) => async (
export type CreateServiceAsync = (
source: Source,
service: NewService
) => (dispatch) => Promise<void>
) => Service
export const createServiceAsync = (
source: Source,
service: NewService
) => async (dispatch): Promise<void> => {
) => async (dispatch): Promise<Service> => {
try {
const s = await createServiceAJAX(source, service)
const metadata = {active: true}
const s = await createServiceAJAX(source, {...service, metadata})
dispatch(addService(s))
return s
} catch (err) {
console.error(err.data)
throw err.data
@ -146,3 +198,18 @@ export const updateServiceAsync = (service: Service) => async (
throw err.data
}
}
export type DeleteServiceAsync = (
service: Service
) => (dispatch) => Promise<void>
export const deleteServiceAsync = (service: Service) => async (
dispatch
): Promise<void> => {
try {
await deleteServiceAJAX(service)
dispatch(deleteService(service))
} catch (err) {
console.error(err.data)
throw err.data
}
}

View File

@ -161,9 +161,7 @@ export type FetchKapacitorsAsync = (
source: Source
) => (dispatch) => Promise<void>
export const fetchKapacitorsAsync = (source: Source) => async (
dispatch
): Promise<void> => {
export const fetchKapacitorsAsync: FetchKapacitorsAsync = source => async dispatch => {
try {
const {data} = await getKapacitorsAJAX(source)
dispatch(fetchKapacitors(source, data.kapacitors))

View File

@ -381,6 +381,23 @@ export const getServices = async (url: string): Promise<Service[]> => {
}
}
export const getService = async (
url: string,
serviceID: string
): Promise<Service> => {
try {
const {data} = await AJAX({
url: `${url}/${serviceID}`,
method: 'GET',
})
return data
} catch (error) {
console.error(error)
throw error
}
}
export const createService = async (
source: Source,
{
@ -390,13 +407,14 @@ export const createService = async (
username,
password,
insecureSkipVerify,
metadata,
}: NewService
): Promise<Service> => {
try {
const {data} = await AJAX({
url: source.links.services,
method: 'POST',
data: {url, name, type, username, password, insecureSkipVerify},
data: {url, name, type, username, password, insecureSkipVerify, metadata},
})
return data
@ -420,3 +438,15 @@ export const updateService = async (service: Service): Promise<Service> => {
throw error
}
}
export const deleteService = async (service: Service): Promise<void> => {
try {
await AJAX({
url: service.links.self,
method: 'DELETE',
})
} catch (error) {
console.error(error)
throw error
}
}

View File

@ -634,7 +634,7 @@ export const notifyKapacitorNameAlreadyTaken = (
kapacitorName: string
): Notification => ({
...defaultErrorNotification,
message: `There is already a Kapacitor Connection named "${kapacitorName}".`,
message: `There is already a Kapacitor Connection named "${kapacitorName}."`,
})
export const notifyCouldNotFindKapacitor = (): Notification => ({
@ -775,7 +775,17 @@ export const notifyCopyToClipboardFailed = (text: string): Notification => ({
message: `'${text}' was not copied to clipboard.`,
})
export const notifyFluxNameAlreadyTaken = (fluxName: string): Notification => ({
...defaultErrorNotification,
message: `There is already a Flux Connection named "${fluxName}."`,
})
// Service notifications
export const couldNotGetFluxService = (id: string): Notification => ({
...defaultErrorNotification,
message: `Could not find Flux with id ${id}.`,
})
export const couldNotGetServices: Notification = {
...defaultErrorNotification,
message: 'We could not get services',

View File

@ -16,8 +16,8 @@ const servicesReducer = (state = initialState, action: Action): Service[] => {
case 'DELETE_SERVICE': {
const {service} = action.payload
return state.filter(s => s.id !== service.id)
const services = state.filter(s => s.id !== service.id)
return services
}
case 'UPDATE_SERVICE': {
@ -34,6 +34,15 @@ const servicesReducer = (state = initialState, action: Action): Service[] => {
}
case 'SET_ACTIVE_SERVICE': {
const {source, service} = action.payload
const services = state.filter(s => {
return s.sourceID === source.id
})
return services.map(s => {
const metadata = {active: s.id === service.id}
s.metadata = metadata
return s
})
}
}

View File

@ -5,7 +5,7 @@ import InfluxTableHead from 'src/sources/components/InfluxTableHead'
import InfluxTableHeader from 'src/sources/components/InfluxTableHeader'
import InfluxTableRow from 'src/sources/components/InfluxTableRow'
import {Source, Me} from 'src/types'
import {Source, Me, Service} from 'src/types'
import {ErrorHandling} from 'src/shared/decorators/errors'
@ -13,14 +13,25 @@ interface Props {
me: Me
source: Source
sources: Source[]
services: Service[]
isUsingAuth: boolean
onDeleteSource: (source: Source) => void
setActiveFlux: (source: Source, service: Service) => void
deleteFlux: (fluxService: Service) => void
}
@ErrorHandling
class InfluxTable extends PureComponent<Props> {
public render() {
const {source, sources, onDeleteSource, isUsingAuth, me} = this.props
const {
source,
sources,
setActiveFlux,
onDeleteSource,
deleteFlux,
isUsingAuth,
me,
} = this.props
return (
<div className="row">
@ -40,8 +51,11 @@ class InfluxTable extends PureComponent<Props> {
<InfluxTableRow
key={s.id}
source={s}
services={this.getServicesForSource(s.id)}
currentSource={source}
onDeleteSource={onDeleteSource}
setActiveFlux={setActiveFlux}
deleteFlux={deleteFlux}
/>
)
})}
@ -53,6 +67,12 @@ class InfluxTable extends PureComponent<Props> {
</div>
)
}
private getServicesForSource(sourceID: string) {
return this.props.services.filter(s => {
return s.sourceID === sourceID
})
}
}
const mapStateToProps = ({auth: {isUsingAuth, me}}) => ({isUsingAuth, me})

View File

@ -1,5 +1,9 @@
import React, {SFC, ReactElement} from 'react'
import QuestionMarkTooltip from 'src/shared/components/QuestionMarkTooltip'
import {FLUX_CONNECTION_TOOLTIP} from 'src/flux/constants/connection'
const InfluxTableHead: SFC = (): ReactElement<HTMLTableHeaderCellElement> => {
return (
<thead>
@ -7,6 +11,13 @@ const InfluxTableHead: SFC = (): ReactElement<HTMLTableHeaderCellElement> => {
<th className="source-table--connect-col" />
<th>InfluxDB Connection</th>
<th className="text-right" />
<th>
Flux Connection
<QuestionMarkTooltip
tipID="kapacitor-node-helper"
tipContent={FLUX_CONNECTION_TOOLTIP}
/>
</th>
</tr>
</thead>
)

View File

@ -4,20 +4,30 @@ import {Link} from 'react-router'
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
import ConfirmButton from 'src/shared/components/ConfirmButton'
import ConnectionLink from 'src/sources/components/ConnectionLink'
import FluxDropdown from 'src/sources/components/FluxDropdown'
import {Source} from 'src/types'
import {Source, Service} from 'src/types'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
source: Source
currentSource: Source
services: Service[]
onDeleteSource: (source: Source) => void
setActiveFlux: (source: Source, service: Service) => void
deleteFlux: (fluxService: Service) => void
}
@ErrorHandling
class InfluxTableRow extends PureComponent<Props> {
public render() {
const {source, currentSource} = this.props
const {
source,
services,
currentSource,
setActiveFlux,
deleteFlux,
} = this.props
return (
<tr className={this.className}>
@ -37,6 +47,14 @@ class InfluxTableRow extends PureComponent<Props> {
/>
</Authorized>
</td>
<td className="source-table--kapacitor">
<FluxDropdown
services={services}
source={source}
setActiveFlux={setActiveFlux}
deleteFlux={deleteFlux}
/>
</td>
</tr>
)
}

View File

@ -6,6 +6,7 @@ import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
import {Source, Service} from 'src/types'
import {SetActiveService} from 'src/shared/actions/services'
import {getDeep} from 'src/utils/wrappers'
interface Props {
source: Source
@ -101,7 +102,10 @@ class ServiceDropdown extends PureComponent<
}
private get activeService(): Service {
return this.props.services.find(s => s.active)
const service = this.props.services.find(s => {
return getDeep<boolean>(s, 'metadata.active', false)
})
return service || this.props.services[0]
}
private get selected(): string {

View File

@ -2,7 +2,8 @@ import React, {PureComponent} from 'react'
import {connect} from 'react-redux'
import {ErrorHandling} from 'src/shared/decorators/errors'
import * as actions from 'src/shared/actions/sources'
import * as sourcesActions from 'src/shared/actions/sources'
import * as servicesActions from 'src/shared/actions/services'
import {notify as notifyAction} from 'src/shared/actions/notifications'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
@ -14,24 +15,33 @@ import {
notifySourceDeleteFailed,
} from 'src/shared/copy/notifications'
import {Source, Notification} from 'src/types'
import {Source, Notification, Service} from 'src/types'
import {getDeep} from 'src/utils/wrappers'
interface Props {
source: Source
sources: Source[]
services: Service[]
notify: (n: Notification) => void
deleteKapacitor: actions.DeleteKapacitorAsync
fetchKapacitors: actions.FetchKapacitorsAsync
removeAndLoadSources: actions.RemoveAndLoadSources
setActiveKapacitor: actions.SetActiveKapacitorAsync
deleteKapacitor: sourcesActions.DeleteKapacitorAsync
fetchKapacitors: sourcesActions.FetchKapacitorsAsync
removeAndLoadSources: sourcesActions.RemoveAndLoadSources
setActiveKapacitor: sourcesActions.SetActiveKapacitorAsync
fetchAllServices: servicesActions.FetchAllFluxServicesAsync
setActiveFlux: servicesActions.SetActiveServiceAsync
deleteFlux: servicesActions.DeleteServiceAsync
}
declare var VERSION: string
@ErrorHandling
class ManageSources extends PureComponent<Props> {
public componentDidMount() {
this.props.fetchAllServices(this.props.sources)
}
public render() {
const {sources, source} = this.props
const {sources, source, deleteFlux, services} = this.props
return (
<div className="page" id="manage-sources-page">
@ -41,7 +51,10 @@ class ManageSources extends PureComponent<Props> {
<InfluxTable
source={source}
sources={sources}
services={services}
deleteFlux={deleteFlux}
onDeleteSource={this.handleDeleteSource}
setActiveFlux={this.handleSetActiveFlux}
/>
<p className="version-number">Chronograf Version: {VERSION}</p>
</div>
@ -50,6 +63,14 @@ class ManageSources extends PureComponent<Props> {
)
}
private handleSetActiveFlux = async (source, service) => {
const {services, setActiveFlux} = this.props
const prevActiveService = services.find(s => {
return getDeep<boolean>(s, 'metadata.active', false)
})
await setActiveFlux(source, service, prevActiveService)
}
private handleDeleteSource = (source: Source) => {
const {notify} = this.props
@ -62,13 +83,20 @@ class ManageSources extends PureComponent<Props> {
}
}
const mstp = ({sources}) => ({
const mstp = ({sources, services}) => ({
sources,
services,
})
const mdtp = {
removeAndLoadSources: actions.removeAndLoadSources,
notify: notifyAction,
removeAndLoadSources: sourcesActions.removeAndLoadSources,
fetchKapacitors: sourcesActions.fetchKapacitorsAsync,
setActiveKapacitor: sourcesActions.setActiveKapacitorAsync,
deleteKapacitor: sourcesActions.deleteKapacitorAsync,
fetchAllServices: servicesActions.fetchAllFluxServicesAsync,
setActiveFlux: servicesActions.setActiveServiceAsync,
deleteFlux: servicesActions.deleteServiceAsync,
}
export default connect(mstp, mdtp)(ManageSources)

View File

@ -4,8 +4,10 @@ export interface NewService {
type: string
username?: string
password?: string
active: boolean
insecureSkipVerify: boolean
metadata?: {
[x: string]: any
}
}
export interface Service {
@ -16,7 +18,6 @@ export interface Service {
type: string
username?: string
password?: string
active: boolean
insecureSkipVerify: boolean
metadata: {
[x: string]: any

View File

@ -1,5 +1,6 @@
import {interval} from 'src/shared/constants'
import {
Service,
Source,
SourceAuthenticationMethod,
CellQuery,
@ -50,6 +51,23 @@ export const source: Source = {
authentication: SourceAuthenticationMethod.Basic,
}
export const service: Service = {
id: '1',
sourceID: '1',
name: 'Flux',
url: 'http://localhost:8093',
insecureSkipVerify: false,
type: 'flux',
metadata: {
active: true,
},
links: {
proxy: '/chronograf/v1/sources/1/services/1/proxy',
self: '/chronograf/v1/sources/1/services/1',
source: '/chronograf/v1/sources/1',
},
}
export const queryConfig: QueryConfig = {
database: 'telegraf',
measurement: 'cpu',

View File

@ -0,0 +1,60 @@
import React from 'react'
import {shallow} from 'enzyme'
import {FluxConnectionPage} from 'src/flux/containers/FluxConnectionPage'
import FluxNew from 'src/flux/components/FluxNew'
import FluxEdit from 'src/flux/components/FluxEdit'
import {source, service} from 'test/fixtures'
jest.mock('src/shared/apis', () => require('mocks/shared/apis'))
const setup = (override = {}) => {
const props = {
source,
services: [],
params: {id: '', sourceID: ''},
router: {push: () => {}},
notify: () => {},
createService: jest.fn(),
updateService: jest.fn(),
setActiveFlux: jest.fn(),
fetchServicesForSource: jest.fn(),
...override,
}
const wrapper = shallow(<FluxConnectionPage {...props} />)
return {
props,
wrapper,
}
}
describe('Flux.Containers.FluxConnectionPage', () => {
describe('when no ID is present on params', () => {
it('renders the FluxNew component', () => {
const {wrapper} = setup()
const fluxNew = wrapper.find(FluxNew)
const fluxEdit = wrapper.find(FluxEdit)
expect(fluxNew.length).toBe(1)
expect(fluxEdit.length).toBe(0)
})
})
describe('when ID is present on params', () => {
it('renders a FluxEdit component', () => {
const services = [service]
const id = service.id
const params = {id, sourceID: service.sourceID}
const {wrapper} = setup({services, id, params})
const fluxNew = wrapper.find(FluxNew)
const fluxEdit = wrapper.find(FluxEdit)
expect(fluxNew.length).toBe(0)
expect(fluxEdit.length).toBe(1)
})
})
})

View File

@ -30,6 +30,7 @@ const setup = () => {
},
}
},
onGoToEditFlux: () => {},
}
const wrapper = shallow(<FluxPage {...props} />)

88
flux/client.go Normal file
View File

@ -0,0 +1,88 @@
package flux
import (
"context"
"crypto/tls"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"time"
"github.com/influxdata/platform/chronograf"
)
// Shared transports for all clients to prevent leaking connections.
var (
skipVerifyTransport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
defaultTransport = &http.Transport{}
)
// Client is how we interact with Flux.
type Client struct {
URL *url.URL
InsecureSkipVerify bool
Timeout time.Duration
}
// Ping checks the connection of a Flux.
func (c *Client) Ping(ctx context.Context) error {
t := 2 * time.Second
if c.Timeout > 0 {
t = c.Timeout
}
ctx, cancel := context.WithTimeout(ctx, t)
defer cancel()
err := c.pingTimeout(ctx)
return err
}
func (c *Client) pingTimeout(ctx context.Context) error {
resps := make(chan (error))
go func() {
resps <- c.ping(c.URL)
}()
select {
case resp := <-resps:
return resp
case <-ctx.Done():
return chronograf.ErrUpstreamTimeout
}
}
func (c *Client) ping(u *url.URL) error {
u.Path = "ping"
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return err
}
hc := &http.Client{}
if c.InsecureSkipVerify {
hc.Transport = skipVerifyTransport
} else {
hc.Transport = defaultTransport
}
resp, err := hc.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent {
var err = fmt.Errorf(string(body))
return err
}
return nil
}