Merge pull request #498 from influxdata/cherry/flux-connection-page
feature(chronograf) flux connections page (#4026)pull/10616/head
commit
09879fa221
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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', '')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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>'
|
|
@ -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))
|
||||
|
|
|
@ -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))
|
|
@ -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 {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -30,6 +30,7 @@ const setup = () => {
|
|||
},
|
||||
}
|
||||
},
|
||||
onGoToEditFlux: () => {},
|
||||
}
|
||||
|
||||
const wrapper = shallow(<FluxPage {...props} />)
|
||||
|
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue