Compare commits

..

1 Commits

Author SHA1 Message Date
Naman Verma
c30ba5414c chore: deprecate v1 dashboard APIs with proper reference to v2 APIs 2026-07-25 16:53:01 +05:30
6 changed files with 30 additions and 344 deletions

View File

@@ -2,7 +2,6 @@ package impldashboard
import (
"context"
"encoding/json"
"net/http"
"strconv"
"time"
@@ -13,10 +12,8 @@ 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"
@@ -33,190 +30,21 @@ func NewHandler(module dashboard.Module, providerSettings factory.ProviderSettin
}
func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) {
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)
render.Error(rw, dashboardtypes.NewV1DeprecatedError(
"create a dashboard with POST /api/v2/dashboards (schema: "+dashboardtypes.V2CreateDashboardSchemaLink+")"))
}
func (handler *handler) Update(rw http.ResponseWriter, r *http.Request) {
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)
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+")"))
}
func (handler *handler) LockUnlock(rw http.ResponseWriter, r *http.Request) {
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)
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"))
}
func (handler *handler) Delete(rw http.ResponseWriter, r *http.Request) {
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)
render.Error(rw, dashboardtypes.NewV1DeprecatedError("delete a dashboard with DELETE /api/v2/dashboards/{id}"))
}
func (handler *handler) CreatePublic(rw http.ResponseWriter, r *http.Request) {

View File

@@ -1078,75 +1078,11 @@ func prepareQuery(r *http.Request) (string, error) {
}
func (aH *APIHandler) Get(rw http.ResponseWriter, r *http.Request) {
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)
render.Error(rw, dashboardtypes.NewV1DeprecatedError("get a dashboard with GET /api/v2/dashboards/{id}"))
}
func (aH *APIHandler) List(rw http.ResponseWriter, r *http.Request) {
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)
render.Error(rw, dashboardtypes.NewV1DeprecatedError("list dashboards with GET /api/v2/dashboards"))
}
func (aH *APIHandler) queryDashboardVarsV2(w http.ResponseWriter, r *http.Request) {

View File

@@ -22,6 +22,7 @@ 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 {

View File

@@ -1,11 +1,5 @@
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
@@ -51,37 +45,8 @@ 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
}

View File

@@ -429,11 +429,6 @@ 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{
@@ -445,66 +440,6 @@ 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.

View File

@@ -0,0 +1,21 @@
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)
}