feat: notebooks HTTP handlers (#21316)

* feat: notebooks HTTP handlers
pull/21321/head^2
William Baker 2021-04-28 11:06:13 -04:00 committed by GitHub
parent 32f24e33e8
commit 0106de9fa7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 428 additions and 1 deletions

View File

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

20
notebook.go Normal file
View File

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

1
notebooks/service.go Normal file
View File

@ -0,0 +1 @@
package notebooks

View File

@ -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"
}
}
`

202
notebooks/transport/http.go Normal file
View File

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

View File

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

View File

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