github.com/influxdata/influxdb/v2@v2.7.6/dashboards/transport/http.go (about) 1 package transport 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "net/http" 8 "path" 9 10 "github.com/go-chi/chi" 11 "github.com/go-chi/chi/middleware" 12 "github.com/influxdata/influxdb/v2" 13 "github.com/influxdata/influxdb/v2/kit/platform" 14 "github.com/influxdata/influxdb/v2/kit/platform/errors" 15 kithttp "github.com/influxdata/influxdb/v2/kit/transport/http" 16 "github.com/influxdata/influxdb/v2/pkg/httpc" 17 "go.uber.org/zap" 18 ) 19 20 // DashboardHandler is the handler for the dashboard service 21 type DashboardHandler struct { 22 chi.Router 23 24 api *kithttp.API 25 log *zap.Logger 26 27 dashboardService influxdb.DashboardService 28 labelService influxdb.LabelService 29 userService influxdb.UserService 30 orgService influxdb.OrganizationService 31 } 32 33 const ( 34 prefixDashboards = "/api/v2/dashboards" 35 ) 36 37 // NewDashboardHandler returns a new instance of DashboardHandler. 38 func NewDashboardHandler( 39 log *zap.Logger, 40 dashboardService influxdb.DashboardService, 41 labelService influxdb.LabelService, 42 userService influxdb.UserService, 43 orgService influxdb.OrganizationService, 44 urmHandler, labelHandler http.Handler, 45 ) *DashboardHandler { 46 h := &DashboardHandler{ 47 log: log, 48 api: kithttp.NewAPI(kithttp.WithLog(log)), 49 dashboardService: dashboardService, 50 labelService: labelService, 51 userService: userService, 52 orgService: orgService, 53 } 54 55 // setup routing 56 { 57 r := chi.NewRouter() 58 r.Use( 59 middleware.Recoverer, 60 middleware.RequestID, 61 middleware.RealIP, 62 ) 63 64 r.Route("/", func(r chi.Router) { 65 r.Post("/", h.handlePostDashboard) 66 r.Get("/", h.handleGetDashboards) 67 68 r.Route("/{id}", func(r chi.Router) { 69 r.Get("/", h.handleGetDashboard) 70 r.Patch("/", h.handlePatchDashboard) 71 r.Delete("/", h.handleDeleteDashboard) 72 73 r.Route("/cells", func(r chi.Router) { 74 r.Put("/", h.handlePutDashboardCells) 75 r.Post("/", h.handlePostDashboardCell) 76 77 r.Route("/{cellID}", func(r chi.Router) { 78 r.Delete("/", h.handleDeleteDashboardCell) 79 r.Patch("/", h.handlePatchDashboardCell) 80 81 r.Route("/view", func(r chi.Router) { 82 r.Get("/", h.handleGetDashboardCellView) 83 r.Patch("/", h.handlePatchDashboardCellView) 84 }) 85 }) 86 }) 87 88 // mount embedded resources 89 mountableRouter := r.With(kithttp.ValidResource(h.api, h.lookupOrgByDashboardID)) 90 mountableRouter.Mount("/members", urmHandler) 91 mountableRouter.Mount("/owners", urmHandler) 92 mountableRouter.Mount("/labels", labelHandler) 93 }) 94 }) 95 96 h.Router = r 97 } 98 99 return h 100 } 101 102 // Prefix returns the mounting prefix for the handler 103 func (h *DashboardHandler) Prefix() string { 104 return prefixDashboards 105 } 106 107 type dashboardLinks struct { 108 Self string `json:"self"` 109 Members string `json:"members"` 110 Owners string `json:"owners"` 111 Cells string `json:"cells"` 112 Labels string `json:"labels"` 113 Organization string `json:"org"` 114 } 115 116 type dashboardResponse struct { 117 ID platform.ID `json:"id,omitempty"` 118 OrganizationID platform.ID `json:"orgID,omitempty"` 119 Name string `json:"name"` 120 Description string `json:"description"` 121 Meta influxdb.DashboardMeta `json:"meta"` 122 Cells []dashboardCellResponse `json:"cells"` 123 Labels []influxdb.Label `json:"labels"` 124 Links dashboardLinks `json:"links"` 125 } 126 127 func (d dashboardResponse) toinfluxdb() *influxdb.Dashboard { 128 var cells []*influxdb.Cell 129 if len(d.Cells) > 0 { 130 cells = make([]*influxdb.Cell, len(d.Cells)) 131 } 132 for i := range d.Cells { 133 cells[i] = d.Cells[i].toinfluxdb() 134 } 135 return &influxdb.Dashboard{ 136 ID: d.ID, 137 OrganizationID: d.OrganizationID, 138 Name: d.Name, 139 Description: d.Description, 140 Meta: d.Meta, 141 Cells: cells, 142 } 143 } 144 145 func newDashboardResponse(d *influxdb.Dashboard, labels []*influxdb.Label) dashboardResponse { 146 res := dashboardResponse{ 147 Links: dashboardLinks{ 148 Self: fmt.Sprintf("/api/v2/dashboards/%s", d.ID), 149 Members: fmt.Sprintf("/api/v2/dashboards/%s/members", d.ID), 150 Owners: fmt.Sprintf("/api/v2/dashboards/%s/owners", d.ID), 151 Cells: fmt.Sprintf("/api/v2/dashboards/%s/cells", d.ID), 152 Labels: fmt.Sprintf("/api/v2/dashboards/%s/labels", d.ID), 153 Organization: fmt.Sprintf("/api/v2/orgs/%s", d.OrganizationID), 154 }, 155 ID: d.ID, 156 OrganizationID: d.OrganizationID, 157 Name: d.Name, 158 Description: d.Description, 159 Meta: d.Meta, 160 Labels: []influxdb.Label{}, 161 Cells: []dashboardCellResponse{}, 162 } 163 164 for _, l := range labels { 165 res.Labels = append(res.Labels, *l) 166 } 167 168 for _, cell := range d.Cells { 169 res.Cells = append(res.Cells, newDashboardCellResponse(d.ID, cell)) 170 } 171 172 return res 173 } 174 175 type dashboardCellResponse struct { 176 influxdb.Cell 177 Properties influxdb.ViewProperties `json:"-"` 178 Name string `json:"name,omitempty"` 179 Links map[string]string `json:"links"` 180 } 181 182 func (d *dashboardCellResponse) MarshalJSON() ([]byte, error) { 183 r := struct { 184 influxdb.Cell 185 Properties json.RawMessage `json:"properties,omitempty"` 186 Name string `json:"name,omitempty"` 187 Links map[string]string `json:"links"` 188 }{ 189 Cell: d.Cell, 190 Links: d.Links, 191 } 192 193 if d.Cell.View != nil { 194 b, err := influxdb.MarshalViewPropertiesJSON(d.Cell.View.Properties) 195 if err != nil { 196 return nil, err 197 } 198 r.Properties = b 199 r.Name = d.Cell.View.Name 200 } 201 202 return json.Marshal(r) 203 } 204 205 func (c dashboardCellResponse) toinfluxdb() *influxdb.Cell { 206 return &c.Cell 207 } 208 209 func newDashboardCellResponse(dashboardID platform.ID, c *influxdb.Cell) dashboardCellResponse { 210 resp := dashboardCellResponse{ 211 Cell: *c, 212 Links: map[string]string{ 213 "self": fmt.Sprintf("/api/v2/dashboards/%s/cells/%s", dashboardID, c.ID), 214 "view": fmt.Sprintf("/api/v2/dashboards/%s/cells/%s/view", dashboardID, c.ID), 215 }, 216 } 217 218 if c.View != nil { 219 resp.Properties = c.View.Properties 220 resp.Name = c.View.Name 221 } 222 return resp 223 } 224 225 type dashboardCellsResponse struct { 226 Cells []dashboardCellResponse `json:"cells"` 227 Links map[string]string `json:"links"` 228 } 229 230 func newDashboardCellsResponse(dashboardID platform.ID, cs []*influxdb.Cell) dashboardCellsResponse { 231 res := dashboardCellsResponse{ 232 Cells: []dashboardCellResponse{}, 233 Links: map[string]string{ 234 "self": fmt.Sprintf("/api/v2/dashboards/%s/cells", dashboardID), 235 }, 236 } 237 238 for _, cell := range cs { 239 res.Cells = append(res.Cells, newDashboardCellResponse(dashboardID, cell)) 240 } 241 242 return res 243 } 244 245 type viewLinks struct { 246 Self string `json:"self"` 247 } 248 249 type dashboardCellViewResponse struct { 250 influxdb.View 251 Links viewLinks `json:"links"` 252 } 253 254 func (r dashboardCellViewResponse) MarshalJSON() ([]byte, error) { 255 props, err := influxdb.MarshalViewPropertiesJSON(r.Properties) 256 if err != nil { 257 return nil, err 258 } 259 260 return json.Marshal(struct { 261 influxdb.ViewContents 262 Links viewLinks `json:"links"` 263 Properties json.RawMessage `json:"properties"` 264 }{ 265 ViewContents: r.ViewContents, 266 Links: r.Links, 267 Properties: props, 268 }) 269 } 270 271 func newDashboardCellViewResponse(dashID, cellID platform.ID, v *influxdb.View) dashboardCellViewResponse { 272 return dashboardCellViewResponse{ 273 Links: viewLinks{ 274 Self: fmt.Sprintf("/api/v2/dashboards/%s/cells/%s", dashID, cellID), 275 }, 276 View: *v, 277 } 278 } 279 280 // handleGetDashboards returns all dashboards within the store. 281 func (h *DashboardHandler) handleGetDashboards(w http.ResponseWriter, r *http.Request) { 282 ctx := r.Context() 283 req, err := decodeGetDashboardsRequest(ctx, r) 284 if err != nil { 285 h.api.Err(w, r, err) 286 return 287 } 288 289 dashboardFilter := req.filter 290 291 if dashboardFilter.Organization != nil { 292 orgNameFilter := influxdb.OrganizationFilter{Name: dashboardFilter.Organization} 293 o, err := h.orgService.FindOrganization(ctx, orgNameFilter) 294 if err != nil { 295 h.api.Err(w, r, err) 296 return 297 } 298 dashboardFilter.OrganizationID = &o.ID 299 } 300 301 dashboards, _, err := h.dashboardService.FindDashboards(ctx, dashboardFilter, req.opts) 302 if err != nil { 303 h.api.Err(w, r, err) 304 return 305 } 306 307 h.log.Debug("List Dashboards", zap.String("dashboards", fmt.Sprint(dashboards))) 308 309 h.api.Respond(w, r, http.StatusOK, newGetDashboardsResponse(ctx, dashboards, req.filter, req.opts, h.labelService)) 310 } 311 312 type getDashboardsRequest struct { 313 filter influxdb.DashboardFilter 314 opts influxdb.FindOptions 315 } 316 317 func decodeGetDashboardsRequest(ctx context.Context, r *http.Request) (*getDashboardsRequest, error) { 318 qp := r.URL.Query() 319 req := &getDashboardsRequest{} 320 321 opts, err := influxdb.DecodeFindOptions(r) 322 if err != nil { 323 return nil, err 324 } 325 req.opts = *opts 326 327 initialID := platform.InvalidID() 328 if ids, ok := qp["id"]; ok { 329 for _, id := range ids { 330 i := initialID 331 if err := i.DecodeFromString(id); err != nil { 332 return nil, err 333 } 334 req.filter.IDs = append(req.filter.IDs, &i) 335 } 336 } else if ownerID := qp.Get("ownerID"); ownerID != "" { 337 req.filter.OwnerID = &initialID 338 if err := req.filter.OwnerID.DecodeFromString(ownerID); err != nil { 339 return nil, err 340 } 341 } else if orgID := qp.Get("orgID"); orgID != "" { 342 id := platform.InvalidID() 343 if err := id.DecodeFromString(orgID); err != nil { 344 return nil, err 345 } 346 req.filter.OrganizationID = &id 347 } else if org := qp.Get("org"); org != "" { 348 req.filter.Organization = &org 349 } 350 351 return req, nil 352 } 353 354 type getDashboardsResponse struct { 355 Links *influxdb.PagingLinks `json:"links"` 356 Dashboards []dashboardResponse `json:"dashboards"` 357 } 358 359 func (d getDashboardsResponse) toinfluxdb() []*influxdb.Dashboard { 360 res := make([]*influxdb.Dashboard, len(d.Dashboards)) 361 for i := range d.Dashboards { 362 res[i] = d.Dashboards[i].toinfluxdb() 363 } 364 return res 365 } 366 367 func newGetDashboardsResponse(ctx context.Context, dashboards []*influxdb.Dashboard, filter influxdb.DashboardFilter, opts influxdb.FindOptions, labelService influxdb.LabelService) getDashboardsResponse { 368 res := getDashboardsResponse{ 369 Links: influxdb.NewPagingLinks(prefixDashboards, opts, filter, len(dashboards)), 370 Dashboards: make([]dashboardResponse, 0, len(dashboards)), 371 } 372 373 for _, dashboard := range dashboards { 374 if dashboard != nil { 375 labels, _ := labelService.FindResourceLabels(ctx, influxdb.LabelMappingFilter{ResourceID: dashboard.ID, ResourceType: influxdb.DashboardsResourceType}) 376 res.Dashboards = append(res.Dashboards, newDashboardResponse(dashboard, labels)) 377 } 378 } 379 380 return res 381 } 382 383 // handlePostDashboard creates a new dashboard. 384 func (h *DashboardHandler) handlePostDashboard(w http.ResponseWriter, r *http.Request) { 385 var ( 386 ctx = r.Context() 387 d influxdb.Dashboard 388 ) 389 390 if err := h.api.DecodeJSON(r.Body, &d); err != nil { 391 h.api.Err(w, r, err) 392 return 393 } 394 395 if err := h.dashboardService.CreateDashboard(ctx, &d); err != nil { 396 h.api.Err(w, r, err) 397 return 398 } 399 400 h.api.Respond(w, r, http.StatusCreated, newDashboardResponse(&d, []*influxdb.Label{})) 401 } 402 403 // handleGetDashboard retrieves a dashboard by ID. 404 func (h *DashboardHandler) handleGetDashboard(w http.ResponseWriter, r *http.Request) { 405 ctx := r.Context() 406 req, err := decodeGetDashboardRequest(ctx, r) 407 if err != nil { 408 h.api.Err(w, r, err) 409 return 410 } 411 412 dashboard, err := h.dashboardService.FindDashboardByID(ctx, req.DashboardID) 413 if err != nil { 414 h.api.Err(w, r, err) 415 return 416 } 417 418 if r.URL.Query().Get("include") == "properties" { 419 for _, c := range dashboard.Cells { 420 view, err := h.dashboardService.GetDashboardCellView(ctx, dashboard.ID, c.ID) 421 if err != nil { 422 h.api.Err(w, r, err) 423 return 424 } 425 426 if view != nil { 427 c.View = view 428 } 429 } 430 } 431 432 labels, err := h.labelService.FindResourceLabels(ctx, influxdb.LabelMappingFilter{ResourceID: dashboard.ID, ResourceType: influxdb.DashboardsResourceType}) 433 if err != nil { 434 h.api.Err(w, r, err) 435 return 436 } 437 438 h.log.Debug("Get Dashboard", zap.String("dashboard", fmt.Sprint(dashboard))) 439 440 h.api.Respond(w, r, http.StatusOK, newDashboardResponse(dashboard, labels)) 441 } 442 443 type getDashboardRequest struct { 444 DashboardID platform.ID 445 } 446 447 func decodeGetDashboardRequest(ctx context.Context, r *http.Request) (*getDashboardRequest, error) { 448 id := chi.URLParam(r, "id") 449 if id == "" { 450 return nil, &errors.Error{ 451 Code: errors.EInvalid, 452 Msg: "url missing id", 453 } 454 } 455 456 var i platform.ID 457 if err := i.DecodeFromString(id); err != nil { 458 return nil, err 459 } 460 461 return &getDashboardRequest{ 462 DashboardID: i, 463 }, nil 464 } 465 466 // handleDeleteDashboard removes a dashboard by ID. 467 func (h *DashboardHandler) handleDeleteDashboard(w http.ResponseWriter, r *http.Request) { 468 ctx := r.Context() 469 req, err := decodeDeleteDashboardRequest(ctx, r) 470 if err != nil { 471 h.api.Err(w, r, err) 472 return 473 } 474 475 if err := h.dashboardService.DeleteDashboard(ctx, req.DashboardID); err != nil { 476 h.api.Err(w, r, err) 477 return 478 } 479 480 h.log.Debug("Dashboard deleted", zap.String("dashboardID", req.DashboardID.String())) 481 482 w.WriteHeader(http.StatusNoContent) 483 } 484 485 type deleteDashboardRequest struct { 486 DashboardID platform.ID 487 } 488 489 func decodeDeleteDashboardRequest(ctx context.Context, r *http.Request) (*deleteDashboardRequest, error) { 490 id := chi.URLParam(r, "id") 491 if id == "" { 492 return nil, &errors.Error{ 493 Code: errors.EInvalid, 494 Msg: "url missing id", 495 } 496 } 497 498 var i platform.ID 499 if err := i.DecodeFromString(id); err != nil { 500 return nil, err 501 } 502 503 return &deleteDashboardRequest{ 504 DashboardID: i, 505 }, nil 506 } 507 508 // handlePatchDashboard updates a dashboard. 509 func (h *DashboardHandler) handlePatchDashboard(w http.ResponseWriter, r *http.Request) { 510 ctx := r.Context() 511 req, err := decodePatchDashboardRequest(ctx, r) 512 if err != nil { 513 h.api.Err(w, r, err) 514 return 515 } 516 dashboard, err := h.dashboardService.UpdateDashboard(ctx, req.DashboardID, req.Upd) 517 if err != nil { 518 h.api.Err(w, r, err) 519 return 520 } 521 522 labels, err := h.labelService.FindResourceLabels(ctx, influxdb.LabelMappingFilter{ResourceID: dashboard.ID, ResourceType: influxdb.DashboardsResourceType}) 523 if err != nil { 524 h.api.Err(w, r, err) 525 return 526 } 527 528 h.log.Debug("Dashboard updated", zap.String("dashboard", fmt.Sprint(dashboard))) 529 530 h.api.Respond(w, r, http.StatusOK, newDashboardResponse(dashboard, labels)) 531 } 532 533 type patchDashboardRequest struct { 534 DashboardID platform.ID 535 Upd influxdb.DashboardUpdate 536 } 537 538 func decodePatchDashboardRequest(ctx context.Context, r *http.Request) (*patchDashboardRequest, error) { 539 req := &patchDashboardRequest{} 540 upd := influxdb.DashboardUpdate{} 541 if err := json.NewDecoder(r.Body).Decode(&upd); err != nil { 542 return nil, &errors.Error{ 543 Code: errors.EInvalid, 544 Err: err, 545 } 546 } 547 req.Upd = upd 548 549 id := chi.URLParam(r, "id") 550 if id == "" { 551 return nil, &errors.Error{ 552 Code: errors.EInvalid, 553 Msg: "url missing id", 554 } 555 } 556 var i platform.ID 557 if err := i.DecodeFromString(id); err != nil { 558 return nil, err 559 } 560 561 req.DashboardID = i 562 563 if err := req.Valid(); err != nil { 564 return nil, &errors.Error{ 565 Code: errors.EInvalid, 566 Err: err, 567 } 568 } 569 570 return req, nil 571 } 572 573 // Valid validates that the dashboard ID is non zero valued and update has expected values set. 574 func (r *patchDashboardRequest) Valid() error { 575 if !r.DashboardID.Valid() { 576 return &errors.Error{ 577 Code: errors.EInvalid, 578 Msg: "missing dashboard ID", 579 } 580 } 581 582 if pe := r.Upd.Valid(); pe != nil { 583 return pe 584 } 585 return nil 586 } 587 588 type postDashboardCellRequest struct { 589 dashboardID platform.ID 590 *influxdb.CellProperty 591 UsingView *platform.ID `json:"usingView"` 592 Name *string `json:"name"` 593 } 594 595 func decodePostDashboardCellRequest(ctx context.Context, r *http.Request) (*postDashboardCellRequest, error) { 596 req := &postDashboardCellRequest{} 597 id := chi.URLParam(r, "id") 598 if id == "" { 599 return nil, &errors.Error{ 600 Code: errors.EInvalid, 601 Msg: "url missing id", 602 } 603 } 604 605 if err := json.NewDecoder(r.Body).Decode(req); err != nil { 606 return nil, &errors.Error{ 607 Code: errors.EInvalid, 608 Msg: "bad request json body", 609 Err: err, 610 } 611 } 612 613 if err := req.dashboardID.DecodeFromString(id); err != nil { 614 return nil, err 615 } 616 617 return req, nil 618 } 619 620 // handlePostDashboardCell creates a dashboard cell. 621 func (h *DashboardHandler) handlePostDashboardCell(w http.ResponseWriter, r *http.Request) { 622 ctx := r.Context() 623 req, err := decodePostDashboardCellRequest(ctx, r) 624 if err != nil { 625 h.api.Err(w, r, err) 626 return 627 } 628 cell := new(influxdb.Cell) 629 630 opts := new(influxdb.AddDashboardCellOptions) 631 if req.UsingView != nil || req.Name != nil { 632 opts.View = new(influxdb.View) 633 if req.UsingView != nil { 634 // load the view 635 opts.View, err = h.dashboardService.GetDashboardCellView(ctx, req.dashboardID, *req.UsingView) 636 if err != nil { 637 h.api.Err(w, r, err) 638 return 639 } 640 } 641 if req.Name != nil { 642 opts.View.Name = *req.Name 643 } 644 } else if req.CellProperty == nil { 645 h.api.Err(w, r, &errors.Error{ 646 Code: errors.EInvalid, 647 Msg: "req body is empty", 648 }) 649 return 650 } 651 652 if req.CellProperty != nil { 653 cell.CellProperty = *req.CellProperty 654 } 655 656 if err := h.dashboardService.AddDashboardCell(ctx, req.dashboardID, cell, *opts); err != nil { 657 h.api.Err(w, r, err) 658 return 659 } 660 661 h.log.Debug("Dashboard cell created", zap.String("dashboardID", req.dashboardID.String()), zap.String("cell", fmt.Sprint(cell))) 662 663 h.api.Respond(w, r, http.StatusCreated, newDashboardCellResponse(req.dashboardID, cell)) 664 } 665 666 type putDashboardCellRequest struct { 667 dashboardID platform.ID 668 cells []*influxdb.Cell 669 } 670 671 func decodePutDashboardCellRequest(ctx context.Context, r *http.Request) (*putDashboardCellRequest, error) { 672 req := &putDashboardCellRequest{} 673 674 id := chi.URLParam(r, "id") 675 if id == "" { 676 return nil, &errors.Error{ 677 Code: errors.EInvalid, 678 Msg: "url missing id", 679 } 680 } 681 if err := req.dashboardID.DecodeFromString(id); err != nil { 682 return nil, err 683 } 684 685 req.cells = []*influxdb.Cell{} 686 if err := json.NewDecoder(r.Body).Decode(&req.cells); err != nil { 687 return nil, err 688 } 689 690 return req, nil 691 } 692 693 // handlePutDashboardCells replaces a dashboards cells. 694 func (h *DashboardHandler) handlePutDashboardCells(w http.ResponseWriter, r *http.Request) { 695 ctx := r.Context() 696 req, err := decodePutDashboardCellRequest(ctx, r) 697 if err != nil { 698 h.api.Err(w, r, err) 699 return 700 } 701 702 if err := h.dashboardService.ReplaceDashboardCells(ctx, req.dashboardID, req.cells); err != nil { 703 h.api.Err(w, r, err) 704 return 705 } 706 707 h.log.Debug("Dashboard cell replaced", zap.String("dashboardID", req.dashboardID.String()), zap.String("cells", fmt.Sprint(req.cells))) 708 709 h.api.Respond(w, r, http.StatusCreated, newDashboardCellsResponse(req.dashboardID, req.cells)) 710 } 711 712 type deleteDashboardCellRequest struct { 713 dashboardID platform.ID 714 cellID platform.ID 715 } 716 717 func decodeDeleteDashboardCellRequest(ctx context.Context, r *http.Request) (*deleteDashboardCellRequest, error) { 718 req := &deleteDashboardCellRequest{} 719 720 id := chi.URLParam(r, "id") 721 if id == "" { 722 return nil, &errors.Error{ 723 Code: errors.EInvalid, 724 Msg: "url missing id", 725 } 726 } 727 if err := req.dashboardID.DecodeFromString(id); err != nil { 728 return nil, err 729 } 730 731 cellID := chi.URLParam(r, "cellID") 732 if cellID == "" { 733 return nil, &errors.Error{ 734 Code: errors.EInvalid, 735 Msg: "url missing cellID", 736 } 737 } 738 if err := req.cellID.DecodeFromString(cellID); err != nil { 739 return nil, err 740 } 741 742 return req, nil 743 } 744 745 type getDashboardCellViewRequest struct { 746 dashboardID platform.ID 747 cellID platform.ID 748 } 749 750 func decodeGetDashboardCellViewRequest(ctx context.Context, r *http.Request) (*getDashboardCellViewRequest, error) { 751 req := &getDashboardCellViewRequest{} 752 753 id := chi.URLParam(r, "id") 754 if id == "" { 755 return nil, errors.NewError(errors.WithErrorMsg("url missing id"), errors.WithErrorCode(errors.EInvalid)) 756 } 757 if err := req.dashboardID.DecodeFromString(id); err != nil { 758 return nil, err 759 } 760 761 cellID := chi.URLParam(r, "cellID") 762 if cellID == "" { 763 return nil, errors.NewError(errors.WithErrorMsg("url missing cellID"), errors.WithErrorCode(errors.EInvalid)) 764 } 765 if err := req.cellID.DecodeFromString(cellID); err != nil { 766 return nil, err 767 } 768 769 return req, nil 770 } 771 772 func (h *DashboardHandler) handleGetDashboardCellView(w http.ResponseWriter, r *http.Request) { 773 ctx := r.Context() 774 req, err := decodeGetDashboardCellViewRequest(ctx, r) 775 if err != nil { 776 h.api.Err(w, r, err) 777 return 778 } 779 780 view, err := h.dashboardService.GetDashboardCellView(ctx, req.dashboardID, req.cellID) 781 if err != nil { 782 h.api.Err(w, r, err) 783 return 784 } 785 786 h.log.Debug("Dashboard cell view retrieved", zap.String("dashboardID", req.dashboardID.String()), zap.String("cellID", req.cellID.String()), zap.String("view", fmt.Sprint(view))) 787 788 h.api.Respond(w, r, http.StatusOK, newDashboardCellViewResponse(req.dashboardID, req.cellID, view)) 789 } 790 791 type patchDashboardCellViewRequest struct { 792 dashboardID platform.ID 793 cellID platform.ID 794 upd influxdb.ViewUpdate 795 } 796 797 func decodePatchDashboardCellViewRequest(ctx context.Context, r *http.Request) (*patchDashboardCellViewRequest, error) { 798 req := &patchDashboardCellViewRequest{} 799 800 id := chi.URLParam(r, "id") 801 if id == "" { 802 return nil, errors.NewError(errors.WithErrorMsg("url missing id"), errors.WithErrorCode(errors.EInvalid)) 803 } 804 if err := req.dashboardID.DecodeFromString(id); err != nil { 805 return nil, err 806 } 807 808 cellID := chi.URLParam(r, "cellID") 809 if cellID == "" { 810 return nil, errors.NewError(errors.WithErrorMsg("url missing cellID"), errors.WithErrorCode(errors.EInvalid)) 811 } 812 if err := req.cellID.DecodeFromString(cellID); err != nil { 813 return nil, err 814 } 815 816 if err := json.NewDecoder(r.Body).Decode(&req.upd); err != nil { 817 return nil, err 818 } 819 820 return req, nil 821 } 822 823 func (h *DashboardHandler) handlePatchDashboardCellView(w http.ResponseWriter, r *http.Request) { 824 ctx := r.Context() 825 req, err := decodePatchDashboardCellViewRequest(ctx, r) 826 if err != nil { 827 h.api.Err(w, r, err) 828 return 829 } 830 831 view, err := h.dashboardService.UpdateDashboardCellView(ctx, req.dashboardID, req.cellID, req.upd) 832 if err != nil { 833 h.api.Err(w, r, err) 834 return 835 } 836 h.log.Debug("Dashboard cell view updated", zap.String("dashboardID", req.dashboardID.String()), zap.String("cellID", req.cellID.String()), zap.String("view", fmt.Sprint(view))) 837 838 h.api.Respond(w, r, http.StatusOK, newDashboardCellViewResponse(req.dashboardID, req.cellID, view)) 839 } 840 841 // handleDeleteDashboardCell deletes a dashboard cell. 842 func (h *DashboardHandler) handleDeleteDashboardCell(w http.ResponseWriter, r *http.Request) { 843 ctx := r.Context() 844 req, err := decodeDeleteDashboardCellRequest(ctx, r) 845 if err != nil { 846 h.api.Err(w, r, err) 847 return 848 } 849 if err := h.dashboardService.RemoveDashboardCell(ctx, req.dashboardID, req.cellID); err != nil { 850 h.api.Err(w, r, err) 851 return 852 } 853 h.log.Debug("Dashboard cell deleted", zap.String("dashboardID", req.dashboardID.String()), zap.String("cellID", req.cellID.String())) 854 855 w.WriteHeader(http.StatusNoContent) 856 } 857 858 type patchDashboardCellRequest struct { 859 dashboardID platform.ID 860 cellID platform.ID 861 upd influxdb.CellUpdate 862 } 863 864 func decodePatchDashboardCellRequest(ctx context.Context, r *http.Request) (*patchDashboardCellRequest, error) { 865 req := &patchDashboardCellRequest{} 866 867 id := chi.URLParam(r, "id") 868 if id == "" { 869 return nil, &errors.Error{ 870 Code: errors.EInvalid, 871 Msg: "url missing id", 872 } 873 } 874 if err := req.dashboardID.DecodeFromString(id); err != nil { 875 return nil, err 876 } 877 878 cellID := chi.URLParam(r, "cellID") 879 if cellID == "" { 880 return nil, &errors.Error{ 881 Code: errors.EInvalid, 882 Msg: "cannot provide empty cell id", 883 } 884 } 885 if err := req.cellID.DecodeFromString(cellID); err != nil { 886 return nil, err 887 } 888 889 if err := json.NewDecoder(r.Body).Decode(&req.upd); err != nil { 890 return nil, &errors.Error{ 891 Code: errors.EInvalid, 892 Err: err, 893 } 894 } 895 896 if pe := req.upd.Valid(); pe != nil { 897 return nil, pe 898 } 899 900 return req, nil 901 } 902 903 // handlePatchDashboardCell updates a dashboard cell. 904 func (h *DashboardHandler) handlePatchDashboardCell(w http.ResponseWriter, r *http.Request) { 905 ctx := r.Context() 906 req, err := decodePatchDashboardCellRequest(ctx, r) 907 if err != nil { 908 h.api.Err(w, r, err) 909 return 910 } 911 cell, err := h.dashboardService.UpdateDashboardCell(ctx, req.dashboardID, req.cellID, req.upd) 912 if err != nil { 913 h.api.Err(w, r, err) 914 return 915 } 916 917 h.log.Debug("Dashboard cell updated", zap.String("dashboardID", req.dashboardID.String()), zap.String("cell", fmt.Sprint(cell))) 918 919 h.api.Respond(w, r, http.StatusOK, newDashboardCellResponse(req.dashboardID, cell)) 920 } 921 922 func (h *DashboardHandler) lookupOrgByDashboardID(ctx context.Context, id platform.ID) (platform.ID, error) { 923 d, err := h.dashboardService.FindDashboardByID(ctx, id) 924 if err != nil { 925 return 0, err 926 } 927 return d.OrganizationID, nil 928 } 929 930 // DashboardService is a dashboard service over HTTP to the influxdb server. 931 type DashboardService struct { 932 Client *httpc.Client 933 } 934 935 // FindDashboardByID returns a single dashboard by ID. 936 func (s *DashboardService) FindDashboardByID(ctx context.Context, id platform.ID) (*influxdb.Dashboard, error) { 937 var dr dashboardResponse 938 err := s.Client. 939 Get(prefixDashboards, id.String()). 940 QueryParams([2]string{"include", "properties"}). 941 DecodeJSON(&dr). 942 Do(ctx) 943 if err != nil { 944 return nil, err 945 } 946 return dr.toinfluxdb(), nil 947 } 948 949 // FindDashboards returns a list of dashboards that match filter and the total count of matching dashboards. 950 // Additional options provide pagination & sorting. 951 func (s *DashboardService) FindDashboards(ctx context.Context, filter influxdb.DashboardFilter, opts influxdb.FindOptions) ([]*influxdb.Dashboard, int, error) { 952 queryPairs := influxdb.FindOptionParams(opts) 953 for _, id := range filter.IDs { 954 queryPairs = append(queryPairs, [2]string{"id", id.String()}) 955 } 956 if filter.OrganizationID != nil { 957 queryPairs = append(queryPairs, [2]string{"orgID", filter.OrganizationID.String()}) 958 } 959 if filter.Organization != nil { 960 queryPairs = append(queryPairs, [2]string{"org", *filter.Organization}) 961 } 962 963 var dr getDashboardsResponse 964 err := s.Client. 965 Get(prefixDashboards). 966 QueryParams(queryPairs...). 967 DecodeJSON(&dr). 968 Do(ctx) 969 if err != nil { 970 return nil, 0, err 971 } 972 973 dashboards := dr.toinfluxdb() 974 return dashboards, len(dashboards), nil 975 } 976 977 // CreateDashboard creates a new dashboard and sets b.ID with the new identifier. 978 func (s *DashboardService) CreateDashboard(ctx context.Context, d *influxdb.Dashboard) error { 979 return s.Client. 980 PostJSON(d, prefixDashboards). 981 DecodeJSON(d). 982 Do(ctx) 983 } 984 985 // UpdateDashboard updates a single dashboard with changeset. 986 // Returns the new dashboard state after update. 987 func (s *DashboardService) UpdateDashboard(ctx context.Context, id platform.ID, upd influxdb.DashboardUpdate) (*influxdb.Dashboard, error) { 988 var d influxdb.Dashboard 989 err := s.Client. 990 PatchJSON(upd, prefixDashboards, id.String()). 991 DecodeJSON(&d). 992 Do(ctx) 993 if err != nil { 994 return nil, err 995 } 996 997 if len(d.Cells) == 0 { 998 // TODO(@jsteenb2): decipher why this is doing this? 999 d.Cells = nil 1000 } 1001 1002 return &d, nil 1003 } 1004 1005 // DeleteDashboard removes a dashboard by ID. 1006 func (s *DashboardService) DeleteDashboard(ctx context.Context, id platform.ID) error { 1007 return s.Client. 1008 Delete(dashboardIDPath(id)). 1009 Do(ctx) 1010 } 1011 1012 // AddDashboardCell adds a cell to a dashboard. 1013 func (s *DashboardService) AddDashboardCell(ctx context.Context, id platform.ID, c *influxdb.Cell, opts influxdb.AddDashboardCellOptions) error { 1014 return s.Client. 1015 PostJSON(c, cellPath(id)). 1016 DecodeJSON(c). 1017 Do(ctx) 1018 } 1019 1020 // RemoveDashboardCell removes a dashboard. 1021 func (s *DashboardService) RemoveDashboardCell(ctx context.Context, dashboardID, cellID platform.ID) error { 1022 return s.Client. 1023 Delete(dashboardCellIDPath(dashboardID, cellID)). 1024 Do(ctx) 1025 } 1026 1027 // UpdateDashboardCell replaces the dashboard cell with the provided ID. 1028 func (s *DashboardService) UpdateDashboardCell(ctx context.Context, dashboardID, cellID platform.ID, upd influxdb.CellUpdate) (*influxdb.Cell, error) { 1029 if err := upd.Valid(); err != nil { 1030 return nil, &errors.Error{ 1031 Err: err, 1032 } 1033 } 1034 1035 var c influxdb.Cell 1036 err := s.Client. 1037 PatchJSON(upd, dashboardCellIDPath(dashboardID, cellID)). 1038 DecodeJSON(&c). 1039 Do(ctx) 1040 if err != nil { 1041 return nil, err 1042 } 1043 1044 return &c, nil 1045 } 1046 1047 // GetDashboardCellView retrieves the view for a dashboard cell. 1048 func (s *DashboardService) GetDashboardCellView(ctx context.Context, dashboardID, cellID platform.ID) (*influxdb.View, error) { 1049 var dcv dashboardCellViewResponse 1050 err := s.Client. 1051 Get(cellViewPath(dashboardID, cellID)). 1052 DecodeJSON(&dcv). 1053 Do(ctx) 1054 if err != nil { 1055 return nil, err 1056 } 1057 1058 return &dcv.View, nil 1059 } 1060 1061 // UpdateDashboardCellView updates the view for a dashboard cell. 1062 func (s *DashboardService) UpdateDashboardCellView(ctx context.Context, dashboardID, cellID platform.ID, upd influxdb.ViewUpdate) (*influxdb.View, error) { 1063 var dcv dashboardCellViewResponse 1064 err := s.Client. 1065 PatchJSON(upd, cellViewPath(dashboardID, cellID)). 1066 DecodeJSON(&dcv). 1067 Do(ctx) 1068 if err != nil { 1069 return nil, err 1070 } 1071 return &dcv.View, nil 1072 } 1073 1074 // ReplaceDashboardCells replaces all cells in a dashboard 1075 func (s *DashboardService) ReplaceDashboardCells(ctx context.Context, id platform.ID, cs []*influxdb.Cell) error { 1076 return s.Client. 1077 PutJSON(cs, cellPath(id)). 1078 // TODO: previous implementation did not do anything with the response except validate it is valid json. 1079 // seems likely we should have to overwrite (:sadpanda:) the incoming cs... 1080 DecodeJSON(&dashboardCellsResponse{}). 1081 Do(ctx) 1082 } 1083 1084 func dashboardIDPath(id platform.ID) string { 1085 return path.Join(prefixDashboards, id.String()) 1086 } 1087 1088 func cellPath(id platform.ID) string { 1089 return path.Join(dashboardIDPath(id), "cells") 1090 } 1091 1092 func cellViewPath(dashboardID, cellID platform.ID) string { 1093 return path.Join(dashboardCellIDPath(dashboardID, cellID), "view") 1094 } 1095 1096 func dashboardCellIDPath(id platform.ID, cellID platform.ID) string { 1097 return path.Join(cellPath(id), cellID.String()) 1098 }