diff --git a/docs/api/openapi.yml b/docs/api/openapi.yml index 2cf99067828..e0806fda6a6 100644 --- a/docs/api/openapi.yml +++ b/docs/api/openapi.yml @@ -1592,13 +1592,6 @@ components: required: - config type: object - CommonDisplay: - properties: - description: - type: string - name: - type: string - type: object CommonJSONRef: properties: $ref: @@ -2732,11 +2725,6 @@ components: type: object DashboardtypesDashboardSpec: properties: - datasources: - additionalProperties: - $ref: '#/components/schemas/DashboardtypesDatasourceSpec' - nullable: true - type: object display: $ref: '#/components/schemas/DashboardtypesDisplay' duration: @@ -2748,6 +2736,7 @@ components: links: items: $ref: '#/components/schemas/DashboardtypesLink' + nullable: true type: array panels: additionalProperties: @@ -2764,7 +2753,6 @@ components: - variables - panels - layouts - - links type: object DashboardtypesDashboardView: properties: @@ -2801,39 +2789,6 @@ components: required: - version type: object - DashboardtypesDatasourcePlugin: - discriminator: - mapping: - signoz/Datasource: '#/components/schemas/DashboardtypesDatasourcePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesSigNozDatasourceSpec' - propertyName: kind - oneOf: - - $ref: '#/components/schemas/DashboardtypesDatasourcePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesSigNozDatasourceSpec' - type: object - DashboardtypesDatasourcePluginKind: - enum: - - signoz/Datasource - type: string - DashboardtypesDatasourcePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesSigNozDatasourceSpec: - properties: - kind: - enum: - - signoz/Datasource - type: string - spec: - $ref: '#/components/schemas/DashboardtypesSigNozDatasourceSpec' - required: - - kind - - spec - type: object - DashboardtypesDatasourceSpec: - properties: - default: - type: boolean - display: - $ref: '#/components/schemas/CommonDisplay' - plugin: - $ref: '#/components/schemas/DashboardtypesDatasourcePlugin' - type: object DashboardtypesDisplay: properties: description: @@ -3401,6 +3356,7 @@ components: links: items: $ref: '#/components/schemas/DashboardtypesLink' + nullable: true type: array plugin: $ref: '#/components/schemas/DashboardtypesPanelPlugin' @@ -3412,7 +3368,6 @@ components: - display - plugin - queries - - links type: object DashboardtypesPatchOp: enum: @@ -3610,8 +3565,6 @@ components: required: - queryValue type: object - DashboardtypesSigNozDatasourceSpec: - type: object DashboardtypesSource: enum: - user diff --git a/frontend/src/api/generated/services/sigNoz.schemas.ts b/frontend/src/api/generated/services/sigNoz.schemas.ts index a4b98f371f0..fd47d519641 100644 --- a/frontend/src/api/generated/services/sigNoz.schemas.ts +++ b/frontend/src/api/generated/services/sigNoz.schemas.ts @@ -3236,17 +3236,6 @@ export interface CloudintegrationtypesUpdatableServiceDTO { config: CloudintegrationtypesServiceConfigDTO; } -export interface CommonDisplayDTO { - /** - * @type string - */ - description?: string; - /** - * @type string - */ - name?: string; -} - export interface CommonJSONRefDTO { /** * @type string @@ -3990,44 +3979,6 @@ export interface DashboardtypesDashboardPanelRefDTO { panelName: string; } -export enum DashboardtypesDatasourcePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesSigNozDatasourceSpecDTOKind { - 'signoz/Datasource' = 'signoz/Datasource', -} -export interface DashboardtypesSigNozDatasourceSpecDTO { - [key: string]: unknown; -} - -export interface DashboardtypesDatasourcePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesSigNozDatasourceSpecDTO { - /** - * @enum signoz/Datasource - * @type string - */ - kind: DashboardtypesDatasourcePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesSigNozDatasourceSpecDTOKind; - spec: DashboardtypesSigNozDatasourceSpecDTO; -} - -export type DashboardtypesDatasourcePluginDTO = - DashboardtypesDatasourcePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesSigNozDatasourceSpecDTO; - -export interface DashboardtypesDatasourceSpecDTO { - /** - * @type boolean - */ - default?: boolean; - display?: CommonDisplayDTO; - plugin?: DashboardtypesDatasourcePluginDTO; -} - -export type DashboardtypesDashboardSpecDTODatasourcesAnyOf = { - [key: string]: DashboardtypesDatasourceSpecDTO; -}; - -/** - * @nullable - */ -export type DashboardtypesDashboardSpecDTODatasources = - DashboardtypesDashboardSpecDTODatasourcesAnyOf | null; - export enum DashboardtypesPanelKindDTO { Panel = 'Panel', } @@ -4626,9 +4577,9 @@ export interface DashboardtypesQueryDTO { export interface DashboardtypesPanelSpecDTO { display: DashboardtypesDisplayDTO; /** - * @type array + * @type array,null */ - links: DashboardtypesLinkDTO[]; + links?: DashboardtypesLinkDTO[] | null; plugin: DashboardtypesPanelPluginDTO; /** * @type array @@ -4807,10 +4758,6 @@ export type DashboardtypesVariableDTO = | DashboardtypesVariableEnvelopeGithubComSigNozSignozPkgTypesDashboardtypesTextVariableSpecDTO; export interface DashboardtypesDashboardSpecDTO { - /** - * @type object,null - */ - datasources?: DashboardtypesDashboardSpecDTODatasources; display: DashboardtypesDisplayDTO; /** * @type string @@ -4821,9 +4768,9 @@ export interface DashboardtypesDashboardSpecDTO { */ layouts: DashboardtypesLayoutDTO[]; /** - * @type array + * @type array,null */ - links: DashboardtypesLinkDTO[]; + links?: DashboardtypesLinkDTO[] | null; /** * @type object */ @@ -4886,9 +4833,6 @@ export interface DashboardtypesDashboardViewDTO { updatedAt?: string; } -export enum DashboardtypesDatasourcePluginKindDTO { - 'signoz/Datasource' = 'signoz/Datasource', -} export interface TagtypesGettableTagDTO { /** * @type string diff --git a/frontend/src/hooks/dashboard/useCreateExportDashboard.ts b/frontend/src/hooks/dashboard/useCreateExportDashboard.ts index ecc5864e806..f135247889a 100644 --- a/frontend/src/hooks/dashboard/useCreateExportDashboard.ts +++ b/frontend/src/hooks/dashboard/useCreateExportDashboard.ts @@ -55,7 +55,6 @@ export function useCreateExportDashboard({ layouts: [], panels: {}, variables: [], - links: [], }, }), { diff --git a/frontend/src/pages/DashboardPageV2/DashboardContainer/PanelEditor/ConfigPane/sectionRegistry.tsx b/frontend/src/pages/DashboardPageV2/DashboardContainer/PanelEditor/ConfigPane/sectionRegistry.tsx index 746ff03f71a..bfc592d82f7 100644 --- a/frontend/src/pages/DashboardPageV2/DashboardContainer/PanelEditor/ConfigPane/sectionRegistry.tsx +++ b/frontend/src/pages/DashboardPageV2/DashboardContainer/PanelEditor/ConfigPane/sectionRegistry.tsx @@ -120,7 +120,7 @@ export const SECTION_REGISTRY: { [SectionKind.ContextLinks]: { Component: ContextLinksSection, // Panel-level slice (spec.links), not under the plugin spec — no cast needed. - get: (spec): DashboardtypesLinkDTO[] => spec.links, + get: (spec): DashboardtypesLinkDTO[] => spec.links || [], update: (spec, links): PanelSpec => ({ ...spec, links }), }, // One editor for every threshold variant (label / comparison / table); the kind's diff --git a/frontend/src/pages/DashboardPageV2/DashboardContainer/Panels/utils/drilldown/resolvePanelContextLinks.ts b/frontend/src/pages/DashboardPageV2/DashboardContainer/Panels/utils/drilldown/resolvePanelContextLinks.ts index 58843eaa165..c56a0a7cc16 100644 --- a/frontend/src/pages/DashboardPageV2/DashboardContainer/Panels/utils/drilldown/resolvePanelContextLinks.ts +++ b/frontend/src/pages/DashboardPageV2/DashboardContainer/Panels/utils/drilldown/resolvePanelContextLinks.ts @@ -1,4 +1,4 @@ -import type { DashboardtypesLinkDTO } from 'api/generated/services/sigNoz.schemas'; +import type { DashboardtypesPanelSpecDTO } from 'api/generated/services/sigNoz.schemas'; import { resolveTexts } from 'hooks/dashboard/useContextVariables'; import { resolveContextLinkUrl } from './resolveContextLinkUrl'; @@ -15,7 +15,7 @@ export interface ResolvedDrilldownLink { * label + URL, drops links without a URL, and skips substitution when `renderVariables === false`. */ export function resolvePanelContextLinks( - links: DashboardtypesLinkDTO[] | undefined, + links: DashboardtypesPanelSpecDTO['links'], processedVariables: Record, ): ResolvedDrilldownLink[] { const usable = (links ?? []).filter((link) => !!link.url); diff --git a/frontend/src/pages/DashboardPageV2/DashboardContainer/PanelsAndSectionsLayout/Panel/DrilldownMenu/DrilldownAggregateMenu.tsx b/frontend/src/pages/DashboardPageV2/DashboardContainer/PanelsAndSectionsLayout/Panel/DrilldownMenu/DrilldownAggregateMenu.tsx index b2f2deab384..56326af3457 100644 --- a/frontend/src/pages/DashboardPageV2/DashboardContainer/PanelsAndSectionsLayout/Panel/DrilldownMenu/DrilldownAggregateMenu.tsx +++ b/frontend/src/pages/DashboardPageV2/DashboardContainer/PanelsAndSectionsLayout/Panel/DrilldownMenu/DrilldownAggregateMenu.tsx @@ -8,7 +8,7 @@ import { ScrollText, } from '@signozhq/icons'; import logEvent from 'api/common/logEvent'; -import type { DashboardtypesLinkDTO } from 'api/generated/services/sigNoz.schemas'; +import type { DashboardtypesPanelSpecDTO } from 'api/generated/services/sigNoz.schemas'; import { getAggregateColumnHeader } from 'container/QueryTable/Drilldown/drilldownUtils'; import ContextMenu from 'periscope/components/ContextMenu'; import type { DrilldownContext } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/drilldown'; @@ -29,7 +29,7 @@ interface DrilldownAggregateMenuProps { /** While dashboard variables resolve, the actions show a spinner and are disabled. */ isResolving?: boolean; /** Panel's context links; resolved against the clicked point + variables here. */ - links: DashboardtypesLinkDTO[] | undefined; + links: DashboardtypesPanelSpecDTO['links']; /** Whether the clicked point exposes group-by fields to bind to dashboard variables. */ canSetDashboardVariables: boolean; onViewLogs: () => void; diff --git a/frontend/src/pages/DashboardPageV2/DashboardContainer/patchOps.ts b/frontend/src/pages/DashboardPageV2/DashboardContainer/patchOps.ts index 58bd49f63d3..fcf137bb0b9 100644 --- a/frontend/src/pages/DashboardPageV2/DashboardContainer/patchOps.ts +++ b/frontend/src/pages/DashboardPageV2/DashboardContainer/patchOps.ts @@ -49,7 +49,6 @@ export function createDefaultPanel( spec: pluginSpec, } as DashboardtypesPanelPluginDTO, queries, - links: [], }, }; } diff --git a/frontend/src/pages/DashboardsListPageV2/components/NewDashboardModal/BlankDashboardPanel.tsx b/frontend/src/pages/DashboardsListPageV2/components/NewDashboardModal/BlankDashboardPanel.tsx index 445154f7fe0..7eca2197a2f 100644 --- a/frontend/src/pages/DashboardsListPageV2/components/NewDashboardModal/BlankDashboardPanel.tsx +++ b/frontend/src/pages/DashboardsListPageV2/components/NewDashboardModal/BlankDashboardPanel.tsx @@ -62,7 +62,6 @@ function BlankDashboardPanel({ onClose }: Props): JSX.Element { layouts: [], panels: {}, variables: [], - links: [], }, }); void logEvent(DashboardListEvents.DashboardCreated, { diff --git a/pkg/telemetrylogs/condition_builder.go b/pkg/telemetrylogs/condition_builder.go index 045629057ee..40c97cecfdc 100644 --- a/pkg/telemetrylogs/condition_builder.go +++ b/pkg/telemetrylogs/condition_builder.go @@ -89,7 +89,8 @@ func (c *conditionBuilder) conditionForArrayFunction( for i, v := range list { vals[i] = legacyCoerceNeedle(v, elemType) } - arrayCond := fmt.Sprintf("%s(%s, %s)", operator.FunctionName(), arrayExpr, sb.Var(vals)) + // Pin the needle array type to the haystack; scalar fallback below coerces value-level. + arrayCond := fmt.Sprintf("%s(%s, %s)", operator.FunctionName(), arrayExpr, castNeedleArray(elemType, sb.Var(vals))) if !hasScalar { return arrayCond, nil } @@ -113,6 +114,27 @@ func (c *conditionBuilder) conditionForArrayFunction( return fmt.Sprintf("(%s OR ifNull(%s, false))", arrayCond, sb.And(sb.E(scalarExpr, typedNeedle), scalarGuard)), nil } +// castNeedleArray pins an Int64 needle array to Array(Int64) so it matches the Array(Nullable(Int64)) +// haystack; without it a needle >= 2^32 binds as Array(UInt64) and hasAny/hasAll error (code 386). +func castNeedleArray(elemType telemetrytypes.FieldDataType, arg string) string { + if elemType == telemetrytypes.FieldDataTypeInt64 { + return fmt.Sprintf("CAST(%s AS Array(Int64))", arg) + } + return arg +} + +// firstTokenSeparator returns the first char of s that hasToken treats as a token separator +// (anything other than an ASCII letter or digit), and whether one was found. +func firstTokenSeparator(s string) (string, bool) { + for _, r := range s { + isAlphaNum := (r >= '0' && r <= '9') || (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') + if !isAlphaNum { + return string(r), true + } + } + return "", false +} + // conditionForHasToken builds a hasToken full-text search over the body column, resolving the // column from the key name + use_json_body flag. func (c *conditionBuilder) conditionForHasToken( @@ -129,11 +151,19 @@ func (c *conditionBuilder) conditionForHasToken( } // hasToken matches string tokens only. - if _, ok := needle.(string); !ok { + needleStr, ok := needle.(string) + if !ok { return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "function `hasToken` expects value parameter to be a string").WithUrl(hasTokenFunctionDocURL) } + // A multi-token needle makes CH hasToken error (code 36); reject up front as a 400. Both modes flow here. + if sep, found := firstTokenSeparator(needleStr); found { + return "", errors.NewInvalidInputf(errors.CodeInvalidInput, + "function `hasToken` matches a single whole token, but %q contains the separator %q; use a substring filter (e.g. `body CONTAINS '%s'`) to search across separators", + needleStr, sep, needleStr).WithUrl(hasTokenFunctionDocURL) + } + bodyJSONEnabled := c.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(orgID)) if !bodyJSONEnabled { diff --git a/pkg/telemetrylogs/filter_expr_logs_body_json_test.go b/pkg/telemetrylogs/filter_expr_logs_body_json_test.go index bf2fd09da11..fed7735c99d 100644 --- a/pkg/telemetrylogs/filter_expr_logs_body_json_test.go +++ b/pkg/telemetrylogs/filter_expr_logs_body_json_test.go @@ -104,6 +104,23 @@ func TestFilterExprLogsBodyJSON(t *testing.T) { expectedArgs: []any{int64(200), int64(200)}, expectedErrorContains: "", }, + { + // Big-int needle CAST to Array(Int64) to match the haystack (else 386). + category: "json", + query: `hasAny(body.ids, ['9007199254740993', '9007199254740994'])`, + shouldPass: true, + expectedQuery: `WHERE (hasAny(JSONExtract(JSON_QUERY(body, '$."ids"[*]'), 'Array(Nullable(Int64))'), CAST(? AS Array(Int64))) OR ifNull((JSONExtract(JSON_VALUE(body, '$."ids"'), 'Nullable(Int64)') IN (?, ?) AND JSONType(body, 'ids') NOT IN ('Array', 'Object', 'Null')), false))`, + expectedArgs: []any{[]any{int64(9007199254740993), int64(9007199254740994)}, int64(9007199254740993), int64(9007199254740994)}, + expectedErrorContains: "", + }, + { + category: "json", + query: `hasAll(body.ids, ['9007199254740993', '9007199254740994'])`, + shouldPass: true, + expectedQuery: `WHERE (hasAll(JSONExtract(JSON_QUERY(body, '$."ids"[*]'), 'Array(Nullable(Int64))'), CAST(? AS Array(Int64))) OR ifNull(((JSONExtract(JSON_VALUE(body, '$."ids"'), 'Nullable(Int64)') = ? AND JSONExtract(JSON_VALUE(body, '$."ids"'), 'Nullable(Int64)') = ?) AND JSONType(body, 'ids') NOT IN ('Array', 'Object', 'Null')), false))`, + expectedArgs: []any{[]any{int64(9007199254740993), int64(9007199254740994)}, int64(9007199254740993), int64(9007199254740994)}, + expectedErrorContains: "", + }, { category: "json", query: "body.message = hello", diff --git a/pkg/telemetrylogs/filter_expr_logs_test.go b/pkg/telemetrylogs/filter_expr_logs_test.go index 3ea1a2612e5..0f783c084d0 100644 --- a/pkg/telemetrylogs/filter_expr_logs_test.go +++ b/pkg/telemetrylogs/filter_expr_logs_test.go @@ -1561,6 +1561,19 @@ func TestFilterExprLogs(t *testing.T) { expectedArgs: []any{"download"}, expectedErrorContains: "function `hasToken` expects value parameter to be a string", }, + // A multi-token needle (separator/whitespace) is a clean 400, not a CH execution error. + { + category: "hasTokenUnderscoreNeedle", + query: "hasToken(body, \"user_id\")", + shouldPass: false, + expectedErrorContains: "function `hasToken` matches a single whole token", + }, + { + category: "hasTokenWhitespaceNeedle", + query: "hasToken(body, \"production node\")", + shouldPass: false, + expectedErrorContains: "function `hasToken` matches a single whole token", + }, // extra / mis-shaped value arguments are rejected, not silently dropped. { category: "hasExtraArgs", diff --git a/pkg/types/cloudintegrationtypes/account.go b/pkg/types/cloudintegrationtypes/account.go index 95502f601df..21afedf9eb4 100644 --- a/pkg/types/cloudintegrationtypes/account.go +++ b/pkg/types/cloudintegrationtypes/account.go @@ -137,6 +137,13 @@ func NewAccountFromStorable(storableAccount *StorableCloudIntegration) (*Account return nil, err } account.Config.Azure = azureConfig + case CloudProviderTypeGCP: + gcpConfig := new(GCPAccountConfig) + err := json.Unmarshal([]byte(storableAccount.Config), gcpConfig) + if err != nil { + return nil, err + } + account.Config.GCP = gcpConfig } if storableAccount.LastAgentReport != nil { diff --git a/pkg/types/dashboardtypes/perses_dashboard_data.go b/pkg/types/dashboardtypes/perses_dashboard_data.go index d163e612430..b56e9f72adc 100644 --- a/pkg/types/dashboardtypes/perses_dashboard_data.go +++ b/pkg/types/dashboardtypes/perses_dashboard_data.go @@ -17,16 +17,17 @@ import ( // DashboardSpec is the SigNoz dashboard v2 spec shape. It mirrors // dashboard.Spec (Perses) field-for-field, except every common.Plugin // occurrence is replaced with a typed SigNoz plugin whose OpenAPI schema is a -// per-site discriminated oneOf. +// per-site discriminated oneOf. Perses's datasources field is deliberately +// dropped: SigNoz never reads it (queries carry their own signal/source), so +// the drift test allowlists it as an intentional omission. type DashboardSpec struct { - Display Display `json:"display" required:"true"` - Datasources map[string]*DatasourceSpec `json:"datasources,omitzero"` - Variables []Variable `json:"variables" required:"true" nullable:"false"` - Panels map[string]*Panel `json:"panels" required:"true" nullable:"false"` - Layouts []Layout `json:"layouts" required:"true" nullable:"false"` - Duration common.DurationString `json:"duration"` - RefreshInterval common.DurationString `json:"refreshInterval"` - Links []Link `json:"links" required:"true" nullable:"false"` + Display Display `json:"display" required:"true"` + Variables []Variable `json:"variables" required:"true" nullable:"false"` + Panels map[string]*Panel `json:"panels" required:"true" nullable:"false"` + Layouts []Layout `json:"layouts" required:"true" nullable:"false"` + Duration common.DurationString `json:"duration"` + RefreshInterval common.DurationString `json:"refreshInterval"` + Links []Link `json:"links,omitzero"` } // ══════════════════════════════════════════════ @@ -45,16 +46,6 @@ func (d *DashboardSpec) UnmarshalJSON(data []byte) error { return d.Validate() } -// validateLinks rejects a missing/null spec.links value: a typed client must -// send [] rather than omitting links, so its value round-trips faithfully. -// Panel links are the panel spec's concern, validated in validatePanels. -func (d *DashboardSpec) validateLinks() error { - if d.Links == nil { - return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "spec.links is required; send [] when there are no links") - } - return nil -} - // ══════════════════════════════════════════════ // Cross-field validation // ══════════════════════════════════════════════ @@ -63,9 +54,6 @@ func (d *DashboardSpec) Validate() error { if err := d.Display.Validate("dashboard", "spec.display.name"); err != nil { return err } - if err := d.validateLinks(); err != nil { - return err - } if err := d.validateVariables(); err != nil { return err } @@ -117,12 +105,9 @@ func (d *DashboardSpec) validatePanels() error { if err := panel.Spec.Display.Validate("panel", path+".spec.display.name"); err != nil { return err } - if err := panel.Spec.validateLinks(path); err != nil { - return err - } panelKind := panel.Spec.Plugin.Kind if len(panel.Spec.Queries) != 1 { - return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "%s.spec.queries: panel must have one query", path) + return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "%s.spec.queries: panel must have one query, found %d", path, len(panel.Spec.Queries)) } allowed := allowedQueryKinds[panelKind] for qi, q := range panel.Spec.Queries { @@ -285,8 +270,8 @@ func (d *DashboardSpec) validateLayouts() error { return errors.NewInternalf(errors.CodeInternal, "spec.layouts[%d].spec: unexpected layout spec type %T", li, layout.Spec) } if grid.Display != nil { - if n := utf8.RuneCountInString(grid.Display.Title); n > MaxDisplayNameLen { - return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "spec.layouts[%d].spec.display.title: layout name must be at most %d characters, got %d", li, MaxDisplayNameLen, n) + if n := utf8.RuneCountInString(grid.Display.Title); n > MaxLayoutTitleLen { + return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "spec.layouts[%d].spec.display.title: layout name must be at most %d characters, got %d", li, MaxLayoutTitleLen, n) } } if err := validateGridLayoutGeometry(grid, li); err != nil { diff --git a/pkg/types/dashboardtypes/perses_dashboard_patch_test.go b/pkg/types/dashboardtypes/perses_dashboard_patch_test.go index 826ef6fdf02..1002ef011a8 100644 --- a/pkg/types/dashboardtypes/perses_dashboard_patch_test.go +++ b/pkg/types/dashboardtypes/perses_dashboard_patch_test.go @@ -30,7 +30,7 @@ const basePostableJSON = `{ "spec": { "name": "service", "allowAllValue": true, - "allowMultiple": false, + "allowMultiple": true, "plugin": { "kind": "signoz/DynamicVariable", "spec": {"name": "service.name", "signal": "metrics"} diff --git a/pkg/types/dashboardtypes/perses_dashboard_test.go b/pkg/types/dashboardtypes/perses_dashboard_test.go index 0ebbd34189e..9a48213ed5f 100644 --- a/pkg/types/dashboardtypes/perses_dashboard_test.go +++ b/pkg/types/dashboardtypes/perses_dashboard_test.go @@ -44,7 +44,7 @@ func TestInvalidateNotAJSON(t *testing.T) { // TestUnmarshalErrorPreservesNestedMessage guards the wrap on dec.Decode in // DashboardSpec.UnmarshalJSON. The wrap stamps a consistent type/code on // decode failures, but must not smother the rich messages produced by nested -// UnmarshalJSON methods (panel/query/variable/datasource plugin envelopes). +// UnmarshalJSON methods (panel/query/variable plugin envelopes). func TestUnmarshalErrorPreservesNestedMessage(t *testing.T) { data := []byte(`{ "panels": { @@ -77,8 +77,8 @@ func TestUnmarshalErrorPreservesNestedMessage(t *testing.T) { } func TestValidateEmptySpec(t *testing.T) { - // no variables no panels - data := []byte(`{"links": []}`) + // no variables no panels no links + data := []byte(`{}`) _, err := unmarshalDashboard(data) assert.NoError(t, err, "expected valid") } @@ -91,7 +91,7 @@ func TestValidateOnlyVariables(t *testing.T) { "spec": { "name": "service", "allowAllValue": true, - "allowMultiple": false, + "allowMultiple": true, "plugin": { "kind": "signoz/DynamicVariable", "spec": { @@ -237,6 +237,12 @@ func TestInvalidateListVariableCrossFields(t *testing.T) { assert.Contains(t, err.Error(), "customAllValue cannot be set") }) + t.Run("allowAllValue without allowMultiple", func(t *testing.T) { + _, err := unmarshalDashboard(listVar(`"allowAllValue": true, "allowMultiple": false,`)) + require.Error(t, err) + assert.Contains(t, err.Error(), "allowAllValue cannot be set") + }) + t.Run("list defaultValue without allowMultiple", func(t *testing.T) { _, err := unmarshalDashboard(listVar(`"allowAllValue": false, "allowMultiple": false, "defaultValue": ["a", "b"],`)) require.Error(t, err) @@ -441,20 +447,6 @@ func TestInvalidateUnknownPluginKind(t *testing.T) { }`, wantContain: "FakeVariable", }, - { - name: "unknown datasource plugin", - data: `{ - "datasources": { - "ds1": { - "default": true, - "plugin": {"kind": "FakeDatasource", "spec": {}} - } - }, - "links": [], - "layouts": [] - }`, - wantContain: "FakeDatasource", - }, } for _, tt := range tests { @@ -1739,55 +1731,61 @@ func TestInvalidateDuplicatePanelReference(t *testing.T) { assert.Contains(t, err.Error(), "spec.layouts[0].spec.items[1].content") } -// Every display name — dashboard, panel, variable — and the grid layout title is -// bounded at MaxDisplayNameLen. The name is one over the limit in each case, and -// the message reads ": name must be at most ...", pairing the -// locatable path (like the other spec errors) with a human field label. +// Every display name — dashboard, panel, variable — is bounded at MaxDisplayNameLen, +// while the grid layout title has its own, larger bound (MaxLayoutTitleLen). The name +// is one over the relevant limit in each case, and the message reads ": +// name must be at most ...", pairing the locatable path (like the other spec +// errors) with a human field label. func TestInvalidateDisplayNameTooLong(t *testing.T) { - tooLong := strings.Repeat("x", MaxDisplayNameLen+1) - lengthMsg := fmt.Sprintf("must be at most %d characters, got %d", MaxDisplayNameLen, MaxDisplayNameLen+1) - testCases := []struct { - scenario string - dashboardJSON string - expectedPath string - expectedLabel string + scenario string + limit int + dashboardJSONFmt string + expectedPath string + expectedLabel string }{ { - scenario: "dashboard display name", - dashboardJSON: `{"display": {"name": "` + tooLong + `"}, "links": [], "layouts": []}`, - expectedLabel: "dashboard", - expectedPath: "spec.display.name", + scenario: "dashboard display name", + limit: MaxDisplayNameLen, + dashboardJSONFmt: `{"display": {"name": "%s"}, "links": [], "layouts": []}`, + expectedLabel: "dashboard", + expectedPath: "spec.display.name", }, { - scenario: "panel display name", - dashboardJSON: `{"panels": {"p1": {"kind": "Panel", "spec": {"links": [],"display": {"name": "` + tooLong + `"}, "plugin": {"kind": "signoz/TablePanel", "spec": {}}, "queries": []}}}, "links": [], "layouts": []}`, - expectedLabel: "panel", - expectedPath: "spec.panels.p1.spec.display.name", + scenario: "panel display name", + limit: MaxDisplayNameLen, + dashboardJSONFmt: `{"panels": {"p1": {"kind": "Panel", "spec": {"links": [], "display": {"name": "%s"}, "plugin": {"kind": "signoz/TablePanel", "spec": {}}, "queries": []}}}, "links": [], "layouts": []}`, + expectedLabel: "panel", + expectedPath: "spec.panels.p1.spec.display.name", }, { - scenario: "list variable display name", - dashboardJSON: `{"variables": [{"kind": "ListVariable", "spec": {"name": "svc", "display": {"name": "` + tooLong + `"}, "plugin": {"kind": "signoz/DynamicVariable", "spec": {"name": "service.name", "signal": "metrics"}}}}], "links": [], "layouts": []}`, - expectedLabel: "variable", - expectedPath: "spec.variables[0].spec.display.name", + scenario: "list variable display name", + limit: MaxDisplayNameLen, + dashboardJSONFmt: `{"variables": [{"kind": "ListVariable", "spec": {"name": "svc", "display": {"name": "%s"}, "plugin": {"kind": "signoz/DynamicVariable", "spec": {"name": "service.name", "signal": "metrics"}}}}], "links": [], "layouts": []}`, + expectedLabel: "variable", + expectedPath: "spec.variables[0].spec.display.name", }, { - scenario: "text variable display name", - dashboardJSON: `{"variables": [{"kind": "TextVariable", "spec": {"name": "mytext", "value": "v", "display": {"name": "` + tooLong + `"}}}], "links": [], "layouts": []}`, - expectedLabel: "variable", - expectedPath: "spec.variables[0].spec.display.name", + scenario: "text variable display name", + limit: MaxDisplayNameLen, + dashboardJSONFmt: `{"variables": [{"kind": "TextVariable", "spec": {"name": "mytext", "value": "v", "display": {"name": "%s"}}}], "links": [], "layouts": []}`, + expectedLabel: "variable", + expectedPath: "spec.variables[0].spec.display.name", }, { - scenario: "layout title", - dashboardJSON: `{"links": [], "layouts": [{"kind": "Grid", "spec": {"display": {"title": "` + tooLong + `"}, "items": []}}]}`, - expectedLabel: "layout", - expectedPath: "spec.layouts[0].spec.display.title", + scenario: "layout title", + limit: MaxLayoutTitleLen, + dashboardJSONFmt: `{"links": [], "layouts": [{"kind": "Grid", "spec": {"display": {"title": "%s"}, "items": []}}]}`, + expectedLabel: "layout", + expectedPath: "spec.layouts[0].spec.display.title", }, } for _, testCase := range testCases { t.Run(testCase.scenario, func(t *testing.T) { - _, err := unmarshalDashboard([]byte(testCase.dashboardJSON)) + tooLong := strings.Repeat("x", testCase.limit+1) + lengthMsg := fmt.Sprintf("must be at most %d characters, got %d", testCase.limit, testCase.limit+1) + _, err := unmarshalDashboard(fmt.Appendf(nil, testCase.dashboardJSONFmt, tooLong)) require.Error(t, err) // Message is ":