mirror of
https://github.com/SigNoz/signoz.git
synced 2026-08-10 15:00:47 +01:00
Compare commits
30 Commits
fix/codemi
...
ns/saved-v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c9c99403d | ||
|
|
aa7f0f7a02 | ||
|
|
819bbeeb73 | ||
|
|
b69c696e6c | ||
|
|
08eff15a62 | ||
|
|
b9e3ddca04 | ||
|
|
81abe54e1e | ||
|
|
7fafcbfbd8 | ||
|
|
c3def283f3 | ||
|
|
2246451b5e | ||
|
|
bfade5b070 | ||
|
|
377eb89f85 | ||
|
|
228a944956 | ||
|
|
ad15653789 | ||
|
|
450685ba3e | ||
|
|
6372af75a6 | ||
|
|
dc86f88bf7 | ||
|
|
c4eff4edcd | ||
|
|
b9069afb4d | ||
|
|
40a036f2b0 | ||
|
|
d752452d40 | ||
|
|
4a8a1efb03 | ||
|
|
7d2afa5a54 | ||
|
|
b579190140 | ||
|
|
6222204af7 | ||
|
|
0fcd6aeada | ||
|
|
32af113d0c | ||
|
|
61588b9303 | ||
|
|
dbc40efe24 | ||
|
|
33a428496b |
1
.github/workflows/integrationci.yaml
vendored
1
.github/workflows/integrationci.yaml
vendored
@@ -61,6 +61,7 @@ jobs:
|
||||
- querierauthz
|
||||
- role
|
||||
- rootuser
|
||||
- savedview
|
||||
- serviceaccount
|
||||
- spanmapper
|
||||
- querier_json_body
|
||||
|
||||
@@ -7880,17 +7880,23 @@ components:
|
||||
type: string
|
||||
SavedviewtypesPostableSavedView:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewData'
|
||||
generateName:
|
||||
type: boolean
|
||||
name:
|
||||
type: string
|
||||
requestType:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5RequestType'
|
||||
schemaVersion:
|
||||
$ref: '#/components/schemas/SavedviewtypesSchemaVersion'
|
||||
source:
|
||||
$ref: '#/components/schemas/SavedviewtypesSource'
|
||||
spec:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewSpec'
|
||||
required:
|
||||
- source
|
||||
- data
|
||||
- schemaVersion
|
||||
- requestType
|
||||
- spec
|
||||
type: object
|
||||
SavedviewtypesSavedView:
|
||||
properties:
|
||||
@@ -7899,14 +7905,16 @@ components:
|
||||
type: string
|
||||
createdBy:
|
||||
type: string
|
||||
data:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewData'
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
schemaVersion:
|
||||
$ref: '#/components/schemas/SavedviewtypesSchemaVersion'
|
||||
source:
|
||||
$ref: '#/components/schemas/SavedviewtypesSource'
|
||||
spec:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewSpec'
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
@@ -7914,14 +7922,6 @@ components:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
type: object
|
||||
SavedviewtypesSavedViewData:
|
||||
properties:
|
||||
schemaVersion:
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewSpec'
|
||||
required:
|
||||
- schemaVersion
|
||||
- spec
|
||||
type: object
|
||||
@@ -7936,6 +7936,7 @@ components:
|
||||
queries:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryEnvelope'
|
||||
minItems: 1
|
||||
type: array
|
||||
selectedFields:
|
||||
items:
|
||||
@@ -7945,9 +7946,11 @@ components:
|
||||
- displayName
|
||||
- panelType
|
||||
- queries
|
||||
- selectedFields
|
||||
- display
|
||||
type: object
|
||||
SavedviewtypesSchemaVersion:
|
||||
enum:
|
||||
- v2
|
||||
type: string
|
||||
SavedviewtypesSource:
|
||||
enum:
|
||||
- traces
|
||||
@@ -7957,13 +7960,19 @@ components:
|
||||
type: string
|
||||
SavedviewtypesUpdatableSavedView:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewData'
|
||||
requestType:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5RequestType'
|
||||
schemaVersion:
|
||||
$ref: '#/components/schemas/SavedviewtypesSchemaVersion'
|
||||
source:
|
||||
$ref: '#/components/schemas/SavedviewtypesSource'
|
||||
spec:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewSpec'
|
||||
required:
|
||||
- source
|
||||
- data
|
||||
- schemaVersion
|
||||
- requestType
|
||||
- spec
|
||||
type: object
|
||||
ServiceaccounttypesDeprecatedPostableServiceAccountRole:
|
||||
properties:
|
||||
@@ -22972,6 +22981,12 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"409":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Conflict
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
|
||||
@@ -8991,8 +8991,17 @@ export enum SavedviewtypesPanelTypeDTO {
|
||||
list = 'list',
|
||||
trace = 'trace',
|
||||
}
|
||||
export enum SavedviewtypesSchemaVersionDTO {
|
||||
v2 = 'v2',
|
||||
}
|
||||
export enum SavedviewtypesSourceDTO {
|
||||
traces = 'traces',
|
||||
logs = 'logs',
|
||||
metrics = 'metrics',
|
||||
meter = 'meter',
|
||||
}
|
||||
export interface SavedviewtypesSavedViewSpecDTO {
|
||||
display: SavedviewtypesDisplayDTO;
|
||||
display?: SavedviewtypesDisplayDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -9005,25 +9014,10 @@ export interface SavedviewtypesSavedViewSpecDTO {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
selectedFields: TelemetrytypesTelemetryFieldKeyDTO[];
|
||||
selectedFields?: TelemetrytypesTelemetryFieldKeyDTO[];
|
||||
}
|
||||
|
||||
export interface SavedviewtypesSavedViewDataDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
schemaVersion: string;
|
||||
spec: SavedviewtypesSavedViewSpecDTO;
|
||||
}
|
||||
|
||||
export enum SavedviewtypesSourceDTO {
|
||||
traces = 'traces',
|
||||
logs = 'logs',
|
||||
metrics = 'metrics',
|
||||
meter = 'meter',
|
||||
}
|
||||
export interface SavedviewtypesPostableSavedViewDTO {
|
||||
data: SavedviewtypesSavedViewDataDTO;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
@@ -9032,7 +9026,10 @@ export interface SavedviewtypesPostableSavedViewDTO {
|
||||
* @type string
|
||||
*/
|
||||
name?: string;
|
||||
requestType: Querybuildertypesv5RequestTypeDTO;
|
||||
schemaVersion: SavedviewtypesSchemaVersionDTO;
|
||||
source: SavedviewtypesSourceDTO;
|
||||
spec: SavedviewtypesSavedViewSpecDTO;
|
||||
}
|
||||
|
||||
export interface SavedviewtypesSavedViewDTO {
|
||||
@@ -9045,7 +9042,6 @@ export interface SavedviewtypesSavedViewDTO {
|
||||
* @type string
|
||||
*/
|
||||
createdBy?: string;
|
||||
data?: SavedviewtypesSavedViewDataDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -9054,7 +9050,9 @@ export interface SavedviewtypesSavedViewDTO {
|
||||
* @type string
|
||||
*/
|
||||
name?: string;
|
||||
schemaVersion: SavedviewtypesSchemaVersionDTO;
|
||||
source?: SavedviewtypesSourceDTO;
|
||||
spec: SavedviewtypesSavedViewSpecDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
@@ -9067,8 +9065,10 @@ export interface SavedviewtypesSavedViewDTO {
|
||||
}
|
||||
|
||||
export interface SavedviewtypesUpdatableSavedViewDTO {
|
||||
data: SavedviewtypesSavedViewDataDTO;
|
||||
requestType: Querybuildertypesv5RequestTypeDTO;
|
||||
schemaVersion: SavedviewtypesSchemaVersionDTO;
|
||||
source: SavedviewtypesSourceDTO;
|
||||
spec: SavedviewtypesSavedViewSpecDTO;
|
||||
}
|
||||
|
||||
export interface ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO {
|
||||
|
||||
@@ -51,7 +51,7 @@ func (provider *provider) addSavedViewRoutes(router *mux.Router) error {
|
||||
Response: new(types.Identifiable),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceSavedView.Scope(coretypes.VerbCreate)}),
|
||||
},
|
||||
|
||||
@@ -39,26 +39,28 @@ type legacyExtraData struct {
|
||||
func newPostableSavedViewFromLegacyView(v *v3.SavedView) savedviewtypes.PostableSavedView {
|
||||
var legacy legacyExtraData
|
||||
if v.ExtraData != "" {
|
||||
// Best-effort: malformed/older extraData shapes never fail the request
|
||||
// Best-effort: malformed/older extraData shapes never fail the request.
|
||||
_ = json.Unmarshal([]byte(v.ExtraData), &legacy)
|
||||
}
|
||||
|
||||
panelType := savedviewtypes.PanelType{String: valuer.NewString(string(v.CompositeQuery.PanelType))}
|
||||
|
||||
return savedviewtypes.PostableSavedView{
|
||||
GenerateName: true,
|
||||
Source: savedviewtypes.Source{String: valuer.NewString(v.SourcePage)},
|
||||
Data: savedviewtypes.SavedViewData{
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: v.Name,
|
||||
PanelType: savedviewtypes.PanelType{String: valuer.NewString(string(v.CompositeQuery.PanelType))},
|
||||
Queries: v.CompositeQuery.Queries,
|
||||
SelectedFields: legacy.SelectColumns,
|
||||
Display: savedviewtypes.Display{
|
||||
MaxLines: legacy.MaxLines,
|
||||
FontSize: legacy.FontSize,
|
||||
Format: legacy.Format,
|
||||
Color: legacy.Color,
|
||||
},
|
||||
GenerateName: true,
|
||||
Source: savedviewtypes.Source{String: valuer.NewString(v.SourcePage)},
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
// v1 has no requestType concept of its own -- fall back to the panelType-derived guess.
|
||||
RequestType: savedviewtypes.LegacyRequestTypeForPanelType(panelType),
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: v.Name,
|
||||
PanelType: panelType,
|
||||
Queries: v.CompositeQuery.Queries,
|
||||
SelectedFields: legacy.SelectColumns,
|
||||
Display: savedviewtypes.Display{
|
||||
MaxLines: legacy.MaxLines,
|
||||
FontSize: legacy.FontSize,
|
||||
Format: legacy.Format,
|
||||
Color: legacy.Color,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -68,25 +70,27 @@ func newPostableSavedViewFromLegacyView(v *v3.SavedView) savedviewtypes.Postable
|
||||
func newUpdatableSavedViewFromLegacyView(v *v3.SavedView) savedviewtypes.UpdatableSavedView {
|
||||
var legacy legacyExtraData
|
||||
if v.ExtraData != "" {
|
||||
// Best-effort: malformed/older extraData shapes never fail the request
|
||||
// Best-effort: malformed/older extraData shapes never fail the request.
|
||||
_ = json.Unmarshal([]byte(v.ExtraData), &legacy)
|
||||
}
|
||||
|
||||
panelType := savedviewtypes.PanelType{String: valuer.NewString(string(v.CompositeQuery.PanelType))}
|
||||
|
||||
return savedviewtypes.UpdatableSavedView{
|
||||
Source: savedviewtypes.Source{String: valuer.NewString(v.SourcePage)},
|
||||
Data: savedviewtypes.SavedViewData{
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: v.Name,
|
||||
PanelType: savedviewtypes.PanelType{String: valuer.NewString(string(v.CompositeQuery.PanelType))},
|
||||
Queries: v.CompositeQuery.Queries,
|
||||
SelectedFields: legacy.SelectColumns,
|
||||
Display: savedviewtypes.Display{
|
||||
MaxLines: legacy.MaxLines,
|
||||
FontSize: legacy.FontSize,
|
||||
Format: legacy.Format,
|
||||
Color: legacy.Color,
|
||||
},
|
||||
Source: savedviewtypes.Source{String: valuer.NewString(v.SourcePage)},
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
// v1 has no requestType concept of its own -- fall back to the panelType-derived guess.
|
||||
RequestType: savedviewtypes.LegacyRequestTypeForPanelType(panelType),
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: v.Name,
|
||||
PanelType: panelType,
|
||||
Queries: v.CompositeQuery.Queries,
|
||||
SelectedFields: legacy.SelectColumns,
|
||||
Display: savedviewtypes.Display{
|
||||
MaxLines: legacy.MaxLines,
|
||||
FontSize: legacy.FontSize,
|
||||
Format: legacy.Format,
|
||||
Color: legacy.Color,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -95,11 +99,11 @@ func newUpdatableSavedViewFromLegacyView(v *v3.SavedView) savedviewtypes.Updatab
|
||||
// newLegacyViewFromSavedView renders a v2 SavedView back into the v1 shape.
|
||||
func newLegacyViewFromSavedView(v *savedviewtypes.SavedView) (*v3.SavedView, error) {
|
||||
extraData, err := json.Marshal(legacyExtraData{
|
||||
Color: v.Data.Spec.Display.Color,
|
||||
SelectColumns: v.Data.Spec.SelectedFields,
|
||||
Format: v.Data.Spec.Display.Format,
|
||||
MaxLines: v.Data.Spec.Display.MaxLines,
|
||||
FontSize: v.Data.Spec.Display.FontSize,
|
||||
Color: v.Spec.Display.Color,
|
||||
SelectColumns: v.Spec.SelectedFields,
|
||||
Format: v.Spec.Display.Format,
|
||||
MaxLines: v.Spec.Display.MaxLines,
|
||||
FontSize: v.Spec.Display.FontSize,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error in marshalling extra data")
|
||||
@@ -107,17 +111,17 @@ func newLegacyViewFromSavedView(v *savedviewtypes.SavedView) (*v3.SavedView, err
|
||||
|
||||
return &v3.SavedView{
|
||||
ID: v.ID,
|
||||
Name: v.Data.Spec.DisplayName,
|
||||
Name: v.Spec.DisplayName,
|
||||
CreatedAt: v.CreatedAt,
|
||||
CreatedBy: v.CreatedBy,
|
||||
UpdatedAt: v.UpdatedAt,
|
||||
UpdatedBy: v.UpdatedBy,
|
||||
SourcePage: v.Source.StringValue(),
|
||||
CompositeQuery: &v3.CompositeQuery{
|
||||
PanelType: v3.PanelType(v.Data.Spec.PanelType.StringValue()),
|
||||
PanelType: v3.PanelType(v.Spec.PanelType.StringValue()),
|
||||
// Saved views are only ever created from the explorer's builder mode.
|
||||
QueryType: v3.QueryTypeBuilder,
|
||||
Queries: v.Data.Spec.Queries,
|
||||
Queries: v.Spec.Queries,
|
||||
},
|
||||
ExtraData: string(extraData),
|
||||
}, nil
|
||||
@@ -156,7 +160,14 @@ func (handler *handler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
uuid, err := handler.module.CreateView(ctx, claims.OrgID, newPostableSavedViewFromLegacyView(&view))
|
||||
postable := newPostableSavedViewFromLegacyView(&view)
|
||||
|
||||
if err := postable.Validate(); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
uuid, err := handler.module.CreateView(ctx, claims.OrgID, postable)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
@@ -224,8 +235,14 @@ func (handler *handler) Update(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.module.UpdateView(ctx, claims.OrgID, viewUUID, newUpdatableSavedViewFromLegacyView(&view))
|
||||
if err != nil {
|
||||
updatable := newUpdatableSavedViewFromLegacyView(&view)
|
||||
|
||||
if err := updatable.Validate(); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := handler.module.UpdateView(ctx, claims.OrgID, viewUUID, updatable); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -42,13 +42,14 @@ func TestNewPostableSavedViewFromLegacyView(t *testing.T) {
|
||||
|
||||
assert.Empty(t, postable.Name, "v1 has no slug concept -- name must always be generated")
|
||||
assert.True(t, postable.GenerateName, "v1 has no slug concept -- name must always be generated")
|
||||
assert.Equal(t, "my view", postable.Data.Spec.DisplayName)
|
||||
assert.Equal(t, "my view", postable.Spec.DisplayName)
|
||||
assert.Equal(t, savedviewtypes.SourceLogs, postable.Source)
|
||||
assert.Equal(t, savedviewtypes.SavedViewSchemaVersion, postable.Data.SchemaVersion)
|
||||
assert.Equal(t, savedviewtypes.PanelTypeGraph, postable.Data.Spec.PanelType)
|
||||
assert.Equal(t, legacy.CompositeQuery.Queries, postable.Data.Spec.Queries)
|
||||
assert.Equal(t, []telemetrytypes.TelemetryFieldKey{{Name: "service.name"}}, postable.Data.Spec.SelectedFields)
|
||||
assert.Equal(t, savedviewtypes.Display{MaxLines: 10, FontSize: "large", Format: "table", Color: "blue"}, postable.Data.Spec.Display)
|
||||
assert.Equal(t, savedviewtypes.SavedViewSchemaVersion, postable.SchemaVersion)
|
||||
assert.Equal(t, savedviewtypes.PanelTypeGraph, postable.Spec.PanelType)
|
||||
assert.Equal(t, qbtypes.RequestTypeTimeSeries, postable.RequestType, "graph panel type must map to the time_series request type")
|
||||
assert.Equal(t, legacy.CompositeQuery.Queries, postable.Spec.Queries)
|
||||
assert.Equal(t, []telemetrytypes.TelemetryFieldKey{{Name: "service.name"}}, postable.Spec.SelectedFields)
|
||||
assert.Equal(t, savedviewtypes.Display{MaxLines: 10, FontSize: "large", Format: "table", Color: "blue"}, postable.Spec.Display)
|
||||
})
|
||||
|
||||
t.Run("empty extra data leaves display and selected fields zero-valued", func(t *testing.T) {
|
||||
@@ -64,8 +65,8 @@ func TestNewPostableSavedViewFromLegacyView(t *testing.T) {
|
||||
|
||||
postable := newPostableSavedViewFromLegacyView(legacy)
|
||||
|
||||
assert.Equal(t, savedviewtypes.Display{}, postable.Data.Spec.Display)
|
||||
assert.Nil(t, postable.Data.Spec.SelectedFields)
|
||||
assert.Equal(t, savedviewtypes.Display{}, postable.Spec.Display)
|
||||
assert.Nil(t, postable.Spec.SelectedFields)
|
||||
})
|
||||
|
||||
t.Run("malformed extra data is ignored, not an error", func(t *testing.T) {
|
||||
@@ -81,8 +82,47 @@ func TestNewPostableSavedViewFromLegacyView(t *testing.T) {
|
||||
|
||||
postable := newPostableSavedViewFromLegacyView(legacy)
|
||||
|
||||
assert.Equal(t, "malformed extra data", postable.Data.Spec.DisplayName)
|
||||
assert.Equal(t, savedviewtypes.Display{}, postable.Data.Spec.Display)
|
||||
assert.Equal(t, "malformed extra data", postable.Spec.DisplayName)
|
||||
assert.Equal(t, savedviewtypes.Display{}, postable.Spec.Display)
|
||||
})
|
||||
|
||||
t.Run("legacy validation gap: empty builderQueries map with no queries", func(t *testing.T) {
|
||||
legacy := &v3.SavedView{
|
||||
Name: "no real queries",
|
||||
SourcePage: "logs",
|
||||
CompositeQuery: &v3.CompositeQuery{
|
||||
PanelType: v3.PanelTypeGraph,
|
||||
QueryType: v3.QueryTypeBuilder,
|
||||
BuilderQueries: map[string]*v3.BuilderQuery{},
|
||||
},
|
||||
}
|
||||
|
||||
require.NoError(t, legacy.Validate(), "the legacy CompositeQuery check is expected to miss this")
|
||||
|
||||
postable := newPostableSavedViewFromLegacyView(legacy)
|
||||
assert.Error(t, postable.Validate(), "the converted postable must catch what the legacy check missed")
|
||||
})
|
||||
|
||||
t.Run("list panel query with no aggregation is valid", func(t *testing.T) {
|
||||
legacy := &v3.SavedView{
|
||||
Name: "raw list view",
|
||||
SourcePage: "traces",
|
||||
CompositeQuery: &v3.CompositeQuery{
|
||||
PanelType: v3.PanelTypeList,
|
||||
Queries: []qbtypes.QueryEnvelope{{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
Filter: &qbtypes.Filter{Expression: "db_name = 'two'"},
|
||||
},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
postable := newPostableSavedViewFromLegacyView(legacy)
|
||||
|
||||
assert.Equal(t, qbtypes.RequestTypeRaw, postable.RequestType, "list panel type must map to the raw request type")
|
||||
assert.NoError(t, postable.Validate(), "a raw list query must not be required to carry an aggregation")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -99,24 +139,23 @@ func TestNewUpdatableSavedViewFromLegacyView(t *testing.T) {
|
||||
|
||||
updatable := newUpdatableSavedViewFromLegacyView(legacy)
|
||||
|
||||
assert.Equal(t, "renamed view", updatable.Data.Spec.DisplayName)
|
||||
assert.Equal(t, "renamed view", updatable.Spec.DisplayName)
|
||||
assert.Equal(t, savedviewtypes.SourceTraces, updatable.Source)
|
||||
assert.Equal(t, qbtypes.RequestTypeScalar, updatable.RequestType, "table panel type must map to the scalar request type")
|
||||
}
|
||||
|
||||
func TestNewLegacyViewFromSavedView(t *testing.T) {
|
||||
now := time.Now()
|
||||
savedView := &savedviewtypes.SavedView{
|
||||
Name: "my-view-abc123ef",
|
||||
Source: savedviewtypes.SourceLogs,
|
||||
Data: savedviewtypes.SavedViewData{
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: "my view",
|
||||
PanelType: savedviewtypes.PanelTypeGraph,
|
||||
Queries: testQueries(),
|
||||
SelectedFields: []telemetrytypes.TelemetryFieldKey{{Name: "service.name"}},
|
||||
Display: savedviewtypes.Display{MaxLines: 10, FontSize: "large", Format: "table", Color: "blue"},
|
||||
},
|
||||
Name: "my-view-abc123ef",
|
||||
Source: savedviewtypes.SourceLogs,
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: "my view",
|
||||
PanelType: savedviewtypes.PanelTypeGraph,
|
||||
Queries: testQueries(),
|
||||
SelectedFields: []telemetrytypes.TelemetryFieldKey{{Name: "service.name"}},
|
||||
Display: savedviewtypes.Display{MaxLines: 10, FontSize: "large", Format: "table", Color: "blue"},
|
||||
},
|
||||
}
|
||||
savedView.ID = valuer.GenerateUUID()
|
||||
@@ -129,7 +168,7 @@ func TestNewLegacyViewFromSavedView(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, savedView.ID, legacy.ID)
|
||||
assert.Equal(t, savedView.Data.Spec.DisplayName, legacy.Name)
|
||||
assert.Equal(t, savedView.Spec.DisplayName, legacy.Name)
|
||||
assert.Equal(t, savedView.CreatedAt, legacy.CreatedAt)
|
||||
assert.Equal(t, savedView.CreatedBy, legacy.CreatedBy)
|
||||
assert.Equal(t, savedView.UpdatedAt, legacy.UpdatedAt)
|
||||
@@ -137,20 +176,20 @@ func TestNewLegacyViewFromSavedView(t *testing.T) {
|
||||
assert.Equal(t, "logs", legacy.SourcePage)
|
||||
assert.Equal(t, v3.PanelTypeGraph, legacy.CompositeQuery.PanelType)
|
||||
assert.Equal(t, v3.QueryTypeBuilder, legacy.CompositeQuery.QueryType)
|
||||
assert.Equal(t, savedView.Data.Spec.Queries, legacy.CompositeQuery.Queries)
|
||||
assert.Equal(t, savedView.Spec.Queries, legacy.CompositeQuery.Queries)
|
||||
|
||||
var extra legacyExtraData
|
||||
require.NoError(t, json.Unmarshal([]byte(legacy.ExtraData), &extra))
|
||||
assert.Equal(t, "blue", extra.Color)
|
||||
assert.Equal(t, savedView.Data.Spec.SelectedFields, extra.SelectColumns)
|
||||
assert.Equal(t, savedView.Spec.SelectedFields, extra.SelectColumns)
|
||||
assert.Equal(t, "table", extra.Format)
|
||||
assert.Equal(t, 10, extra.MaxLines)
|
||||
assert.Equal(t, "large", extra.FontSize)
|
||||
}
|
||||
|
||||
func TestNewLegacyViewsFromSavedViews(t *testing.T) {
|
||||
a := &savedviewtypes.SavedView{Name: "a-slug", Source: savedviewtypes.SourceLogs, Data: savedviewtypes.SavedViewData{Spec: savedviewtypes.SavedViewSpec{DisplayName: "a", PanelType: savedviewtypes.PanelTypeGraph, Queries: testQueries()}}}
|
||||
b := &savedviewtypes.SavedView{Name: "b-slug", Source: savedviewtypes.SourceTraces, Data: savedviewtypes.SavedViewData{Spec: savedviewtypes.SavedViewSpec{DisplayName: "b", PanelType: savedviewtypes.PanelTypeTable, Queries: testQueries()}}}
|
||||
a := &savedviewtypes.SavedView{Name: "a-slug", Source: savedviewtypes.SourceLogs, Spec: savedviewtypes.SavedViewSpec{DisplayName: "a", PanelType: savedviewtypes.PanelTypeGraph, Queries: testQueries()}}
|
||||
b := &savedviewtypes.SavedView{Name: "b-slug", Source: savedviewtypes.SourceTraces, Spec: savedviewtypes.SavedViewSpec{DisplayName: "b", PanelType: savedviewtypes.PanelTypeTable, Queries: testQueries()}}
|
||||
|
||||
legacyViews, err := newLegacyViewsFromSavedViews([]*savedviewtypes.SavedView{a, b})
|
||||
require.NoError(t, err)
|
||||
@@ -167,17 +206,15 @@ func TestNewLegacyViewsFromSavedViews(t *testing.T) {
|
||||
// slug (Name) is deliberately NOT part of this contract -- v1 never sees it.
|
||||
func TestLegacyViewRoundTrip(t *testing.T) {
|
||||
original := &savedviewtypes.SavedView{
|
||||
Name: "round-trip-abc123ef",
|
||||
Source: savedviewtypes.SourceMetrics,
|
||||
Data: savedviewtypes.SavedViewData{
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: "round trip",
|
||||
PanelType: savedviewtypes.PanelTypeTable,
|
||||
Queries: testQueries(),
|
||||
SelectedFields: []telemetrytypes.TelemetryFieldKey{{Name: "service.name"}},
|
||||
Display: savedviewtypes.Display{MaxLines: 5, FontSize: "small", Format: "list", Color: "red"},
|
||||
},
|
||||
Name: "round-trip-abc123ef",
|
||||
Source: savedviewtypes.SourceMetrics,
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: "round trip",
|
||||
PanelType: savedviewtypes.PanelTypeTable,
|
||||
Queries: testQueries(),
|
||||
SelectedFields: []telemetrytypes.TelemetryFieldKey{{Name: "service.name"}},
|
||||
Display: savedviewtypes.Display{MaxLines: 5, FontSize: "small", Format: "list", Color: "red"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -188,10 +225,37 @@ func TestLegacyViewRoundTrip(t *testing.T) {
|
||||
|
||||
assert.Empty(t, roundTripped.Name)
|
||||
assert.True(t, roundTripped.GenerateName)
|
||||
assert.Equal(t, original.Data.Spec.DisplayName, roundTripped.Data.Spec.DisplayName)
|
||||
assert.Equal(t, original.Spec.DisplayName, roundTripped.Spec.DisplayName)
|
||||
assert.Equal(t, original.Source, roundTripped.Source)
|
||||
assert.Equal(t, original.Data.Spec.PanelType, roundTripped.Data.Spec.PanelType)
|
||||
assert.Equal(t, original.Data.Spec.Queries, roundTripped.Data.Spec.Queries)
|
||||
assert.Equal(t, original.Data.Spec.SelectedFields, roundTripped.Data.Spec.SelectedFields)
|
||||
assert.Equal(t, original.Data.Spec.Display, roundTripped.Data.Spec.Display)
|
||||
assert.Equal(t, original.Spec.PanelType, roundTripped.Spec.PanelType)
|
||||
assert.Equal(t, original.Spec.Queries, roundTripped.Spec.Queries)
|
||||
assert.Equal(t, original.Spec.SelectedFields, roundTripped.Spec.SelectedFields)
|
||||
assert.Equal(t, original.Spec.Display, roundTripped.Spec.Display)
|
||||
}
|
||||
|
||||
func TestLegacyViewRoundTrip_EmptySelectedFieldsAndDisplay(t *testing.T) {
|
||||
original := &savedviewtypes.SavedView{
|
||||
Name: "round-trip-empty-abc123ef",
|
||||
Source: savedviewtypes.SourceMetrics,
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: "round trip empty",
|
||||
PanelType: savedviewtypes.PanelTypeTable,
|
||||
Queries: testQueries(),
|
||||
SelectedFields: []telemetrytypes.TelemetryFieldKey{},
|
||||
Display: savedviewtypes.Display{},
|
||||
},
|
||||
}
|
||||
|
||||
legacy, err := newLegacyViewFromSavedView(original)
|
||||
require.NoError(t, err)
|
||||
|
||||
var extra legacyExtraData
|
||||
require.NoError(t, json.Unmarshal([]byte(legacy.ExtraData), &extra))
|
||||
assert.Nil(t, extra.SelectColumns, "omitempty drops an empty selectColumns from extraData entirely")
|
||||
|
||||
roundTripped := newPostableSavedViewFromLegacyView(legacy)
|
||||
|
||||
assert.Empty(t, roundTripped.Spec.SelectedFields, "empty, not necessarily non-nil, on this leg of the round trip")
|
||||
assert.Equal(t, savedviewtypes.Display{}, roundTripped.Spec.Display)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,11 @@ func NewModule(store savedviewtypes.Store) savedview.Module {
|
||||
}
|
||||
|
||||
func (module *module) GetViewsForFilters(ctx context.Context, orgID string, source savedviewtypes.Source, name string) ([]*savedviewtypes.SavedView, error) {
|
||||
return module.store.List(ctx, orgID, source, name)
|
||||
storables, err := module.store.List(ctx, orgID, source, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return savedviewtypes.NewSavedViewsFromStorableSavedViews(storables), nil
|
||||
}
|
||||
|
||||
func (module *module) CreateView(ctx context.Context, orgID string, view savedviewtypes.PostableSavedView) (valuer.UUID, error) {
|
||||
@@ -30,14 +34,19 @@ func (module *module) CreateView(ctx context.Context, orgID string, view savedvi
|
||||
|
||||
dbView := view.ToSavedView(orgID, claims.Email)
|
||||
|
||||
if err := module.store.Create(ctx, dbView); err != nil {
|
||||
if err := module.store.Create(ctx, savedviewtypes.NewStorableSavedView(dbView)); err != nil {
|
||||
return valuer.UUID{}, err
|
||||
}
|
||||
return dbView.ID, nil
|
||||
}
|
||||
|
||||
func (module *module) GetView(ctx context.Context, orgID string, uuid valuer.UUID) (*savedviewtypes.SavedView, error) {
|
||||
return module.store.Get(ctx, orgID, uuid)
|
||||
storable, err := module.store.Get(ctx, orgID, uuid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return storable.ToSavedView(), nil
|
||||
}
|
||||
|
||||
func (module *module) UpdateView(ctx context.Context, orgID string, uuid valuer.UUID, view savedviewtypes.UpdatableSavedView) error {
|
||||
@@ -46,7 +55,8 @@ func (module *module) UpdateView(ctx context.Context, orgID string, uuid valuer.
|
||||
return errors.NewInternalf(errors.CodeInternal, "error in getting email from context")
|
||||
}
|
||||
|
||||
return module.store.Update(ctx, view.ToSavedView(uuid, orgID, claims.Email))
|
||||
dbView := view.ToSavedView(uuid, orgID, claims.Email)
|
||||
return module.store.Update(ctx, savedviewtypes.NewStorableSavedView(dbView))
|
||||
}
|
||||
|
||||
func (module *module) DeleteView(ctx context.Context, orgID string, uuid valuer.UUID) error {
|
||||
@@ -54,10 +64,10 @@ func (module *module) DeleteView(ctx context.Context, orgID string, uuid valuer.
|
||||
}
|
||||
|
||||
func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) {
|
||||
savedViews, err := module.store.List(ctx, orgID.StringValue(), savedviewtypes.Source{}, "")
|
||||
storables, err := module.store.List(ctx, orgID.StringValue(), savedviewtypes.Source{}, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return savedviewtypes.NewStatsFromSavedViews(savedViews), nil
|
||||
return savedviewtypes.NewStatsFromStorableSavedViews(storables), nil
|
||||
}
|
||||
|
||||
@@ -28,24 +28,22 @@ func newTestStore() (savedview.Module, *savedviewtypestest.StoreTest) {
|
||||
|
||||
func testPostableSavedView(name string, source savedviewtypes.Source) savedviewtypes.PostableSavedView {
|
||||
return savedviewtypes.PostableSavedView{
|
||||
Name: name,
|
||||
Source: source,
|
||||
Data: savedviewtypes.SavedViewData{
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: name,
|
||||
PanelType: savedviewtypes.PanelTypeGraph,
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Aggregations: []qbtypes.LogAggregation{{Expression: "count()"}},
|
||||
},
|
||||
Name: name,
|
||||
Source: source,
|
||||
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
|
||||
Spec: savedviewtypes.SavedViewSpec{
|
||||
DisplayName: name,
|
||||
PanelType: savedviewtypes.PanelTypeGraph,
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Aggregations: []qbtypes.LogAggregation{{Expression: "count()"}},
|
||||
},
|
||||
},
|
||||
SelectedFields: []telemetrytypes.TelemetryFieldKey{},
|
||||
},
|
||||
SelectedFields: []telemetrytypes.TelemetryFieldKey{},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -53,8 +51,9 @@ func testPostableSavedView(name string, source savedviewtypes.Source) savedviewt
|
||||
func testUpdatableSavedView(displayName string, source savedviewtypes.Source) savedviewtypes.UpdatableSavedView {
|
||||
postable := testPostableSavedView(displayName, source)
|
||||
return savedviewtypes.UpdatableSavedView{
|
||||
Source: postable.Source,
|
||||
Data: postable.Data,
|
||||
Source: postable.Source,
|
||||
SchemaVersion: postable.SchemaVersion,
|
||||
Spec: postable.Spec,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +92,22 @@ func TestModule_CreateAndGetView(t *testing.T) {
|
||||
assert.Equal(t, savedviewtypes.SourceLogs, got.Source)
|
||||
assert.Equal(t, "creator@signoz.io", got.CreatedBy)
|
||||
assert.Equal(t, "creator@signoz.io", got.UpdatedBy)
|
||||
assert.Equal(t, savedviewtypes.PanelTypeGraph, got.Data.Spec.PanelType)
|
||||
assert.Equal(t, savedviewtypes.PanelTypeGraph, got.Spec.PanelType)
|
||||
|
||||
require.NoError(t, st.AssertExpectations())
|
||||
}
|
||||
|
||||
// A duplicate-name insert failure must surface as errors.TypeAlreadyExists, not a generic internal error.
|
||||
func TestModule_CreateView_DuplicateNameIsConflict(t *testing.T) {
|
||||
m, st := newTestStore()
|
||||
|
||||
orgID := valuer.GenerateUUID().StringValue()
|
||||
ctx := contextWithClaims(orgID, "creator@signoz.io")
|
||||
|
||||
st.ExpectCreateError(errors.Newf(errors.TypeInternal, errors.CodeInternal, "UNIQUE constraint failed: saved_view.org_id, saved_view.name"))
|
||||
_, err := m.CreateView(ctx, orgID, testPostableSavedView("same-name", savedviewtypes.SourceLogs))
|
||||
require.Error(t, err)
|
||||
assert.True(t, errors.Ast(err, errors.TypeAlreadyExists), "expected an already-exists error, got %v", err)
|
||||
|
||||
require.NoError(t, st.AssertExpectations())
|
||||
}
|
||||
@@ -138,21 +152,21 @@ func TestModule_UpdateView(t *testing.T) {
|
||||
existingName := existing.Name
|
||||
|
||||
updated := testUpdatableSavedView("renamed", savedviewtypes.SourceTraces)
|
||||
updated.Data.Spec.PanelType = savedviewtypes.PanelTypeTable
|
||||
updated.Spec.PanelType = savedviewtypes.PanelTypeTable
|
||||
|
||||
st.ExpectUpdate(orgID, id, 1)
|
||||
require.NoError(t, m.UpdateView(contextWithClaims(orgID, "updater@signoz.io"), orgID, id, updated))
|
||||
|
||||
stored := testSavedView(orgID, id, "updater@signoz.io", testPostableSavedView("renamed", savedviewtypes.SourceTraces))
|
||||
stored.Name = existingName
|
||||
stored.Data.Spec.PanelType = savedviewtypes.PanelTypeTable
|
||||
stored.Spec.PanelType = savedviewtypes.PanelTypeTable
|
||||
st.ExpectGet(orgID, id, stored)
|
||||
got, err := m.GetView(contextWithClaims(orgID, "creator@signoz.io"), orgID, id)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, existingName, got.Name, "name must not change on update")
|
||||
assert.Equal(t, "renamed", got.Data.Spec.DisplayName)
|
||||
assert.Equal(t, "renamed", got.Spec.DisplayName)
|
||||
assert.Equal(t, savedviewtypes.SourceTraces, got.Source)
|
||||
assert.Equal(t, savedviewtypes.PanelTypeTable, got.Data.Spec.PanelType)
|
||||
assert.Equal(t, savedviewtypes.PanelTypeTable, got.Spec.PanelType)
|
||||
assert.Equal(t, "updater@signoz.io", got.UpdatedBy)
|
||||
|
||||
require.NoError(t, st.AssertExpectations())
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types/savedviewtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
@@ -18,32 +17,31 @@ func NewStore(sqlstore sqlstore.SQLStore) savedviewtypes.Store {
|
||||
return &store{sqlstore: sqlstore}
|
||||
}
|
||||
|
||||
func (store *store) Create(ctx context.Context, view *savedviewtypes.SavedView) error {
|
||||
_, err := store.sqlstore.BunDB().NewInsert().Model(view).Exec(ctx)
|
||||
func (store *store) Create(ctx context.Context, storable *savedviewtypes.StorableSavedView) error {
|
||||
_, err := store.sqlstore.BunDB().NewInsert().Model(storable).Exec(ctx)
|
||||
if err != nil {
|
||||
return store.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "saved view with name %s already exists", view.Name)
|
||||
return store.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "saved view with name %s already exists", storable.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *store) Get(ctx context.Context, orgID string, id valuer.UUID) (*savedviewtypes.SavedView, error) {
|
||||
var view savedviewtypes.SavedView
|
||||
err := store.sqlstore.BunDB().NewSelect().Model(&view).Where("org_id = ? AND id = ?", orgID, id.StringValue()).Scan(ctx)
|
||||
func (store *store) Get(ctx context.Context, orgID string, id valuer.UUID) (*savedviewtypes.StorableSavedView, error) {
|
||||
var storable savedviewtypes.StorableSavedView
|
||||
err := store.sqlstore.BunDB().NewSelect().Model(&storable).Where("org_id = ? AND id = ?", orgID, id.StringValue()).Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, store.sqlstore.WrapNotFoundErrf(err, savedviewtypes.ErrCodeSavedViewNotFound, "saved view %s not found", id.StringValue())
|
||||
}
|
||||
|
||||
normalizeSelectedFields(&view)
|
||||
return &view, nil
|
||||
return &storable, nil
|
||||
}
|
||||
|
||||
func (store *store) Update(ctx context.Context, view *savedviewtypes.SavedView) error {
|
||||
func (store *store) Update(ctx context.Context, storable *savedviewtypes.StorableSavedView) error {
|
||||
res, err := store.sqlstore.BunDB().NewUpdate().
|
||||
Model(&savedviewtypes.SavedView{}).
|
||||
Model((*savedviewtypes.StorableSavedView)(nil)).
|
||||
Set("updated_at = ?, updated_by = ?, source = ?, data = ?",
|
||||
view.UpdatedAt, view.UpdatedBy, view.Source, view.Data).
|
||||
Where("id = ?", view.ID.StringValue()).
|
||||
Where("org_id = ?", view.OrgID).
|
||||
storable.UpdatedAt, storable.UpdatedBy, storable.Source, storable.Data).
|
||||
Where("id = ?", storable.ID.StringValue()).
|
||||
Where("org_id = ?", storable.OrgID).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return errors.WrapInternalf(err, errors.CodeInternal, "error in updating saved view")
|
||||
@@ -54,7 +52,7 @@ func (store *store) Update(ctx context.Context, view *savedviewtypes.SavedView)
|
||||
return errors.WrapInternalf(err, errors.CodeInternal, "error in verifying the updated saved view")
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
return errors.NewNotFoundf(savedviewtypes.ErrCodeSavedViewNotFound, "saved view %s not found", view.ID.StringValue())
|
||||
return errors.NewNotFoundf(savedviewtypes.ErrCodeSavedViewNotFound, "saved view %s not found", storable.ID.StringValue())
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -62,7 +60,7 @@ func (store *store) Update(ctx context.Context, view *savedviewtypes.SavedView)
|
||||
|
||||
func (store *store) Delete(ctx context.Context, orgID string, id valuer.UUID) error {
|
||||
res, err := store.sqlstore.BunDB().NewDelete().
|
||||
Model(&savedviewtypes.SavedView{}).
|
||||
Model((*savedviewtypes.StorableSavedView)(nil)).
|
||||
Where("id = ?", id.StringValue()).
|
||||
Where("org_id = ?", orgID).
|
||||
Exec(ctx)
|
||||
@@ -81,9 +79,9 @@ func (store *store) Delete(ctx context.Context, orgID string, id valuer.UUID) er
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *store) List(ctx context.Context, orgID string, source savedviewtypes.Source, name string) ([]*savedviewtypes.SavedView, error) {
|
||||
var views []*savedviewtypes.SavedView
|
||||
q := store.sqlstore.BunDB().NewSelect().Model(&views).
|
||||
func (store *store) List(ctx context.Context, orgID string, source savedviewtypes.Source, name string) ([]*savedviewtypes.StorableSavedView, error) {
|
||||
var storables []*savedviewtypes.StorableSavedView
|
||||
q := store.sqlstore.BunDB().NewSelect().Model(&storables).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("name LIKE ?", "%"+name+"%")
|
||||
if !source.IsZero() {
|
||||
@@ -94,16 +92,5 @@ func (store *store) List(ctx context.Context, orgID string, source savedviewtype
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error in getting saved views")
|
||||
}
|
||||
|
||||
for _, view := range views {
|
||||
normalizeSelectedFields(view)
|
||||
}
|
||||
|
||||
return views, nil
|
||||
}
|
||||
|
||||
// normalizeSelectedFields fixes up a scanned row's nil SelectedFields.
|
||||
func normalizeSelectedFields(view *savedviewtypes.SavedView) {
|
||||
if view.Data.Spec.SelectedFields == nil {
|
||||
view.Data.Spec.SelectedFields = []telemetrytypes.TelemetryFieldKey{}
|
||||
}
|
||||
return storables, nil
|
||||
}
|
||||
|
||||
@@ -237,6 +237,7 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewAddDashboardTuplesFactory(sqlstore),
|
||||
sqlmigration.NewRestructureSavedViewSpecFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewAddSavedViewTuplesFactory(sqlstore),
|
||||
sqlmigration.NewFixSavedViewSelectedFieldsFactory(sqlstore),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
220
pkg/sqlmigration/111_fix_saved_view_selected_fields.go
Normal file
220
pkg/sqlmigration/111_fix_saved_view_selected_fields.go
Normal file
@@ -0,0 +1,220 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
)
|
||||
|
||||
// knownQueryTypes mirrors the discriminator values qbtypes.QueryType currently defines.
|
||||
var knownQueryTypes = map[string]bool{
|
||||
"builder_query": true,
|
||||
"builder_ai_query": true,
|
||||
"builder_formula": true,
|
||||
"builder_sub_query": true,
|
||||
"builder_join": true,
|
||||
"builder_trace_operator": true,
|
||||
"clickhouse_sql": true,
|
||||
"promql": true,
|
||||
}
|
||||
|
||||
// specFieldZeroValueJSON is the JSON to substitute for a spec key that fails to unmarshal.
|
||||
var specFieldZeroValueJSON = map[string]string{
|
||||
"displayName": `""`,
|
||||
"panelType": `""`,
|
||||
"queries": `[]`,
|
||||
"selectedFields": `[]`,
|
||||
"display": `{}`,
|
||||
}
|
||||
|
||||
// storableSavedViewData is the shape of the `saved_view` table this migration repairs.
|
||||
type storableSavedViewData struct {
|
||||
bun.BaseModel `bun:"table:saved_view"`
|
||||
|
||||
ID string `bun:"id,pk,type:text"`
|
||||
Data string `bun:"data,type:text"`
|
||||
}
|
||||
|
||||
// queryEnvelope mirrors minimal required qbtypes.QueryEnvelope.
|
||||
type queryEnvelope struct {
|
||||
Type string `json:"type"`
|
||||
Spec json.RawMessage `json:"spec"`
|
||||
}
|
||||
|
||||
// telemetryFieldKey mirrors telemetrytypes.TelemetryFieldKey's JSON-visible fields.
|
||||
// Signal/FieldContext/FieldDataType are plain strings to test UnmarshalJSON.
|
||||
type telemetryFieldKey struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Unit string `json:"unit"`
|
||||
Signal string `json:"signal"`
|
||||
FieldContext string `json:"fieldContext"`
|
||||
FieldDataType string `json:"fieldDataType"`
|
||||
}
|
||||
|
||||
// fixDisplay mirrors savedviewtypes.Display.
|
||||
type fixDisplay struct {
|
||||
MaxLines int `json:"maxLines"`
|
||||
FontSize string `json:"fontSize"`
|
||||
Format string `json:"format"`
|
||||
Color string `json:"color"`
|
||||
}
|
||||
|
||||
// fixSpec mirrors savedviewtypes.SavedViewSpec.
|
||||
type fixSpec struct {
|
||||
DisplayName string `json:"displayName"`
|
||||
PanelType string `json:"panelType"`
|
||||
Queries []queryEnvelope `json:"queries"`
|
||||
SelectedFields []telemetryFieldKey `json:"selectedFields"`
|
||||
Display fixDisplay `json:"display"`
|
||||
}
|
||||
|
||||
// fixData mirrors savedviewtypes.SavedViewData.
|
||||
type fixData struct {
|
||||
SchemaVersion string `json:"schemaVersion"`
|
||||
Spec fixSpec `json:"spec"`
|
||||
}
|
||||
|
||||
type fixSavedViewSelectedFields struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
settings factory.ProviderSettings
|
||||
}
|
||||
|
||||
func NewFixSavedViewSelectedFieldsFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("fix_saved_view_selected_fields"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &fixSavedViewSelectedFields{sqlstore: sqlstore, settings: ps}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (migration *fixSavedViewSelectedFields) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(migration.Up, migration.Down)
|
||||
}
|
||||
|
||||
func (migration *fixSavedViewSelectedFields) Up(ctx context.Context, db *bun.DB) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
var rows []*storableSavedViewData
|
||||
if err := tx.NewSelect().Model(&rows).Scan(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var repaired, deleted int
|
||||
for _, row := range rows {
|
||||
fixedData, blanked, ok := repairSavedViewData(row.Data)
|
||||
if ok && len(blanked) == 0 {
|
||||
// already scans cleanly field-by-field -- nothing to repair.
|
||||
continue
|
||||
}
|
||||
if !ok {
|
||||
migration.settings.Logger.WarnContext(ctx, "saved view data could not be repaired field-by-field, deleting the row", slog.String("saved_view_id", row.ID), slog.String("raw_data", row.Data))
|
||||
if _, err := tx.NewDelete().Model((*storableSavedViewData)(nil)).Where("id = ?", row.ID).Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
deleted++
|
||||
continue
|
||||
}
|
||||
|
||||
repaired++
|
||||
migration.settings.Logger.WarnContext(ctx, "repaired saved view data by blanking fields that failed to unmarshal", slog.String("saved_view_id", row.ID), slog.Any("fields_blanked", blanked))
|
||||
|
||||
if _, err := tx.NewUpdate().Model((*storableSavedViewData)(nil)).Set("data = ?", fixedData).Where("id = ?", row.ID).Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
migration.settings.Logger.InfoContext(ctx, "checked saved views for unreadable data", slog.Int("total", len(rows)), slog.Int("repaired", repaired), slog.Int("deleted", deleted))
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (migration *fixSavedViewSelectedFields) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// specFieldUnmarshalsCleanly reports whether value can be unmarshalled into
|
||||
// the expected shape of the given savedviewtypes.SavedViewSpec JSON key.
|
||||
func specFieldUnmarshalsCleanly(key string, value json.RawMessage) bool {
|
||||
switch key {
|
||||
case "displayName", "panelType":
|
||||
var s string
|
||||
return json.Unmarshal(value, &s) == nil
|
||||
case "queries":
|
||||
var q []queryEnvelope
|
||||
if err := json.Unmarshal(value, &q); err != nil {
|
||||
return false
|
||||
}
|
||||
if q == nil {
|
||||
// a JSON null unmarshals into a nil slice with no error; treat it as unclean so it
|
||||
// gets blanked to [] rather than shipping "queries": null against a nullable:false schema.
|
||||
return false
|
||||
}
|
||||
for _, e := range q {
|
||||
if !knownQueryTypes[e.Type] || len(e.Spec) == 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case "selectedFields":
|
||||
var f []telemetryFieldKey
|
||||
if err := json.Unmarshal(value, &f); err != nil {
|
||||
return false
|
||||
}
|
||||
// same null-vs-[] gap as "queries" above: blank a JSON null to [] instead of leaving it.
|
||||
return f != nil
|
||||
case "display":
|
||||
var d fixDisplay
|
||||
return json.Unmarshal(value, &d) == nil
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// repairSavedViewData tries to make data unmarshal cleanly by blanking, one key at a time,
|
||||
// whichever top-level spec fields fail to unmarshal into their expected shape.
|
||||
func repairSavedViewData(data string) (fixed string, blanked []string, ok bool) {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal([]byte(data), &raw); err != nil {
|
||||
return "", nil, false
|
||||
}
|
||||
|
||||
var spec map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw["spec"], &spec); err != nil {
|
||||
return "", nil, false
|
||||
}
|
||||
|
||||
for key, value := range spec {
|
||||
if specFieldUnmarshalsCleanly(key, value) {
|
||||
continue
|
||||
}
|
||||
spec[key] = json.RawMessage(specFieldZeroValueJSON[key])
|
||||
blanked = append(blanked, key)
|
||||
}
|
||||
|
||||
fixedSpec, err := json.Marshal(spec)
|
||||
if err != nil {
|
||||
return "", nil, false
|
||||
}
|
||||
raw["spec"] = fixedSpec
|
||||
|
||||
fixedData, err := json.Marshal(raw)
|
||||
if err != nil {
|
||||
return "", nil, false
|
||||
}
|
||||
|
||||
// verify the fix actually round-trips before writing it.
|
||||
if err := json.Unmarshal(fixedData, new(fixData)); err != nil {
|
||||
return "", nil, false
|
||||
}
|
||||
|
||||
return string(fixedData), blanked, true
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/uptrace/bun"
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
@@ -28,27 +30,81 @@ var (
|
||||
)
|
||||
|
||||
type SavedView struct {
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
types.UserAuditable
|
||||
OrgID string `json:"-"`
|
||||
Name string `json:"name"`
|
||||
Source Source `json:"source"`
|
||||
SchemaVersion SchemaVersion `json:"schemaVersion" required:"true"`
|
||||
Spec SavedViewSpec `json:"spec" required:"true"`
|
||||
}
|
||||
|
||||
type StorableSavedView struct {
|
||||
bun.BaseModel `bun:"table:saved_view"`
|
||||
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
types.UserAuditable
|
||||
OrgID string `json:"-" bun:"org_id,notnull"`
|
||||
Name string `json:"name" bun:"name,type:text,notnull"`
|
||||
Source Source `json:"source" bun:"source,type:text,notnull"`
|
||||
Data SavedViewData `json:"data" bun:"data,type:text,notnull"`
|
||||
OrgID string `bun:"org_id,notnull"`
|
||||
Name string `bun:"name,type:text,notnull"`
|
||||
Source Source `bun:"source,type:text,notnull"`
|
||||
Data SavedViewData `bun:"data,type:text,notnull"`
|
||||
}
|
||||
|
||||
func (s *StorableSavedView) ToSavedView() *SavedView {
|
||||
spec := s.Data.Spec
|
||||
if spec.Queries == nil {
|
||||
spec.Queries = []qbtypes.QueryEnvelope{}
|
||||
}
|
||||
if spec.SelectedFields == nil {
|
||||
spec.SelectedFields = []telemetrytypes.TelemetryFieldKey{}
|
||||
}
|
||||
|
||||
return &SavedView{
|
||||
Identifiable: s.Identifiable,
|
||||
TimeAuditable: s.TimeAuditable,
|
||||
UserAuditable: s.UserAuditable,
|
||||
OrgID: s.OrgID,
|
||||
Name: s.Name,
|
||||
Source: s.Source,
|
||||
SchemaVersion: SchemaVersion{valuer.NewString(s.Data.SchemaVersion)},
|
||||
Spec: spec,
|
||||
}
|
||||
}
|
||||
|
||||
func NewStorableSavedView(view *SavedView) *StorableSavedView {
|
||||
return &StorableSavedView{
|
||||
Identifiable: view.Identifiable,
|
||||
TimeAuditable: view.TimeAuditable,
|
||||
UserAuditable: view.UserAuditable,
|
||||
OrgID: view.OrgID,
|
||||
Name: view.Name,
|
||||
Source: view.Source,
|
||||
Data: SavedViewData{
|
||||
SchemaVersion: view.SchemaVersion.StringValue(),
|
||||
Spec: view.Spec,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RequestType selects which query-validation rules apply (see SavedViewSpec.Validate);
|
||||
// it is not persisted and must not be added to SavedViewSpec.
|
||||
type PostableSavedView struct {
|
||||
Name string `json:"name"`
|
||||
GenerateName bool `json:"generateName"`
|
||||
Source Source `json:"source" required:"true"`
|
||||
Data SavedViewData `json:"data" required:"true"`
|
||||
Name string `json:"name"`
|
||||
GenerateName bool `json:"generateName"`
|
||||
Source Source `json:"source" required:"true"`
|
||||
SchemaVersion SchemaVersion `json:"schemaVersion" required:"true"`
|
||||
RequestType qbtypes.RequestType `json:"requestType" required:"true"`
|
||||
Spec SavedViewSpec `json:"spec" required:"true"`
|
||||
}
|
||||
|
||||
// RequestType has the same meaning as PostableSavedView.RequestType.
|
||||
type UpdatableSavedView struct {
|
||||
Source Source `json:"source" required:"true"`
|
||||
Data SavedViewData `json:"data" required:"true"`
|
||||
Source Source `json:"source" required:"true"`
|
||||
SchemaVersion SchemaVersion `json:"schemaVersion" required:"true"`
|
||||
RequestType qbtypes.RequestType `json:"requestType" required:"true"`
|
||||
Spec SavedViewSpec `json:"spec" required:"true"`
|
||||
}
|
||||
|
||||
type ListSavedViewsParams struct {
|
||||
@@ -83,7 +139,7 @@ func (postable PostableSavedView) ToSavedView(orgID string, createdBy string) *S
|
||||
|
||||
name := postable.Name
|
||||
if postable.GenerateName {
|
||||
name = generateSavedViewName(postable.Data.Spec.DisplayName)
|
||||
name = generateSavedViewName(postable.Spec.DisplayName)
|
||||
}
|
||||
|
||||
return &SavedView{
|
||||
@@ -93,7 +149,8 @@ func (postable PostableSavedView) ToSavedView(orgID string, createdBy string) *S
|
||||
OrgID: orgID,
|
||||
Name: name,
|
||||
Source: postable.Source,
|
||||
Data: postable.Data,
|
||||
SchemaVersion: postable.SchemaVersion,
|
||||
Spec: postable.Spec,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +163,8 @@ func (updatable UpdatableSavedView) ToSavedView(id valuer.UUID, orgID string, up
|
||||
UserAuditable: types.UserAuditable{UpdatedBy: updatedBy},
|
||||
OrgID: orgID,
|
||||
Source: updatable.Source,
|
||||
Data: updatable.Data,
|
||||
SchemaVersion: updatable.SchemaVersion,
|
||||
Spec: updatable.Spec,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,8 +175,14 @@ func (p *PostableSavedView) Validate() error {
|
||||
if err := p.Source.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := p.SchemaVersion.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if p.RequestType.IsZero() {
|
||||
return errors.NewInvalidInputf(ErrCodeSavedViewInvalidInput, "requestType is required")
|
||||
}
|
||||
|
||||
return p.Data.Validate()
|
||||
return p.Spec.Validate(p.RequestType)
|
||||
}
|
||||
|
||||
func (p *PostableSavedView) validateName() error {
|
||||
@@ -135,8 +199,14 @@ func (u *UpdatableSavedView) Validate() error {
|
||||
if err := u.Source.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := u.SchemaVersion.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if u.RequestType.IsZero() {
|
||||
return errors.NewInvalidInputf(ErrCodeSavedViewInvalidInput, "requestType is required")
|
||||
}
|
||||
|
||||
return u.Data.Validate()
|
||||
return u.Spec.Validate(u.RequestType)
|
||||
}
|
||||
|
||||
func (p *ListSavedViewsParams) Validate() error {
|
||||
@@ -147,7 +217,17 @@ func (p *ListSavedViewsParams) Validate() error {
|
||||
return p.Source.Validate()
|
||||
}
|
||||
|
||||
func NewStatsFromSavedViews(savedViews []*SavedView) map[string]any {
|
||||
// NewSavedViewsFromStorableSavedViews converts scanned rows to their domain shape.
|
||||
func NewSavedViewsFromStorableSavedViews(storableSavedViews []*StorableSavedView) []*SavedView {
|
||||
savedViews := make([]*SavedView, len(storableSavedViews))
|
||||
for idx, storableSavedView := range storableSavedViews {
|
||||
savedViews[idx] = storableSavedView.ToSavedView()
|
||||
}
|
||||
|
||||
return savedViews
|
||||
}
|
||||
|
||||
func NewStatsFromStorableSavedViews(savedViews []*StorableSavedView) map[string]any {
|
||||
stats := make(map[string]any)
|
||||
for _, savedView := range savedViews {
|
||||
key := "savedview.source." + strings.ToLower(savedView.Source.StringValue()) + ".count"
|
||||
|
||||
@@ -4,29 +4,30 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
)
|
||||
|
||||
func validPostableSavedView() PostableSavedView {
|
||||
return PostableSavedView{
|
||||
Name: "my-view",
|
||||
Source: SourceLogs,
|
||||
Data: SavedViewData{
|
||||
SchemaVersion: SavedViewSchemaVersion,
|
||||
Spec: SavedViewSpec{DisplayName: "My View", PanelType: PanelTypeGraph, Queries: validQueries()},
|
||||
},
|
||||
Name: "my-view",
|
||||
Source: SourceLogs,
|
||||
SchemaVersion: SavedViewSchemaVersion,
|
||||
RequestType: qbtypes.RequestTypeTimeSeries,
|
||||
Spec: SavedViewSpec{DisplayName: "My View", PanelType: PanelTypeGraph, Queries: validQueries()},
|
||||
}
|
||||
}
|
||||
|
||||
func validUpdatableSavedView() UpdatableSavedView {
|
||||
return UpdatableSavedView{
|
||||
Source: SourceLogs,
|
||||
Data: SavedViewData{
|
||||
SchemaVersion: SavedViewSchemaVersion,
|
||||
Spec: SavedViewSpec{DisplayName: "My View", PanelType: PanelTypeGraph, Queries: validQueries()},
|
||||
},
|
||||
Source: SourceLogs,
|
||||
SchemaVersion: SavedViewSchemaVersion,
|
||||
RequestType: qbtypes.RequestTypeTimeSeries,
|
||||
Spec: SavedViewSpec{DisplayName: "My View", PanelType: PanelTypeGraph, Queries: validQueries()},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +70,7 @@ func TestPostableSavedViewValidate(t *testing.T) {
|
||||
|
||||
t.Run("invalid saved view data is rejected", func(t *testing.T) {
|
||||
view := validPostableSavedView()
|
||||
view.Data.SchemaVersion = "v1"
|
||||
view.SchemaVersion = SchemaVersion{valuer.NewString("v1")}
|
||||
assert.Error(t, view.Validate())
|
||||
})
|
||||
|
||||
@@ -100,9 +101,26 @@ func TestPostableSavedViewValidate(t *testing.T) {
|
||||
|
||||
t.Run("empty displayName is rejected", func(t *testing.T) {
|
||||
view := validPostableSavedView()
|
||||
view.Data.Spec.DisplayName = ""
|
||||
view.Spec.DisplayName = ""
|
||||
assert.ErrorContains(t, view.Validate(), "displayName is required")
|
||||
})
|
||||
|
||||
t.Run("missing requestType is rejected", func(t *testing.T) {
|
||||
view := validPostableSavedView()
|
||||
view.RequestType = qbtypes.RequestType{}
|
||||
assert.ErrorContains(t, view.Validate(), "requestType is required")
|
||||
})
|
||||
|
||||
t.Run("requestType is honored over the panelType-derived guess", func(t *testing.T) {
|
||||
view := validPostableSavedView()
|
||||
view.Spec.PanelType = PanelTypeList
|
||||
view.Spec.Queries = []qbtypes.QueryEnvelope{{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{Signal: telemetrytypes.SignalTraces},
|
||||
}}
|
||||
view.RequestType = qbtypes.RequestTypeRaw
|
||||
assert.NoError(t, view.Validate())
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdatableSavedViewValidate(t *testing.T) {
|
||||
@@ -119,9 +137,15 @@ func TestUpdatableSavedViewValidate(t *testing.T) {
|
||||
|
||||
t.Run("empty displayName is rejected", func(t *testing.T) {
|
||||
view := validUpdatableSavedView()
|
||||
view.Data.Spec.DisplayName = ""
|
||||
view.Spec.DisplayName = ""
|
||||
assert.ErrorContains(t, view.Validate(), "displayName is required")
|
||||
})
|
||||
|
||||
t.Run("missing requestType is rejected", func(t *testing.T) {
|
||||
view := validUpdatableSavedView()
|
||||
view.RequestType = qbtypes.RequestType{}
|
||||
assert.ErrorContains(t, view.Validate(), "requestType is required")
|
||||
})
|
||||
}
|
||||
|
||||
func TestListSavedViewsParamsValidate(t *testing.T) {
|
||||
@@ -153,7 +177,8 @@ func TestNewSavedView(t *testing.T) {
|
||||
assert.Equal(t, "creator@signoz.io", savedView.UpdatedBy)
|
||||
assert.Equal(t, view.Name, savedView.Name)
|
||||
assert.Equal(t, view.Source, savedView.Source)
|
||||
assert.Equal(t, view.Data, savedView.Data)
|
||||
assert.Equal(t, view.SchemaVersion, savedView.SchemaVersion)
|
||||
assert.Equal(t, view.Spec, savedView.Spec)
|
||||
assert.False(t, savedView.CreatedAt.IsZero())
|
||||
assert.Equal(t, savedView.CreatedAt, savedView.UpdatedAt)
|
||||
}
|
||||
@@ -163,14 +188,14 @@ func TestNewSavedView_GeneratesNameWhenEmpty(t *testing.T) {
|
||||
view := validPostableSavedView()
|
||||
view.Name = ""
|
||||
view.GenerateName = true
|
||||
view.Data.Spec.DisplayName = "My View!"
|
||||
view.Spec.DisplayName = "My View!"
|
||||
|
||||
savedView := view.ToSavedView(orgID, "creator@signoz.io")
|
||||
|
||||
assert.NotEmpty(t, savedView.Name)
|
||||
assert.Empty(t, validation.IsDNS1123Label(savedView.Name), "generated name must be a valid DNS-1123 label")
|
||||
assert.True(t, strings.HasPrefix(savedView.Name, "my-view-"))
|
||||
assert.Equal(t, "My View!", savedView.Data.Spec.DisplayName)
|
||||
assert.Equal(t, "My View!", savedView.Spec.DisplayName)
|
||||
}
|
||||
|
||||
func TestGenerateSavedViewName(t *testing.T) {
|
||||
@@ -212,17 +237,94 @@ func TestGenerateSavedViewName(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewStatsFromSavedViews(t *testing.T) {
|
||||
views := []*SavedView{
|
||||
func TestStorableSavedView_ToSavedView(t *testing.T) {
|
||||
t.Run("round trip preserves populated fields", func(t *testing.T) {
|
||||
view := &SavedView{
|
||||
Name: "my-view",
|
||||
Source: SourceLogs,
|
||||
SchemaVersion: SavedViewSchemaVersion,
|
||||
Spec: SavedViewSpec{
|
||||
DisplayName: "My View",
|
||||
PanelType: PanelTypeGraph,
|
||||
Queries: validQueries(),
|
||||
SelectedFields: []telemetrytypes.TelemetryFieldKey{{Name: "service.name"}},
|
||||
},
|
||||
}
|
||||
view.OrgID = valuer.GenerateUUID().StringValue()
|
||||
|
||||
roundTripped := NewStorableSavedView(view).ToSavedView()
|
||||
|
||||
assert.Equal(t, view.OrgID, roundTripped.OrgID)
|
||||
assert.Equal(t, view.Name, roundTripped.Name)
|
||||
assert.Equal(t, view.Source, roundTripped.Source)
|
||||
assert.Equal(t, view.SchemaVersion, roundTripped.SchemaVersion)
|
||||
assert.Equal(t, view.Spec, roundTripped.Spec)
|
||||
})
|
||||
|
||||
t.Run("nil selectedFields normalizes to an empty slice, not nil", func(t *testing.T) {
|
||||
storable := &StorableSavedView{
|
||||
Data: SavedViewData{
|
||||
SchemaVersion: SavedViewSchemaVersion.StringValue(),
|
||||
Spec: SavedViewSpec{
|
||||
DisplayName: "My View",
|
||||
PanelType: PanelTypeGraph,
|
||||
Queries: validQueries(),
|
||||
SelectedFields: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
view := storable.ToSavedView()
|
||||
|
||||
assert.NotNil(t, view.Spec.SelectedFields)
|
||||
assert.Empty(t, view.Spec.SelectedFields)
|
||||
})
|
||||
|
||||
t.Run("nil queries normalizes to an empty slice, not nil", func(t *testing.T) {
|
||||
storable := &StorableSavedView{
|
||||
Data: SavedViewData{
|
||||
SchemaVersion: SavedViewSchemaVersion.StringValue(),
|
||||
Spec: SavedViewSpec{
|
||||
DisplayName: "My View",
|
||||
PanelType: PanelTypeGraph,
|
||||
Queries: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
view := storable.ToSavedView()
|
||||
|
||||
assert.NotNil(t, view.Spec.Queries)
|
||||
assert.Empty(t, view.Spec.Queries)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewStatsFromStorableSavedViews(t *testing.T) {
|
||||
storables := []*StorableSavedView{
|
||||
{Source: SourceLogs},
|
||||
{Source: SourceLogs},
|
||||
{Source: SourceTraces},
|
||||
}
|
||||
|
||||
stats := NewStatsFromSavedViews(views)
|
||||
stats := NewStatsFromStorableSavedViews(storables)
|
||||
|
||||
assert.Equal(t, int64(3), stats["savedview.count"])
|
||||
assert.Equal(t, int64(2), stats["savedview.source.logs.count"])
|
||||
assert.Equal(t, int64(1), stats["savedview.source.traces.count"])
|
||||
assert.NotContains(t, stats, "savedview.source.metrics.count")
|
||||
}
|
||||
|
||||
func TestNewSavedViewsFromStorableSavedViews(t *testing.T) {
|
||||
storables := []*StorableSavedView{
|
||||
{Name: "a", Source: SourceLogs, Data: SavedViewData{SchemaVersion: SavedViewSchemaVersion.StringValue(), Spec: SavedViewSpec{DisplayName: "a", PanelType: PanelTypeGraph, Queries: validQueries()}}},
|
||||
{Name: "b", Source: SourceTraces, Data: SavedViewData{SchemaVersion: SavedViewSchemaVersion.StringValue(), Spec: SavedViewSpec{DisplayName: "b", PanelType: PanelTypeTable, Queries: validQueries()}}},
|
||||
}
|
||||
|
||||
views := NewSavedViewsFromStorableSavedViews(storables)
|
||||
|
||||
require.Len(t, views, 2)
|
||||
assert.Equal(t, "a", views[0].Name)
|
||||
assert.Equal(t, SourceLogs, views[0].Source)
|
||||
assert.Equal(t, "b", views[1].Name)
|
||||
assert.Equal(t, SourceTraces, views[1].Source)
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func (t *StoreTest) Store() savedviewtypes.Store { return t.store }
|
||||
func (t *StoreTest) Mock() sqlmock.Sqlmock { return t.mock }
|
||||
|
||||
func savedViewRow(view *savedviewtypes.SavedView) []driver.Value {
|
||||
data, _ := json.Marshal(view.Data)
|
||||
data, _ := json.Marshal(savedviewtypes.NewStorableSavedView(view).Data)
|
||||
return []driver.Value{
|
||||
view.ID.StringValue(),
|
||||
view.CreatedAt,
|
||||
@@ -47,6 +47,12 @@ func (t *StoreTest) ExpectCreate() {
|
||||
t.mock.ExpectExec(`INSERT INTO "saved_view"`).WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
}
|
||||
|
||||
// ExpectCreateError sets up the SQL expectation for a Create call whose insert
|
||||
// fails, e.g. on a UNIQUE(org_id, name) violation.
|
||||
func (t *StoreTest) ExpectCreateError(err error) {
|
||||
t.mock.ExpectExec(`INSERT INTO "saved_view"`).WillReturnError(err)
|
||||
}
|
||||
|
||||
// ExpectGet sets up the SQL expectation for a Get call. Pass view = nil to
|
||||
// simulate a not-found row.
|
||||
func (t *StoreTest) ExpectGet(orgID string, id valuer.UUID, view *savedviewtypes.SavedView) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
// SavedViewSchemaVersion is the only schemaVersion currently.
|
||||
const SavedViewSchemaVersion = "v2"
|
||||
var SavedViewSchemaVersion = SchemaVersion{valuer.NewString("v2")}
|
||||
|
||||
var (
|
||||
PanelTypeValue = PanelType{valuer.NewString("value")}
|
||||
@@ -30,9 +30,9 @@ type Display struct {
|
||||
type SavedViewSpec struct {
|
||||
DisplayName string `json:"displayName" required:"true"`
|
||||
PanelType PanelType `json:"panelType" required:"true"`
|
||||
Queries []qbtypes.QueryEnvelope `json:"queries" required:"true" nullable:"false"`
|
||||
SelectedFields []telemetrytypes.TelemetryFieldKey `json:"selectedFields" required:"true" nullable:"false"`
|
||||
Display Display `json:"display" required:"true"`
|
||||
Queries []qbtypes.QueryEnvelope `json:"queries" required:"true" nullable:"false" minItems:"1"`
|
||||
SelectedFields []telemetrytypes.TelemetryFieldKey `json:"selectedFields" nullable:"false"`
|
||||
Display Display `json:"display"`
|
||||
}
|
||||
|
||||
// SavedViewData is what's persisted as saved view data.
|
||||
@@ -41,6 +41,11 @@ type SavedViewData struct {
|
||||
Spec SavedViewSpec `json:"spec" required:"true"`
|
||||
}
|
||||
|
||||
// SchemaVersion has v2 as the only value currently.
|
||||
type SchemaVersion struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
// PanelType is the explore-page panel a saved view renders as.
|
||||
type PanelType struct {
|
||||
valuer.String
|
||||
@@ -65,7 +70,21 @@ func (p PanelType) Validate() error {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SavedViewSpec) Validate() error {
|
||||
func (SchemaVersion) Enum() []any {
|
||||
return []any{SavedViewSchemaVersion}
|
||||
}
|
||||
|
||||
func (s SchemaVersion) Validate() error {
|
||||
if s != SavedViewSchemaVersion {
|
||||
return errors.NewInvalidInputf(ErrCodeSavedViewInvalidInput, "schemaVersion must be %q, got %q", SavedViewSchemaVersion.StringValue(), s.StringValue())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate validates Queries against the rules for requestType -- the shape
|
||||
// of result they must produce (e.g. a raw/list request must not carry an
|
||||
// aggregation, a scalar request must).
|
||||
func (s *SavedViewSpec) Validate(requestType qbtypes.RequestType) error {
|
||||
if s.DisplayName == "" {
|
||||
return errors.NewInvalidInputf(ErrCodeSavedViewInvalidInput, "displayName is required")
|
||||
}
|
||||
@@ -73,13 +92,20 @@ func (s *SavedViewSpec) Validate() error {
|
||||
return err
|
||||
}
|
||||
|
||||
return (&qbtypes.CompositeQuery{Queries: s.Queries}).Validate()
|
||||
return (&qbtypes.CompositeQuery{Queries: s.Queries}).Validate(qbtypes.GetValidationOptions(requestType)...)
|
||||
}
|
||||
|
||||
func (d *SavedViewData) Validate() error {
|
||||
if d.SchemaVersion != SavedViewSchemaVersion {
|
||||
return errors.NewInvalidInputf(ErrCodeSavedViewInvalidInput, "schemaVersion must be %q, got %q", SavedViewSchemaVersion, d.SchemaVersion)
|
||||
// LegacyRequestTypeForPanelType exists only for the v1 legacy API, which has
|
||||
// no requestType concept of its own to send.
|
||||
func LegacyRequestTypeForPanelType(p PanelType) qbtypes.RequestType {
|
||||
switch p {
|
||||
case PanelTypeList:
|
||||
return qbtypes.RequestTypeRaw
|
||||
case PanelTypeTrace:
|
||||
return qbtypes.RequestTypeTrace
|
||||
case PanelTypeGraph:
|
||||
return qbtypes.RequestTypeTimeSeries
|
||||
default:
|
||||
return qbtypes.RequestTypeScalar
|
||||
}
|
||||
|
||||
return d.Spec.Validate()
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package savedviewtypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func validQueries() []qbtypes.QueryEnvelope {
|
||||
@@ -75,7 +78,7 @@ func TestSavedViewSpecValidate(t *testing.T) {
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "selected fields and display are not required",
|
||||
name: "selectedFields and display populated is still valid",
|
||||
spec: SavedViewSpec{
|
||||
DisplayName: "My View",
|
||||
PanelType: PanelTypeTable,
|
||||
@@ -85,51 +88,149 @@ func TestSavedViewSpecValidate(t *testing.T) {
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
err := c.spec.Validate()
|
||||
if c.expectError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSavedViewDataValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
data SavedViewData
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "valid data",
|
||||
data: SavedViewData{SchemaVersion: SavedViewSchemaVersion, Spec: SavedViewSpec{DisplayName: "My View", PanelType: PanelTypeGraph, Queries: validQueries()}},
|
||||
name: "nil selectedFields is valid -- neither field is actually required",
|
||||
spec: SavedViewSpec{
|
||||
DisplayName: "My View",
|
||||
PanelType: PanelTypeTable,
|
||||
Queries: validQueries(),
|
||||
SelectedFields: nil,
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "wrong schema version is rejected",
|
||||
data: SavedViewData{SchemaVersion: "v1", Spec: SavedViewSpec{DisplayName: "My View", PanelType: PanelTypeGraph, Queries: validQueries()}},
|
||||
expectError: true,
|
||||
name: "empty (non-nil) selectedFields is valid",
|
||||
spec: SavedViewSpec{
|
||||
DisplayName: "My View",
|
||||
PanelType: PanelTypeTable,
|
||||
Queries: validQueries(),
|
||||
SelectedFields: []telemetrytypes.TelemetryFieldKey{},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "empty schema version is rejected",
|
||||
data: SavedViewData{Spec: SavedViewSpec{DisplayName: "My View", PanelType: PanelTypeGraph, Queries: validQueries()}},
|
||||
expectError: true,
|
||||
name: "zero-value display is valid",
|
||||
spec: SavedViewSpec{
|
||||
DisplayName: "My View",
|
||||
PanelType: PanelTypeTable,
|
||||
Queries: validQueries(),
|
||||
Display: Display{},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "invalid spec is rejected",
|
||||
data: SavedViewData{SchemaVersion: SavedViewSchemaVersion, Spec: SavedViewSpec{DisplayName: "My View", PanelType: PanelTypeGraph}},
|
||||
name: "list panel query with no aggregation is valid",
|
||||
spec: SavedViewSpec{
|
||||
DisplayName: "My View",
|
||||
PanelType: PanelTypeList,
|
||||
Queries: []qbtypes.QueryEnvelope{{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
},
|
||||
}},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "trace panel query with no aggregation is valid",
|
||||
spec: SavedViewSpec{
|
||||
DisplayName: "My View",
|
||||
PanelType: PanelTypeTrace,
|
||||
Queries: []qbtypes.QueryEnvelope{{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
},
|
||||
}},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "graph panel query with no aggregation is still rejected",
|
||||
spec: SavedViewSpec{
|
||||
DisplayName: "My View",
|
||||
PanelType: PanelTypeGraph,
|
||||
Queries: []qbtypes.QueryEnvelope{{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
},
|
||||
}},
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
err := c.data.Validate()
|
||||
err := c.spec.Validate(LegacyRequestTypeForPanelType(c.spec.PanelType))
|
||||
if c.expectError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSavedViewSpecValidate_HonorsExplicitRequestType(t *testing.T) {
|
||||
// requestType is a caller-supplied validation input, not derived from PanelType --
|
||||
// a raw query without aggregations must pass under RequestTypeRaw even though its
|
||||
// PanelType is graph (which LegacyRequestTypeForPanelType would map to time_series).
|
||||
spec := SavedViewSpec{
|
||||
DisplayName: "My View",
|
||||
PanelType: PanelTypeGraph,
|
||||
Queries: []qbtypes.QueryEnvelope{{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
assert.NoError(t, spec.Validate(qbtypes.RequestTypeRaw))
|
||||
assert.Error(t, spec.Validate(qbtypes.RequestTypeTimeSeries))
|
||||
}
|
||||
|
||||
func TestSavedViewSpecJSONUnmarshal_OptionalFields(t *testing.T) {
|
||||
base := `"displayName":"My View","panelType":"table","queries":[{"type":"builder_query","spec":{"signal":"logs","aggregations":[{"expression":"count()"}]}}]`
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
json string
|
||||
}{
|
||||
{name: "selectedFields and display omitted entirely", json: `{` + base + `}`},
|
||||
{name: "selectedFields and display explicitly null", json: `{` + base + `,"selectedFields":null,"display":null}`},
|
||||
{name: "selectedFields empty array, display empty object", json: `{` + base + `,"selectedFields":[],"display":{}}`},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
var spec SavedViewSpec
|
||||
err := json.Unmarshal([]byte(c.json), &spec)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, spec.Validate(LegacyRequestTypeForPanelType(spec.PanelType)))
|
||||
assert.Empty(t, spec.SelectedFields)
|
||||
assert.Equal(t, Display{}, spec.Display)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchemaVersionValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
schemaVersion SchemaVersion
|
||||
expectError bool
|
||||
}{
|
||||
{name: "valid schema version", schemaVersion: SavedViewSchemaVersion, expectError: false},
|
||||
{name: "wrong schema version is rejected", schemaVersion: SchemaVersion{valuer.NewString("v1")}, expectError: true},
|
||||
{name: "empty schema version is rejected", schemaVersion: SchemaVersion{}, expectError: true},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
err := c.schemaVersion.Validate()
|
||||
if c.expectError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
)
|
||||
|
||||
type Store interface {
|
||||
Create(ctx context.Context, view *SavedView) error
|
||||
Get(ctx context.Context, orgID string, id valuer.UUID) (*SavedView, error)
|
||||
Update(ctx context.Context, view *SavedView) error
|
||||
Create(ctx context.Context, view *StorableSavedView) error
|
||||
Get(ctx context.Context, orgID string, id valuer.UUID) (*StorableSavedView, error)
|
||||
Update(ctx context.Context, view *StorableSavedView) error
|
||||
Delete(ctx context.Context, orgID string, id valuer.UUID) error
|
||||
List(ctx context.Context, orgID string, source Source, name string) ([]*SavedView, error)
|
||||
List(ctx context.Context, orgID string, source Source, name string) ([]*StorableSavedView, error)
|
||||
}
|
||||
|
||||
16
tests/fixtures/savedview.py
vendored
16
tests/fixtures/savedview.py
vendored
@@ -13,15 +13,13 @@ def _body(name: str, source: str = "logs") -> dict:
|
||||
return {
|
||||
"name": name,
|
||||
"source": source,
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": name,
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": name,
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -26,15 +26,13 @@ def test_create_rejects_wrong_schema_version(
|
||||
"name": "my-view",
|
||||
"generateName": False,
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v9",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v9",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -59,15 +57,13 @@ def test_create_rejects_invalid_panel_type(
|
||||
"name": "my-view",
|
||||
"generateName": False,
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "bogus",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "bogus",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -91,15 +87,13 @@ def test_create_rejects_empty_queries(
|
||||
"name": "my-view",
|
||||
"generateName": False,
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -127,15 +121,13 @@ def test_create_rejects_empty_display_name(
|
||||
"name": "my-view",
|
||||
"generateName": False,
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -160,15 +152,13 @@ def test_create_rejects_invalid_source(
|
||||
"name": "my-view",
|
||||
"generateName": False,
|
||||
"source": "bogus",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -192,15 +182,13 @@ def test_create_rejects_invalid_name(
|
||||
"name": "Not A Valid Slug",
|
||||
"generateName": False,
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -226,15 +214,13 @@ def test_create_rejects_empty_name_without_generate_name(
|
||||
"name": "",
|
||||
"generateName": False,
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -261,15 +247,13 @@ def test_create_rejects_name_when_generate_name_is_true(
|
||||
"name": "explicit-name",
|
||||
"generateName": True,
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -298,15 +282,13 @@ def test_create_rejects_unknown_field(
|
||||
"name": "my-view",
|
||||
"generateName": False,
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"unknownfield": "boom",
|
||||
},
|
||||
@@ -366,15 +348,13 @@ def test_update_missing_view_returns_not_found(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{uuid.uuid4()}"),
|
||||
json={
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -402,15 +382,13 @@ def test_update_rejects_name_field(
|
||||
"name": "update-rejects-name-field",
|
||||
"generateName": False,
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers=headers,
|
||||
@@ -424,15 +402,13 @@ def test_update_rejects_name_field(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{view_id}"),
|
||||
json={
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My View",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"name": "update-rejects-name-field",
|
||||
},
|
||||
@@ -485,15 +461,13 @@ def test_saved_view_lifecycle(
|
||||
"name": "lc-logs-overview",
|
||||
"generateName": False,
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "lc-logs-overview",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "lc-logs-overview",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers=headers,
|
||||
@@ -508,15 +482,13 @@ def test_saved_view_lifecycle(
|
||||
"name": "lc-traces-overview",
|
||||
"generateName": False,
|
||||
"source": "traces",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "lc-traces-overview",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "lc-traces-overview",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers=headers,
|
||||
@@ -535,9 +507,9 @@ def test_saved_view_lifecycle(
|
||||
got = response.json()["data"]
|
||||
assert got["id"] == view_id
|
||||
assert got["name"] == "lc-logs-overview"
|
||||
assert got["data"]["spec"]["displayName"] == "lc-logs-overview"
|
||||
assert got["spec"]["displayName"] == "lc-logs-overview"
|
||||
assert got["source"] == "logs"
|
||||
assert got["data"]["spec"]["panelType"] == "table"
|
||||
assert got["spec"]["panelType"] == "table"
|
||||
|
||||
# ── list filters by source and name ──────────────────────────────
|
||||
response = requests.get(
|
||||
@@ -564,15 +536,13 @@ def test_saved_view_lifecycle(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{view_id}"),
|
||||
json={
|
||||
"source": "metrics",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "lc-logs-overview-renamed",
|
||||
"panelType": "graph",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "lc-logs-overview-renamed",
|
||||
"panelType": "graph",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers=headers,
|
||||
@@ -588,9 +558,9 @@ def test_saved_view_lifecycle(
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
updated = response.json()["data"]
|
||||
assert updated["name"] == "lc-logs-overview", "name is immutable"
|
||||
assert updated["data"]["spec"]["displayName"] == "lc-logs-overview-renamed"
|
||||
assert updated["spec"]["displayName"] == "lc-logs-overview-renamed"
|
||||
assert updated["source"] == "metrics"
|
||||
assert updated["data"]["spec"]["panelType"] == "graph"
|
||||
assert updated["spec"]["panelType"] == "graph"
|
||||
finally:
|
||||
requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{view_id}"),
|
||||
@@ -621,15 +591,13 @@ def test_empty_name_derives_a_slug_from_display_name(
|
||||
"name": "",
|
||||
"generateName": True,
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My Generated View!",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "My Generated View!",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers=headers,
|
||||
@@ -646,7 +614,7 @@ def test_empty_name_derives_a_slug_from_display_name(
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
got = response.json()["data"]
|
||||
assert got["data"]["spec"]["displayName"] == "My Generated View!"
|
||||
assert got["spec"]["displayName"] == "My Generated View!"
|
||||
assert got["name"].startswith("my-generated-view-")
|
||||
assert got["name"] != "my-generated-view-", "expected a random suffix, not just the slugified prefix"
|
||||
finally:
|
||||
@@ -681,15 +649,13 @@ def test_create_roundtrip_preserves_zero_values(
|
||||
"name": "create-zero-values",
|
||||
"generateName": False,
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "create-zero-values",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "create-zero-values",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers=headers,
|
||||
@@ -705,7 +671,7 @@ def test_create_roundtrip_preserves_zero_values(
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
spec = response.json()["data"]["data"]["spec"]
|
||||
spec = response.json()["data"]["spec"]
|
||||
query = spec["queries"][0]["spec"]
|
||||
|
||||
cases = [
|
||||
@@ -741,14 +707,12 @@ def test_selected_fields_omitted_on_create_reads_back_as_empty_list_not_null(
|
||||
"name": "omitted-selected-fields",
|
||||
"generateName": False,
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "omitted-selected-fields",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "omitted-selected-fields",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers=headers,
|
||||
@@ -764,7 +728,151 @@ def test_selected_fields_omitted_on_create_reads_back_as_empty_list_not_null(
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert response.json()["data"]["data"]["spec"]["selectedFields"] == []
|
||||
assert response.json()["data"]["spec"]["selectedFields"] == []
|
||||
finally:
|
||||
requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{view_id}"),
|
||||
headers=headers,
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
|
||||
def test_display_omitted_on_create_reads_back_as_zero_value(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
"""display's counterpart to test_selected_fields_omitted_on_create_reads_back_as_empty_list_not_null:
|
||||
neither field is required, so omitting display entirely must not 400 or
|
||||
leave it null -- it reads back as the zero-value Display object."""
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(BASE_URL),
|
||||
json={
|
||||
"name": "omitted-display",
|
||||
"generateName": False,
|
||||
"source": "logs",
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "omitted-display",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}],
|
||||
"selectedFields": [],
|
||||
},
|
||||
},
|
||||
headers=headers,
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, response.text
|
||||
view_id = response.json()["data"]["id"]
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{view_id}"),
|
||||
headers=headers,
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert response.json()["data"]["spec"]["display"] == {"maxLines": 0, "fontSize": "", "format": "", "color": ""}
|
||||
finally:
|
||||
requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{view_id}"),
|
||||
headers=headers,
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
|
||||
def test_selected_fields_and_display_explicit_null_on_create(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
"""JSON null decodes as a no-op onto a non-pointer Go field (struct/slice), so
|
||||
an explicit null is expected to behave identically to omitting the field."""
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(BASE_URL),
|
||||
json={
|
||||
"name": "null-selected-fields-and-display",
|
||||
"generateName": False,
|
||||
"source": "logs",
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "null-selected-fields-and-display",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}],
|
||||
"selectedFields": None,
|
||||
"display": None,
|
||||
},
|
||||
},
|
||||
headers=headers,
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, response.text
|
||||
view_id = response.json()["data"]["id"]
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{view_id}"),
|
||||
headers=headers,
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
spec = response.json()["data"]["spec"]
|
||||
assert spec["selectedFields"] == []
|
||||
assert spec["display"] == {"maxLines": 0, "fontSize": "", "format": "", "color": ""}
|
||||
finally:
|
||||
requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{view_id}"),
|
||||
headers=headers,
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
|
||||
def test_create_with_partial_display_defaults_missing_fields(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
"""display's fields are each independently optional -- sending only one
|
||||
(color) must not 400, and the fields left unset must default to their own
|
||||
zero value rather than being rejected or dropped."""
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(BASE_URL),
|
||||
json={
|
||||
"name": "partial-display-color-only",
|
||||
"generateName": False,
|
||||
"source": "logs",
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "partial-display-color-only",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}],
|
||||
"selectedFields": [],
|
||||
"display": {"color": "test"},
|
||||
},
|
||||
},
|
||||
headers=headers,
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, response.text
|
||||
view_id = response.json()["data"]["id"]
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{view_id}"),
|
||||
headers=headers,
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert response.json()["data"]["spec"]["display"] == {"maxLines": 0, "fontSize": "", "format": "", "color": "test"}
|
||||
finally:
|
||||
requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{view_id}"),
|
||||
@@ -793,15 +901,13 @@ def test_update_does_not_corrupt_zero_values(
|
||||
"name": "update-zero-values",
|
||||
"generateName": False,
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "update-zero-values",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": True, "legend": "Custom Legend"}}],
|
||||
"selectedFields": [{"name": "service.name"}],
|
||||
"display": {"maxLines": 25, "fontSize": "large", "format": "table", "color": "blue"},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "update-zero-values",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": True, "legend": "Custom Legend"}}],
|
||||
"selectedFields": [{"name": "service.name"}],
|
||||
"display": {"maxLines": 25, "fontSize": "large", "format": "table", "color": "blue"},
|
||||
},
|
||||
},
|
||||
headers=headers,
|
||||
@@ -817,7 +923,7 @@ def test_update_does_not_corrupt_zero_values(
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
spec = response.json()["data"]["data"]["spec"]
|
||||
spec = response.json()["data"]["spec"]
|
||||
assert spec["display"]["maxLines"] == 25
|
||||
# signal/fieldContext/fieldDataType always serialize on TelemetryFieldKey
|
||||
# (no omitempty -- see pkg/types/telemetrytypes/field.go), so an entry sent
|
||||
@@ -830,15 +936,13 @@ def test_update_does_not_corrupt_zero_values(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{view_id}"),
|
||||
json={
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "update-zero-values",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "update-zero-values",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}], "disabled": False, "legend": ""}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers=headers,
|
||||
@@ -853,7 +957,7 @@ def test_update_does_not_corrupt_zero_values(
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
spec = response.json()["data"]["data"]["spec"]
|
||||
spec = response.json()["data"]["spec"]
|
||||
query = spec["queries"][0]["spec"]
|
||||
|
||||
cases = [
|
||||
@@ -873,3 +977,69 @@ def test_update_does_not_corrupt_zero_values(
|
||||
headers=headers,
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
|
||||
def test_update_with_partial_display_replaces_whole_object(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
"""Update is a whole-object replace, not a merge: sending only "color" on
|
||||
update must not preserve the previous fontSize/format/maxLines -- those
|
||||
reset to their zero value exactly as if display had been sent in full."""
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(BASE_URL),
|
||||
json={
|
||||
"name": "update-partial-display",
|
||||
"generateName": False,
|
||||
"source": "logs",
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "update-partial-display",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 10, "fontSize": "large", "format": "table", "color": "blue"},
|
||||
},
|
||||
},
|
||||
headers=headers,
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED, response.text
|
||||
view_id = response.json()["data"]["id"]
|
||||
|
||||
try:
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{view_id}"),
|
||||
json={
|
||||
"source": "logs",
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "update-partial-display",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}],
|
||||
"selectedFields": [],
|
||||
"display": {"color": "green"},
|
||||
},
|
||||
},
|
||||
headers=headers,
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{view_id}"),
|
||||
headers=headers,
|
||||
timeout=5,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert response.json()["data"]["spec"]["display"] == {"maxLines": 0, "fontSize": "", "format": "", "color": "green"}
|
||||
finally:
|
||||
requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"{BASE_URL}/{view_id}"),
|
||||
headers=headers,
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
@@ -112,15 +112,13 @@ def test_write_forbidden_without_grant(
|
||||
signoz.self.host_configs["8080"].get(f"{SAVED_VIEW_BASE}/{target_id}"),
|
||||
json={
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": _SAVED_VIEW_FGA_TARGET_NAME,
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": _SAVED_VIEW_FGA_TARGET_NAME,
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -133,15 +131,13 @@ def test_write_forbidden_without_grant(
|
||||
json={
|
||||
"name": "saved-view-fga-create-attempt",
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "saved-view-fga-create-attempt",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": "saved-view-fga-create-attempt",
|
||||
"panelType": "table",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
@@ -212,15 +208,13 @@ def test_update_scoped_to_granted_view(
|
||||
token = get_token(_SAVED_VIEW_FGA_CUSTOM_USER_EMAIL, _SAVED_VIEW_FGA_CUSTOM_USER_PASSWORD)
|
||||
updated_body = {
|
||||
"source": "logs",
|
||||
"data": {
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": _SAVED_VIEW_FGA_TARGET_NAME,
|
||||
"panelType": "graph",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
"schemaVersion": "v2",
|
||||
"spec": {
|
||||
"displayName": _SAVED_VIEW_FGA_TARGET_NAME,
|
||||
"panelType": "graph",
|
||||
"queries": [{"type": "builder_query", "spec": {"name": "A", "signal": "logs", "aggregations": [{"expression": "count()"}]}}],
|
||||
"selectedFields": [],
|
||||
"display": {"maxLines": 0, "fontSize": "", "format": "", "color": ""},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user