Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions cue/dashboard/dashboard_go_gen.cue
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@ import plugin_9 "github.com/perses/spec/cue/plugin"
plugin: plugin_9.#Plugin @go(Plugin)
queries?: [...#Query] @go(Queries,[]Query)
links?: [...#Link] @go(Links,[]Link)
annotations?: null | #PanelAnnotations @go(Annotations,*PanelAnnotations)
}

// PanelAnnotations controls the annotations displayed on a single panel.
#PanelAnnotations: {
// Enabled toggles the display of dashboard-level annotations on this panel.
// A nil value is treated as enabled, matching the "on by default" behavior of the
// two-level annotation design.
enabled?: null | bool @go(Enabled,*bool)

// Definitions holds annotations defined locally on this panel. They use the same shape
// as dashboard-level annotations (AnnotationSpec).
definitions?: [...#AnnotationSpec] @go(Definitions,[]AnnotationSpec)
}

#Panel: {
Expand Down
20 changes: 16 additions & 4 deletions go/dashboard/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,22 @@ type PanelDisplay struct {
}

type PanelSpec struct {
Display *PanelDisplay `json:"display,omitempty" yaml:"display,omitempty"`
Plugin plugin.Plugin `json:"plugin" yaml:"plugin"`
Queries []Query `json:"queries,omitempty" yaml:"queries,omitempty"`
Links []Link `json:"links,omitempty" yaml:"links,omitempty"`
Display *PanelDisplay `json:"display,omitempty" yaml:"display,omitempty"`
Plugin plugin.Plugin `json:"plugin" yaml:"plugin"`
Queries []Query `json:"queries,omitempty" yaml:"queries,omitempty"`
Links []Link `json:"links,omitempty" yaml:"links,omitempty"`
Annotations *PanelAnnotations `json:"annotations,omitempty" yaml:"annotations,omitempty"`
}

// PanelAnnotations controls the annotations displayed on a single panel.
type PanelAnnotations struct {
// Enabled toggles the display of dashboard-level annotations on this panel.
// A nil value is treated as enabled, matching the "on by default" behavior of the
// two-level annotation design.
Enabled *bool `json:"enabled,omitempty" yaml:"enabled,omitempty"`
// Definitions holds annotations defined locally on this panel. They use the same shape
// as dashboard-level annotations (AnnotationSpec).
Definitions []AnnotationSpec `json:"definitions,omitempty" yaml:"definitions,omitempty"`
}

type Panel struct {
Expand Down
107 changes: 107 additions & 0 deletions go/dashboard/panel_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright The Perses Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package dashboard

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)

// A panel with an annotations block: the dashboard-level toggle turned off plus one panel-local
// definition. This exercises the new PanelSpec.Annotations field end to end.
const panelWithAnnotationsJSON = `{
"plugin": {"kind": "TimeSeriesChart", "spec": {}},
"annotations": {
"enabled": false,
"definitions": [
{
"display": {"name": "Deploys"},
"plugin": {"kind": "PrometheusPromQLAnnotation", "spec": {}}
}
]
}
}`

func TestPanelSpecAnnotationsRoundTripJSON(t *testing.T) {
var spec PanelSpec
require.NoError(t, json.Unmarshal([]byte(panelWithAnnotationsJSON), &spec))

require.NotNil(t, spec.Annotations)
require.NotNil(t, spec.Annotations.Enabled)
assert.False(t, *spec.Annotations.Enabled)
require.Len(t, spec.Annotations.Definitions, 1)
assert.Equal(t, "Deploys", spec.Annotations.Definitions[0].Display.Name)
assert.Equal(t, "PrometheusPromQLAnnotation", spec.Annotations.Definitions[0].Plugin.Kind)

// Marshal back and unmarshal again; the annotations block must survive the round trip.
data, err := json.Marshal(spec)
require.NoError(t, err)

var reparsed PanelSpec
require.NoError(t, json.Unmarshal(data, &reparsed))
assert.Equal(t, spec, reparsed)
}

func TestPanelSpecAnnotationsDefaultOnWhenOmitted(t *testing.T) {
// A panel without an annotations block leaves the field nil, which the consumer treats as
// "dashboard annotations enabled" (default on).
var spec PanelSpec
require.NoError(t, json.Unmarshal([]byte(`{"plugin": {"kind": "TimeSeriesChart", "spec": {}}}`), &spec))
assert.Nil(t, spec.Annotations)
}

func TestPanelSpecAnnotationsRejectsEmptyDefinitionName(t *testing.T) {
// The panel-local definitions reuse AnnotationSpec, whose UnmarshalJSON validates a non-empty
// name. That validation fires automatically for the nested definition, so no separate plumbing
// is needed on PanelSpec.
const invalid = `{
"plugin": {"kind": "TimeSeriesChart", "spec": {}},
"annotations": {
"definitions": [
{"display": {"name": ""}, "plugin": {"kind": "PrometheusPromQLAnnotation", "spec": {}}}
]
}
}`
var spec PanelSpec
err := json.Unmarshal([]byte(invalid), &spec)
require.Error(t, err)
assert.Contains(t, err.Error(), "annotation name cannot be empty")
}

func TestPanelSpecAnnotationsRoundTripYAML(t *testing.T) {
const in = `
plugin:
kind: TimeSeriesChart
spec: {}
annotations:
enabled: true
definitions:
- display:
name: Incidents
plugin:
kind: PrometheusPromQLAnnotation
spec: {}
`
var spec PanelSpec
require.NoError(t, yaml.Unmarshal([]byte(in), &spec))
require.NotNil(t, spec.Annotations)
require.NotNil(t, spec.Annotations.Enabled)
assert.True(t, *spec.Annotations.Enabled)
require.Len(t, spec.Annotations.Definitions, 1)
assert.Equal(t, "Incidents", spec.Annotations.Definitions[0].Display.Name)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright The Perses Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package dev.perses.spec.dashboard.panel;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import dev.perses.spec.dashboard.annotation.AnnotationSpec;

import java.util.List;

@JsonInclude(JsonInclude.Include.NON_NULL)
public class PanelAnnotations {
@JsonProperty("enabled")
public Boolean enabled;

@JsonProperty("definitions")
public List<AnnotationSpec> definitions;

public PanelAnnotations() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ public class PanelSpec {
@JsonProperty("links")
public List<Link> links;

@JsonProperty("annotations")
public PanelAnnotations annotations;

public PanelSpec() {
}
}
9 changes: 9 additions & 0 deletions ts/src/dashboard/panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// limitations under the License.

import { Definition, UnknownSpec } from '../common';
import { AnnotationSpec } from './annotation';
import { Link } from './link';
import { QueryDefinition } from './query-type';

Expand All @@ -20,6 +21,13 @@ export interface PanelDisplay {
description?: string;
}

export interface PanelAnnotations {
// Toggles the display of dashboard-level annotations on this panel. Undefined is treated as enabled.
enabled?: boolean;
// Annotations defined locally on this panel, using the same shape as dashboard-level annotations.
definitions?: AnnotationSpec[];
}

export interface PanelDefinition<PluginSpec = UnknownSpec> extends Definition<PanelSpec<PluginSpec>> {
kind: 'Panel';
}
Expand All @@ -29,6 +37,7 @@ export interface PanelSpec<PluginSpec = UnknownSpec> {
plugin: Definition<PluginSpec>;
queries?: QueryDefinition[];
links?: Link[];
annotations?: PanelAnnotations;
}

/**
Expand Down
Loading