Skip to content

Commit 12252d0

Browse files
feat(r/flexible_board): allow panel positions to be autogenerated (#716)
## Which problem is this PR solving? - changes internals of position on flexible board panel to handle computed values better. - This allows the API to compute height, width and coordinates (x,y) - Without this change, we cannot solve the problem of auto assigning panels to specific grid spots ## How to verify that this has the expected result - create a flexible board with panels without positions - create a flexible board with panels with positions fixes #692
1 parent f2ab595 commit 12252d0

File tree

13 files changed

+982
-150
lines changed

13 files changed

+982
-150
lines changed

client/board.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ type Board struct {
4444
// Defaults to "classic".
4545
BoardType BoardType `json:"type,omitempty"`
4646

47+
// Layout generation controls how the board layout is generated.
48+
// Defaults to "manual".
49+
LayoutGeneration LayoutGeneration `json:"layout_generation,omitempty"`
50+
4751
// Board panels are the individual panels that make up a board. Each panel can
4852
// be a query or an SLO panel. The panels are laid out in a grid.
4953
Panels []BoardPanel `json:"panels,omitempty"`
@@ -80,6 +84,10 @@ type BoardPanel struct {
8084
SLOPanel *BoardSLOPanel `json:"slo_panel,omitempty"`
8185
}
8286

87+
func (b *BoardPanel) IsBlank() bool {
88+
return b.PanelPosition.X == 0 && b.PanelPosition.Y == 0 && b.PanelPosition.Height == 0 && b.PanelPosition.Width == 0
89+
}
90+
8391
type BoardPanelType string
8492

8593
const (
@@ -128,6 +136,13 @@ const (
128136
BoardTypeFlexible BoardType = "flexible"
129137
)
130138

139+
type LayoutGeneration string
140+
141+
const (
142+
LayoutGenerationManual LayoutGeneration = "manual"
143+
LayoutGenerationAuto LayoutGeneration = "auto"
144+
)
145+
131146
// BoardStyle determines how a Board should be displayed within the Honeycomb UI.
132147
type BoardStyle string
133148

client/board_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,55 @@ func TestFlexibleBoards(t *testing.T) {
248248
assert.Equal(t, data, flexibleBoard)
249249
})
250250

251+
t.Run("Create flexible board with auto layout generation", func(t *testing.T) {
252+
data := &client.Board{
253+
Name: test.RandomStringWithPrefix("test.", 8),
254+
BoardType: "flexible",
255+
Description: "A board with some panels",
256+
LayoutGeneration: client.LayoutGenerationAuto,
257+
Panels: []client.BoardPanel{
258+
{
259+
PanelType: client.BoardPanelTypeQuery,
260+
QueryPanel: &client.BoardQueryPanel{
261+
QueryID: *query.ID,
262+
QueryAnnotationID: queryAnnotation.ID,
263+
Style: client.BoardQueryStyleGraph,
264+
},
265+
},
266+
{
267+
PanelType: client.BoardPanelTypeSLO,
268+
SLOPanel: &client.BoardSLOPanel{
269+
SLOID: slo.ID,
270+
},
271+
},
272+
},
273+
}
274+
flexibleBoard, err = c.Boards.Create(ctx, data)
275+
require.NoError(t, err)
276+
assert.NotNil(t, flexibleBoard.ID)
277+
278+
// copy ID before asserting equality
279+
data.ID = flexibleBoard.ID
280+
281+
// ensure the board URL got populated
282+
assert.NotEmpty(t, flexibleBoard.Links.BoardURL)
283+
data.Links.BoardURL = flexibleBoard.Links.BoardURL
284+
285+
for i, panel := range flexibleBoard.Panels {
286+
assert.Equal(t, data.Panels[i].PanelType, panel.PanelType)
287+
assert.Equal(t, data.Panels[i].QueryPanel, panel.QueryPanel)
288+
assert.Equal(t, data.Panels[i].SLOPanel, panel.SLOPanel)
289+
290+
// since positions are auto generated, we can't assert their exact values
291+
// but we can assert that they are not empty
292+
assert.NotEmpty(t, panel.PanelPosition)
293+
assert.GreaterOrEqual(t, panel.PanelPosition.X, 0)
294+
assert.GreaterOrEqual(t, panel.PanelPosition.Y, 0)
295+
assert.Positive(t, panel.PanelPosition.Height)
296+
assert.Positive(t, panel.PanelPosition.Width)
297+
}
298+
})
299+
251300
t.Run("Create flexible board with tags", func(t *testing.T) {
252301
data := &client.Board{
253302
Name: test.RandomStringWithPrefix("test.", 8),

docs/resources/board.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# Resource: honeycombio_board
22

3-
Creates a classic board. Classic boards are deprecated, use [honeycombio_flexible_board](./flexible_board.md) resource instead. For more information about boards, check out [Create Custom Boards](https://docs.honeycomb.io/observe/boards).
3+
Creates a legacy board. Legacy boards are deprecated, use [honeycombio_flexible_board](./flexible_board.md) resource instead. For more information about boards, check out [Create Custom Boards](https://docs.honeycomb.io/observe/boards).
44

55
## Example Usage
66

7-
### Simple Classic Board
7+
### Simple Legacy Board
88

99
```hcl
1010
data "honeycombio_query_specification" "query" {
@@ -35,7 +35,7 @@ resource "honeycombio_board" "board" {
3535
}
3636
```
3737

38-
### Classic Board with a Service Level Objective (SLO) and an annotated query
38+
### Legacy Board with a Service Level Objective (SLO) and an annotated query
3939

4040
```hcl
4141
data "honeycombio_query_specification" "latency_by_userid" {
@@ -116,7 +116,6 @@ Each board configuration may have zero or more `query` blocks, which accept the
116116
- `graph_settings` - (Optional) A map of boolean toggles to manages the settings for this query's graph on the board.
117117
If a value is unspecified, it is assumed to be false.
118118
Currently supported toggles are:
119-
120119
- `hide_markers`
121120
- `log_scale`
122121
- `omit_missing_values`

docs/resources/flexible_board.md

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,6 @@ resource "honeycombio_flexible_board" "overview" {
9090
panel {
9191
type = "query"
9292
93-
position {
94-
x_coordinate = 0
95-
y_coordinate = 0
96-
width = 6
97-
height = 6
98-
}
99-
10093
query_panel {
10194
query_id = honeycombio_query.latency_by_userid.id
10295
query_annotation_id = honeycombio_query_annotation.latency_by_userid.id
@@ -139,10 +132,10 @@ Each board configuration may have zero or more `panel` blocks which accept the f
139132

140133
Each `position` configuration accepts the following arguments:
141134

142-
- `x_coordinate` - (Optional) The x-axis origin point for placing the panel within the layout.
143-
- `y_coordinate` - (Optional) The y-axis origin point for placing the panel within the layout.
144-
- `width` - (Optional) The width of the panel in honeycomb UI columns. Defaults to 6 for queries and 3 for slos. Maximum value is 12.
145-
- `height` - (Optional) The height of the panel in rows. Defaults to 4.
135+
- `x_coordinate` - (Optional) The x-axis origin point for placing the panel within the layout. Must be provided with `y_coordinate`.
136+
- `y_coordinate` - (Optional) The y-axis origin point for placing the panel within the layout. Must be provided with `x_coordinate`.
137+
- `width` - (Optional) The width of the panel in honeycomb UI columns. Automatically calculated when not provided. Maximum value is 12.
138+
- `height` - (Optional) The height of the panel in rows. Automatically calculated when not provided.
146139

147140
Each `slo_panel` configuration accepts the following arguments:
148141

example/flexible_board/main.tf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,5 +111,11 @@ resource "honeycombio_flexible_board" "overview" {
111111
slo_panel {
112112
slo_id = honeycombio_slo.slo.id
113113
}
114+
position {
115+
x_coordinate = 6
116+
y_coordinate = 0
117+
width = 6
118+
height = 6
119+
}
114120
}
115121
}
File renamed without changes.
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package validation
2+
3+
import (
4+
"context"
5+
6+
"github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag"
7+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
8+
"github.com/hashicorp/terraform-plugin-framework/types"
9+
)
10+
11+
var _ validator.List = panelPositionsConsistencyValidator{}
12+
13+
// panelPositionsConsistencyValidator validates that either all panels have positions or none of them do.
14+
type panelPositionsConsistencyValidator struct{}
15+
16+
func (v panelPositionsConsistencyValidator) Description(_ context.Context) string {
17+
return "either all panels must have positions or none of them should have positions"
18+
}
19+
20+
func (v panelPositionsConsistencyValidator) MarkdownDescription(ctx context.Context) string {
21+
return v.Description(ctx)
22+
}
23+
24+
func (v panelPositionsConsistencyValidator) ValidateList(ctx context.Context, request validator.ListRequest, response *validator.ListResponse) {
25+
if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() {
26+
return
27+
}
28+
29+
elements := request.ConfigValue.Elements()
30+
if len(elements) == 0 {
31+
return
32+
}
33+
34+
var hasPositionCount int
35+
var noPositionCount int
36+
37+
for _, element := range elements {
38+
if element.IsNull() || element.IsUnknown() {
39+
continue
40+
}
41+
42+
panelObj, ok := element.(types.Object)
43+
if !ok {
44+
continue
45+
}
46+
47+
attrs := panelObj.Attributes()
48+
position, exists := attrs["position"]
49+
if !exists {
50+
continue
51+
}
52+
53+
// Check if position is set (not null and not unknown)
54+
if position.IsNull() || position.IsUnknown() {
55+
noPositionCount++
56+
} else {
57+
// Check if position object has any actual values set
58+
positionObj, ok := position.(types.Object)
59+
if !ok {
60+
noPositionCount++
61+
continue
62+
}
63+
64+
positionAttrs := positionObj.Attributes()
65+
hasAnyCoordinate := false
66+
67+
// Check if any coordinate is set
68+
for _, coordName := range []string{"x_coordinate", "y_coordinate"} {
69+
if coord, exists := positionAttrs[coordName]; exists {
70+
if !coord.IsNull() && !coord.IsUnknown() {
71+
hasAnyCoordinate = true
72+
break
73+
}
74+
}
75+
}
76+
77+
if hasAnyCoordinate {
78+
hasPositionCount++
79+
} else {
80+
noPositionCount++
81+
}
82+
}
83+
}
84+
85+
// If we have both panels with positions and panels without positions, that's invalid
86+
if hasPositionCount > 0 && noPositionCount > 0 {
87+
response.Diagnostics.Append(
88+
validatordiag.InvalidAttributeValueDiagnostic(
89+
request.Path,
90+
v.Description(ctx),
91+
"Found panels with positions and panels without positions. All panels must have consistent position configuration.",
92+
),
93+
)
94+
}
95+
}
96+
97+
// RequireConsistentPanelPositions returns a ListValidator which ensures that either all panels
98+
// have positions or none of them do.
99+
//
100+
// Null (unconfigured) and unknown (known after apply) values are skipped.
101+
func RequireConsistentPanelPositions() validator.List {
102+
return panelPositionsConsistencyValidator{}
103+
}

0 commit comments

Comments
 (0)