mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-15 21:20:28 +01:00
Compare commits
4 Commits
main
...
nv/v2-publ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b990d40c5f | ||
|
|
95a0d7c035 | ||
|
|
e678728c61 | ||
|
|
42d3e7e0e4 |
@@ -29,6 +29,7 @@ type module struct {
|
||||
settings factory.ScopedProviderSettings
|
||||
querier querier.Querier
|
||||
licensing licensing.Licensing
|
||||
tagModule tag.Module
|
||||
}
|
||||
|
||||
func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing, tagModule tag.Module) dashboard.Module {
|
||||
@@ -41,6 +42,7 @@ func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, an
|
||||
settings: scopedProviderSettings,
|
||||
querier: querier,
|
||||
licensing: licensing,
|
||||
tagModule: tagModule,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +134,49 @@ func (module *module) GetPublicWidgetQueryRange(ctx context.Context, id valuer.U
|
||||
return module.querier.QueryRange(ctx, dashboard.OrgID, query)
|
||||
}
|
||||
|
||||
func (module *module) GetDashboardByPublicIDV2(ctx context.Context, id valuer.UUID) (*dashboardtypes.DashboardV2, error) {
|
||||
storableDashboard, err := module.store.GetDashboardByPublicID(ctx, id.StringValue())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tags, err := module.tagModule.ListForResource(ctx, storableDashboard.OrgID, coretypes.KindDashboard, storableDashboard.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return storableDashboard.ToDashboardV2(tags)
|
||||
}
|
||||
|
||||
func (module *module) GetPublicWidgetQueryRangeV2(ctx context.Context, id valuer.UUID, panelKey, startTimeRaw, endTimeRaw string) (*querybuildertypesv5.QueryRangeResponse, error) {
|
||||
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
|
||||
instrumentationtypes.CodeNamespace: "dashboard",
|
||||
instrumentationtypes.CodeFunctionName: "GetPublicWidgetQueryRangeV2",
|
||||
})
|
||||
|
||||
dashboard, err := module.GetDashboardByPublicIDV2(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
publicDashboard, err := module.GetPublic(ctx, dashboard.OrgID, dashboard.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
startTime, endTime, err := publicDashboard.ResolveTimeRange(startTimeRaw, endTimeRaw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query, err := dashboard.GetPanelQuery(startTime, endTime, panelKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return module.querier.QueryRange(ctx, dashboard.OrgID, query)
|
||||
}
|
||||
|
||||
func (module *module) UpdatePublic(ctx context.Context, orgID valuer.UUID, publicDashboard *dashboardtypes.PublicDashboard) error {
|
||||
_, err := module.licensing.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
|
||||
@@ -336,5 +336,61 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/public/dashboards/{id}", handler.New(provider.authzMiddleware.CheckWithoutClaims(
|
||||
provider.dashboardHandler.GetPublicDataV2,
|
||||
authtypes.Relation{Verb: coretypes.VerbRead},
|
||||
coretypes.ResourceMetaResourcePublicDashboard,
|
||||
func(req *http.Request, orgs []*types.Organization) ([]coretypes.Selector, valuer.UUID, error) {
|
||||
id, err := valuer.NewUUID(mux.Vars(req)["id"])
|
||||
if err != nil {
|
||||
return nil, valuer.UUID{}, err
|
||||
}
|
||||
|
||||
return provider.dashboardModule.GetPublicDashboardSelectorsAndOrg(req.Context(), id, orgs)
|
||||
}, []string{}), handler.OpenAPIDef{
|
||||
ID: "GetPublicDashboardDataV2",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Get public dashboard data (v2)",
|
||||
Description: "This endpoint returns the sanitized v2-shape dashboard data for public access. Each panel query is reduced to a safe field subset, so filters and raw query strings are not exposed.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(dashboardtypes.GettablePublicDashboardDataV2),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newAnonymousSecuritySchemes([]string{coretypes.ResourceMetaResourcePublicDashboard.Scope(coretypes.VerbRead)}),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/public/dashboards/{id}/panels/{key}/query_range", handler.New(provider.authzMiddleware.CheckWithoutClaims(
|
||||
provider.dashboardHandler.GetPublicWidgetQueryRangeV2,
|
||||
authtypes.Relation{Verb: coretypes.VerbRead},
|
||||
coretypes.ResourceMetaResourcePublicDashboard,
|
||||
func(req *http.Request, orgs []*types.Organization) ([]coretypes.Selector, valuer.UUID, error) {
|
||||
id, err := valuer.NewUUID(mux.Vars(req)["id"])
|
||||
if err != nil {
|
||||
return nil, valuer.UUID{}, err
|
||||
}
|
||||
|
||||
return provider.dashboardModule.GetPublicDashboardSelectorsAndOrg(req.Context(), id, orgs)
|
||||
}, []string{}), handler.OpenAPIDef{
|
||||
ID: "GetPublicDashboardPanelQueryRangeV2",
|
||||
Tags: []string{"dashboard"},
|
||||
Summary: "Get query range result (v2)",
|
||||
Description: "This endpoint returns query range results for a panel of a v2-shape public dashboard. The panel is addressed by its key in spec.panels.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(querybuildertypesv5.QueryRangeResponse),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newAnonymousSecuritySchemes([]string{coretypes.ResourceMetaResourcePublicDashboard.Scope(coretypes.VerbRead)}),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -78,6 +78,12 @@ type Module interface {
|
||||
DeleteV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error
|
||||
|
||||
DeletePreferencesForUser(ctx context.Context, orgID valuer.UUID, userID valuer.UUID) error
|
||||
|
||||
// get the v2 dashboard data by public dashboard id
|
||||
GetDashboardByPublicIDV2(context.Context, valuer.UUID) (*dashboardtypes.DashboardV2, error)
|
||||
|
||||
// gets the query results by panel key and public shared id for a v2 dashboard
|
||||
GetPublicWidgetQueryRangeV2(ctx context.Context, id valuer.UUID, panelKey, startTimeRaw, endTimeRaw string) (*querybuildertypesv5.QueryRangeResponse, error)
|
||||
}
|
||||
|
||||
type Handler interface {
|
||||
@@ -89,6 +95,10 @@ type Handler interface {
|
||||
|
||||
GetPublicWidgetQueryRange(http.ResponseWriter, *http.Request)
|
||||
|
||||
GetPublicDataV2(http.ResponseWriter, *http.Request)
|
||||
|
||||
GetPublicWidgetQueryRangeV2(http.ResponseWriter, *http.Request)
|
||||
|
||||
UpdatePublic(http.ResponseWriter, *http.Request)
|
||||
|
||||
DeletePublic(http.ResponseWriter, *http.Request)
|
||||
|
||||
@@ -247,6 +247,14 @@ func (module *module) GetPublicWidgetQueryRange(context.Context, valuer.UUID, ui
|
||||
return nil, errors.Newf(errors.TypeUnsupported, dashboardtypes.ErrCodePublicDashboardUnsupported, "not implemented")
|
||||
}
|
||||
|
||||
func (module *module) GetDashboardByPublicIDV2(_ context.Context, _ valuer.UUID) (*dashboardtypes.DashboardV2, error) {
|
||||
return nil, errors.Newf(errors.TypeUnsupported, dashboardtypes.ErrCodePublicDashboardUnsupported, "not implemented")
|
||||
}
|
||||
|
||||
func (module *module) GetPublicWidgetQueryRangeV2(context.Context, valuer.UUID, string, string, string) (*qbtypes.QueryRangeResponse, error) {
|
||||
return nil, errors.Newf(errors.TypeUnsupported, dashboardtypes.ErrCodePublicDashboardUnsupported, "not implemented")
|
||||
}
|
||||
|
||||
func (module *module) GetPublicDashboardSelectorsAndOrg(_ context.Context, _ valuer.UUID, _ []*types.Organization) ([]coretypes.Selector, valuer.UUID, error) {
|
||||
return nil, valuer.UUID{}, errors.Newf(errors.TypeUnsupported, dashboardtypes.ErrCodePublicDashboardUnsupported, "not implemented")
|
||||
}
|
||||
|
||||
@@ -344,3 +344,53 @@ func (handler *handler) DeleteV2(rw http.ResponseWriter, r *http.Request) {
|
||||
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
func (handler *handler) GetPublicDataV2(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
id, err := valuer.NewUUID(mux.Vars(r)["id"])
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
dashboard, err := handler.module.GetDashboardByPublicIDV2(ctx, id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
publicDashboard, err := handler.module.GetPublic(ctx, dashboard.OrgID, dashboard.ID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, dashboardtypes.NewPublicDashboardDataFromDashboardV2(dashboard, publicDashboard))
|
||||
}
|
||||
|
||||
func (handler *handler) GetPublicWidgetQueryRangeV2(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
id, err := valuer.NewUUID(mux.Vars(r)["id"])
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
panelKey, ok := mux.Vars(r)["key"]
|
||||
if !ok || panelKey == "" {
|
||||
render.Error(rw, errors.New(errors.TypeInvalidInput, dashboardtypes.ErrCodePublicDashboardInvalidInput, "panel key is missing from the path"))
|
||||
return
|
||||
}
|
||||
|
||||
queryRangeResults, err := handler.module.GetPublicWidgetQueryRangeV2(ctx, id, panelKey, r.URL.Query().Get("startTime"), r.URL.Query().Get("endTime"))
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, queryRangeResults)
|
||||
}
|
||||
|
||||
207
pkg/types/dashboardtypes/perses_public_dashboard.go
Normal file
207
pkg/types/dashboardtypes/perses_public_dashboard.go
Normal file
@@ -0,0 +1,207 @@
|
||||
package dashboardtypes
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
qb "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/tagtypes"
|
||||
)
|
||||
|
||||
// GettablePublicDashboardDataV2 is the anonymous-facing payload of a v2 dashboard.
|
||||
type GettablePublicDashboardDataV2 struct {
|
||||
Dashboard *GettableDashboardV2 `json:"dashboard"`
|
||||
PublicDashboard *GettablePublicDasbhboard `json:"publicDashboard"`
|
||||
}
|
||||
|
||||
// NewPublicDashboardDataFromDashboardV2 builds the anonymous v2 payload: panel queries
|
||||
// are redacted, and only the body fields v1 exposed (name, metadata, tags, spec) are set.
|
||||
func NewPublicDashboardDataFromDashboardV2(dashboard *DashboardV2, publicDashboard *PublicDashboard) *GettablePublicDashboardDataV2 {
|
||||
spec := dashboard.Spec
|
||||
redactPanelQueries(&spec)
|
||||
|
||||
return &GettablePublicDashboardDataV2{
|
||||
Dashboard: &GettableDashboardV2{
|
||||
DashboardV2MetadataBase: dashboard.DashboardV2MetadataBase,
|
||||
Name: dashboard.Name,
|
||||
Tags: tagtypes.NewGettableTagsFromTags(dashboard.Tags),
|
||||
Spec: spec,
|
||||
},
|
||||
PublicDashboard: &GettablePublicDasbhboard{
|
||||
TimeRangeEnabled: publicDashboard.TimeRangeEnabled,
|
||||
DefaultTimeRange: publicDashboard.DefaultTimeRange,
|
||||
PublicPath: publicDashboard.PublicPath(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// redactPanelQueries rebuilds the panel map with each query redacted, leaving the
|
||||
// source spec untouched.
|
||||
func redactPanelQueries(spec *DashboardSpec) {
|
||||
panels := make(map[string]*Panel, len(spec.Panels))
|
||||
for key, panel := range spec.Panels {
|
||||
if panel == nil {
|
||||
panels[key] = nil
|
||||
continue
|
||||
}
|
||||
redacted := *panel
|
||||
queries := make([]Query, len(redacted.Spec.Queries))
|
||||
for i, query := range redacted.Spec.Queries {
|
||||
query.Spec.Plugin.Spec = redactQueryPluginSpec(query.Spec.Plugin.Kind, query.Spec.Plugin.Spec)
|
||||
queries[i] = query
|
||||
}
|
||||
redacted.Spec.Queries = queries
|
||||
panels[key] = &redacted
|
||||
}
|
||||
spec.Panels = panels
|
||||
}
|
||||
|
||||
// redactQueryPluginSpec redacts a single panel query plugin spec. Composite specs
|
||||
// hold their sub-queries as values; promql/clickhouse/formula/trace specs are pointers.
|
||||
func redactQueryPluginSpec(kind QueryPluginKind, spec any) any {
|
||||
switch kind {
|
||||
case QueryKindComposite:
|
||||
composite, ok := spec.(*qb.CompositeQuery)
|
||||
if !ok || composite == nil {
|
||||
return spec
|
||||
}
|
||||
queries := make([]qb.QueryEnvelope, len(composite.Queries))
|
||||
for i, envelope := range composite.Queries {
|
||||
envelope.Spec = redactQuery(envelope.Spec)
|
||||
queries[i] = envelope
|
||||
}
|
||||
return &qb.CompositeQuery{Queries: queries}
|
||||
case QueryKindBuilder:
|
||||
builder, ok := spec.(*BuilderQuerySpec)
|
||||
if !ok || builder == nil {
|
||||
return spec
|
||||
}
|
||||
return &BuilderQuerySpec{Spec: redactQuery(builder.Spec)}
|
||||
case QueryKindPromQL:
|
||||
if s, ok := spec.(*qb.PromQuery); ok {
|
||||
redacted := redactQuery(*s).(qb.PromQuery)
|
||||
return &redacted
|
||||
}
|
||||
case QueryKindClickHouseSQL:
|
||||
if s, ok := spec.(*qb.ClickHouseQuery); ok {
|
||||
redacted := redactQuery(*s).(qb.ClickHouseQuery)
|
||||
return &redacted
|
||||
}
|
||||
case QueryKindFormula:
|
||||
if s, ok := spec.(*qb.QueryBuilderFormula); ok {
|
||||
redacted := redactQuery(*s).(qb.QueryBuilderFormula)
|
||||
return &redacted
|
||||
}
|
||||
case QueryKindTraceOperator:
|
||||
if s, ok := spec.(*qb.QueryBuilderTraceOperator); ok {
|
||||
redacted := redactQuery(*s).(qb.QueryBuilderTraceOperator)
|
||||
return &redacted
|
||||
}
|
||||
}
|
||||
return spec
|
||||
}
|
||||
|
||||
// redactQuery returns a copy of a query value carrying only public-safe fields.
|
||||
// Building up an allowlist (rather than clearing fields) fails closed: any new
|
||||
// field is excluded until explicitly added here.
|
||||
func redactQuery(spec any) any {
|
||||
switch s := spec.(type) {
|
||||
case qb.QueryBuilderQuery[qb.LogAggregation]:
|
||||
return redactBuilderQuery(s)
|
||||
case qb.QueryBuilderQuery[qb.MetricAggregation]:
|
||||
return redactBuilderQuery(s)
|
||||
case qb.QueryBuilderQuery[qb.TraceAggregation]:
|
||||
return redactBuilderQuery(s)
|
||||
case qb.PromQuery:
|
||||
return qb.PromQuery{Name: s.Name, Step: s.Step, Disabled: s.Disabled, Legend: s.Legend}
|
||||
case qb.ClickHouseQuery:
|
||||
return qb.ClickHouseQuery{Name: s.Name, Disabled: s.Disabled, Legend: s.Legend}
|
||||
case qb.QueryBuilderFormula:
|
||||
return qb.QueryBuilderFormula{Name: s.Name, Expression: s.Expression, Disabled: s.Disabled, Legend: s.Legend}
|
||||
case qb.QueryBuilderTraceOperator:
|
||||
return qb.QueryBuilderTraceOperator{
|
||||
Name: s.Name,
|
||||
Expression: s.Expression,
|
||||
Aggregations: s.Aggregations,
|
||||
GroupBy: s.GroupBy,
|
||||
StepInterval: s.StepInterval,
|
||||
Disabled: s.Disabled,
|
||||
Legend: s.Legend,
|
||||
}
|
||||
}
|
||||
return spec
|
||||
}
|
||||
|
||||
// redactBuilderQuery keeps only display-relevant fields, dropping filter, having,
|
||||
// order, limit, and any other query internals.
|
||||
func redactBuilderQuery[T any](q qb.QueryBuilderQuery[T]) qb.QueryBuilderQuery[T] {
|
||||
return qb.QueryBuilderQuery[T]{
|
||||
Name: q.Name,
|
||||
Signal: q.Signal,
|
||||
Source: q.Source,
|
||||
Aggregations: q.Aggregations,
|
||||
GroupBy: q.GroupBy,
|
||||
StepInterval: q.StepInterval,
|
||||
Disabled: q.Disabled,
|
||||
Legend: q.Legend,
|
||||
}
|
||||
}
|
||||
|
||||
// GetPanelQuery builds a v5 QueryRangeRequest for the given panel. v2 panel queries
|
||||
// are already native v5 shapes, so the request is assembled directly: a composite is
|
||||
// used as-is, every other kind becomes a single-envelope composite.
|
||||
func (d *DashboardV2) GetPanelQuery(startTime, endTime uint64, panelKey string) (*qb.QueryRangeRequest, error) {
|
||||
panel, ok := d.Spec.Panels[panelKey]
|
||||
if !ok || panel == nil {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardInvalidInput, "panel with key %q doesn't exist", panelKey)
|
||||
}
|
||||
// Validator guarantees exactly one query per panel.
|
||||
if len(panel.Spec.Queries) != 1 {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "panel %q must have exactly one query", panelKey)
|
||||
}
|
||||
|
||||
query := panel.Spec.Queries[0]
|
||||
composite, err := compositeQueryFromPlugin(query.Spec.Plugin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &qb.QueryRangeRequest{
|
||||
SchemaVersion: "v1",
|
||||
Start: startTime,
|
||||
End: endTime,
|
||||
RequestType: query.Kind,
|
||||
CompositeQuery: composite,
|
||||
FormatOptions: &qb.FormatOptions{
|
||||
FormatTableResultForUI: panel.Spec.Plugin.Kind == PanelKindTable,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// compositeQueryFromPlugin turns a panel's single query plugin into a v5 composite
|
||||
// query. The plugin spec is already a typed v5 value, so no JSON round-trip is needed.
|
||||
func compositeQueryFromPlugin(plugin QueryPlugin) (qb.CompositeQuery, error) {
|
||||
switch spec := plugin.Spec.(type) {
|
||||
case *qb.CompositeQuery:
|
||||
if spec == nil {
|
||||
return qb.CompositeQuery{}, errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "composite query is empty")
|
||||
}
|
||||
return *spec, nil
|
||||
case *BuilderQuerySpec:
|
||||
if spec == nil {
|
||||
return qb.CompositeQuery{}, errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "builder query is empty")
|
||||
}
|
||||
return wrapEnvelope(qb.QueryTypeBuilder, spec.Spec), nil
|
||||
case *qb.PromQuery:
|
||||
return wrapEnvelope(qb.QueryTypePromQL, *spec), nil
|
||||
case *qb.ClickHouseQuery:
|
||||
return wrapEnvelope(qb.QueryTypeClickHouseSQL, *spec), nil
|
||||
case *qb.QueryBuilderFormula:
|
||||
return wrapEnvelope(qb.QueryTypeFormula, *spec), nil
|
||||
case *qb.QueryBuilderTraceOperator:
|
||||
return wrapEnvelope(qb.QueryTypeTraceOperator, *spec), nil
|
||||
}
|
||||
return qb.CompositeQuery{}, errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "unsupported query kind %q", plugin.Kind)
|
||||
}
|
||||
|
||||
func wrapEnvelope(queryType qb.QueryType, spec any) qb.CompositeQuery {
|
||||
return qb.CompositeQuery{Queries: []qb.QueryEnvelope{{Type: queryType, Spec: spec}}}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package dashboardtypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
@@ -212,6 +213,30 @@ func (typ *PublicDashboard) PublicPath() string {
|
||||
return "/public/dashboard/" + typ.ID.StringValue()
|
||||
}
|
||||
|
||||
// ResolveTimeRange returns the [start, end] window in epoch millis for a public
|
||||
// widget/panel query: the caller-supplied range when the dashboard allows it,
|
||||
// otherwise now minus the configured default range.
|
||||
func (typ *PublicDashboard) ResolveTimeRange(startTimeRaw, endTimeRaw string) (uint64, uint64, error) {
|
||||
if typ.TimeRangeEnabled {
|
||||
startTime, err := strconv.ParseUint(startTimeRaw, 10, 64)
|
||||
if err != nil {
|
||||
return 0, 0, errors.New(errors.TypeInvalidInput, ErrCodePublicDashboardInvalidInput, "invalid startTime")
|
||||
}
|
||||
endTime, err := strconv.ParseUint(endTimeRaw, 10, 64)
|
||||
if err != nil {
|
||||
return 0, 0, errors.New(errors.TypeInvalidInput, ErrCodePublicDashboardInvalidInput, "invalid endTime")
|
||||
}
|
||||
return startTime, endTime, nil
|
||||
}
|
||||
|
||||
timeRange, err := time.ParseDuration(typ.DefaultTimeRange)
|
||||
if err != nil {
|
||||
return 0, 0, errors.WrapInternalf(err, errors.CodeInternal, "stored defaultTimeRange %q is not a valid duration", typ.DefaultTimeRange)
|
||||
}
|
||||
now := time.Now()
|
||||
return uint64(now.Add(-timeRange).UnixMilli()), uint64(now.UnixMilli()), nil
|
||||
}
|
||||
|
||||
func (typ *PostablePublicDashboard) UnmarshalJSON(data []byte) error {
|
||||
type alias PostablePublicDashboard
|
||||
var temp alias
|
||||
|
||||
117
pkg/types/dashboardtypes/public_dashboard_test.go
Normal file
117
pkg/types/dashboardtypes/public_dashboard_test.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package dashboardtypes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPublicDashboardResolveTimeRange(t *testing.T) {
|
||||
t.Run("returns the explicit range when time range is enabled", func(t *testing.T) {
|
||||
cases := []struct {
|
||||
description string
|
||||
startTimeRaw string
|
||||
endTimeRaw string
|
||||
expectedStart uint64
|
||||
expectedEnd uint64
|
||||
}{
|
||||
{
|
||||
description: "valid epoch millis",
|
||||
startTimeRaw: "1700000000000",
|
||||
endTimeRaw: "1700000600000",
|
||||
expectedStart: 1700000000000,
|
||||
expectedEnd: 1700000600000,
|
||||
},
|
||||
{
|
||||
description: "zero start is allowed",
|
||||
startTimeRaw: "0",
|
||||
endTimeRaw: "1700000600000",
|
||||
expectedStart: 0,
|
||||
expectedEnd: 1700000600000,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.description, func(t *testing.T) {
|
||||
publicDashboard := &PublicDashboard{TimeRangeEnabled: true}
|
||||
|
||||
startTime, endTime, err := publicDashboard.ResolveTimeRange(tc.startTimeRaw, tc.endTimeRaw)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.expectedStart, startTime)
|
||||
assert.Equal(t, tc.expectedEnd, endTime)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rejects an invalid explicit range when time range is enabled", func(t *testing.T) {
|
||||
cases := []struct {
|
||||
description string
|
||||
startTimeRaw string
|
||||
endTimeRaw string
|
||||
}{
|
||||
{description: "non-numeric startTime", startTimeRaw: "abc", endTimeRaw: "1700000600000"},
|
||||
{description: "empty startTime", startTimeRaw: "", endTimeRaw: "1700000600000"},
|
||||
{description: "negative startTime", startTimeRaw: "-1", endTimeRaw: "1700000600000"},
|
||||
{description: "non-numeric endTime", startTimeRaw: "1700000000000", endTimeRaw: "xyz"},
|
||||
{description: "empty endTime", startTimeRaw: "1700000000000", endTimeRaw: ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.description, func(t *testing.T) {
|
||||
publicDashboard := &PublicDashboard{TimeRangeEnabled: true}
|
||||
|
||||
_, _, err := publicDashboard.ResolveTimeRange(tc.startTimeRaw, tc.endTimeRaw)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, errors.Ast(err, errors.TypeInvalidInput))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("derives the range from now and the default when time range is disabled", func(t *testing.T) {
|
||||
cases := []struct {
|
||||
description string
|
||||
defaultTimeRange string
|
||||
expectedWidthMS uint64
|
||||
}{
|
||||
{description: "one hour", defaultTimeRange: "1h", expectedWidthMS: uint64(time.Hour.Milliseconds())},
|
||||
{description: "thirty minutes", defaultTimeRange: "30m", expectedWidthMS: uint64((30 * time.Minute).Milliseconds())},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.description, func(t *testing.T) {
|
||||
publicDashboard := &PublicDashboard{TimeRangeEnabled: false, DefaultTimeRange: tc.defaultTimeRange}
|
||||
|
||||
before := uint64(time.Now().UnixMilli())
|
||||
startTime, endTime, err := publicDashboard.ResolveTimeRange("ignored", "ignored")
|
||||
after := uint64(time.Now().UnixMilli())
|
||||
|
||||
require.NoError(t, err)
|
||||
// end is "now"; both bounds share the same instant, so the width is exact.
|
||||
assert.GreaterOrEqual(t, endTime, before)
|
||||
assert.LessOrEqual(t, endTime, after)
|
||||
assert.Equal(t, tc.expectedWidthMS, endTime-startTime)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ignores caller-supplied bounds when time range is disabled", func(t *testing.T) {
|
||||
publicDashboard := &PublicDashboard{TimeRangeEnabled: false, DefaultTimeRange: "1h"}
|
||||
|
||||
startTime, endTime, err := publicDashboard.ResolveTimeRange("123", "456")
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, uint64(123), startTime)
|
||||
assert.NotEqual(t, uint64(456), endTime)
|
||||
assert.Equal(t, uint64(time.Hour.Milliseconds()), endTime-startTime)
|
||||
})
|
||||
|
||||
t.Run("returns an internal error for an unparseable stored default range", func(t *testing.T) {
|
||||
publicDashboard := &PublicDashboard{TimeRangeEnabled: false, DefaultTimeRange: "not-a-duration"}
|
||||
|
||||
_, _, err := publicDashboard.ResolveTimeRange("", "")
|
||||
assert.Error(t, err)
|
||||
assert.True(t, errors.Ast(err, errors.TypeInternal))
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user