Repository: honeycombio/terraform-provider-honeycombio
Pull request #676 (2025-05-09 17:46:39)

feat(boards): Support panels in board client
## Which problem is this PR solving?

- Breaking down my massive PR into incremental PRs
- This adds panels to board client for flexible boards support



Patch:
diff --git a/client/board.go b/client/board.go
index 995bfa2..93090e6 100644
--- a/client/board.go
+++ b/client/board.go
@@ -44,6 +44,10 @@ type Board struct {
 	// Defaults to "classic".
 	BoardType BoardType `json:"type,omitempty"`
 
+	// Board panels are the individual panels that make up a board. Each panel can
+	// be a query or an SLO panel. The panels are laid out in a grid.
+	Panels []BoardPanel `json:"panels,omitempty"`
+
 	// Name of the board, this is displayed in the Honeycomb UI. This field is
 	// required.
 	Name string `json:"name"`
@@ -65,6 +69,57 @@ type Board struct {
 	SLOs []string `json:"slos"`
 }
 
+// BoardPanel represents a single panel on a board.
+type BoardPanel struct {
+	PanelType     BoardPanelType     `json:"type,omitempty"` // "query" or "slo"
+	PanelPosition BoardPanelPosition `json:"position,omitempty"`
+
+	QueryPanel *BoardQueryPanel `json:"query_panel,omitempty"`
+	SLOPanel   *BoardSLOPanel   `json:"slo_panel,omitempty"`
+}
+
+type BoardPanelType string
+
+const (
+	BoardPanelTypeQuery BoardPanelType = "query"
+	BoardPanelTypeSLO   BoardPanelType = "slo"
+)
+
+type BoardPanelPosition struct {
+	X      int `json:"x_coordinate"`
+	Y      int `json:"y_coordinate"`
+	Height int `json:"height"`
+	Width  int `json:"width"`
+}
+
+type BoardQueryPanel struct {
+	QueryID               string                           `json:"query_id,omitempty"`
+	QueryAnnotationID     string                           `json:"query_annotation_id,omitempty"`
+	VisualizationSettings *BoardQueryVisualizationSettings `json:"visualization_settings,omitempty"`
+	Style                 BoardQueryStyle                  `json:"query_style,omitempty"`
+	Dataset               string                           `json:"dataset,omitempty"`
+}
+
+type BoardQueryVisualizationSettings struct {
+	UseUTCXAxis          bool             `json:"utc_xaxis,omitempty"`
+	HideMarkers          bool             `json:"hide_markers,omitempty"`
+	HideHovers           bool             `json:"hide_hovers,omitempty"`
+	PreferOverlaidCharts bool             `json:"overlaid_charts,omitempty"`
+	HideCompare          bool             `json:"hide_compare,omitempty"`
+	Charts               []*ChartSettings `json:"charts,omitempty"`
+}
+
+type ChartSettings struct {
+	ChartType         string `json:"chart_type,omitempty"` // "default", "line", "stacked", "stat", "tsbar"
+	ChartIndex        int    `json:"chart_index"`
+	OmitMissingValues bool   `json:"omit_missing_values,omitempty"`
+	UseLogScale       bool   `json:"log_scale,omitempty"`
+}
+
+type BoardSLOPanel struct {
+	SLOID string `json:"slo_id,omitempty"`
+}
+
 type BoardType string
 
 const (


Test patch:
diff --git a/client/board_test.go b/client/board_test.go
index f22e542..04712de 100644
--- a/client/board_test.go
+++ b/client/board_test.go
@@ -143,3 +143,109 @@ func TestClassicBoards(t *testing.T) {
 		assert.True(t, de.IsNotFound())
 	})
 }
+
+func TestFlexibleBoards(t *testing.T) {
+	t.Parallel()
+
+	ctx := context.Background()
+	var flexibleBoard *client.Board
+	var err error
+
+	c := newTestClient(t)
+	dataset := testDataset(t)
+
+	column, err := c.Columns.Create(ctx, dataset, &client.Column{
+		KeyName: test.RandomStringWithPrefix("test.", 8),
+	})
+	require.NoError(t, err)
+	t.Cleanup(func() {
+		c.Columns.Delete(ctx, dataset, column.ID)
+	})
+
+	query, err := c.Queries.Create(ctx, dataset, &client.QuerySpec{
+		Calculations: []client.CalculationSpec{
+			{
+				Op:     client.CalculationOpAvg,
+				Column: &column.KeyName,
+			},
+		},
+		TimeRange: client.ToPtr(3600), // 1 hour
+	})
+	require.NoError(t, err)
+	require.NotEmpty(t, query.ID)
+
+	qa := &client.QueryAnnotation{
+		Name:        test.RandomStringWithPrefix("test.", 20),
+		Description: "This derived column is created by a test",
+		QueryID:     *query.ID,
+	}
+	queryAnnotation, err := c.QueryAnnotations.Create(ctx, dataset, qa)
+	require.NoError(t, err)
+	require.NotNil(t, queryAnnotation.ID)
+
+	sli, err := c.DerivedColumns.Create(ctx, dataset, &client.DerivedColumn{
+		Alias:      test.RandomStringWithPrefix("test.", 8),
+		Expression: "BOOL(1)",
+	})
+	require.NoError(t, err)
+	slo, err := c.SLOs.Create(ctx, dataset, &client.SLO{
+		Name:             test.RandomStringWithPrefix("test.", 8),
+		TimePeriodDays:   7,
+		TargetPerMillion: 990000,
+		SLI:              client.SLIRef{Alias: sli.Alias},
+	})
+	require.NoError(t, err)
+	t.Cleanup(func() {
+		c.SLOs.Delete(ctx, dataset, slo.ID)
+		c.DerivedColumns.Delete(ctx, dataset, sli.ID)
+	})
+
+	t.Run("Create flexible board", func(t *testing.T) {
+		data := &client.Board{
+			Name:        test.RandomStringWithPrefix("test.", 8),
+			BoardType:   "flexible",
+			Description: "A board with some panels",
+			Panels: []client.BoardPanel{
+				{
+					PanelType: client.BoardPanelTypeSLO,
+					PanelPosition: client.BoardPanelPosition{
+						X:      6,
+						Y:      0,
+						Height: 3,
+						Width:  4,
+					},
+					SLOPanel: &client.BoardSLOPanel{
+						SLOID: slo.ID,
+					},
+				},
+			},
+		}
+		flexibleBoard, err = c.Boards.Create(ctx, data)
+		require.NoError(t, err)
+		assert.NotNil(t, flexibleBoard.ID)
+
+		// copy ID before asserting equality
+		data.ID = flexibleBoard.ID
+
+		// ensure the board URL got populated
+		assert.NotEmpty(t, flexibleBoard.Links.BoardURL)
+		data.Links.BoardURL = flexibleBoard.Links.BoardURL
+
+		assert.Equal(t, data, flexibleBoard)
+	})
+
+	t.Run("List", func(t *testing.T) {
+		result, err := c.Boards.List(ctx)
+		require.NoError(t, err)
+
+		assert.Contains(t, result, *flexibleBoard, "could not find newly created board with List")
+	})
+
+	t.Run("Get", func(t *testing.T) {
+		board, err := c.Boards.Get(ctx, flexibleBoard.ID)
+		require.NoError(t, err)
+
+		assert.Equal(t, *board, *flexibleBoard)
+	})
+
+}