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  }