parent
32f24e33e8
commit
0106de9fa7
|
@ -43,6 +43,7 @@ import (
|
|||
"github.com/influxdata/influxdb/v2/kv/migration/all"
|
||||
"github.com/influxdata/influxdb/v2/label"
|
||||
"github.com/influxdata/influxdb/v2/nats"
|
||||
notebookTransport "github.com/influxdata/influxdb/v2/notebooks/transport"
|
||||
endpointservice "github.com/influxdata/influxdb/v2/notification/endpoint/service"
|
||||
ruleservice "github.com/influxdata/influxdb/v2/notification/rule/service"
|
||||
"github.com/influxdata/influxdb/v2/pkger"
|
||||
|
@ -897,6 +898,8 @@ func (m *Launcher) run(ctx context.Context, opts *InfluxdOpts) (err error) {
|
|||
)
|
||||
}
|
||||
|
||||
notebookServer := notebookTransport.NewNotebookHandler(m.log.With(zap.String("handler", "notebooks")))
|
||||
|
||||
platformHandler := http.NewPlatformHandler(
|
||||
m.apibackend,
|
||||
http.WithResourceHandler(stacksHTTPServer),
|
||||
|
@ -912,6 +915,7 @@ func (m *Launcher) run(ctx context.Context, opts *InfluxdOpts) (err error) {
|
|||
http.WithResourceHandler(bucketHTTPServer),
|
||||
http.WithResourceHandler(v1AuthHTTPServer),
|
||||
http.WithResourceHandler(dashboardServer),
|
||||
http.WithResourceHandler(notebookServer),
|
||||
)
|
||||
|
||||
httpLogger := m.log.With(zap.String("service", "http"))
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
package influxdb
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/influxdata/influxdb/v2/kit/platform"
|
||||
)
|
||||
|
||||
// Notebook represents all visual and query data for a notebook.
|
||||
type Notebook struct {
|
||||
OrgID platform.ID `json:"orgID"`
|
||||
ID platform.ID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Spec NotebookSpec `json:"spec"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// Spec is a specification which is just a blob of content provided by the client.
|
||||
type NotebookSpec interface{}
|
|
@ -0,0 +1 @@
|
|||
package notebooks
|
|
@ -0,0 +1,199 @@
|
|||
package transport
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
"github.com/influxdata/influxdb/v2/kit/platform"
|
||||
)
|
||||
|
||||
// these functions are for generating demo data for development purposes.
|
||||
|
||||
func demoNotebook(orgID, notebookID platform.ID) influxdb.Notebook {
|
||||
return influxdb.Notebook{
|
||||
OrgID: orgID,
|
||||
ID: notebookID,
|
||||
Name: "demo notebook",
|
||||
Spec: demoSpec(1),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func demoNotebooks(n int, orgID platform.ID) []influxdb.Notebook {
|
||||
o := []influxdb.Notebook{}
|
||||
|
||||
for i := 1; i <= n; i++ {
|
||||
id, _ := platform.IDFromString(strconv.Itoa(1000000000000000 + i))
|
||||
|
||||
o = append(o, influxdb.Notebook{
|
||||
OrgID: orgID,
|
||||
ID: *id,
|
||||
Name: fmt.Sprintf("demo notebook %d", i),
|
||||
Spec: demoSpec(i),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
func demoSpec(n int) map[string]interface{} {
|
||||
s := map[string]interface{}{}
|
||||
json.Unmarshal([]byte(fmt.Sprintf(demoSpecBlob, n)), &s)
|
||||
return s
|
||||
}
|
||||
|
||||
const demoSpecBlob = `
|
||||
{
|
||||
"name":"demo notebook %d",
|
||||
"pipes":[
|
||||
{
|
||||
"aggregateFunction":{
|
||||
"name":"mean"
|
||||
},
|
||||
"field":"",
|
||||
"measurement":"",
|
||||
"tags":{
|
||||
|
||||
},
|
||||
"title":"Select a Metric",
|
||||
"type":"metricSelector",
|
||||
"visible":true
|
||||
},
|
||||
{
|
||||
"functions":[
|
||||
{
|
||||
"name":"mean"
|
||||
}
|
||||
],
|
||||
"panelHeight":200,
|
||||
"panelVisibility":"visible",
|
||||
"period":"10s",
|
||||
"properties":{
|
||||
"axes":{
|
||||
"x":{
|
||||
"base":"10",
|
||||
"bounds":[
|
||||
"",
|
||||
""
|
||||
],
|
||||
"label":"",
|
||||
"prefix":"",
|
||||
"scale":"linear",
|
||||
"suffix":""
|
||||
},
|
||||
"y":{
|
||||
"base":"10",
|
||||
"bounds":[
|
||||
"",
|
||||
""
|
||||
],
|
||||
"label":"",
|
||||
"prefix":"",
|
||||
"scale":"linear",
|
||||
"suffix":""
|
||||
}
|
||||
},
|
||||
"colors":[
|
||||
{
|
||||
"hex":"#31C0F6",
|
||||
"id":"c1f3c9a6-3404-4418-a43b-266a91da6790",
|
||||
"name":"Nineteen Eighty Four",
|
||||
"type":"scale",
|
||||
"value":0
|
||||
},
|
||||
{
|
||||
"hex":"#A500A5",
|
||||
"id":"be814008-8f22-4f50-a96a-f8d076b93dff",
|
||||
"name":"Nineteen Eighty Four",
|
||||
"type":"scale",
|
||||
"value":0
|
||||
},
|
||||
{
|
||||
"hex":"#FF7E27",
|
||||
"id":"9e5f2432-fcd8-4eac-9952-b26bb951fd8d",
|
||||
"name":"Nineteen Eighty Four",
|
||||
"type":"scale",
|
||||
"value":0
|
||||
}
|
||||
],
|
||||
"generateXAxisTicks":[
|
||||
|
||||
],
|
||||
"generateYAxisTicks":[
|
||||
|
||||
],
|
||||
"geom":"line",
|
||||
"hoverDimension":"auto",
|
||||
"legendOpacity":1,
|
||||
"legendOrientationThreshold":100000000,
|
||||
"note":"",
|
||||
"position":"overlaid",
|
||||
"queries":[
|
||||
{
|
||||
"builderConfig":{
|
||||
"aggregateWindow":{
|
||||
"fillValues":false,
|
||||
"period":"auto"
|
||||
},
|
||||
"buckets":[
|
||||
|
||||
],
|
||||
"functions":[
|
||||
{
|
||||
"name":"mean"
|
||||
}
|
||||
],
|
||||
"tags":[
|
||||
{
|
||||
"aggregateFunctionType":"filter",
|
||||
"key":"_measurement",
|
||||
"values":[
|
||||
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"editMode":"builder",
|
||||
"name":"",
|
||||
"text":""
|
||||
}
|
||||
],
|
||||
"shape":"chronograf-v2",
|
||||
"showNoteWhenEmpty":false,
|
||||
"type":"xy",
|
||||
"xColumn":null,
|
||||
"xTickStart":null,
|
||||
"xTickStep":null,
|
||||
"xTotalTicks":null,
|
||||
"yColumn":null,
|
||||
"yTickStart":null,
|
||||
"yTickStep":null,
|
||||
"yTotalTicks":null
|
||||
},
|
||||
"title":"Visualize the Result",
|
||||
"type":"visualization",
|
||||
"visible":true
|
||||
}
|
||||
],
|
||||
"range":{
|
||||
"duration":"1h",
|
||||
"label":"Past 1h",
|
||||
"lower":"now() - 1h",
|
||||
"seconds":3600,
|
||||
"type":"selectable-duration",
|
||||
"upper":null,
|
||||
"windowPeriod":10000
|
||||
},
|
||||
"readOnly":false,
|
||||
"refresh":{
|
||||
"interval":0,
|
||||
"status":"paused"
|
||||
}
|
||||
}
|
||||
`
|
|
@ -0,0 +1,202 @@
|
|||
package transport
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
feature "github.com/influxdata/influxdb/v2/kit/feature"
|
||||
"github.com/influxdata/influxdb/v2/kit/platform"
|
||||
"github.com/influxdata/influxdb/v2/kit/platform/errors"
|
||||
kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
prefixNotebooks = "/api/v2private/flows"
|
||||
errMissingParam = "url missing %s"
|
||||
errInvalidParam = "url %s is invalid"
|
||||
)
|
||||
|
||||
// NotebookHandler is the handler for the notebook service
|
||||
type NotebookHandler struct {
|
||||
chi.Router
|
||||
|
||||
api *kithttp.API
|
||||
log *zap.Logger
|
||||
}
|
||||
|
||||
func NewNotebookHandler(log *zap.Logger) *NotebookHandler {
|
||||
h := &NotebookHandler{
|
||||
log: log,
|
||||
api: kithttp.NewAPI(kithttp.WithLog(log)),
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Use(
|
||||
middleware.Recoverer,
|
||||
middleware.RequestID,
|
||||
middleware.RealIP,
|
||||
h.notebookFlag, // temporary, remove when feature flag for notebooks is removed
|
||||
)
|
||||
|
||||
r.Route("/orgs/{orgID}/flows", func(r chi.Router) {
|
||||
r.Get("/", h.handleGetNotebooks)
|
||||
r.Post("/", h.handleCreateNotebook)
|
||||
|
||||
r.Route("/{id}", func(r chi.Router) {
|
||||
r.Get("/", h.handleGetNotebook)
|
||||
r.Patch("/", h.handlePatchNotebook)
|
||||
r.Delete("/", h.handleDeleteNotebook)
|
||||
})
|
||||
})
|
||||
|
||||
h.Router = r
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *NotebookHandler) Prefix() string {
|
||||
return prefixNotebooks
|
||||
}
|
||||
|
||||
// notebookFlag is middleware for returning no content if the notebooks feature
|
||||
// flag is set to false. remove this middleware when the feature flag is removed.
|
||||
func (h *NotebookHandler) notebookFlag(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
flags := feature.FlagsFromContext(r.Context())
|
||||
|
||||
if !flags["notebooks"].(bool) {
|
||||
h.api.Respond(w, r, http.StatusNoContent, nil)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
|
||||
// get a list of all notebooks for an org
|
||||
func (h *NotebookHandler) handleGetNotebooks(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, err := getIDfromReq(r, "orgID")
|
||||
if err != nil {
|
||||
h.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Demo data - for development purposes.
|
||||
d := map[string][]influxdb.Notebook{}
|
||||
d["flows"] = demoNotebooks(3, *orgID)
|
||||
|
||||
h.api.Respond(w, r, http.StatusOK, d)
|
||||
}
|
||||
|
||||
// create a single notebook
|
||||
func (h *NotebookHandler) handleCreateNotebook(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, err := getIDfromReq(r, "orgID")
|
||||
if err != nil {
|
||||
h.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Demo data - just return the body from the request with a generated ID
|
||||
b := influxdb.Notebook{}
|
||||
if err := h.api.DecodeJSON(r.Body, &b); err != nil {
|
||||
h.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
b.OrgID = *orgID // this isn't necessary with the demo data, but keeping it here for future
|
||||
id, _ := platform.IDFromString(strconv.Itoa(1000000000000000 + 1)) // give it an ID from the getNotebooks list so that the UI doesn't break
|
||||
b.ID = *id
|
||||
|
||||
h.api.Respond(w, r, http.StatusOK, b)
|
||||
}
|
||||
|
||||
// get a single notebook
|
||||
func (h *NotebookHandler) handleGetNotebook(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, err := getIDfromReq(r, "orgID")
|
||||
if err != nil {
|
||||
h.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
notebookID, err := getIDfromReq(r, "id")
|
||||
if err != nil {
|
||||
h.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Demo data - for development purposes.
|
||||
d := demoNotebook(*orgID, *notebookID)
|
||||
|
||||
h.api.Respond(w, r, http.StatusOK, d)
|
||||
}
|
||||
|
||||
// update a single notebook
|
||||
func (h *NotebookHandler) handlePatchNotebook(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, err := getIDfromReq(r, "orgID")
|
||||
if err != nil {
|
||||
h.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := getIDfromReq(r, "id")
|
||||
if err != nil {
|
||||
h.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Demo data - just return the body from the request with a generated ID
|
||||
b := influxdb.Notebook{}
|
||||
if err := h.api.DecodeJSON(r.Body, &b); err != nil {
|
||||
h.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
b.OrgID = *orgID // this isn't necessary with the demo data, but keeping it here for future
|
||||
b.ID = *id // ditto
|
||||
|
||||
h.api.Respond(w, r, http.StatusOK, b)
|
||||
}
|
||||
|
||||
// delete a single notebook
|
||||
// for now, just respond with 200 unless there is a problem with the orgID or notebook ID
|
||||
func (h *NotebookHandler) handleDeleteNotebook(w http.ResponseWriter, r *http.Request) {
|
||||
_, err := getIDfromReq(r, "orgID")
|
||||
if err != nil {
|
||||
h.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = getIDfromReq(r, "id")
|
||||
if err != nil {
|
||||
h.api.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
h.api.Respond(w, r, http.StatusOK, nil)
|
||||
}
|
||||
|
||||
func getIDfromReq(r *http.Request, param string) (*platform.ID, error) {
|
||||
id := chi.URLParam(r, param)
|
||||
if id == "" {
|
||||
return nil, &errors.Error{
|
||||
Code: errors.EInvalid,
|
||||
Msg: fmt.Sprintf(errMissingParam, param),
|
||||
}
|
||||
}
|
||||
|
||||
var i platform.ID
|
||||
if err := i.DecodeFromString(id); err != nil {
|
||||
return nil, &errors.Error{
|
||||
Code: errors.EInvalid,
|
||||
Msg: fmt.Sprintf(errInvalidParam, param),
|
||||
}
|
||||
}
|
||||
|
||||
return &i, nil
|
||||
}
|
|
@ -168,6 +168,7 @@ func encodeCookieSession(w http.ResponseWriter, s *influxdb.Session) {
|
|||
c := &http.Cookie{
|
||||
Name: cookieSessionName,
|
||||
Value: s.Key,
|
||||
Path: "/api/",
|
||||
}
|
||||
|
||||
http.SetCookie(w, c)
|
||||
|
|
|
@ -59,7 +59,7 @@ func TestSessionHandler_handleSignin(t *testing.T) {
|
|||
password: "supersecret",
|
||||
},
|
||||
wants: wants{
|
||||
cookie: "session=abc123xyz",
|
||||
cookie: "session=abc123xyz; Path=/api/",
|
||||
code: http.StatusNoContent,
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue