11898abccb
* wip; public dashboards and snapshots work * Chore: Fix example of major release (#110007) baldm0mma/ fix example of major release * CI: Push docker images to dockerhub on merges to main (#110056) * support extracting queries in schema V2 * fix lint and test * fix test * clean up * clean up * apply feedback about early returns * fix url issue when clicking open original dashboard in v1 * refactor to early returns * fix api version comparison --------- Co-authored-by: Jev Forsberg <46619047+baldm0mma@users.noreply.github.com> Co-authored-by: Kevin Minehart <5140827+kminehart@users.noreply.github.com>
639 lines
20 KiB
Go
639 lines
20 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
|
"github.com/grafana/grafana-plugin-sdk-go/backend/gtime"
|
|
"github.com/grafana/grafana/pkg/api/dtos"
|
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
|
"github.com/grafana/grafana/pkg/expr"
|
|
"github.com/grafana/grafana/pkg/services/annotations"
|
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
|
"github.com/grafana/grafana/pkg/services/publicdashboards/models"
|
|
"github.com/grafana/grafana/pkg/services/publicdashboards/validation"
|
|
"github.com/grafana/grafana/pkg/tsdb/grafanads"
|
|
)
|
|
|
|
// FindAnnotations returns annotations for a public dashboard
|
|
func (pd *PublicDashboardServiceImpl) FindAnnotations(ctx context.Context, reqDTO models.AnnotationsQueryDTO, accessToken string) ([]models.AnnotationEvent, error) {
|
|
pub, dash, err := pd.FindEnabledPublicDashboardAndDashboardByAccessToken(ctx, accessToken)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !pub.AnnotationsEnabled {
|
|
return []models.AnnotationEvent{}, nil
|
|
}
|
|
|
|
annoDto, err := UnmarshalDashboardAnnotations(dash.Data)
|
|
if err != nil {
|
|
return nil, models.ErrInternalServerError.Errorf("FindAnnotations: failed to unmarshal dashboard annotations: %w", err)
|
|
}
|
|
|
|
// We don't have a signed in user for public dashboards. We are using Grafana's Identity to query the annotations.
|
|
svcCtx, svcIdent := identity.WithServiceIdentity(ctx, dash.OrgID)
|
|
uniqueEvents := make(map[int64]models.AnnotationEvent, 0)
|
|
for _, anno := range annoDto.Annotations.List {
|
|
// skip annotations that are not enabled or are not a grafana datasource
|
|
if !anno.Enable || (*anno.Datasource.Uid != grafanads.DatasourceUID && *anno.Datasource.Uid != grafanads.DatasourceName) {
|
|
continue
|
|
}
|
|
annoQuery := &annotations.ItemQuery{
|
|
From: reqDTO.From,
|
|
To: reqDTO.To,
|
|
OrgID: dash.OrgID,
|
|
DashboardID: dash.ID,
|
|
DashboardUID: dash.UID,
|
|
SignedInUser: svcIdent,
|
|
}
|
|
|
|
if anno.Target != nil {
|
|
annoQuery.Limit = anno.Target.Limit
|
|
annoQuery.MatchAny = anno.Target.MatchAny
|
|
if anno.Target.Type == "tags" {
|
|
annoQuery.DashboardID = 0 // nolint: staticcheck
|
|
annoQuery.DashboardUID = ""
|
|
annoQuery.Tags = anno.Target.Tags
|
|
}
|
|
}
|
|
|
|
annotationItems, err := pd.AnnotationsRepo.Find(svcCtx, annoQuery)
|
|
if err != nil {
|
|
return nil, models.ErrInternalServerError.Errorf("FindAnnotations: failed to find annotations: %w", err)
|
|
}
|
|
|
|
for _, item := range annotationItems {
|
|
event := models.AnnotationEvent{
|
|
Id: item.ID,
|
|
DashboardId: item.DashboardID, // nolint: staticcheck
|
|
Tags: item.Tags,
|
|
IsRegion: item.TimeEnd > 0 && item.Time != item.TimeEnd,
|
|
Text: item.Text,
|
|
Color: anno.IconColor,
|
|
Time: item.Time,
|
|
TimeEnd: item.TimeEnd,
|
|
Source: anno,
|
|
}
|
|
|
|
if item.DashboardUID != nil {
|
|
event.DashboardUID = *item.DashboardUID
|
|
}
|
|
|
|
// We want dashboard annotations to reference the panel they're for. If no panelId is provided, they'll show up on all panels
|
|
// which is only intended for tag and org annotations.
|
|
if anno.Type != nil && *anno.Type == "dashboard" {
|
|
event.PanelId = item.PanelID
|
|
}
|
|
|
|
// We want events from tag queries to overwrite existing events
|
|
_, has := uniqueEvents[event.Id]
|
|
if !has || (has && anno.Target != nil && anno.Target.Type == "tags") {
|
|
uniqueEvents[event.Id] = event
|
|
}
|
|
}
|
|
}
|
|
|
|
results := make([]models.AnnotationEvent, 0, len(uniqueEvents))
|
|
for _, result := range uniqueEvents {
|
|
results = append(results, result)
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
// GetMetricRequest returns a metric request for the given panel and query
|
|
func (pd *PublicDashboardServiceImpl) GetMetricRequest(ctx context.Context, dashboard *dashboards.Dashboard, publicDashboard *models.PublicDashboard, panelId int64, queryDto models.PublicDashboardQueryDTO) (dtos.MetricRequest, error) {
|
|
err := validation.ValidateQueryPublicDashboardRequest(queryDto, publicDashboard)
|
|
if err != nil {
|
|
return dtos.MetricRequest{}, err
|
|
}
|
|
|
|
metricReqDTO, err := pd.buildMetricRequest(
|
|
dashboard,
|
|
publicDashboard,
|
|
panelId,
|
|
queryDto,
|
|
)
|
|
if err != nil {
|
|
return dtos.MetricRequest{}, err
|
|
}
|
|
|
|
return metricReqDTO, nil
|
|
}
|
|
|
|
// GetQueryDataResponse returns a query data response for the given panel and query
|
|
func (pd *PublicDashboardServiceImpl) GetQueryDataResponse(ctx context.Context, skipDSCache bool, queryDto models.PublicDashboardQueryDTO, panelId int64, accessToken string) (*backend.QueryDataResponse, error) {
|
|
publicDashboard, dashboard, err := pd.FindEnabledPublicDashboardAndDashboardByAccessToken(ctx, accessToken)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
metricReq, err := pd.GetMetricRequest(ctx, dashboard, publicDashboard, panelId, queryDto)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(metricReq.Queries) == 0 {
|
|
return nil, models.ErrPanelQueriesNotFound.Errorf("GetQueryDataResponse: failed to extract queries from panel")
|
|
}
|
|
|
|
// We don't have a signed in user for public dashboards. We are using Grafana's Identity to query the datasource.
|
|
svcCtx, svcIdent := identity.WithServiceIdentity(ctx, dashboard.OrgID)
|
|
res, err := pd.QueryDataService.QueryData(svcCtx, svcIdent, skipDSCache, metricReq)
|
|
|
|
reqDatasources := metricReq.GetUniqueDatasourceTypes()
|
|
if err != nil {
|
|
LogQueryFailure(reqDatasources, pd.log, err)
|
|
return nil, err
|
|
}
|
|
LogQuerySuccess(reqDatasources, pd.log)
|
|
|
|
sanitizeMetadataFromQueryData(res)
|
|
|
|
return res, nil
|
|
}
|
|
|
|
// buildMetricRequest merges public dashboard parameters with dashboard and returns a metrics request to be sent to query backend
|
|
func (pd *PublicDashboardServiceImpl) buildMetricRequest(dashboard *dashboards.Dashboard, publicDashboard *models.PublicDashboard, panelID int64, reqDTO models.PublicDashboardQueryDTO) (dtos.MetricRequest, error) {
|
|
isV2 := dashboard.Data.Get("elements").Interface() != nil
|
|
|
|
if isV2 {
|
|
return pd.buildMetricRequestV2(dashboard, publicDashboard, panelID, reqDTO)
|
|
}
|
|
|
|
// group queries by panel
|
|
queriesByPanel := groupQueriesByPanelId(dashboard.Data)
|
|
queries, ok := queriesByPanel[panelID]
|
|
if !ok {
|
|
return dtos.MetricRequest{}, models.ErrPanelNotFound.Errorf("buildMetricRequest: public dashboard panel not found")
|
|
}
|
|
|
|
ts := buildTimeSettings(dashboard, reqDTO, publicDashboard, panelID)
|
|
|
|
// determine safe resolution to query data at
|
|
safeInterval, safeResolution := pd.getSafeIntervalAndMaxDataPoints(reqDTO, ts)
|
|
for i := range queries {
|
|
queries[i].Set("intervalMs", safeInterval)
|
|
queries[i].Set("maxDataPoints", safeResolution)
|
|
queries[i].Set("queryCachingTTL", reqDTO.QueryCachingTTL)
|
|
}
|
|
|
|
return dtos.MetricRequest{
|
|
From: ts.From,
|
|
To: ts.To,
|
|
Queries: queries,
|
|
}, nil
|
|
}
|
|
|
|
func (pd *PublicDashboardServiceImpl) buildMetricRequestV2(dashboard *dashboards.Dashboard, publicDashboard *models.PublicDashboard, panelID int64, reqDTO models.PublicDashboardQueryDTO) (dtos.MetricRequest, error) {
|
|
// group queries by panel for V2
|
|
queriesByPanel := groupQueriesByPanelIdV2(dashboard.Data)
|
|
queries, ok := queriesByPanel[panelID]
|
|
if !ok {
|
|
return dtos.MetricRequest{}, models.ErrPanelNotFound.Errorf("buildMetricRequestV2: public dashboard panel not found")
|
|
}
|
|
|
|
ts := buildTimeSettingsV2(dashboard, reqDTO, publicDashboard, panelID)
|
|
|
|
// determine safe resolution to query data at
|
|
safeInterval, safeResolution := pd.getSafeIntervalAndMaxDataPoints(reqDTO, ts)
|
|
for i := range queries {
|
|
queries[i].Set("intervalMs", safeInterval)
|
|
queries[i].Set("maxDataPoints", safeResolution)
|
|
queries[i].Set("queryCachingTTL", reqDTO.QueryCachingTTL)
|
|
}
|
|
|
|
return dtos.MetricRequest{
|
|
From: ts.From,
|
|
To: ts.To,
|
|
Queries: queries,
|
|
}, nil
|
|
}
|
|
|
|
func groupQueriesByPanelId(dashboard *simplejson.Json) map[int64][]*simplejson.Json {
|
|
result := make(map[int64][]*simplejson.Json)
|
|
|
|
extractQueriesFromPanels(dashboard.Get("panels").MustArray(), result)
|
|
|
|
return result
|
|
}
|
|
|
|
func groupQueriesByPanelIdV2(dashboard *simplejson.Json) map[int64][]*simplejson.Json {
|
|
result := make(map[int64][]*simplejson.Json)
|
|
|
|
elementsMap := dashboard.Get("elements").MustMap()
|
|
for _, element := range elementsMap {
|
|
element := simplejson.NewFromAny(element)
|
|
|
|
var panelQueries []*simplejson.Json
|
|
hasExpression := panelHasAnExpressionSchemaV2(element)
|
|
|
|
// For schema v2, queries are nested in element.spec.data.spec.queries
|
|
spec := element.Get("spec")
|
|
if spec.Interface() == nil {
|
|
result[element.Get("spec").Get("id").MustInt64()] = panelQueries
|
|
continue
|
|
}
|
|
|
|
data := spec.Get("data")
|
|
if data.Interface() == nil {
|
|
result[element.Get("spec").Get("id").MustInt64()] = panelQueries
|
|
continue
|
|
}
|
|
|
|
dataSpec := data.Get("spec")
|
|
if dataSpec.Interface() == nil {
|
|
result[element.Get("spec").Get("id").MustInt64()] = panelQueries
|
|
continue
|
|
}
|
|
|
|
queries := dataSpec.Get("queries")
|
|
if queries.Interface() == nil {
|
|
result[element.Get("spec").Get("id").MustInt64()] = panelQueries
|
|
continue
|
|
}
|
|
|
|
for _, queryObj := range queries.MustArray() {
|
|
query := simplejson.NewFromAny(queryObj)
|
|
|
|
// Check if query is hidden (PanelQuery.spec.hidden)
|
|
panelQuerySpec := query.Get("spec")
|
|
if panelQuerySpec.Interface() == nil {
|
|
continue
|
|
}
|
|
|
|
if !hasExpression && panelQuerySpec.Get("hidden").MustBool() {
|
|
continue
|
|
}
|
|
|
|
// Extract the actual query from PanelQuery.spec.query
|
|
dataQueryKind := panelQuerySpec.Get("query")
|
|
if dataQueryKind.Interface() == nil {
|
|
continue
|
|
}
|
|
|
|
dataQuerySpec := dataQueryKind.Get("spec")
|
|
if dataQuerySpec.Interface() == nil {
|
|
continue
|
|
}
|
|
|
|
dataQuerySpec.Del("exemplar")
|
|
|
|
group := dataQueryKind.Get("group").MustString()
|
|
|
|
// if query target has no datasource, set it to have the datasource on the panel
|
|
if _, ok := dataQuerySpec.CheckGet("datasource"); !ok {
|
|
uid := getDataSourceUidFromJsonSchemaV2(dataQueryKind)
|
|
datasource := map[string]any{"type": group, "uid": uid}
|
|
dataQuerySpec.Set("datasource", datasource)
|
|
}
|
|
|
|
// We don't support exemplars for public dashboards currently
|
|
dataQuerySpec.Del("exemplar")
|
|
|
|
// The query object contains the DataQuery with the actual expression
|
|
panelQueries = append(panelQueries, dataQuerySpec)
|
|
}
|
|
|
|
result[element.Get("spec").Get("id").MustInt64()] = panelQueries
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func extractQueriesFromPanels(panels []any, result map[int64][]*simplejson.Json) {
|
|
for _, panelObj := range panels {
|
|
panel := simplejson.NewFromAny(panelObj)
|
|
|
|
// if the panel is a row and it is collapsed, get the queries from the panels inside the row
|
|
if panel.Get("type").MustString() == "row" && panel.Get("collapsed").MustBool() {
|
|
// recursive call to get queries from panels inside a row
|
|
extractQueriesFromPanels(panel.Get("panels").MustArray(), result)
|
|
continue
|
|
}
|
|
|
|
var panelQueries []*simplejson.Json
|
|
hasExpression := panelHasAnExpression(panel)
|
|
|
|
for _, queryObj := range panel.Get("targets").MustArray() {
|
|
query := simplejson.NewFromAny(queryObj)
|
|
|
|
// it the panel doesn't have an expression and the query is disabled (hide is true), skip the query
|
|
// the expression handler will take care later of removing hidden queries which could be necessary to calculate
|
|
// the value of other queries
|
|
if !hasExpression && query.Get("hide").MustBool() {
|
|
continue
|
|
}
|
|
|
|
// We don't support exemplars for public dashboards currently
|
|
query.Del("exemplar")
|
|
|
|
// if query target has no datasource, set it to have the datasource on the panel
|
|
if _, ok := query.CheckGet("datasource"); !ok {
|
|
uid := getDataSourceUidFromJson(panel)
|
|
datasource := map[string]any{"type": "public-ds", "uid": uid}
|
|
query.Set("datasource", datasource)
|
|
}
|
|
panelQueries = append(panelQueries, query)
|
|
}
|
|
|
|
result[panel.Get("id").MustInt64()] = panelQueries
|
|
}
|
|
}
|
|
|
|
func panelHasAnExpression(panel *simplejson.Json) bool {
|
|
var hasExpression bool
|
|
for _, queryObj := range panel.Get("targets").MustArray() {
|
|
query := simplejson.NewFromAny(queryObj)
|
|
if expr.NodeTypeFromDatasourceUID(getDataSourceUidFromJson(query)) == expr.TypeCMDNode {
|
|
hasExpression = true
|
|
}
|
|
}
|
|
return hasExpression
|
|
}
|
|
|
|
func panelHasAnExpressionSchemaV2(panel *simplejson.Json) bool {
|
|
var hasExpression bool
|
|
|
|
// For schema v2, check the nested structure: spec.data.spec.queries[].spec.query
|
|
spec := panel.Get("spec")
|
|
if spec.Interface() == nil {
|
|
return hasExpression
|
|
}
|
|
|
|
data := spec.Get("data")
|
|
if data.Interface() == nil {
|
|
return hasExpression
|
|
}
|
|
|
|
dataSpec := data.Get("spec")
|
|
if dataSpec.Interface() == nil {
|
|
return hasExpression
|
|
}
|
|
|
|
queries := dataSpec.Get("queries")
|
|
if queries.Interface() == nil {
|
|
return hasExpression
|
|
}
|
|
|
|
for _, queryObj := range queries.MustArray() {
|
|
query := simplejson.NewFromAny(queryObj)
|
|
|
|
// Navigate to the actual query object
|
|
querySpec := query.Get("spec")
|
|
if querySpec.Interface() == nil {
|
|
continue
|
|
}
|
|
|
|
queryData := querySpec.Get("query")
|
|
if queryData.Interface() == nil {
|
|
continue
|
|
}
|
|
|
|
// Check if this query is an expression
|
|
if expr.NodeTypeFromDatasourceUID(getDataSourceUidFromJsonSchemaV2(queryData)) == expr.TypeCMDNode {
|
|
hasExpression = true
|
|
break
|
|
}
|
|
}
|
|
|
|
return hasExpression
|
|
}
|
|
|
|
func getDataSourceUidFromJson(query *simplejson.Json) string {
|
|
uid := query.Get("datasource").Get("uid").MustString()
|
|
|
|
// before 8.3 special types could be sent as datasource (expr)
|
|
if uid == "" {
|
|
uid = query.Get("datasource").MustString()
|
|
}
|
|
|
|
return uid
|
|
}
|
|
|
|
func getDataSourceUidFromJsonSchemaV2(query *simplejson.Json) string {
|
|
// For schema v2, datasource info is in query.datasource
|
|
uid := query.Get("datasource").Get("name").MustString()
|
|
|
|
// before 8.3 special types could be sent as datasource (expr)
|
|
if uid == "" {
|
|
uid = query.Get("datasource").MustString()
|
|
}
|
|
|
|
return uid
|
|
}
|
|
|
|
func sanitizeMetadataFromQueryData(res *backend.QueryDataResponse) {
|
|
for k := range res.Responses {
|
|
frames := res.Responses[k].Frames
|
|
for i := range frames {
|
|
if frames[i].Meta != nil {
|
|
frames[i].Meta.ExecutedQueryString = ""
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// sanitizeData removes the query expressions from the dashboard data
|
|
func sanitizeData(data *simplejson.Json) {
|
|
for _, panelObj := range data.Get("panels").MustArray() {
|
|
panel := simplejson.NewFromAny(panelObj)
|
|
|
|
// if the panel is a row and it is collapsed, get the queries from the panels inside the row
|
|
if panel.Get("type").MustString() == "row" && panel.Get("collapsed").MustBool() {
|
|
// recursive call to get queries from panels inside a row
|
|
sanitizeData(panel)
|
|
continue
|
|
}
|
|
|
|
for _, targetObj := range panel.Get("targets").MustArray() {
|
|
target := simplejson.NewFromAny(targetObj)
|
|
target.Del("expr")
|
|
target.Del("query")
|
|
target.Del("rawSql")
|
|
}
|
|
}
|
|
}
|
|
|
|
// NewTimeRange declared to be able to stub this function in tests
|
|
var NewTimeRange = gtime.NewTimeRange
|
|
|
|
// BuildTimeSettings build time settings object using selected values if enabled and are valid or dashboard default values
|
|
func buildTimeSettings(d *dashboards.Dashboard, reqDTO models.PublicDashboardQueryDTO, pd *models.PublicDashboard, panelID int64) models.TimeSettings {
|
|
from, to, timezone := getTimeRangeValuesOrDefault(reqDTO, d, pd.TimeSelectionEnabled, panelID)
|
|
|
|
timeRange := NewTimeRange(from, to)
|
|
|
|
timeFrom, _ := timeRange.ParseFrom(
|
|
gtime.WithLocation(timezone),
|
|
)
|
|
timeTo, _ := timeRange.ParseTo(
|
|
gtime.WithLocation(timezone),
|
|
)
|
|
timeToAsEpoch := timeTo.UnixMilli()
|
|
timeFromAsEpoch := timeFrom.UnixMilli()
|
|
|
|
// Were using epoch ms because this is used to build a MetricRequest, which is used by query caching, which want the time range in epoch milliseconds.
|
|
return models.TimeSettings{
|
|
From: strconv.FormatInt(timeFromAsEpoch, 10),
|
|
To: strconv.FormatInt(timeToAsEpoch, 10),
|
|
}
|
|
}
|
|
|
|
// buildTimeSettingsV2 builds time settings for V2 dashboards
|
|
func buildTimeSettingsV2(d *dashboards.Dashboard, reqDTO models.PublicDashboardQueryDTO, pd *models.PublicDashboard, panelID int64) models.TimeSettings {
|
|
from, to, timezone := getTimeRangeValuesOrDefaultV2(d, reqDTO, pd.TimeSelectionEnabled, panelID)
|
|
|
|
timeRange := NewTimeRange(from, to)
|
|
|
|
timeFrom, _ := timeRange.ParseFrom(
|
|
gtime.WithLocation(timezone),
|
|
)
|
|
timeTo, _ := timeRange.ParseTo(
|
|
gtime.WithLocation(timezone),
|
|
)
|
|
timeToAsEpoch := timeTo.UnixMilli()
|
|
timeFromAsEpoch := timeFrom.UnixMilli()
|
|
|
|
// Were using epoch ms because this is used to build a MetricRequest, which is used by query caching, which want the time range in epoch milliseconds.
|
|
return models.TimeSettings{
|
|
From: strconv.FormatInt(timeFromAsEpoch, 10),
|
|
To: strconv.FormatInt(timeToAsEpoch, 10),
|
|
}
|
|
}
|
|
|
|
// returns from, to and timezone from the request if the timeSelection is enabled or the dashboard default values
|
|
func getTimeRangeValuesOrDefault(reqDTO models.PublicDashboardQueryDTO, d *dashboards.Dashboard, timeSelectionEnabled bool, panelID int64) (string, string, *time.Location) {
|
|
from := d.Data.GetPath("time", "from").MustString()
|
|
to := d.Data.GetPath("time", "to").MustString()
|
|
dashboardTimezone := d.Data.GetPath("timezone").MustString()
|
|
|
|
panelRelativeTime := getPanelRelativeTimeRange(d.Data, panelID)
|
|
if panelRelativeTime != "" {
|
|
from = panelRelativeTime
|
|
}
|
|
|
|
// we use the values from the request if the time selection is enabled and the values are valid
|
|
if timeSelectionEnabled {
|
|
if reqDTO.TimeRange.From != "" && reqDTO.TimeRange.To != "" {
|
|
from = reqDTO.TimeRange.From
|
|
to = reqDTO.TimeRange.To
|
|
}
|
|
|
|
if reqDTO.TimeRange.Timezone != "" {
|
|
if userTimezone, err := time.LoadLocation(reqDTO.TimeRange.Timezone); err == nil {
|
|
return from, to, userTimezone
|
|
}
|
|
}
|
|
}
|
|
|
|
// if the dashboardTimezone is blank or there is an error default is UTC
|
|
timezone, err := time.LoadLocation(dashboardTimezone)
|
|
if err != nil {
|
|
return from, to, time.UTC
|
|
}
|
|
|
|
return from, to, timezone
|
|
}
|
|
|
|
// getTimeRangeValuesOrDefaultV2 returns from, to and timezone from the request if the timeSelection is enabled or the dashboard default values for V2
|
|
func getTimeRangeValuesOrDefaultV2(d *dashboards.Dashboard, reqDTO models.PublicDashboardQueryDTO, timeSelectionEnabled bool, panelID int64) (string, string, *time.Location) {
|
|
// In V2, time settings are in dashboard.timeSettings
|
|
timeSettings := d.Data.Get("timeSettings")
|
|
from := timeSettings.Get("from").MustString()
|
|
to := timeSettings.Get("to").MustString()
|
|
dashboardTimezone := timeSettings.Get("timezone").MustString()
|
|
|
|
// Check for panel-specific time override in V2 structure
|
|
panelRelativeTime := getPanelRelativeTimeRangeV2(d.Data, panelID)
|
|
if panelRelativeTime != "" {
|
|
from = panelRelativeTime
|
|
}
|
|
|
|
// we use the values from the request if the time selection is enabled and the values are valid
|
|
if timeSelectionEnabled {
|
|
if reqDTO.TimeRange.From != "" && reqDTO.TimeRange.To != "" {
|
|
from = reqDTO.TimeRange.From
|
|
to = reqDTO.TimeRange.To
|
|
}
|
|
|
|
if reqDTO.TimeRange.Timezone != "" {
|
|
if userTimezone, err := time.LoadLocation(reqDTO.TimeRange.Timezone); err == nil {
|
|
return from, to, userTimezone
|
|
}
|
|
}
|
|
}
|
|
|
|
// if the dashboardTimezone is blank or there is an error default is UTC
|
|
timezone, err := time.LoadLocation(dashboardTimezone)
|
|
if err != nil {
|
|
return from, to, time.UTC
|
|
}
|
|
|
|
return from, to, timezone
|
|
}
|
|
|
|
func getPanelRelativeTimeRange(dashboard *simplejson.Json, panelID int64) string {
|
|
for _, panelObj := range dashboard.Get("panels").MustArray() {
|
|
panel := simplejson.NewFromAny(panelObj)
|
|
|
|
if panel.Get("id").MustInt64() == panelID {
|
|
return panel.Get("timeFrom").MustString()
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func getPanelRelativeTimeRangeV2(dashboard *simplejson.Json, panelID int64) string {
|
|
// In V2, check elements for panel-specific time settings
|
|
elements := dashboard.Get("elements")
|
|
if elements.Interface() == nil {
|
|
return ""
|
|
}
|
|
|
|
elementsMap := elements.MustMap()
|
|
for _, element := range elementsMap {
|
|
element := simplejson.NewFromAny(element)
|
|
|
|
// Check if this is the panel we're looking for
|
|
if element.Get("spec").Get("id").MustInt64() != panelID {
|
|
continue
|
|
}
|
|
|
|
// Check for time override in data.spec.queryOptions.timeFrom
|
|
spec := element.Get("spec")
|
|
if spec.Interface() == nil {
|
|
return ""
|
|
}
|
|
|
|
data := spec.Get("data")
|
|
if data.Interface() == nil {
|
|
return ""
|
|
}
|
|
|
|
dataSpec := data.Get("spec")
|
|
if dataSpec.Interface() == nil {
|
|
return ""
|
|
}
|
|
|
|
queryOptions := dataSpec.Get("queryOptions")
|
|
if queryOptions.Interface() == nil {
|
|
return ""
|
|
}
|
|
|
|
timeFrom := queryOptions.Get("timeFrom")
|
|
if timeFrom.Interface() != nil {
|
|
return timeFrom.MustString()
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
return ""
|
|
}
|