mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-25 23:40:35 +01:00
Compare commits
2 Commits
nv/v1-depr
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e728942fec | ||
|
|
5bf95534ad |
@@ -2,6 +2,7 @@ package impldashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -12,8 +13,10 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/http/binding"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/transition"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/gorilla/mux"
|
||||
@@ -30,21 +33,190 @@ func NewHandler(module dashboard.Module, providerSettings factory.ProviderSettin
|
||||
}
|
||||
|
||||
func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) {
|
||||
render.Error(rw, dashboardtypes.NewV1DeprecatedError(
|
||||
"create a dashboard with POST /api/v2/dashboards (schema: "+dashboardtypes.V2CreateDashboardSchemaLink+")"))
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := dashboardtypes.PostableDashboard{}
|
||||
err = json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
dashboardMigrator := transition.NewDashboardMigrateV5(handler.providerSettings.Logger, nil, nil)
|
||||
if req["version"] != "v5" {
|
||||
dashboardMigrator.Migrate(ctx, req)
|
||||
}
|
||||
|
||||
dashboard, err := handler.module.Create(ctx, orgID, claims.Email, valuer.MustNewUUID(claims.IdentityID()), dashboardtypes.SourceUser, req)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
gettableDashboard, err := dashboardtypes.NewGettableDashboardFromDashboard(dashboard)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusCreated, gettableDashboard)
|
||||
}
|
||||
|
||||
func (handler *handler) Update(rw http.ResponseWriter, r *http.Request) {
|
||||
render.Error(rw, dashboardtypes.NewV1DeprecatedError(
|
||||
"update a dashboard with PUT /api/v2/dashboards/{id} (schema: "+dashboardtypes.V2UpdateDashboardSchemaLink+"), or patch it with PATCH /api/v2/dashboards/{id} (schema: "+dashboardtypes.V2PatchDashboardSchemaLink+")"))
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
id := mux.Vars(r)["id"]
|
||||
if id == "" {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
|
||||
return
|
||||
}
|
||||
dashboardID, err := valuer.NewUUID(id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := dashboardtypes.UpdatableDashboard{}
|
||||
err = json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
diff := 0
|
||||
// Allow multiple deletions for API key requests; enforce for others
|
||||
if claims.IdentNProvider == authtypes.IdentNProviderTokenizer {
|
||||
diff = 1
|
||||
}
|
||||
|
||||
dashboard, err := handler.module.Update(ctx, orgID, dashboardID, claims.Email, req, diff)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, dashboard)
|
||||
}
|
||||
|
||||
func (handler *handler) LockUnlock(rw http.ResponseWriter, r *http.Request) {
|
||||
render.Error(rw, dashboardtypes.NewV1DeprecatedError("lock a dashboard with PUT /api/v2/dashboards/{id}/lock, or unlock it with DELETE /api/v2/dashboards/{id}/lock"))
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
id := mux.Vars(r)["id"]
|
||||
if id == "" {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
|
||||
return
|
||||
}
|
||||
dashboardID, err := valuer.NewUUID(id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := new(dashboardtypes.LockUnlockDashboard)
|
||||
err = json.NewDecoder(r.Body).Decode(req)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
isAdmin := false
|
||||
selectors := []coretypes.Selector{
|
||||
coretypes.TypeRole.MustSelector(authtypes.SigNozAdminRoleName),
|
||||
}
|
||||
err = handler.authz.CheckWithTupleCreation(
|
||||
ctx,
|
||||
claims,
|
||||
valuer.MustNewUUID(claims.OrgID),
|
||||
authtypes.Relation{Verb: coretypes.VerbAssignee},
|
||||
coretypes.NewResourceRole(),
|
||||
selectors,
|
||||
selectors,
|
||||
)
|
||||
if err == nil {
|
||||
isAdmin = true
|
||||
}
|
||||
|
||||
err = handler.module.LockUnlock(ctx, orgID, dashboardID, claims.Email, isAdmin, *req.Locked)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, nil)
|
||||
|
||||
}
|
||||
|
||||
func (handler *handler) Delete(rw http.ResponseWriter, r *http.Request) {
|
||||
render.Error(rw, dashboardtypes.NewV1DeprecatedError("delete a dashboard with DELETE /api/v2/dashboards/{id}"))
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
id := mux.Vars(r)["id"]
|
||||
if id == "" {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
|
||||
return
|
||||
}
|
||||
dashboardID, err := valuer.NewUUID(id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
err = handler.module.Delete(ctx, orgID, dashboardID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
func (handler *handler) CreatePublic(rw http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -1078,11 +1078,75 @@ func prepareQuery(r *http.Request) (string, error) {
|
||||
}
|
||||
|
||||
func (aH *APIHandler) Get(rw http.ResponseWriter, r *http.Request) {
|
||||
render.Error(rw, dashboardtypes.NewV1DeprecatedError("get a dashboard with GET /api/v2/dashboards/{id}"))
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
id := mux.Vars(r)["id"]
|
||||
if id == "" {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
|
||||
return
|
||||
}
|
||||
|
||||
dashboardID, err := valuer.NewUUID(id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
dashboard, err := aH.Signoz.Modules.Dashboard.Get(ctx, orgID, dashboardID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
gettableDashboard, err := dashboardtypes.NewGettableDashboardFromDashboard(dashboard)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, gettableDashboard)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) List(rw http.ResponseWriter, r *http.Request) {
|
||||
render.Error(rw, dashboardtypes.NewV1DeprecatedError("list dashboards with GET /api/v2/dashboards"))
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
dashboards, err := aH.Signoz.Modules.Dashboard.List(ctx, orgID)
|
||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
gettableDashboards, err := dashboardtypes.NewGettableDashboardsFromDashboards(dashboards)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
render.Success(rw, http.StatusOK, gettableDashboards)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) queryDashboardVarsV2(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -22,7 +22,6 @@ var (
|
||||
ErrCodeDashboardImmutable = errors.MustNewCode("dashboard_immutable")
|
||||
ErrCodeDashboardInvalidPatch = errors.MustNewCode("dashboard_invalid_patch")
|
||||
ErrCodeDashboardMigrationFailed = errors.MustNewCode("dashboard_migration_failed")
|
||||
ErrCodeDashboardV1Deprecated = errors.MustNewCode("dashboard_deprecated")
|
||||
)
|
||||
|
||||
type StorableDashboard struct {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
package dashboardtypes
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// v1 dashboards stored their `image` as one of a fixed set of base64 icon data
|
||||
// URIs. v2 references the same icons by a stable asset path (dashboardIconPathPrefix
|
||||
// + name). This maps each preset v1 base64 blob to its v2 icon path so a migrated
|
||||
@@ -45,8 +51,37 @@ func resolveV1Image(image string) string {
|
||||
if path, ok := v1Base64IconToAssetPath[image]; ok {
|
||||
return path
|
||||
}
|
||||
if reencoded, ok := reencodeInlineSVGImage(image); ok {
|
||||
image = reencoded
|
||||
}
|
||||
if (DashboardV2MetadataBase{Image: image}).validateImage() == nil {
|
||||
return image
|
||||
}
|
||||
return defaultDashboardIconPath
|
||||
}
|
||||
|
||||
// reencodeInlineSVGImage rewrites a percent-encoded inline SVG data URI into the
|
||||
// base64 form the v2 schema accepts, preserving an icon that would otherwise fail
|
||||
// validation. Returns (image, false) unchanged for anything else.
|
||||
func reencodeInlineSVGImage(image string) (string, bool) {
|
||||
const mediaType = "data:image/svg+xml"
|
||||
if !strings.HasPrefix(image, mediaType) {
|
||||
return image, false
|
||||
}
|
||||
rest := image[len(mediaType):]
|
||||
// The media type must be followed by its params/data separator, not more type
|
||||
// text (guards against a longer type that merely shares this prefix).
|
||||
if len(rest) == 0 || (rest[0] != ',' && rest[0] != ';') {
|
||||
return image, false
|
||||
}
|
||||
params, encoded, found := strings.Cut(rest, ",")
|
||||
// A base64 URI carries ";base64" before the comma and is already valid — leave it.
|
||||
if !found || strings.Contains(params, "base64") {
|
||||
return image, false
|
||||
}
|
||||
svg, err := url.PathUnescape(encoded)
|
||||
if err != nil {
|
||||
return image, false // malformed escaping — leave it; conversion falls back to the default icon
|
||||
}
|
||||
return "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte(svg)), true
|
||||
}
|
||||
|
||||
@@ -266,11 +266,27 @@ func (d *v1Decoder) legendFromWidget(w map[string]any) Legend {
|
||||
}
|
||||
}
|
||||
|
||||
// widgetBuilderSignal returns the signal of the widget's first builder query. Empty
|
||||
// for non-builder (promql/clickhouse) widgets, which have no per-signal selected fields.
|
||||
func (d *v1Decoder) widgetBuilderSignal(w map[string]any) telemetrytypes.Signal {
|
||||
builder := d.readObject(d.readObject(w, "query"), "builder")
|
||||
for _, q := range d.readObjects(builder, "queryData") {
|
||||
return signalFromDataSource(q["dataSource"])
|
||||
}
|
||||
return telemetrytypes.Signal{}
|
||||
}
|
||||
|
||||
func (d *v1Decoder) mapV1SelectFields(w map[string]any) []telemetrytypes.TelemetryFieldKey {
|
||||
field := "selectedLogFields"
|
||||
// Read the array matching the query's signal: traces and logs each keep their own
|
||||
// selected columns, and a widget can carry a stale array for the other signal.
|
||||
primary, fallback := "selectedLogFields", "selectedTracesFields"
|
||||
if d.widgetBuilderSignal(w) == telemetrytypes.SignalTraces {
|
||||
primary, fallback = "selectedTracesFields", "selectedLogFields"
|
||||
}
|
||||
field := primary
|
||||
raw := d.readArray(w, field)
|
||||
if len(raw) == 0 {
|
||||
field = "selectedTracesFields"
|
||||
field = fallback
|
||||
raw = d.readArray(w, field)
|
||||
}
|
||||
if len(raw) == 0 {
|
||||
|
||||
@@ -33,6 +33,7 @@ var preV5Migrator = transition.NewDashboardMigrateV5(slog.New(slog.DiscardHandle
|
||||
func normalizePreV5QueryData(query map[string]any, widgetType string) {
|
||||
dropLegacyFilter(query)
|
||||
normalizeFilterItemOps(query)
|
||||
foldReduceToIntoMetricAggregations(query)
|
||||
preV5Migrator.MigrateQueryDataShapeSafe(context.Background(), query, widgetType)
|
||||
normalizePreV5LogTraceAggregations(query)
|
||||
normalizeMetricAggregations(query)
|
||||
@@ -226,7 +227,7 @@ func dropLegacyFilter(query map[string]any) {
|
||||
|
||||
// normalizeFilterItemOps lowercases exists/nexists filter ops (frontend stores them
|
||||
// uppercase) to the spelling transition's buildCondition (pkg/transition/migrate_common.go)
|
||||
// matches; otherwise it appends a spurious empty value ("svc EXISTS ''"). Value
|
||||
// matches; otherwise it appends a spurious empty value ("svc EXISTS ”"). Value
|
||||
// operators already round-trip via that switch's default case.
|
||||
func normalizeFilterItemOps(query map[string]any) {
|
||||
filters, ok := query["filters"].(map[string]any)
|
||||
@@ -303,6 +304,32 @@ func normalizeMetricAggregations(query map[string]any) {
|
||||
}
|
||||
}
|
||||
|
||||
// foldReduceToIntoMetricAggregations moves a metric query's top-level reduceTo onto
|
||||
// its existing aggregations[] (where v5 wants it), which the shared migrator only does
|
||||
// when building an aggregation from flat fields. Runs before it so the value survives.
|
||||
func foldReduceToIntoMetricAggregations(query map[string]any) {
|
||||
if signalFromDataSource(query["dataSource"]) != telemetrytypes.SignalMetrics {
|
||||
return
|
||||
}
|
||||
reduceTo, ok := query["reduceTo"].(string)
|
||||
if !ok || reduceTo == "" {
|
||||
return
|
||||
}
|
||||
aggs, ok := query["aggregations"].([]any)
|
||||
if !ok || len(aggs) == 0 {
|
||||
return
|
||||
}
|
||||
for _, a := range aggs {
|
||||
agg, ok := a.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if _, exists := agg["reduceTo"]; !exists {
|
||||
agg["reduceTo"] = reduceTo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// normalizePreV5LogTraceAggregations reshapes an existing logs/traces aggregations[]
|
||||
// via parseAggregations (extract func(args), lift inline "as alias", split
|
||||
// multi-part, drop metric-only fields; empty → count()). Covers the case the
|
||||
|
||||
@@ -429,6 +429,11 @@ func TestResolveV1Image(t *testing.T) {
|
||||
assert.Equal(t, passthrough, resolveV1Image(passthrough))
|
||||
}
|
||||
|
||||
// A percent-encoded inline SVG icon is preserved by re-encoding it to base64
|
||||
// (the form v2 accepts) rather than being dropped to the default.
|
||||
svgResolved := resolveV1Image("data:image/svg+xml,%3Csvg%2F%3E")
|
||||
assert.Equal(t, "data:image/svg+xml;base64,PHN2Zy8+", svgResolved)
|
||||
|
||||
// A value that v2 would reject (a URL, a corrupt data URI) falls back to the
|
||||
// default eight-ball icon rather than failing the dashboard migration.
|
||||
for _, invalid := range []string{
|
||||
@@ -440,6 +445,66 @@ func TestResolveV1Image(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReencodeInlineSVGImage(t *testing.T) {
|
||||
testCases := []struct {
|
||||
scenario string
|
||||
image string
|
||||
wantRewritten bool
|
||||
want string
|
||||
}{
|
||||
{
|
||||
scenario: "percent-encoded inline svg",
|
||||
image: "data:image/svg+xml,%3Csvg%2F%3E",
|
||||
wantRewritten: true,
|
||||
want: "data:image/svg+xml;base64,PHN2Zy8+",
|
||||
},
|
||||
{
|
||||
scenario: "percent-encoded inline svg with charset param",
|
||||
image: "data:image/svg+xml;charset=utf-8,%3Csvg%2F%3E",
|
||||
wantRewritten: true,
|
||||
want: "data:image/svg+xml;base64,PHN2Zy8+",
|
||||
},
|
||||
{
|
||||
scenario: "already base64 svg is left unchanged",
|
||||
image: "data:image/svg+xml;base64,PHN2Zy8+",
|
||||
wantRewritten: false,
|
||||
want: "data:image/svg+xml;base64,PHN2Zy8+",
|
||||
},
|
||||
{
|
||||
scenario: "non-svg data uri is left unchanged",
|
||||
image: "data:image/png,%89PNG",
|
||||
wantRewritten: false,
|
||||
want: "data:image/png,%89PNG",
|
||||
},
|
||||
{
|
||||
scenario: "an icon path is left unchanged",
|
||||
image: "/assets/Icons/eight-ball",
|
||||
wantRewritten: false,
|
||||
want: "/assets/Icons/eight-ball",
|
||||
},
|
||||
{
|
||||
scenario: "a longer media type merely sharing the svg prefix is left unchanged",
|
||||
image: "data:image/svg+xmlish,%3Csvg%2F%3E",
|
||||
wantRewritten: false,
|
||||
want: "data:image/svg+xmlish,%3Csvg%2F%3E",
|
||||
},
|
||||
{
|
||||
scenario: "empty is left unchanged",
|
||||
image: "",
|
||||
wantRewritten: false,
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.scenario, func(t *testing.T) {
|
||||
got, rewritten := reencodeInlineSVGImage(testCase.image)
|
||||
assert.Equal(t, testCase.wantRewritten, rewritten)
|
||||
assert.Equal(t, testCase.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertV1ToV2ReplacesInvalidImage(t *testing.T) {
|
||||
// An image v2 would reject must not sink the whole migration — it falls back
|
||||
// to the default icon so the dashboard still converts.
|
||||
@@ -459,6 +524,48 @@ func TestConvertV1ToV2ReplacesInvalidImage(t *testing.T) {
|
||||
assert.Equal(t, "/assets/Icons/eight-ball", dashboard.Image)
|
||||
}
|
||||
|
||||
func TestFoldReduceToIntoMetricAggregations(t *testing.T) {
|
||||
// A metric query's top-level reduceTo lands on its aggregation (where v5 wants it).
|
||||
query := map[string]any{
|
||||
"dataSource": "metrics",
|
||||
"reduceTo": "sum",
|
||||
"aggregations": []any{map[string]any{"metricName": "m", "spaceAggregation": "sum"}},
|
||||
}
|
||||
foldReduceToIntoMetricAggregations(query)
|
||||
assert.Equal(t, "sum", query["aggregations"].([]any)[0].(map[string]any)["reduceTo"])
|
||||
|
||||
// An aggregation that already carries a reduceTo is not overwritten.
|
||||
query = map[string]any{
|
||||
"dataSource": "metrics",
|
||||
"reduceTo": "sum",
|
||||
"aggregations": []any{map[string]any{"metricName": "m", "reduceTo": "avg"}},
|
||||
}
|
||||
foldReduceToIntoMetricAggregations(query)
|
||||
assert.Equal(t, "avg", query["aggregations"].([]any)[0].(map[string]any)["reduceTo"])
|
||||
}
|
||||
|
||||
func TestMapV1SelectFieldsPicksBySignal(t *testing.T) {
|
||||
d := &v1Decoder{}
|
||||
// A list widget carrying both arrays; the query's signal must decide which wins.
|
||||
widget := func(dataSource string) map[string]any {
|
||||
return map[string]any{
|
||||
"query": map[string]any{
|
||||
"builder": map[string]any{"queryData": []any{map[string]any{"dataSource": dataSource}}},
|
||||
},
|
||||
"selectedLogFields": []any{map[string]any{"name": "log_field"}},
|
||||
"selectedTracesFields": []any{map[string]any{"name": "trace_field"}},
|
||||
}
|
||||
}
|
||||
|
||||
logs := d.mapV1SelectFields(widget("logs"))
|
||||
require.Len(t, logs, 1)
|
||||
assert.Equal(t, "log_field", logs[0].Name)
|
||||
|
||||
traces := d.mapV1SelectFields(widget("traces"))
|
||||
require.Len(t, traces, 1)
|
||||
assert.Equal(t, "trace_field", traces[0].Name)
|
||||
}
|
||||
|
||||
func TestConvertV1ToV2RejectsAlreadyV2(t *testing.T) {
|
||||
storable := &StorableDashboard{
|
||||
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package dashboardtypes
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/errors"
|
||||
|
||||
// The v1 dashboard API is superseded by the v2 dashboard API; every v1 endpoint
|
||||
// now returns a deprecation error pointing at its v2 replacement. The body-carrying
|
||||
// endpoints (create, update) point to the v2 request-schema docs below; the
|
||||
// body-less endpoints just name the v2 endpoint to call.
|
||||
const (
|
||||
V2CreateDashboardSchemaLink = "https://signoz.io/dashboards/schema/dashboard_create.json"
|
||||
V2UpdateDashboardSchemaLink = "https://signoz.io/dashboards/schema/dashboard_update.json"
|
||||
V2PatchDashboardSchemaLink = "https://signoz.io/dashboards/schema/dashboard_patch.json"
|
||||
)
|
||||
|
||||
// NewV1DeprecatedError builds the error a deprecated v1 dashboard endpoint returns.
|
||||
// useInstead names the v2 replacement — a schema link for the create/update
|
||||
// endpoints, or the v2 endpoint to call for the body-less ones.
|
||||
func NewV1DeprecatedError(useInstead string) error {
|
||||
return errors.Newf(errors.TypeUnsupported, ErrCodeDashboardV1Deprecated,
|
||||
"the v1 dashboard API is deprecated; instead, %s", useInstead)
|
||||
}
|
||||
Reference in New Issue
Block a user