PublicDashboards: support time range selection on the backend (#60203)

This commit is contained in:
Ezequiel Victorero 2022-12-15 10:44:33 -03:00 committed by GitHub
parent 6928ad2949
commit 8d5b19bc61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 176 additions and 22 deletions

View File

@ -19,4 +19,5 @@ var (
ErrPublicDashboardHasTemplateVariables = errutil.NewBase(errutil.StatusBadRequest, "publicdashboards.hasTemplateVariables", errutil.WithPublicMessage("Public dashboard has template variables")) ErrPublicDashboardHasTemplateVariables = errutil.NewBase(errutil.StatusBadRequest, "publicdashboards.hasTemplateVariables", errutil.WithPublicMessage("Public dashboard has template variables"))
ErrInvalidInterval = errutil.NewBase(errutil.StatusBadRequest, "publicdashboards.invalidInterval", errutil.WithPublicMessage("intervalMS should be greater than 0")) ErrInvalidInterval = errutil.NewBase(errutil.StatusBadRequest, "publicdashboards.invalidInterval", errutil.WithPublicMessage("intervalMS should be greater than 0"))
ErrInvalidMaxDataPoints = errutil.NewBase(errutil.StatusBadRequest, "publicdashboards.maxDataPoints", errutil.WithPublicMessage("maxDataPoints should be greater than 0")) ErrInvalidMaxDataPoints = errutil.NewBase(errutil.StatusBadRequest, "publicdashboards.maxDataPoints", errutil.WithPublicMessage("maxDataPoints should be greater than 0"))
ErrInvalidTimeRange = errutil.NewBase(errutil.StatusBadRequest, "publicdashboards.invalidTimeRange", errutil.WithPublicMessage("Invalid time range"))
) )

View File

@ -94,24 +94,23 @@ func (ts *TimeSettings) ToDB() ([]byte, error) {
return json.Marshal(ts) return json.Marshal(ts)
} }
// build time settings object from json on public dashboard. If empty, use // BuildTimeSettings build time settings object using selected values if enabled and are valid or dashboard default values
// defaults on the dashboard func (pd PublicDashboard) BuildTimeSettings(dashboard *models.Dashboard, reqDTO PublicDashboardQueryDTO) TimeSettings {
func (pd PublicDashboard) BuildTimeSettings(dashboard *models.Dashboard) TimeSettings {
from := dashboard.Data.GetPath("time", "from").MustString() from := dashboard.Data.GetPath("time", "from").MustString()
to := dashboard.Data.GetPath("time", "to").MustString() to := dashboard.Data.GetPath("time", "to").MustString()
if pd.TimeSelectionEnabled {
from = reqDTO.TimeRange.From
to = reqDTO.TimeRange.To
}
timeRange := legacydata.NewDataTimeRange(from, to) timeRange := legacydata.NewDataTimeRange(from, to)
// Were using epoch ms because this is used to build a MetricRequest, which is used by query caching, which expected the time range in epoch milliseconds. // Were using epoch ms because this is used to build a MetricRequest, which is used by query caching, which expected the time range in epoch milliseconds.
ts := TimeSettings{ return TimeSettings{
From: strconv.FormatInt(timeRange.GetFromAsMsEpoch(), 10), From: strconv.FormatInt(timeRange.GetFromAsMsEpoch(), 10),
To: strconv.FormatInt(timeRange.GetToAsMsEpoch(), 10), To: strconv.FormatInt(timeRange.GetToAsMsEpoch(), 10),
} }
if pd.TimeSettings == nil {
return ts
}
return ts
} }
// DTO for transforming user input in the api // DTO for transforming user input in the api
@ -125,6 +124,7 @@ type SavePublicDashboardDTO struct {
type PublicDashboardQueryDTO struct { type PublicDashboardQueryDTO struct {
IntervalMs int64 IntervalMs int64
MaxDataPoints int64 MaxDataPoints int64
TimeRange TimeSettings
} }
type AnnotationsQueryDTO struct { type AnnotationsQueryDTO struct {

View File

@ -1,7 +1,9 @@
package models package models
import ( import (
"strconv"
"testing" "testing"
"time"
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
@ -15,36 +17,58 @@ func TestPublicDashboardTableName(t *testing.T) {
func TestBuildTimeSettings(t *testing.T) { func TestBuildTimeSettings(t *testing.T) {
var dashboardData = simplejson.NewFromAny(map[string]interface{}{"time": map[string]interface{}{"from": "2022-09-01T00:00:00.000Z", "to": "2022-09-01T12:00:00.000Z"}}) var dashboardData = simplejson.NewFromAny(map[string]interface{}{"time": map[string]interface{}{"from": "2022-09-01T00:00:00.000Z", "to": "2022-09-01T12:00:00.000Z"}})
fromMs, toMs := internal.GetTimeRangeFromDashboard(t, dashboardData) defaultFromMs, defaultToMs := internal.GetTimeRangeFromDashboard(t, dashboardData)
selectionFromMs := strconv.FormatInt(time.Now().UnixMilli(), 10)
selectionToMs := strconv.FormatInt(time.Now().Add(time.Hour).UnixMilli(), 10)
testCases := []struct { testCases := []struct {
name string name string
dashboard *models.Dashboard dashboard *models.Dashboard
pubdash *PublicDashboard pubdash *PublicDashboard
timeResult TimeSettings timeResult TimeSettings
reqDTO PublicDashboardQueryDTO
}{ }{
{ {
name: "should use dashboard time if pubdash time empty", name: "should use dashboard time if pubdash time empty",
dashboard: &models.Dashboard{Data: dashboardData}, dashboard: &models.Dashboard{Data: dashboardData},
pubdash: &PublicDashboard{}, pubdash: &PublicDashboard{TimeSelectionEnabled: false},
timeResult: TimeSettings{ timeResult: TimeSettings{
From: fromMs, From: defaultFromMs,
To: toMs, To: defaultToMs,
}, },
reqDTO: PublicDashboardQueryDTO{},
}, },
{ {
name: "should use dashboard time even if pubdash time exists", name: "should use dashboard time when time selection is disabled",
dashboard: &models.Dashboard{Data: dashboardData}, dashboard: &models.Dashboard{Data: dashboardData},
pubdash: &PublicDashboard{TimeSettings: &TimeSettings{From: "now-12", To: "now"}}, pubdash: &PublicDashboard{TimeSelectionEnabled: false, TimeSettings: &TimeSettings{From: "now-12", To: "now"}},
timeResult: TimeSettings{ timeResult: TimeSettings{
From: fromMs, From: defaultFromMs,
To: toMs, To: defaultToMs,
},
reqDTO: PublicDashboardQueryDTO{},
},
{
name: "should use selected values if time selection is enabled",
dashboard: &models.Dashboard{Data: dashboardData},
pubdash: &PublicDashboard{TimeSelectionEnabled: true, TimeSettings: &TimeSettings{From: "now-12", To: "now"}},
reqDTO: PublicDashboardQueryDTO{
TimeRange: TimeSettings{
From: selectionFromMs,
To: selectionToMs,
},
},
timeResult: TimeSettings{
From: selectionFromMs,
To: selectionToMs,
}, },
}, },
} }
for _, test := range testCases { for _, test := range testCases {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.timeResult, test.pubdash.BuildTimeSettings(test.dashboard)) assert.Equal(t, test.timeResult, test.pubdash.BuildTimeSettings(test.dashboard, test.reqDTO))
}) })
} }
} }

View File

@ -99,7 +99,7 @@ func (pd *PublicDashboardServiceImpl) FindAnnotations(ctx context.Context, reqDT
// GetMetricRequest returns a metric request for the given panel and query // GetMetricRequest returns a metric request for the given panel and query
func (pd *PublicDashboardServiceImpl) GetMetricRequest(ctx context.Context, dashboard *dashmodels.Dashboard, publicDashboard *models.PublicDashboard, panelId int64, queryDto models.PublicDashboardQueryDTO) (dtos.MetricRequest, error) { func (pd *PublicDashboardServiceImpl) GetMetricRequest(ctx context.Context, dashboard *dashmodels.Dashboard, publicDashboard *models.PublicDashboard, panelId int64, queryDto models.PublicDashboardQueryDTO) (dtos.MetricRequest, error) {
err := validation.ValidateQueryPublicDashboardRequest(queryDto) err := validation.ValidateQueryPublicDashboardRequest(queryDto, publicDashboard)
if err != nil { if err != nil {
return dtos.MetricRequest{}, err return dtos.MetricRequest{}, err
} }
@ -158,7 +158,7 @@ func (pd *PublicDashboardServiceImpl) buildMetricRequest(ctx context.Context, da
return dtos.MetricRequest{}, models.ErrPanelNotFound.Errorf("buildMetricRequest: public dashboard panel not found") return dtos.MetricRequest{}, models.ErrPanelNotFound.Errorf("buildMetricRequest: public dashboard panel not found")
} }
ts := publicDashboard.BuildTimeSettings(dashboard) ts := publicDashboard.BuildTimeSettings(dashboard, reqDTO)
// determine safe resolution to query data at // determine safe resolution to query data at
safeInterval, safeResolution := pd.getSafeIntervalAndMaxDataPoints(reqDTO, ts) safeInterval, safeResolution := pd.getSafeIntervalAndMaxDataPoints(reqDTO, ts)

View File

@ -3,6 +3,7 @@ package validation
import ( import (
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
. "github.com/grafana/grafana/pkg/services/publicdashboards/models" . "github.com/grafana/grafana/pkg/services/publicdashboards/models"
"github.com/grafana/grafana/pkg/tsdb/legacydata"
) )
func ValidatePublicDashboard(dto *SavePublicDashboardDTO, dashboard *models.Dashboard) error { func ValidatePublicDashboard(dto *SavePublicDashboardDTO, dashboard *models.Dashboard) error {
@ -19,7 +20,7 @@ func hasTemplateVariables(dashboard *models.Dashboard) bool {
return len(templateVariables) > 0 return len(templateVariables) > 0
} }
func ValidateQueryPublicDashboardRequest(req PublicDashboardQueryDTO) error { func ValidateQueryPublicDashboardRequest(req PublicDashboardQueryDTO, pd *PublicDashboard) error {
if req.IntervalMs < 0 { if req.IntervalMs < 0 {
return ErrInvalidInterval.Errorf("ValidateQueryPublicDashboardRequest: intervalMS should be greater than 0") return ErrInvalidInterval.Errorf("ValidateQueryPublicDashboardRequest: intervalMS should be greater than 0")
} }
@ -28,5 +29,18 @@ func ValidateQueryPublicDashboardRequest(req PublicDashboardQueryDTO) error {
return ErrInvalidMaxDataPoints.Errorf("ValidateQueryPublicDashboardRequest: maxDataPoints should be greater than 0") return ErrInvalidMaxDataPoints.Errorf("ValidateQueryPublicDashboardRequest: maxDataPoints should be greater than 0")
} }
if pd.TimeSelectionEnabled {
timeRange := legacydata.NewDataTimeRange(req.TimeRange.From, req.TimeRange.To)
_, err := timeRange.ParseFrom()
if err != nil {
return ErrInvalidTimeRange.Errorf("ValidateQueryPublicDashboardRequest: time range from is invalid")
}
_, err = timeRange.ParseTo()
if err != nil {
return ErrInvalidTimeRange.Errorf("ValidateQueryPublicDashboardRequest: time range to is invalid")
}
}
return nil return nil
} }

View File

@ -42,3 +42,118 @@ func TestValidatePublicDashboard(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
}) })
} }
func TestValidateQueryPublicDashboardRequest(t *testing.T) {
type args struct {
req PublicDashboardQueryDTO
pd *PublicDashboard
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "Returns no error when input is valid",
args: args{
req: PublicDashboardQueryDTO{
IntervalMs: 1000,
MaxDataPoints: 1000,
TimeRange: TimeSettings{
From: "now-1h",
To: "now",
},
},
pd: &PublicDashboard{
TimeSelectionEnabled: true,
},
},
wantErr: false,
},
{
name: "Returns no error when input is valid and time selection is disabled",
args: args{
req: PublicDashboardQueryDTO{
IntervalMs: 1000,
MaxDataPoints: 1000,
},
pd: &PublicDashboard{
TimeSelectionEnabled: false,
},
},
wantErr: false,
},
{
name: "Returns validation error when intervalMs is less than 0",
args: args{
req: PublicDashboardQueryDTO{
IntervalMs: -1,
},
pd: &PublicDashboard{},
},
wantErr: true,
},
{
name: "Returns validation error when maxDataPoints is less than 0",
args: args{
req: PublicDashboardQueryDTO{
MaxDataPoints: -1,
},
pd: &PublicDashboard{},
},
wantErr: true,
},
{
name: "Returns validation error when time range from is invalid",
args: args{
req: PublicDashboardQueryDTO{
TimeRange: TimeSettings{
From: "invalid",
To: "1622560000000",
},
},
pd: &PublicDashboard{
TimeSelectionEnabled: true,
},
},
wantErr: true,
},
{
name: "Returns validation error when time range to is invalid",
args: args{
req: PublicDashboardQueryDTO{
TimeRange: TimeSettings{
From: "1622560000000",
To: "invalid",
},
},
pd: &PublicDashboard{
TimeSelectionEnabled: true,
},
},
wantErr: true,
},
{
name: "Returns validation error when time range from or to is blank",
args: args{
req: PublicDashboardQueryDTO{
TimeRange: TimeSettings{
From: "",
To: "",
},
},
pd: &PublicDashboard{
TimeSelectionEnabled: true,
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := ValidateQueryPublicDashboardRequest(tt.args.req, tt.args.pd); (err != nil) != tt.wantErr {
t.Errorf("ValidateQueryPublicDashboardRequest() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}