diff --git a/pkg/plugin/dashboard/dashboard.go b/pkg/plugin/dashboard/dashboard.go index d5adadf..89a0afa 100644 --- a/pkg/plugin/dashboard/dashboard.go +++ b/pkg/plugin/dashboard/dashboard.go @@ -6,7 +6,6 @@ import ( "fmt" "math" "net/url" - "slices" "strconv" "strings" @@ -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) @@ -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) @@ -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) -} diff --git a/pkg/plugin/dashboard/dashboard_test.go b/pkg/plugin/dashboard/dashboard_test.go index 0af1628..3616222 100644 --- a/pkg/plugin/dashboard/dashboard_test.go +++ b/pkg/plugin/dashboard/dashboard_test.go @@ -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) - }) - } - }) -} diff --git a/pkg/plugin/report/helpers.go b/pkg/plugin/report/helpers.go new file mode 100644 index 0000000..dd103fd --- /dev/null +++ b/pkg/plugin/report/helpers.go @@ -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 +} diff --git a/pkg/plugin/report/helpers_test.go b/pkg/plugin/report/helpers_test.go new file mode 100644 index 0000000..9e71fcd --- /dev/null +++ b/pkg/plugin/report/helpers_test.go @@ -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) + }) + } + }) +} diff --git a/pkg/plugin/report/report.go b/pkg/plugin/report/report.go index 2b5d1d7..1a58153 100644 --- a/pkg/plugin/report/report.go +++ b/pkg/plugin/report/report.go @@ -8,7 +8,6 @@ import ( "html/template" "io" "reflect" - "slices" "strings" "sync" "time" @@ -205,14 +204,17 @@ func (r *PDF) renderPNGsParallel(ctx context.Context) error { numPanels := len(r.grafanaDashboard.Panels) errs := make(chan error, numPanels) + // Get the indexes of PNG panels that need to be included in the report + pngPanels := selectPanels(r.grafanaDashboard.Panels, r.conf.IncludePanelIDs, r.conf.ExcludePanelIDs, true) + wg := sync.WaitGroup{} - wg.Add(numPanels) + wg.Add(len(pngPanels)) - for iPanel := range numPanels { + for _, panelIndex := range pngPanels { r.workerPools[worker.Renderer].Do(func() { defer wg.Done() - errs <- r.renderPNG(ctx, iPanel) + errs <- r.renderPNG(ctx, panelIndex) }) } @@ -234,19 +236,18 @@ func (r *PDF) renderPNGsParallel(ctx context.Context) error { func (r *PDF) renderCSVsParallel(ctx context.Context) error { numPanels := len(r.grafanaDashboard.Panels) - tablePanelIDs := make([]int, 0, numPanels) errs := make(chan error, numPanels) - for panelIndex, panel := range r.grafanaDashboard.Panels { - if slices.Contains(r.conf.IncludePanelDataIDs, panel.ID) { - tablePanelIDs = append(tablePanelIDs, panelIndex) - } + // Get the indexes of table panels that need to be included in the report + tablePanels := selectPanels(r.grafanaDashboard.Panels, r.conf.IncludePanelDataIDs, nil, false) + if len(tablePanels) == 0 { + return nil } wg := sync.WaitGroup{} - wg.Add(len(tablePanelIDs)) + wg.Add(len(tablePanels)) - for _, panelIndex := range tablePanelIDs { + for _, panelIndex := range tablePanels { r.workerPools[worker.Browser].Do(func() { defer wg.Done() @@ -309,7 +310,11 @@ func (r *PDF) generateHTMLFile() error { // Template functions funcMap := template.FuncMap{ // The name "inc" is what the function will be called in the template text. - "inc": func(i float64) float64 { + "inc": func(i int) int { + return i + 1 + }, + + "add": func(i float64) float64 { return i + 1 }, diff --git a/pkg/plugin/report/templates/report.gohtml b/pkg/plugin/report/templates/report.gohtml index 60c02f7..7a7458a 100644 --- a/pkg/plugin/report/templates/report.gohtml +++ b/pkg/plugin/report/templates/report.gohtml @@ -56,24 +56,28 @@ } {{- if .IsGridLayout}} - {{- range $i, $v := .Panels}} + {{- range $i, $v := .Dashboard.Panels}} .grid-image-{{$i}} { - grid-column: {{inc $v.GridPos.X}} / span {{$v.GridPos.W}}; - grid-row: {{inc $v.GridPos.Y}} / span {{$v.GridPos.H}}; + grid-column: {{add $v.GridPos.X}} / span {{$v.GridPos.W}}; + grid-row: {{add $v.GridPos.Y}} / span {{$v.GridPos.H}}; } {{end}} {{else}} - {{- range $i, $v := .Panels}} + {{$p := 0}} + {{- range $i, $v := .Dashboard.Panels}} + {{- if $v.EncodedImage.Image }} .grid-image-{{$i}} { grid-column: 1 / span 24; - grid-row: {{mult $i}} / span 30; + grid-row: {{mult $p}} / span 30; } + {{$p = inc $p}} + {{- end }} - {{end}} + {{- end}} - {{end}} + {{- end}} @@ -85,10 +89,12 @@
{{- range $i, $v := .Dashboard.Panels}} + {{- if $v.EncodedImage.Image }}
{{$v.Title}}
{{- end }} + {{- end }}
{{- range $i, $v := .Dashboard.Panels }} diff --git a/src/README.md b/src/README.md index 5f1f724..f1c3550 100755 --- a/src/README.md +++ b/src/README.md @@ -330,8 +330,7 @@ Besides there are **two** special query parameters available namely: > [!NOTE] > If a given panel ID is set in both `includePanelID` and `excludePanelID` query parameter, - it will be **included** in the report. Query parameter `includePanelID` has more - precedence over `excludePanelID`. + it will be **excluded** in the report. #### Rendering tabular data in the report @@ -472,6 +471,7 @@ to a version `> 1.5.0` ASAP. --> Here are the example reports that are generated out of the test dashboards - [Report with portrait orientation, simple layout and full dashboard mode](https://github.com/mahendrapaipuri/grafana-dashboard-reporter-app/blob/main/docs/reports/report_portrait_simple_full.pdf) +- [Report with portrait orientation, simple layout, full dashboard mode and tabular data](https://github.com/mahendrapaipuri/grafana-dashboard-reporter-app/blob/main/docs/reports/report_portrait_simple_full_table.pdf) - [Report with landscape orientation, simple layout and full dashboard mode](https://github.com/mahendrapaipuri/grafana-dashboard-reporter-app/blob/main/docs/reports/report_landscape_simple_full.pdf) - [Report with portrait orientation, grid layout and full dashboard mode](https://github.com/mahendrapaipuri/grafana-dashboard-reporter-app/blob/main/docs/reports/report_portrait_grid_full.pdf) - [Report with landscape orientation, grid layout and full dashboard mode](https://github.com/mahendrapaipuri/grafana-dashboard-reporter-app/blob/main/docs/reports/report_landscape_grid_full.pdf)