This PR moves public dashboards into its own self contained service including API, Service, Database, and Models. Routes are mounted on the Grafana HTTPServer by the API service at injection time with wire.go. The main route that loads the frontend for public dashboards is still handled by the API package. Co-authored-by: Jesse Weaver <jesse.weaver@grafana.com> Co-authored-by: Owen Smallwood <owen.smallwood@grafana.com>
235 lines
8.4 KiB
Go
235 lines
8.4 KiB
Go
package api
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
|
"github.com/grafana/grafana/pkg/api/dtos"
|
|
"github.com/grafana/grafana/pkg/api/response"
|
|
"github.com/grafana/grafana/pkg/api/routing"
|
|
"github.com/grafana/grafana/pkg/middleware"
|
|
"github.com/grafana/grafana/pkg/models"
|
|
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
|
"github.com/grafana/grafana/pkg/services/datasources"
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
"github.com/grafana/grafana/pkg/services/publicdashboards"
|
|
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
|
|
"github.com/grafana/grafana/pkg/services/query"
|
|
"github.com/grafana/grafana/pkg/util"
|
|
"github.com/grafana/grafana/pkg/web"
|
|
)
|
|
|
|
type Api struct {
|
|
PublicDashboardService publicdashboards.Service
|
|
RouteRegister routing.RouteRegister
|
|
AccessControl accesscontrol.AccessControl
|
|
QueryDataService *query.Service
|
|
Features *featuremgmt.FeatureManager
|
|
}
|
|
|
|
func ProvideApi(
|
|
pd publicdashboards.Service,
|
|
rr routing.RouteRegister,
|
|
ac accesscontrol.AccessControl,
|
|
qds *query.Service,
|
|
features *featuremgmt.FeatureManager,
|
|
) *Api {
|
|
api := &Api{
|
|
PublicDashboardService: pd,
|
|
RouteRegister: rr,
|
|
AccessControl: ac,
|
|
QueryDataService: qds,
|
|
Features: features,
|
|
}
|
|
|
|
// attach api if PublicDashboards feature flag is enabled
|
|
if features.IsEnabled(featuremgmt.FlagPublicDashboards) {
|
|
api.RegisterAPIEndpoints()
|
|
}
|
|
|
|
return api
|
|
}
|
|
|
|
func (api *Api) RegisterAPIEndpoints() {
|
|
auth := accesscontrol.Middleware(api.AccessControl)
|
|
reqSignedIn := middleware.ReqSignedIn
|
|
|
|
// Anonymous access to public dashboard route is configured in pkg/api/api.go
|
|
// because it is deeply dependent on the HTTPServer.Index() method and would result in a
|
|
// circular dependency
|
|
|
|
api.RouteRegister.Get("/api/public/dashboards/:accessToken", routing.Wrap(api.GetPublicDashboard))
|
|
api.RouteRegister.Post("/api/public/dashboards/:accessToken/panels/:panelId/query", routing.Wrap(api.QueryPublicDashboard))
|
|
|
|
// Create/Update Public Dashboard
|
|
api.RouteRegister.Get("/api/dashboards/uid/:uid/public-config", auth(reqSignedIn, accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(api.GetPublicDashboardConfig))
|
|
api.RouteRegister.Post("/api/dashboards/uid/:uid/public-config", auth(reqSignedIn, accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(api.SavePublicDashboardConfig))
|
|
}
|
|
|
|
// gets public dashboard
|
|
func (api *Api) GetPublicDashboard(c *models.ReqContext) response.Response {
|
|
accessToken := web.Params(c.Req)[":accessToken"]
|
|
|
|
dash, err := api.PublicDashboardService.GetPublicDashboard(c.Req.Context(), accessToken)
|
|
if err != nil {
|
|
return handleDashboardErr(http.StatusInternalServerError, "Failed to get public dashboard", err)
|
|
}
|
|
|
|
meta := dtos.DashboardMeta{
|
|
Slug: dash.Slug,
|
|
Type: models.DashTypeDB,
|
|
CanStar: false,
|
|
CanSave: false,
|
|
CanEdit: false,
|
|
CanAdmin: false,
|
|
CanDelete: false,
|
|
Created: dash.Created,
|
|
Updated: dash.Updated,
|
|
Version: dash.Version,
|
|
IsFolder: false,
|
|
FolderId: dash.FolderId,
|
|
PublicDashboardAccessToken: accessToken,
|
|
}
|
|
|
|
dto := dtos.DashboardFullWithMeta{Meta: meta, Dashboard: dash.Data}
|
|
|
|
return response.JSON(http.StatusOK, dto)
|
|
}
|
|
|
|
// gets public dashboard configuration for dashboard
|
|
func (api *Api) GetPublicDashboardConfig(c *models.ReqContext) response.Response {
|
|
pdc, err := api.PublicDashboardService.GetPublicDashboardConfig(c.Req.Context(), c.OrgId, web.Params(c.Req)[":uid"])
|
|
if err != nil {
|
|
return handleDashboardErr(http.StatusInternalServerError, "Failed to get public dashboard config", err)
|
|
}
|
|
return response.JSON(http.StatusOK, pdc)
|
|
}
|
|
|
|
// sets public dashboard configuration for dashboard
|
|
func (api *Api) SavePublicDashboardConfig(c *models.ReqContext) response.Response {
|
|
pubdash := &PublicDashboard{}
|
|
if err := web.Bind(c.Req, pubdash); err != nil {
|
|
return response.Error(http.StatusBadRequest, "bad request data", err)
|
|
}
|
|
|
|
// Always set the org id to the current auth session orgId
|
|
pubdash.OrgId = c.OrgId
|
|
|
|
dto := SavePublicDashboardConfigDTO{
|
|
OrgId: c.OrgId,
|
|
DashboardUid: web.Params(c.Req)[":uid"],
|
|
UserId: c.UserId,
|
|
PublicDashboard: pubdash,
|
|
}
|
|
|
|
pubdash, err := api.PublicDashboardService.SavePublicDashboardConfig(c.Req.Context(), &dto)
|
|
if err != nil {
|
|
return handleDashboardErr(http.StatusInternalServerError, "Failed to save public dashboard configuration", err)
|
|
}
|
|
|
|
return response.JSON(http.StatusOK, pubdash)
|
|
}
|
|
|
|
// QueryPublicDashboard returns all results for a given panel on a public dashboard
|
|
// POST /api/public/dashboard/:accessToken/panels/:panelId/query
|
|
func (api *Api) QueryPublicDashboard(c *models.ReqContext) response.Response {
|
|
panelId, err := strconv.ParseInt(web.Params(c.Req)[":panelId"], 10, 64)
|
|
if err != nil {
|
|
return response.Error(http.StatusBadRequest, "invalid panel ID", err)
|
|
}
|
|
|
|
dashboard, err := api.PublicDashboardService.GetPublicDashboard(c.Req.Context(), web.Params(c.Req)[":accessToken"])
|
|
if err != nil {
|
|
return response.Error(http.StatusInternalServerError, "could not fetch dashboard", err)
|
|
}
|
|
|
|
publicDashboard, err := api.PublicDashboardService.GetPublicDashboardConfig(c.Req.Context(), dashboard.OrgId, dashboard.Uid)
|
|
if err != nil {
|
|
return response.Error(http.StatusInternalServerError, "could not fetch public dashboard", err)
|
|
}
|
|
|
|
reqDTO, err := api.PublicDashboardService.BuildPublicDashboardMetricRequest(
|
|
c.Req.Context(),
|
|
dashboard,
|
|
publicDashboard,
|
|
panelId,
|
|
)
|
|
if err != nil {
|
|
return handleDashboardErr(http.StatusInternalServerError, "Failed to get queries for public dashboard", err)
|
|
}
|
|
|
|
anonymousUser, err := api.PublicDashboardService.BuildAnonymousUser(c.Req.Context(), dashboard)
|
|
|
|
if err != nil {
|
|
return response.Error(http.StatusInternalServerError, "could not create anonymous user", err)
|
|
}
|
|
|
|
resp, err := api.QueryDataService.QueryDataMultipleSources(c.Req.Context(), anonymousUser, c.SkipCache, reqDTO, true)
|
|
|
|
if err != nil {
|
|
return handleQueryMetricsError(err)
|
|
}
|
|
return toJsonStreamingResponse(api.Features, resp)
|
|
}
|
|
|
|
// util to help us unpack dashboard and publicdashboard errors or use default http code and message
|
|
// we should look to do some future refactoring of these errors as publicdashboard err is the same as a dashboarderr, just defined in a
|
|
// different package.
|
|
func handleDashboardErr(defaultCode int, defaultMsg string, err error) response.Response {
|
|
var publicDashboardErr PublicDashboardErr
|
|
|
|
// handle public dashboard er
|
|
if ok := errors.As(err, &publicDashboardErr); ok {
|
|
return response.Error(publicDashboardErr.StatusCode, publicDashboardErr.Error(), publicDashboardErr)
|
|
}
|
|
|
|
// handle dashboard errors as well
|
|
var dashboardErr dashboards.DashboardErr
|
|
if ok := errors.As(err, &dashboardErr); ok {
|
|
return response.Error(dashboardErr.StatusCode, dashboardErr.Error(), dashboardErr)
|
|
}
|
|
|
|
return response.Error(defaultCode, defaultMsg, err)
|
|
}
|
|
|
|
// Copied from pkg/api/metrics.go
|
|
func handleQueryMetricsError(err error) *response.NormalResponse {
|
|
if errors.Is(err, datasources.ErrDataSourceAccessDenied) {
|
|
return response.Error(http.StatusForbidden, "Access denied to data source", err)
|
|
}
|
|
if errors.Is(err, datasources.ErrDataSourceNotFound) {
|
|
return response.Error(http.StatusNotFound, "Data source not found", err)
|
|
}
|
|
var badQuery *query.ErrBadQuery
|
|
if errors.As(err, &badQuery) {
|
|
return response.Error(http.StatusBadRequest, util.Capitalize(badQuery.Message), err)
|
|
}
|
|
|
|
if errors.Is(err, backendplugin.ErrPluginNotRegistered) {
|
|
return response.Error(http.StatusNotFound, "Plugin not found", err)
|
|
}
|
|
|
|
return response.Error(http.StatusInternalServerError, "Query data error", err)
|
|
}
|
|
|
|
// Copied from pkg/api/metrics.go
|
|
func toJsonStreamingResponse(features *featuremgmt.FeatureManager, qdr *backend.QueryDataResponse) response.Response {
|
|
statusWhenError := http.StatusBadRequest
|
|
if features.IsEnabled(featuremgmt.FlagDatasourceQueryMultiStatus) {
|
|
statusWhenError = http.StatusMultiStatus
|
|
}
|
|
|
|
statusCode := http.StatusOK
|
|
for _, res := range qdr.Responses {
|
|
if res.Error != nil {
|
|
statusCode = statusWhenError
|
|
}
|
|
}
|
|
|
|
return response.JSONStreaming(statusCode, qdr)
|
|
}
|