Skip to content

Commit

Permalink
fix: Ensure excludePanelID and includePanelDataID works together
Browse files Browse the repository at this point in the history
* Dont filter panels based on includePanelID and excludePanelID too soon. Always include all the panels on the dashboard struct and only render the ones based on query parameters

* Ensure we do not leave gaps in report when panels are excluded in the report in simple layout.

* Add an example report with tabular data in docs

Signed-off-by: Mahendra Paipuri <[email protected]>
  • Loading branch information
mahendrapaipuri committed Dec 12, 2024
1 parent 52f4ec8 commit f62c440
Show file tree
Hide file tree
Showing 7 changed files with 232 additions and 172 deletions.
78 changes: 6 additions & 72 deletions pkg/plugin/dashboard/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"fmt"
"math"
"net/url"
"slices"
"strconv"
"strings"

Expand Down Expand Up @@ -111,6 +110,10 @@ type PanelImage struct {
MimeType string
}

func (p PanelImage) String() string {
return fmt.Sprintf("data:%s;base64,%s", p.MimeType, p.Image)
}

// IsSingleStat returns true if panel is of type SingleStat.
func (p Panel) IsSingleStat() bool {
return p.Is(SingleStat)
Expand Down Expand Up @@ -183,20 +186,15 @@ func New(log log.Logger, config config.Config, dashJSON []byte, dashData []inter
}

// Attempt to update panels from browser data
// If there are no errors, update the panels from browser dashboard model and
// return
var panels []Panel

var err error
if panels, err = panelsFromBrowser(dashboard, dashData); err != nil {
if dashboard.Panels, err = panelsFromBrowser(dashboard, dashData); err != nil {
log.Warn("failed to get panels from browser data", "error", err)
// If we fail to get panels from browser data, get them from dashboard JSON model
// and correct grid positions
panels = panelsFromJSON(dashboard.RowOrPanels, config.DashboardMode)
dashboard.Panels = panelsFromJSON(dashboard.RowOrPanels, config.DashboardMode)
}

// Filter the panels based on IncludePanelIDs/ExcludePanelIDs
dashboard.Panels = filterPanels(panels, config)
// Add query parameters to dashboard model
dashboard.VariableValues = variablesValues(queryParams)

Expand Down Expand Up @@ -384,67 +382,3 @@ func panelsFromJSON(rowOrPanels []RowOrPanel, dashboardMode string) []Panel {

return panels
}

// filterPanels filters the panels based on IncludePanelIDs and ExcludePanelIDs
// config parameters.
func filterPanels(panels []Panel, config config.Config) []Panel {
// If config parameters are empty, return original panels
if len(config.IncludePanelIDs) == 0 && len(config.ExcludePanelIDs) == 0 {
return panels
}

// Iterate over all panels and check if they should be included or not
var filteredPanels []Panel

var filteredPanelIDs []string

for _, panel := range panels {
// Attempt to convert panel ID to int. If we succeed, do direct
// comparison else do prefix check
var doDirectComp bool
if _, err := strconv.ParseInt(panel.ID, 10, 0); err == nil {
doDirectComp = true
}

for _, id := range config.IncludePanelIDs {
if !doDirectComp {
if strings.HasPrefix(panel.ID, id) && !slices.Contains(filteredPanelIDs, panel.ID) {
filteredPanelIDs = append(filteredPanelIDs, panel.ID)
filteredPanels = append(filteredPanels, panel)
}
} else {
if panel.ID == id && !slices.Contains(filteredPanelIDs, panel.ID) {
filteredPanelIDs = append(filteredPanelIDs, panel.ID)
filteredPanels = append(filteredPanels, panel)
}
}
}

if len(config.ExcludePanelIDs) > 0 {
exclude := false

for _, id := range config.ExcludePanelIDs {
if !doDirectComp {
if strings.HasPrefix(panel.ID, id) {
exclude = true
}
} else {
if panel.ID == id {
exclude = true
}
}
}

if !exclude && !slices.Contains(filteredPanelIDs, panel.ID) {
filteredPanelIDs = append(filteredPanelIDs, panel.ID)
filteredPanels = append(filteredPanels, panel)
}
}
}

return filteredPanels
}

func (p PanelImage) String() string {
return fmt.Sprintf("data:%s;base64,%s", p.MimeType, p.Image)
}
79 changes: 0 additions & 79 deletions pkg/plugin/dashboard/dashboard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,82 +102,3 @@ func TestVariableValues(t *testing.T) {
})
})
}

func TestFilterPanels(t *testing.T) {
Convey("When filtering panels based on integer panel IDs", t, func() {
allPanels := []Panel{
{ID: "1"}, {ID: "2"}, {ID: "3"}, {ID: "4"}, {ID: "15"}, {ID: "26"}, {ID: "37"},
}
cases := map[string]struct {
Config config.Config
Result []Panel
}{
"include": {
config.Config{
IncludePanelIDs: []string{"1", "4", "3"},
},
[]Panel{{ID: "1"}, {ID: "3"}, {ID: "4"}},
},
"exclude": {
config.Config{
ExcludePanelIDs: []string{"2", "4", "3"},
},
[]Panel{{ID: "1"}, {ID: "15"}, {ID: "26"}, {ID: "37"}},
},
"include_and_exclude": {
config.Config{
ExcludePanelIDs: []string{"2", "4", "3"},
IncludePanelIDs: []string{"1", "4", "6"},
},
[]Panel{{ID: "1"}, {ID: "4"}, {ID: "15"}, {ID: "26"}, {ID: "37"}},
},
}

for clName, cl := range cases {
filteredPanels := filterPanels(allPanels, cl.Config)

Convey("Panels should be properly filtered: "+clName, func() {
So(filteredPanels, ShouldResemble, cl.Result)
})
}
})

// For Grafana >= v11.3.0
Convey("When filtering panels based on string panel IDs", t, func() {
allPanels := []Panel{
{ID: "panel-1-clone-0"}, {ID: "panel-1-clone-1"}, {ID: "panel-3"}, {ID: "panel-4"}, {ID: "panel-5"}, {ID: "panel-6"}, {ID: "panel-7"},
}
cases := map[string]struct {
Config config.Config
Result []Panel
}{
"include": {
config.Config{
IncludePanelIDs: []string{"panel-1", "panel-4", "panel-6"},
},
[]Panel{{ID: "panel-1-clone-0"}, {ID: "panel-1-clone-1"}, {ID: "panel-4"}, {ID: "panel-6"}},
},
"exclude": {
config.Config{
ExcludePanelIDs: []string{"panel-1", "panel-4", "panel-3"},
},
[]Panel{{ID: "panel-5"}, {ID: "panel-6"}, {ID: "panel-7"}},
},
"include_and_exclude": {
config.Config{
ExcludePanelIDs: []string{"panel-2", "panel-4", "panel-3"},
IncludePanelIDs: []string{"panel-1", "panel-4", "panel-6"},
},
[]Panel{{ID: "panel-1-clone-0"}, {ID: "panel-1-clone-1"}, {ID: "panel-4"}, {ID: "panel-5"}, {ID: "panel-6"}, {ID: "panel-7"}},
},
}

for clName, cl := range cases {
filteredPanels := filterPanels(allPanels, cl.Config)

Convey("Panels should be properly filtered: "+clName, func() {
So(filteredPanels, ShouldResemble, cl.Result)
})
}
})
}
77 changes: 77 additions & 0 deletions pkg/plugin/report/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package report

import (
"slices"
"strconv"
"strings"

"github.com/mahendrapaipuri/grafana-dashboard-reporter-app/pkg/plugin/dashboard"
)

// remove removes a element by value in slice and returns a new slice.
func remove[T comparable](l []T, item T) []T {
out := make([]T, 0)

for _, element := range l {
if element != item {
out = append(out, element)
}
}

return out
}

// selectPanels returns panel indexes to render based on IncludePanelIDs and ExcludePanelIDs
// config parameters.
func selectPanels(panels []dashboard.Panel, includeIDs, excludeIDs []string, defaultInclude bool) []int {
var renderPanels []int

// If includeIDs is empty and default behaviour is to include all, setuo
// includeIDs
if len(includeIDs) == 0 && defaultInclude {
for _, p := range panels {
includeIDs = append(includeIDs, p.ID)
}
}

for iPanel, panel := range panels {
// Attempt to convert panel ID to int. If we succeed, do direct
// comparison else do prefix check
var doDirectComp bool
if _, err := strconv.ParseInt(panel.ID, 10, 0); err == nil {
doDirectComp = true
}

for _, id := range includeIDs {
if !doDirectComp {
if strings.HasPrefix(panel.ID, id) && !slices.Contains(renderPanels, iPanel) {
renderPanels = append(renderPanels, iPanel)
}
} else {
if panel.ID == id && !slices.Contains(renderPanels, iPanel) {
renderPanels = append(renderPanels, iPanel)
}
}
}

exclude := false

for _, id := range excludeIDs {
if !doDirectComp {
if strings.HasPrefix(panel.ID, id) {
exclude = true
}
} else {
if panel.ID == id {
exclude = true
}
}
}

if exclude && slices.Contains(renderPanels, iPanel) {
renderPanels = remove(renderPanels, iPanel)
}
}

return renderPanels
}
117 changes: 117 additions & 0 deletions pkg/plugin/report/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package report

import (
"testing"

"github.com/mahendrapaipuri/grafana-dashboard-reporter-app/pkg/plugin/dashboard"
. "github.com/smartystreets/goconvey/convey"
)

func TestPanelSelector(t *testing.T) {
Convey("When selecting panels based on integer panel IDs", t, func() {
allPanels := []dashboard.Panel{
{ID: "1"}, {ID: "2"}, {ID: "3"}, {ID: "4"}, {ID: "15"}, {ID: "26"}, {ID: "37"},
}
cases := map[string]struct {
IncludeIDs, ExcludeIDs []string
DefaultInclude bool
Result []int
}{
"empty_true": {
nil,
nil,
true,
[]int{0, 1, 2, 3, 4, 5, 6},
},
"empty_false": {
nil,
nil,
false,
nil,
},
"include": {
[]string{"1", "4", "3"},
nil,
true,
[]int{0, 2, 3},
},
"exclude": {
nil,
[]string{"2", "4", "3"},
true,
[]int{0, 4, 5, 6},
},
"exclude_false": {
nil,
[]string{"2", "4", "3"},
false,
nil,
},
"include_and_exclude_1": {
[]string{"1", "4", "6"},
[]string{"2", "4", "3"},
true,
[]int{0},
},
"include_and_exclude_2": {
[]string{"1"},
[]string{"2"},
true,
[]int{0},
},
}

for clName, cl := range cases {
filteredPanels := selectPanels(allPanels, cl.IncludeIDs, cl.ExcludeIDs, cl.DefaultInclude)

Convey("Panels should be properly selected: "+clName, func() {
So(filteredPanels, ShouldResemble, cl.Result)
})
}
})

// For Grafana >= v11.3.0
Convey("When filtering png panels based on string panel IDs", t, func() {
allPanels := []dashboard.Panel{
{ID: "panel-1-clone-0"}, {ID: "panel-1-clone-1"}, {ID: "panel-3"}, {ID: "panel-4"}, {ID: "panel-5"}, {ID: "panel-6"}, {ID: "panel-7"},
}
cases := map[string]struct {
IncludeIDs, ExcludeIDs []string
DefaultInclude bool
Result []int
}{
"include": {
[]string{"panel-1", "panel-4", "panel-6"},
nil,
true,
[]int{0, 1, 3, 5},
},
"exclude": {
nil,
[]string{"panel-1", "panel-4", "panel-3"},
true,
[]int{4, 5, 6},
},
"exclude_false": {
nil,
[]string{"panel-1", "panel-4", "panel-3"},
false,
nil,
},
"include_and_exclude": {
[]string{"panel-1", "panel-4", "panel-6"},
[]string{"panel-2", "panel-4", "panel-3"},
true,
[]int{0, 1, 5},
},
}

for clName, cl := range cases {
filteredPanels := selectPanels(allPanels, cl.IncludeIDs, cl.ExcludeIDs, cl.DefaultInclude)

Convey("Panels should be properly filtered: "+clName, func() {
So(filteredPanels, ShouldResemble, cl.Result)
})
}
})
}
Loading

0 comments on commit f62c440

Please sign in to comment.