mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-07 23:20:37 +01:00
Compare commits
2 Commits
issue-5535
...
nv/list-v2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
181878f1df | ||
|
|
db54237f77 |
@@ -3148,6 +3148,8 @@ components:
|
||||
type: string
|
||||
image:
|
||||
type: string
|
||||
legacy:
|
||||
type: boolean
|
||||
locked:
|
||||
type: boolean
|
||||
name:
|
||||
@@ -3180,6 +3182,7 @@ components:
|
||||
- name
|
||||
- tags
|
||||
- spec
|
||||
- legacy
|
||||
- pinned
|
||||
type: object
|
||||
DashboardtypesListedDashboardV2:
|
||||
@@ -3193,6 +3196,8 @@ components:
|
||||
type: string
|
||||
image:
|
||||
type: string
|
||||
legacy:
|
||||
type: boolean
|
||||
locked:
|
||||
type: boolean
|
||||
name:
|
||||
@@ -3223,6 +3228,7 @@ components:
|
||||
- name
|
||||
- tags
|
||||
- spec
|
||||
- legacy
|
||||
type: object
|
||||
DashboardtypesListedDashboardV2Spec:
|
||||
properties:
|
||||
|
||||
@@ -5005,6 +5005,10 @@ export interface DashboardtypesListedDashboardForUserV2DTO {
|
||||
* @type string
|
||||
*/
|
||||
image?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
legacy: boolean;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
@@ -5080,6 +5084,10 @@ export interface DashboardtypesListedDashboardV2DTO {
|
||||
* @type string
|
||||
*/
|
||||
image?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
legacy: boolean;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
|
||||
@@ -71,7 +71,7 @@ func (module *module) ListV2(ctx context.Context, orgID valuer.UUID, params *das
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dashboardtypes.NewListableDashboardV2(dashboards, total, tagsByDashboard, allTags)
|
||||
return dashboardtypes.NewListableDashboardV2(dashboards, total, tagsByDashboard, allTags), nil
|
||||
}
|
||||
|
||||
func (module *module) ListForUserV2(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, params *dashboardtypes.ListDashboardsV2Params) (*dashboardtypes.ListableDashboardForUserV2, error) {
|
||||
@@ -90,7 +90,7 @@ func (module *module) ListForUserV2(ctx context.Context, orgID valuer.UUID, user
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dashboardtypes.NewListableDashboardForUserV2(rows, total, tagsByDashboard, allTags)
|
||||
return dashboardtypes.NewListableDashboardForUserV2(rows, total, tagsByDashboard, allTags), nil
|
||||
}
|
||||
|
||||
func (module *module) fetchDashboardTags(ctx context.Context, orgID valuer.UUID, dashboardIDs []valuer.UUID) (map[valuer.UUID][]*tagtypes.Tag, []*tagtypes.Tag, error) {
|
||||
|
||||
@@ -64,6 +64,13 @@ type (
|
||||
ListableDashboard []*GettableDashboard
|
||||
)
|
||||
|
||||
// readString reads a string field from the untyped data blob, yielding "" when
|
||||
// the key is absent, null, or not a string.
|
||||
func (d StorableDashboardData) readString(key string) string {
|
||||
s, _ := d[key].(string)
|
||||
return s
|
||||
}
|
||||
|
||||
func NewStorableDashboardFromDashboard(dashboard *Dashboard) (*StorableDashboard, error) {
|
||||
dashboardID, err := valuer.NewUUID(dashboard.ID)
|
||||
if err != nil {
|
||||
|
||||
@@ -122,6 +122,10 @@ type listedDashboardV2 struct {
|
||||
Image string `json:"image,omitempty"`
|
||||
Tags []*tagtypes.GettableTag `json:"tags" required:"true" nullable:"false"`
|
||||
Spec listedDashboardV2Spec `json:"spec" required:"true"`
|
||||
// Legacy marks a dashboard whose stored data is not yet in the v2 (perses)
|
||||
// schema. Such rows are extracted best-effort from the v1 shape so the list
|
||||
// still surfaces them; callers should route them to the legacy view.
|
||||
Legacy bool `json:"legacy" required:"true"`
|
||||
}
|
||||
|
||||
type listedDashboardV2Spec struct {
|
||||
@@ -144,6 +148,41 @@ func newListedDashboardV2(v2 *DashboardV2) *listedDashboardV2 {
|
||||
}
|
||||
}
|
||||
|
||||
// newListedDashboardForList builds the list view for a single dashboard. A row
|
||||
// whose stored data is not in the v2 (perses) schema is extracted best-effort
|
||||
// from the v1 shape and flagged Legacy, so one legacy or malformed dashboard
|
||||
// can't fail the whole list.
|
||||
func newListedDashboardForList(storable *StorableDashboard, tags []*tagtypes.Tag) *listedDashboardV2 {
|
||||
if v2, err := storable.ToDashboardV2(tags); err == nil {
|
||||
return newListedDashboardV2(v2)
|
||||
}
|
||||
return newLegacyListedDashboardV2(storable, tags)
|
||||
}
|
||||
|
||||
// newLegacyListedDashboardV2 pulls the display-relevant fields out of a v1
|
||||
// dashboard blob. Column-backed fields (name, source, timestamps, …) come
|
||||
// straight off the row; title/description/image/version are read leniently from
|
||||
// the untyped v1 data, defaulting to zero when absent or of the wrong type.
|
||||
func newLegacyListedDashboardV2(storable *StorableDashboard, tags []*tagtypes.Tag) *listedDashboardV2 {
|
||||
return &listedDashboardV2{
|
||||
Identifiable: storable.Identifiable,
|
||||
TimeAuditable: storable.TimeAuditable,
|
||||
UserAuditable: storable.UserAuditable,
|
||||
OrgID: storable.OrgID,
|
||||
Locked: storable.Locked,
|
||||
Source: storable.Source,
|
||||
SchemaVersion: storable.Data.readString("version"),
|
||||
Name: storable.Name,
|
||||
Image: storable.Data.readString("image"),
|
||||
Tags: tagtypes.NewGettableTagsFromTags(tags),
|
||||
Spec: listedDashboardV2Spec{Display: Display{
|
||||
Name: storable.Data.readString("title"),
|
||||
Description: storable.Data.readString("description"),
|
||||
}},
|
||||
Legacy: true,
|
||||
}
|
||||
}
|
||||
|
||||
type ListableDashboardV2 struct {
|
||||
Dashboards []*listedDashboardV2 `json:"dashboards" required:"true" nullable:"false"`
|
||||
Total int64 `json:"total" required:"true"`
|
||||
@@ -151,21 +190,17 @@ type ListableDashboardV2 struct {
|
||||
ReservedKeywords []DSLKey `json:"reservedKeywords" required:"true" nullable:"false"`
|
||||
}
|
||||
|
||||
func NewListableDashboardV2(dashboards []*StorableDashboard, total int64, tagsByEntity map[valuer.UUID][]*tagtypes.Tag, allTags []*tagtypes.Tag) (*ListableDashboardV2, error) {
|
||||
func NewListableDashboardV2(dashboards []*StorableDashboard, total int64, tagsByEntity map[valuer.UUID][]*tagtypes.Tag, allTags []*tagtypes.Tag) *ListableDashboardV2 {
|
||||
items := make([]*listedDashboardV2, len(dashboards))
|
||||
for i, d := range dashboards {
|
||||
v2, err := d.ToDashboardV2(tagsByEntity[d.ID])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items[i] = newListedDashboardV2(v2)
|
||||
items[i] = newListedDashboardForList(d, tagsByEntity[d.ID])
|
||||
}
|
||||
return &ListableDashboardV2{
|
||||
Dashboards: items,
|
||||
Total: total,
|
||||
Tags: tagtypes.NewGettableTagsFromTags(allTags),
|
||||
ReservedKeywords: ReservedFilterKeys(),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// listedDashboardForUserV2 is a listed dashboard plus the calling user's pin
|
||||
@@ -190,15 +225,11 @@ type StorableDashboardWithPinInfo struct {
|
||||
Pinned bool
|
||||
}
|
||||
|
||||
func NewListableDashboardForUserV2(rows []*StorableDashboardWithPinInfo, total int64, tagsByEntity map[valuer.UUID][]*tagtypes.Tag, allTags []*tagtypes.Tag) (*ListableDashboardForUserV2, error) {
|
||||
func NewListableDashboardForUserV2(rows []*StorableDashboardWithPinInfo, total int64, tagsByEntity map[valuer.UUID][]*tagtypes.Tag, allTags []*tagtypes.Tag) *ListableDashboardForUserV2 {
|
||||
items := make([]*listedDashboardForUserV2, len(rows))
|
||||
for i, r := range rows {
|
||||
v2, err := r.Dashboard.ToDashboardV2(tagsByEntity[r.Dashboard.ID])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items[i] = &listedDashboardForUserV2{
|
||||
listedDashboardV2: *newListedDashboardV2(v2),
|
||||
listedDashboardV2: *newListedDashboardForList(r.Dashboard, tagsByEntity[r.Dashboard.ID]),
|
||||
Pinned: r.Pinned,
|
||||
}
|
||||
}
|
||||
@@ -207,5 +238,5 @@ func NewListableDashboardForUserV2(rows []*StorableDashboardWithPinInfo, total i
|
||||
Total: total,
|
||||
Tags: tagtypes.NewGettableTagsFromTags(allTags),
|
||||
ReservedKeywords: ReservedFilterKeys(),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,6 +305,146 @@ func TestNextCloneDisplayName(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewLegacyListedDashboardV2(t *testing.T) {
|
||||
orgID := valuer.GenerateUUID()
|
||||
dashboardID := valuer.GenerateUUID()
|
||||
createdAt := time.Date(2026, time.January, 1, 12, 0, 0, 0, time.UTC)
|
||||
updatedAt := time.Date(2026, time.January, 2, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
t.Run("extracts display fields from a well-formed v1 blob", func(t *testing.T) {
|
||||
storable := &StorableDashboard{
|
||||
Identifiable: types.Identifiable{ID: dashboardID},
|
||||
TimeAuditable: types.TimeAuditable{CreatedAt: createdAt, UpdatedAt: updatedAt},
|
||||
UserAuditable: types.UserAuditable{CreatedBy: "alice", UpdatedBy: "bob"},
|
||||
OrgID: orgID,
|
||||
Locked: true,
|
||||
Source: SourceUser,
|
||||
Name: "legacy-dashboard",
|
||||
Data: StorableDashboardData{
|
||||
"title": "Legacy Title",
|
||||
"description": "an old v1 dashboard",
|
||||
"image": "data:image/png;base64,xyz",
|
||||
"version": "v5",
|
||||
"widgets": []any{},
|
||||
},
|
||||
}
|
||||
listed := newLegacyListedDashboardV2(storable, nil)
|
||||
|
||||
assert.True(t, listed.Legacy, "a non-v2 dashboard must be flagged legacy")
|
||||
assert.Equal(t, storable.Identifiable, listed.Identifiable)
|
||||
assert.Equal(t, storable.TimeAuditable, listed.TimeAuditable)
|
||||
assert.Equal(t, storable.UserAuditable, listed.UserAuditable)
|
||||
assert.Equal(t, orgID, listed.OrgID)
|
||||
assert.True(t, listed.Locked)
|
||||
assert.Equal(t, SourceUser, listed.Source)
|
||||
assert.Equal(t, "legacy-dashboard", listed.Name, "name comes off the column, not the blob")
|
||||
assert.Equal(t, "v5", listed.SchemaVersion)
|
||||
assert.Equal(t, "data:image/png;base64,xyz", listed.Image)
|
||||
assert.Equal(t, "Legacy Title", listed.Spec.Display.Name)
|
||||
assert.Equal(t, "an old v1 dashboard", listed.Spec.Display.Description)
|
||||
assert.Empty(t, listed.Tags, "v1 dashboards predate tags; nil converts to an empty, non-nil slice")
|
||||
})
|
||||
|
||||
t.Run("yields zero values for absent or wrongly-typed fields", func(t *testing.T) {
|
||||
storable := &StorableDashboard{
|
||||
OrgID: orgID,
|
||||
Source: SourceUser,
|
||||
Data: StorableDashboardData{
|
||||
"title": 42, // wrong type
|
||||
"version": []any{"v5"}, // wrong type
|
||||
"image": nil, // null
|
||||
},
|
||||
}
|
||||
|
||||
listed := newLegacyListedDashboardV2(storable, nil)
|
||||
|
||||
assert.True(t, listed.Legacy)
|
||||
assert.Empty(t, listed.Spec.Display.Name)
|
||||
assert.Empty(t, listed.SchemaVersion)
|
||||
assert.Empty(t, listed.Image)
|
||||
assert.Empty(t, listed.Tags, "nil tags convert to an empty, non-nil slice")
|
||||
})
|
||||
|
||||
t.Run("tolerates entirely empty data", func(t *testing.T) {
|
||||
storable := &StorableDashboard{OrgID: orgID, Source: SourceUser, Name: "bare"}
|
||||
|
||||
listed := newLegacyListedDashboardV2(storable, nil)
|
||||
|
||||
assert.True(t, listed.Legacy)
|
||||
assert.Equal(t, "bare", listed.Name)
|
||||
assert.Empty(t, listed.Spec.Display.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewListableDashboardV2MixedSchemas(t *testing.T) {
|
||||
orgID := valuer.GenerateUUID()
|
||||
|
||||
v2Dashboard := newTestDashboardV2(t, orgID, SourceUser)
|
||||
v2Storable, err := v2Dashboard.ToStorableDashboard()
|
||||
require.NoError(t, err)
|
||||
|
||||
v1Storable := &StorableDashboard{
|
||||
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
|
||||
OrgID: orgID,
|
||||
Source: SourceUser,
|
||||
Name: "legacy-dashboard",
|
||||
Data: StorableDashboardData{
|
||||
"title": "Legacy Title",
|
||||
"version": "v5",
|
||||
"widgets": []any{},
|
||||
},
|
||||
}
|
||||
|
||||
tagsByEntity := map[valuer.UUID][]*tagtypes.Tag{
|
||||
v2Storable.ID: v2Dashboard.Tags,
|
||||
}
|
||||
|
||||
listable := NewListableDashboardV2([]*StorableDashboard{v2Storable, v1Storable}, 2, tagsByEntity, nil)
|
||||
|
||||
require.Len(t, listable.Dashboards, 2, "a single legacy dashboard must not drop rows from the list")
|
||||
assert.Equal(t, int64(2), listable.Total)
|
||||
|
||||
v2Row := listable.Dashboards[0]
|
||||
assert.False(t, v2Row.Legacy, "a v2 dashboard is not legacy")
|
||||
assert.Equal(t, SchemaVersion, v2Row.SchemaVersion)
|
||||
assert.Equal(t, "Test Dashboard", v2Row.Spec.Display.Name)
|
||||
|
||||
v1Row := listable.Dashboards[1]
|
||||
assert.True(t, v1Row.Legacy, "a v1 dashboard is flagged legacy")
|
||||
assert.Equal(t, "v5", v1Row.SchemaVersion)
|
||||
assert.Equal(t, "Legacy Title", v1Row.Spec.Display.Name)
|
||||
assert.Equal(t, "legacy-dashboard", v1Row.Name)
|
||||
}
|
||||
|
||||
func TestNewListableDashboardForUserV2MixedSchemas(t *testing.T) {
|
||||
orgID := valuer.GenerateUUID()
|
||||
|
||||
v2Dashboard := newTestDashboardV2(t, orgID, SourceUser)
|
||||
v2Storable, err := v2Dashboard.ToStorableDashboard()
|
||||
require.NoError(t, err)
|
||||
|
||||
v1Storable := &StorableDashboard{
|
||||
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
|
||||
OrgID: orgID,
|
||||
Source: SourceUser,
|
||||
Name: "legacy-dashboard",
|
||||
Data: StorableDashboardData{"title": "Legacy Title", "version": "v5"},
|
||||
}
|
||||
|
||||
rows := []*StorableDashboardWithPinInfo{
|
||||
{Dashboard: v2Storable, Pinned: true},
|
||||
{Dashboard: v1Storable, Pinned: false},
|
||||
}
|
||||
|
||||
listable := NewListableDashboardForUserV2(rows, 2, nil, nil)
|
||||
|
||||
require.Len(t, listable.Dashboards, 2)
|
||||
assert.False(t, listable.Dashboards[0].Legacy)
|
||||
assert.True(t, listable.Dashboards[0].Pinned)
|
||||
assert.True(t, listable.Dashboards[1].Legacy)
|
||||
assert.False(t, listable.Dashboards[1].Pinned)
|
||||
}
|
||||
|
||||
func TestDashboardV2StorableRoundTrip(t *testing.T) {
|
||||
orgID := valuer.GenerateUUID()
|
||||
original := newTestDashboardV2(t, orgID, SourceIntegration)
|
||||
|
||||
Reference in New Issue
Block a user