From 82710dda85a9835b753cbec4364ad64b677e4377 Mon Sep 17 00:00:00 2001 From: Mahendra Paipuri Date: Sat, 14 Sep 2024 17:03:49 +0200 Subject: [PATCH] fix: Set a viewport to browser when fetching dashboard model * Without a fixed viewport, seems like chromium is choosing one at runtime based on server and ending up with wierd panel sizes. By setting a viewport, we should be able to control the panel sizes better. Signed-off-by: Mahendra Paipuri --- pkg/plugin/client/client.go | 19 ++++++++++++ pkg/plugin/dashboard/dashboard.go | 50 +++++++++++++++---------------- 2 files changed, 44 insertions(+), 25 deletions(-) diff --git a/pkg/plugin/client/client.go b/pkg/plugin/client/client.go index 9a58bce..a2145e4 100644 --- a/pkg/plugin/client/client.go +++ b/pkg/plugin/client/client.go @@ -34,6 +34,23 @@ var ( dashboardDataJS = `[...document.getElementsByClassName('react-grid-item')].map((e) => ({"width": e.style.width, "height": e.style.height, "transform": e.style.transform, "id": e.getAttribute("data-panelid")}))` ) +// Browser vars. +var ( + // We must set a view port to browser to ensure chromedp (or chromium) + // does not choose one randomly based on the current host. + // + // The idea here is to use a "regular" viewport of 1920x1080. However + // seems like Grafana uses a 16px margin on either side and hence the + // "effective" width of panels is only 1888px which is not a multiple of + // 24 (which is column measure of Grafana panels). So we add that additional + // 32px + 1920px = 1952px so that "effective" width becomes 1920px which is + // multiple of 24. This should give us nicer panels without overlaps. + // + // This can be flaky though! Need to make it better in the future?! + viewportWidth int64 = 1952 + viewportHeight int64 = 1080 +) + var getPanelRetrySleepTime = time.Duration(10) * time.Second // Grafana is a Grafana API httpClient. @@ -215,7 +232,9 @@ func (g GrafanaClient) dashboardFromBrowser(dashUID string) ([]interface{}, erro var dashboardData []interface{} // JS that will fetch dashboard model + tasks = append(tasks, chromedp.EmulateViewport(viewportWidth, viewportHeight)) tasks = append(tasks, chromedp.Evaluate(dashboardDataJS, &dashboardData)) + if err := tab.Run(tasks); err != nil { return nil, fmt.Errorf("error fetching dashboard URL from browser %s: %w", dashURL, err) } diff --git a/pkg/plugin/dashboard/dashboard.go b/pkg/plugin/dashboard/dashboard.go index b13cb42..e2f69b9 100644 --- a/pkg/plugin/dashboard/dashboard.go +++ b/pkg/plugin/dashboard/dashboard.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "math" "net/url" "regexp" "slices" @@ -16,10 +17,15 @@ import ( // Regex for parsing X and Y co-ordinates from CSS // Scales for converting width and height to Grafana units. +// +// This is based on viewportWidth that we used in client.go which +// is 1952px. Stripping margin 32px we get 1920px / 24 = 80px +// height scale should be fine with 36px as width and aspect ratio +// should choose a height appropriately. var ( translateRegex = regexp.MustCompile(`translate\((?P\d+)px, (?P\d+)px\)`) - scales = map[string]int{ - "width": 30, + scales = map[string]float64{ + "width": 80, "height": 36, } ) @@ -170,10 +176,12 @@ func panelsFromBrowser(dashData []interface{}) ([]Panel, error) { ) // Iterate over the slice of interfaces and build each panel - for _, p := range dashData { - var id, x, y, w, h, vInt, xInt, yInt int + for _, panelData := range dashData { + var vInt, xInt, yInt float64 - pMap, ok := p.(map[string]interface{}) + var p Panel + + pMap, ok := panelData.(map[string]interface{}) if !ok { continue } @@ -186,58 +194,50 @@ func panelsFromBrowser(dashData []interface{}) ([]Panel, error) { switch k { case "width": - if vInt, err = strconv.Atoi(strings.TrimSuffix(vString, "px")); err != nil { + if vInt, err = strconv.ParseFloat(strings.TrimSuffix(vString, "px"), 64); err != nil { allErrs = errors.Join(err, allErrs) } - w = vInt / scales[k] + p.GridPos.W = math.Round(vInt / scales[k]) case "height": - if vInt, err = strconv.Atoi(strings.TrimSuffix(vString, "px")); err != nil { + if vInt, err = strconv.ParseFloat(strings.TrimSuffix(vString, "px"), 64); err != nil { allErrs = errors.Join(err, allErrs) } - h = vInt / scales[k] + p.GridPos.H = math.Round(vInt / scales[k]) case "transform": matches := translateRegex.FindStringSubmatch(vString) if len(matches) == 3 { xCoord := matches[translateRegex.SubexpIndex("X")] - if xInt, err = strconv.Atoi(xCoord); err != nil { + if xInt, err = strconv.ParseFloat(xCoord, 64); err != nil { allErrs = errors.Join(err, allErrs) } else { - x = xInt / scales["width"] + p.GridPos.X = math.Round(xInt / scales["width"]) } yCoord := matches[translateRegex.SubexpIndex("Y")] - if yInt, err = strconv.Atoi(yCoord); err != nil { + if yInt, err = strconv.ParseFloat(yCoord, 64); err != nil { allErrs = errors.Join(err, allErrs) } else { - y = yInt / scales["height"] + p.GridPos.Y = math.Round(yInt / scales["height"]) } } else { allErrs = errors.Join(errors.New("failed to capture X and Y coordinate regex groups"), allErrs) } case "id": - if id, err = strconv.Atoi(vString); err != nil { + if p.ID, err = strconv.Atoi(vString); err != nil { allErrs = errors.Join(err, allErrs) } } } - // If height comes to zero, it is row panel and ignore it - if h == 0 { + // If height comes to 1 or less, it is row panel and ignore it + if p.GridPos.H <= 1 { continue } // Create panel model and append to panels - panels = append(panels, Panel{ - ID: id, - GridPos: GridPos{ - X: float64(x), - Y: float64(y), - H: float64(h), - W: float64(w), - }, - }) + panels = append(panels, p) } // Check if we fetched any panels