Skip to content

OPCT-TBD: Add interactive etcd metric charts to web UI report#218

Open
mtulio wants to merge 6 commits into
redhat-openshift-ecosystem:mainfrom
mtulio:webui-etcd-charts
Open

OPCT-TBD: Add interactive etcd metric charts to web UI report#218
mtulio wants to merge 6 commits into
redhat-openshift-ecosystem:mainfrom
mtulio:webui-etcd-charts

Conversation

@mtulio

@mtulio mtulio commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR enhances the etcd page in the OPCT web UI report by adding all 6 etcd and API performance metric charts with interactive visualizations. It also removes legacy standalone metrics pages (metrics.html, index.html) that are no longer needed since charts are now integrated into the main web UI.

Changes

1. Fix Metrics Processor (NaN Handling)

Problem: Two charts (Kube API request p99, etcd request duration p99) were embedded in the legacy metrics.html but failed to export as JSON files due to json: unsupported value: NaN errors.

Solution: Added NaN/Inf filtering in processMetricV2() before JSON marshalling:

  • Detects and skips NaN/Inf values (common in metrics with missing data)
  • Logs skipped datapoints at debug level
  • All 6 charts now successfully export to index.json

Files: internal/openshift/mustgathermetrics/plotly.go

2. Add Missing Charts to Web UI

Added the 6th chart to the etcd tab:

  • etcd-chart-5: Kube API request p99 (HTTP verb breakdown: GET/POST/PUT/PATCH/DELETE/APPLY/LIST)
  • etcd-chart-4: etcd request duration p99 (already defined in previous commit)

Complete chart inventory (6 total):

  1. etcd fsync WAL p99 - Write-ahead log sync latency
  2. etcd fsync DB p99 - Database sync latency
  3. etcd peer round trip time - Network latency between etcd members
  4. etcd total leader elections - Cluster stability indicator
  5. etcd request duration p99 - etcd operations (create/delete/get/list/listWithCount/update)
  6. Kube API request p99 - API server latency by HTTP verb

Files: data/templates/report/report.html

3. Remove Legacy Metrics Pages

Removed standalone metrics pages that are redundant with the new web UI integration:

Removed generation:

  • metrics.html - Large echarts-based standalone page (480KB+)
  • index.html - Plotly-based standalone page
  • index.js - JavaScript for Plotly page

Kept (required by web UI):

  • index.json - Index of available charts
  • *.json.gz.json - Individual chart data files (6 files)

Code cleanup:

  • Removed echarts page generation (SaveMetricsPageReport, newMetricsPage)
  • Removed Plotly HTML/JS template constants
  • Removed unused imports (bytes, text/template, sort)
  • Simplified RenderPage() to only generate JSON files

Files: internal/openshift/mustgathermetrics/main.go, internal/openshift/mustgathermetrics/plotly.go

Interactive Chart Features

All 6 charts include:

  • Drag-to-zoom: Select time range by clicking and dragging
  • Reset zoom: Double-click or click reset button
  • Fullscreen expand: Popup overlay for detailed analysis
  • Legend: Toggle series visibility by clicking labels
  • Split-pane layout: 70% tables / 30% charts (draggable divider)

Technical Details

Data Source: Reuses existing Plotly JSON files from ./metrics/ directory generated by the must-gather-monitoring plugin.

Conditional Rendering: Charts only appear when hasMetricsData feature flag is true (metrics were collected during OPCT run).

Layout Pattern: Uses v-if/v-else for split-pane container to prevent CSS conflicts with other pages.

Chart.js Integration: Version 4.4.9 with time-series adapters (chartjs-adapter-date-fns), touch gestures (hammerjs), and interactive zoom (chartjs-plugin-zoom).

Testing

Tested with local OPCT archive:

make build
./build/opct-linux-amd64 report -s <output-dir> <archive>.tar.gz

Validated:

  • ✅ All 6 charts render correctly with real metric data
  • ✅ NaN values filtered out (no JSON marshalling errors)
  • ✅ Drag-to-zoom functionality works
  • ✅ Fullscreen expand/collapse behavior
  • ✅ Split-pane resize via dragging divider
  • ✅ No layout regressions on other pages (Summary, Checks, Network)
  • ✅ Legacy pages (metrics.html, index.html, index.js) NOT generated
  • ✅ Only required files created (index.json + 6 chart JSON files)

Related Issues

🤖 Generated with Claude Code

— AI Claude

Embed etcd Prometheus metric charts inline on the etcd page using
Chart.js, replacing the previous Plotly-based approach for lighter
rendering (~200KB vs ~3.5MB).

Layout:
- Split-pane with draggable divider (70% tables / 30% charts default)
- Independent scroll on each panel
- v-if/v-else ensures non-etcd pages use full-width layout unchanged

Chart features:
- Drag-to-select time range zoom on X axis (chartjs-plugin-zoom)
- Reset zoom button per chart
- Expand button opens chart in fullscreen overlay with zoom support
- Responsive sizing, auto-resize on panel drag

Charts displayed:
- etcd fsync WAL/DB duration p99
- etcd peer round trip time
- etcd total leader elections
- etcd request duration p99

Data source: reuses existing Plotly JSON files already generated
at metrics/ directory during report processing — no backend changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@openshift-ci openshift-ci Bot requested review from rvanderp3 and vr4manta June 3, 2026 04:15
@openshift-ci

openshift-ci Bot commented Jun 3, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign bshaw7 for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Interactive metrics dashboard with split-panel layout for improved data organization.
    • Expandable charts with modal view and zoom/pan controls for detailed exploration.
    • Draggable panel divider for customizable layout resizing.
    • Chart.js-powered data visualization with enhanced interactivity.
  • Bug Fixes

    • Fixed missing CSS styling issue.

Walkthrough

Adds an ETCd split-pane chart UI (styles, Chart.js deps, Vue state, rendering, overlay, and draggable divider) and changes server-side metrics extraction to emit per-chart JSON files plus an index.json while filtering NaN/Inf datapoints.

Changes

ETCd Chart Visualization

Layer / File(s) Summary
CSS styling for split-pane and chart UI
data/templates/report/report.css
Fixed incomplete .closebtn:hover rule and added CSS for split-pane panels, draggable divider, chart cards (header/toolbar/buttons), and a fixed overlay modal for expanded charts.
Chart.js dependencies and split-panel HTML layout
data/templates/report/report.html
Adds script tags for Chart.js v4, date-fns adapter, Hammer.js, and zoom plugin; adds a Dashboard tab and replaces the single content container with conditional left (menuBody) / right (menuBodyRight) panels.
Vue app state and cleanup
data/templates/report/report.html
Adds menuBodyRight, chartColors, chartInstances, and etcdCharts to the Vue app; fixes small ETCd markup; updates changeMenuCleanup() to clear right-panel state and destroy Chart.js instances.
Chart rendering and expand-overlay logic
data/templates/report/report.html
Implements renderEtcdCharts() (axios JSON fetch, Chart.js time-series with time x-axis, zoom/pan), resetChartZoom, and expandChart() to create an overlay Chart.js instance that reparses timestamps and guards against stale renders.
Split-pane drag-to-resize interaction
data/templates/report/report.html
Adds a document-level mousedown handler to make #panel-divider draggable, updating left/right panel widths and min-widths as percentages during drag.

Metrics export / JSON output

Layer / File(s) Summary
Charts configuration
internal/openshift/mustgathermetrics/charts-config.json
Adds an embedded JSON config mapping metric artifact filenames to chart IDs, labels, and titles for extraction and output.
Types and embedded resources
internal/openshift/mustgathermetrics/main.go
Adds embedded resources and new data/config types for chart configs and Prometheus responses.
MustGatherMetrics structs and constructor
internal/openshift/mustgathermetrics/main.go
Defines MustGatherMetrics storing reportPath, input buffer, and a charts map; NewMustGatherMetrics loads the embedded charts-config and initializes charts.
Process flow and archive extraction
internal/openshift/mustgathermetrics/main.go
Process() reads the .tar.xz, readArchive builds the tar reader, and extractMetrics filters monitoring/prometheus/metrics/*.json.gz, decompresses, unmarshals Prometheus JSON, and assigns results per configured chart.
Datapoint filtering and JSON output
internal/openshift/mustgathermetrics/main.go
Adds filterInvalidValues to remove NaN/Inf datapoints and empty series, saveChartJSON, and generateOutputFiles to write per-chart *.json and index.json, plus embedded metrics.html/index.html.
Dashboard assets and integration
internal/openshift/mustgathermetrics/*, go.mod, pkg/cmd/adm/parsemetrics.go, internal/opct/summary/result.go
Adds metrics.html and an index.html redirect, updates go.mod, adjusts NewMustGatherMetrics callsites, and updates CLI success logging to reference generated index.json and per-chart JSON files and the OPCT UI tip.

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding interactive etcd metric charts to the OPCT web UI report.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, covering the summary, specific changes, features, technical details, and testing validation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment on lines 11 to +16
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.9/dist/chart.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/hammerjs@2.0.8/hammer.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom@2.2.0/dist/chartjs-plugin-zoom.min.js"></script>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we decrease the amount of dependencies or build it together, preferably with what we are using only, serving as static ?

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@data/templates/report/report.html`:
- Around line 13-16: The template loads Chart.js, adapter-date-fns, hammerjs and
chartjs-plugin-zoom from public CDNs without integrity/crossorigin—either add
Subresource Integrity (integrity attributes) plus crossorigin="anonymous" to
those script tags in report.html or switch to locally bundled copies to mitigate
supply-chain risk; additionally, in renderEtcdCharts() where this.chartInstances
is reset, first iterate existing Chart instances in this.chartInstances and call
their destroy() before clearing the map, and add a per-call render token (or
cancelation/sequence check) so async axios responses verify the token/sequence
before creating/updating charts to ignore stale responses and avoid
leaked/duplicated charts.
- Around line 313-320: changeMenuCleanup currently clears UI but doesn't destroy
Chart.js instances and renderEtcdCharts resets chartInstances and fires axios
requests without guarding against late responses; update changeMenuCleanup to
iterate over this.chartInstances and call destroy() on each Chart instance (and
then clear the map) before touching DOM, and modify renderEtcdCharts to (1)
avoid blindly replacing this.chartInstances = {} without first destroying any
existing charts, (2) attach a per-render token/id (or cancel token) to each
axios.get so late responses can be ignored or canceled, and (3) verify the
target canvas still matches the expected id/token before creating a new Chart to
prevent resurrecting charts on reused canvases.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Enterprise

Run ID: 3c8a48ee-26bc-4111-8afb-6a245de36022

📥 Commits

Reviewing files that changed from the base of the PR and between 4fa5637 and a24d6af.

📒 Files selected for processing (2)
  • data/templates/report/report.css
  • data/templates/report/report.html

Comment on lines +13 to +16
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.9/dist/chart.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/hammerjs@2.0.8/hammer.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom@2.2.0/dist/chartjs-plugin-zoom.min.js"></script>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="data/templates/report/report.html"

echo "== Snippet around scripts (lines 1-60) =="
sed -n '1,60p' "$file" | nl -ba | sed -n '1,80p'

echo "== Snippet around lines 280-360 =="
sed -n '280,360p' "$file" | nl -ba | sed -n '1,120p'

echo "== Find chart lifecycle bits =="
rg -n "chartInstances|renderEtcdCharts|destroy\(|new Chart\(|getContext\(|canvas|update\(" "$file" || true

echo "== Snippet around renderEtcdCharts (best-effort) =="
# try to locate function and print nearby
lnum="$(rg -n "renderEtcdCharts" "$file" | head -n1 | cut -d: -f1 || true)"
if [[ -n "${lnum}" ]]; then
  start=$((lnum-80))
  end=$((lnum+180))
  sed -n "${start},${end}p" "$file" | nl -ba | sed -n "1,200p"
fi

echo "== Snippet around lines 780-980 =="
sed -n '780,980p' "$file" | nl -ba | sed -n '1,220p'

Repository: redhat-openshift-ecosystem/opct

Length of output: 178


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="data/templates/report/report.html"

echo "== File exists? =="
if [[ -f "$file" ]]; then echo "FOUND: $file"; else echo "MISSING: $file"; exit 0; fi

echo "== Snippet around scripts (start of file) =="
sed -n '1,60p' "$file" | sed '=' | sed 'N; s/\n/ /'

echo "== Snippet around lines 280-360 =="
sed -n '280,360p' "$file" | sed '=' | sed 'N; s/\n/ /'

echo "== Search chart lifecycle markers =="
rg -n "chartInstances|renderEtcdCharts|destroy\(|new Chart\(" "$file" || true

echo "== Snippet around renderEtcdCharts =="
lnum="$(rg -n "renderEtcdCharts" "$file" | head -n1 | cut -d: -f1 || true)"
if [[ -n "${lnum}" ]]; then
  start=$((lnum-80))
  end=$((lnum+200))
  sed -n "${start},${end}p" "$file" | sed '=' | sed 'N; s/\n/ /'
fi

echo "== Snippet around ~lines 780-980 (ETCD chart area best-effort) =="
sed -n '780,980p' "$file" | sed '=' | sed 'N; s/\n/ /'

Repository: redhat-openshift-ecosystem/opct

Length of output: 32703


🏁 Script executed:

#!/bin/bash
set -euo pipefail
file="data/templates/report/report.html"

echo "== Calls to changeMenuCleanup() =="
rg -n "changeMenuCleanup\(" "$file" || true

echo "== Any destroy() usage on chartInstances/base charts =="
rg -n "chartInstances\]|chartInstances|\.destroy\(\)|destroy\(\)" "$file" || true

echo "== Any loop over chartInstances with destroy =="
rg -n "for .*chartInstances|Object\.values\(this\.chartInstances\)|Object\.keys\(this\.chartInstances\)" "$file" || true

Repository: redhat-openshift-ecosystem/opct

Length of output: 806


Fix supply-chain risk and Chart.js lifecycle issues in the report template

  • data/templates/report/report.html: loads Chart.js/adapter/zoom/hammer from public CDNs without integrity/crossorigin; add SRI + crossorigin or bundle locally.
  • renderEtcdCharts() (~877+): resets this.chartInstances = {} but never calls destroy() on existing Chart.js instances; repeated/late async axios responses can recreate charts on the same canvases, leaking memory and causing duplicated charts—destroy prior instances before clearing and ignore stale responses (e.g., render token).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/templates/report/report.html` around lines 13 - 16, The template loads
Chart.js, adapter-date-fns, hammerjs and chartjs-plugin-zoom from public CDNs
without integrity/crossorigin—either add Subresource Integrity (integrity
attributes) plus crossorigin="anonymous" to those script tags in report.html or
switch to locally bundled copies to mitigate supply-chain risk; additionally, in
renderEtcdCharts() where this.chartInstances is reset, first iterate existing
Chart instances in this.chartInstances and call their destroy() before clearing
the map, and add a per-call render token (or cancelation/sequence check) so
async axios responses verify the token/sequence before creating/updating charts
to ignore stale responses and avoid leaked/duplicated charts.

Comment thread data/templates/report/report.html
…ages

This commit enhances the etcd tab in the web UI by adding 2 missing charts
and removes legacy standalone metrics pages that are no longer needed.

Changes:

1. Fix metrics processor to export all charts (NaN handling):
   - Added NaN/Inf filtering in processMetricV2 to prevent JSON marshalling errors
   - Previously, API and etcd request duration charts failed with 'unsupported value: NaN'
   - Now all 6 etcd/API charts are successfully exported to index.json

2. Add missing charts to web UI etcd tab:
   - Added chart-5: Kube API request p99 (HTTP verb breakdown)
   - Chart-4 (etcd request duration p99) was already defined
   - Web UI now displays all 6 etcd/API performance charts

3. Remove legacy metrics pages (pre-1.0 deprecation):
   - Removed metrics.html generation (echarts-based standalone page)
   - Removed index.html generation (Plotly-based standalone page)
   - Removed index.js generation (only used by index.html)
   - Cleaned up unused template constants and imports
   - Keep index.json and chart JSON files (used by web UI)

Chart inventory:
- etcd fsync WAL p99
- etcd fsync DB p99
- etcd peer round trip time
- etcd total leader elections
- etcd request duration p99 (operations: create/delete/get/list/update)
- Kube API request p99 (verbs: GET/POST/PUT/PATCH/DELETE/APPLY/LIST)

All charts use Chart.js with interactive features: drag-to-zoom, fullscreen
expand, and split-pane layout.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/openshift/mustgathermetrics/plotly.go`:
- Around line 65-68: The error log in the os.WriteFile error branch is using the
wrong variable: log.Errorf("Unable to save file %s: %v", indexJsonFileData, err)
prints the JSON bytes instead of the file path. Change the log.Errorf call to
use indexJsonFilePath as the %s argument (keep the error err as %v) so the
message reads the file path and the actual error; locate this in the block
wrapping os.WriteFile and update the call referencing indexJsonFileData to
indexJsonFilePath.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Enterprise

Run ID: 5a08c8f9-ee32-4250-af66-6a0e2dde1652

📥 Commits

Reviewing files that changed from the base of the PR and between a24d6af and 58b6842.

📒 Files selected for processing (3)
  • data/templates/report/report.html
  • internal/openshift/mustgathermetrics/main.go
  • internal/openshift/mustgathermetrics/plotly.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • data/templates/report/report.html

Comment thread internal/openshift/mustgathermetrics/plotly.go Outdated
…ON directly

This commit dramatically simplifies the metrics processing by eliminating
unnecessary format conversions and consolidating the package into a single file.

**Architecture Changes:**

1. Removed Plotly format conversion (was: Prometheus → Plotly → Chart.js)
   - Now: Prometheus JSON → Chart.js directly
   - Chart.js consumes Prometheus query_range API responses natively
   - Eliminated 150 lines of format transformation code

2. Deleted obsolete files (dead code from metrics.html removal):
   - charts.go (199 lines) - 100% unused echarts integration
   - plotly.go (178 lines) - Plotly format conversion, now direct Prometheus
   - Consolidated everything into main.go (270 lines, down from 609)

3. Externalized chart configuration:
   - Created charts-config.json with chart metadata
   - Embedded via go:embed for zero-config deployment
   - Easy to add new charts without code changes

**Code Reduction:**

- Before: 609 lines across 3 files
- After: 270 lines in 1 file + 30 lines config
- Savings: 58% code reduction (-356 lines)

**Functional Changes:**

1. Simplified API:
   - NewMustGatherMetrics(reportPath, data) - removed unused params
   - Process() extracts and saves Prometheus JSON directly

2. Better NaN/Inf handling:
   - Filter at processing time (not per-chart)
   - Aggregate stats logged once (was: per-datapoint debug spam)
   - Example: "Filtered 12 NaN/Inf datapoints from 1850 total (0.6% invalid)"

3. Web UI updates (data/templates/report/report.html):
   - Parse Prometheus JSON format in renderEtcdCharts()
   - Extract labels from metric.metric{} object
   - Convert Unix timestamps to JS Date objects
   - Removed unused parseMetricTimestamp() method

**Dependency Cleanup:**

Removed unused dependencies:
- github.com/go-echarts/go-echarts/v2 (echarts charts)
- k8s.io/utils/ptr (pointer helpers for echarts)

**File Changes:**

Modified:
- internal/openshift/mustgathermetrics/main.go - Complete rewrite
- data/templates/report/report.html - Prometheus JSON parser
- internal/opct/summary/result.go - Updated caller
- pkg/cmd/adm/parsemetrics.go - Updated caller and messages

Added:
- internal/openshift/mustgathermetrics/charts-config.json - Chart metadata

Deleted:
- internal/openshift/mustgathermetrics/charts.go - Dead code
- internal/openshift/mustgathermetrics/plotly.go - Unnecessary conversion

**Testing:**

Validated with real OPCT archive:
- ✅ All 6 charts generate correctly
- ✅ Prometheus JSON format preserved
- ✅ NaN/Inf filtering works (no JSON marshalling errors)
- ✅ index.json created with correct IDs
- ✅ Web UI parses Prometheus format (needs browser test)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/opct/summary/result.go (1)

515-523: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Set HasMetrics only after a successful metrics export.

HasMetrics is flipped to true even when rs.Metrics.Process() fails. That advertises metrics assets that were never generated and can break the report flow downstream.

Suggested fix
 			} else {
-				err := rs.Metrics.Process()
-				if err != nil {
+				if err := rs.Metrics.Process(); err != nil {
 					log.Errorf("Processing MetricsData: %v", err)
+				} else {
+					rs.HasMetrics = true
 				}
-				rs.HasMetrics = true
 			}
 		} else {

As per coding guidelines, **: Focus on major issues impacting performance, readability, maintainability and security. Avoid nitpicks and avoid verbosity.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/opct/summary/result.go` around lines 515 - 523, The code sets
rs.HasMetrics = true even when rs.Metrics.Process() fails; change the logic so
you only set rs.HasMetrics to true after
mustgathermetrics.NewMustGatherMetrics(...) succeeds and rs.Metrics.Process()
returns nil. Specifically, in the block that calls
mustgathermetrics.NewMustGatherMetrics and then rs.Metrics.Process(), move the
assignment to rs.HasMetrics = true so it happens only on successful Process(),
and on Process() error log the failure and clear or leave rs.Metrics unset (or
nil) to avoid advertising non-existent assets.
♻️ Duplicate comments (1)
data/templates/report/report.html (1)

884-887: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use configured chart label metadata for legend labels (avoid metric key-order-dependent labeling).

  • data/templates/report/report.html (lines 884-887) derives the legend from Object.keys(result.metric).find(k => k !== '__name__'), so the same series can be mislabeled depending on metric key order.
  • internal/openshift/mustgathermetrics/charts-config.json already defines the intended label dimension per chart; add label to each etcdCharts entry and use result.metric[chart.label] here.
Suggested fix
-                let labelKey = Object.keys(result.metric).find(k => k !== '__name__') || 'instance';
-                let labelValue = result.metric[labelKey] || 'unknown';
+                let labelValue = result.metric[chart.label];
+                if (labelValue === undefined) {
+                  labelValue = result.metric.instance || result.metric.verb || result.metric.operation || 'unknown';
+                }

Companion change (add label to each chart in etcdCharts):

etcdCharts: [
  { id: "etcd-chart-0", path: "./metrics/query_range-etcd-disk-fsync-wal-duration-p99.json.gz.json", title: "etcd fsync WAL p99", label: "instance" },
  { id: "etcd-chart-1", path: "./metrics/query_range-etcd-disk-fsync-db-duration-p99.json.gz.json", title: "etcd fsync DB p99", label: "instance" },
  { id: "etcd-chart-2", path: "./metrics/query_range-etcd-peer-round-trip-time.json.gz.json", title: "etcd peer round trip", label: "instance" },
  { id: "etcd-chart-3", path: "./metrics/query_range-etcd-total-leader-elections-day.json.gz.json", title: "etcd total leader elections", label: "instance" },
  { id: "etcd-chart-4", path: "./metrics/query_range-etcd-request-duration-p99.json.gz.json", title: "etcd req duration p99", label: "operation" },
  { id: "etcd-chart-5", path: "./metrics/query_range-api-kas-request-duration-p99.json.gz.json", title: "Kube API request p99", label: "verb" },
],
  • Chart.js lifecycle: renderEtcdCharts() resets this.chartInstances = {} without destroying existing Chart instances before re-rendering, so repeated navigation/re-rendering can leave stale charts/handlers behind.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/templates/report/report.html` around lines 884 - 887, The
legend-labeling currently picks the first non-__name__ metric key which is
order-dependent; update the series label logic to read the configured label key
from the chart config (use chart.label when building datasets and fall back to
'instance' or an explicit fallback like result.metric[chart.label] ||
result.metric.instance || 'unknown') so legend names are stable, and ensure you
add a "label" field to each etcdCharts entry; additionally, in
renderEtcdCharts() destroy any existing Chart instances (call destroy() on
entries in this.chartInstances) before clearing this.chartInstances = {} to
avoid leaking handlers/stale charts during re-renders.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/openshift/mustgathermetrics/main.go`:
- Around line 218-223: The code currently logs per-chart write failures and
continues, and treats an empty index as success; change this so the first
failure to save a chart via mg.saveChartJSON(chartPath, filteredData) causes the
function to return that error immediately instead of continue, and after
generating the index (before returning success) check if len(index) == 0 and
return a descriptive error if so; update any loops that iterate
fileName/chartPath to propagate/save errors from mg.saveChartJSON rather than
only logging them, and ensure the calling function receives the error so exports
that are partial or empty are rejected.

---

Outside diff comments:
In `@internal/opct/summary/result.go`:
- Around line 515-523: The code sets rs.HasMetrics = true even when
rs.Metrics.Process() fails; change the logic so you only set rs.HasMetrics to
true after mustgathermetrics.NewMustGatherMetrics(...) succeeds and
rs.Metrics.Process() returns nil. Specifically, in the block that calls
mustgathermetrics.NewMustGatherMetrics and then rs.Metrics.Process(), move the
assignment to rs.HasMetrics = true so it happens only on successful Process(),
and on Process() error log the failure and clear or leave rs.Metrics unset (or
nil) to avoid advertising non-existent assets.

---

Duplicate comments:
In `@data/templates/report/report.html`:
- Around line 884-887: The legend-labeling currently picks the first
non-__name__ metric key which is order-dependent; update the series label logic
to read the configured label key from the chart config (use chart.label when
building datasets and fall back to 'instance' or an explicit fallback like
result.metric[chart.label] || result.metric.instance || 'unknown') so legend
names are stable, and ensure you add a "label" field to each etcdCharts entry;
additionally, in renderEtcdCharts() destroy any existing Chart instances (call
destroy() on entries in this.chartInstances) before clearing this.chartInstances
= {} to avoid leaking handlers/stale charts during re-renders.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Enterprise

Run ID: 8c7358e4-0a4f-423b-9d48-40f3751e7118

📥 Commits

Reviewing files that changed from the base of the PR and between 58b6842 and 98f5816.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (8)
  • data/templates/report/report.html
  • go.mod
  • internal/opct/summary/result.go
  • internal/openshift/mustgathermetrics/charts-config.json
  • internal/openshift/mustgathermetrics/charts.go
  • internal/openshift/mustgathermetrics/main.go
  • internal/openshift/mustgathermetrics/plotly.go
  • pkg/cmd/adm/parsemetrics.go
💤 Files with no reviewable changes (3)
  • internal/openshift/mustgathermetrics/charts.go
  • internal/openshift/mustgathermetrics/plotly.go
  • go.mod
✅ Files skipped from review due to trivial changes (1)
  • internal/openshift/mustgathermetrics/charts-config.json

Comment thread internal/openshift/mustgathermetrics/main.go
@mtulio mtulio changed the title OPCT-400: Add interactive etcd metric charts to web UI report OPCT-TBD: Add interactive etcd metric charts to web UI report Jun 3, 2026
@mtulio

mtulio commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator Author
Screenshot From 2026-06-03 03-21-24 Screenshot From 2026-06-03 03-21-05

@mtulio mtulio added the kind/feature Categorizes issue or PR as related to a new feature. label Jun 3, 2026
mtulio and others added 2 commits June 3, 2026 03:27
…rsion

Created a new standalone metrics dashboard with an interactive grid layout
that displays all 6 Prometheus charts as small widgets with click-to-expand
functionality. Standardized Y-axis units to milliseconds for all duration
metrics to improve readability and cross-chart comparison.

Changes:
- Added metrics.html with grid layout (3-4 columns, responsive design)
- Implemented click-to-expand modal for full-size chart viewing
- Converted duration metrics from seconds to milliseconds
- Added Y-axis labels showing units (ms/count)
- Limited etcd tab to 2 core fsync charts (WAL and DB)
- Added info banner in etcd tab linking to full dashboard
- Renamed "Metrics" tab to "Dashboard"
- Reordered tabs: OPCT → Dashboard → Tests → Events → CAMGI
- Auto-generated files: index.html (redirect), metrics.html (dashboard)

Dashboard Features:
- Grid layout with 3-4 columns of chart widgets
- Click any chart to open full-size interactive modal
- All 6 charts: etcd fsync WAL/DB p99, peer round trip, leader elections,
  etcd request duration p99, Kube API request duration p99
- Chart.js with zoom/pan controls in both views
- Responsive design for mobile/desktop

Metric Improvements:
- Duration metrics now show milliseconds (5 ms vs 0.005 s)
- Y-axis labeled with unit (ms or count)
- Consistent units across all charts for easy comparison
- Applied to grid dashboard, modal, and etcd tab

Files Modified:
- internal/openshift/mustgathermetrics/main.go - HTML embedding and generation
- data/templates/report/report.html - etcd charts ms conversion, tab reordering
- internal/openshift/mustgathermetrics/metrics.html - new grid dashboard (embedded)
- internal/openshift/mustgathermetrics/index.html - redirect (embedded)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…or handling

Fixed memory leaks and stale render issues in Chart.js usage, and improved
error handling in metrics export to fail fast on errors.

Changes:
- Fixed Chart.js memory leaks: destroy() existing instances before cleanup
- Added render token to ignore stale axios responses
- Changed metrics export to fail on first chart write error (not just warn)
- Added validation to reject empty chart exports

Chart.js Lifecycle Fixes (data/templates/report/report.html):
- changeMenuCleanup(): Now calls destroy() on all chartInstances before clearing
- renderEtcdCharts(): Destroys old instances and uses render token to ignore stale responses
- Prevents memory leaks from undestroyed Chart instances
- Prevents duplicate/invalid charts from late axios responses

Error Handling Improvements (internal/openshift/mustgathermetrics/main.go):
- Chart write failures now return error instead of logging warning
- Added len(index) == 0 check to fail if no charts were generated
- Ensures partial/broken exports are rejected, not reported as success

Addresses CodeRabbit review comments from PR redhat-openshift-ecosystem#218

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@mtulio

mtulio commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator Author

Response to CodeRabbit Review Comments

Comment 1: plotly.go error logging (line 68)

Status: ✅ Not applicable
This file was deleted in commit 98f58167 during refactoring. We eliminated the Plotly intermediate format conversion and now use Prometheus JSON directly.

Comment 2: Chart.js lifecycle issues (report.html)

Status: ✅ Fixed in commit 3e5abd8b

  • Added destroy() calls in changeMenuCleanup() to prevent memory leaks
  • Added render token in renderEtcdCharts() to ignore stale axios responses
  • Prevents duplicate/invalid charts from late responses

Comment 3: Error handling in main.go

Status: ✅ Fixed in commit 3e5abd8b

  • Changed chart write failures to return error (was: log warning + continue)
  • Added len(index) == 0 validation before success
  • Ensures partial/broken exports fail fast

Comment 4: CDN dependencies without SRI (report.html:16)

Status: 🔄 Design decision - needs discussion

Current approach: Load Chart.js, date-fns adapter, hammerjs, and zoom plugin from public CDNs

Pros:

  • Simpler deployment (no bundling step)
  • Better browser caching (shared across sites)
  • Automatic updates for patch versions

Cons:

  • Supply-chain risk without SRI attributes
  • Network dependency

Options:

  1. Add SRI attributes + crossorigin (quick fix)
  2. Bundle locally (better security, more complexity)
  3. Hybrid: Bundle Chart.js core, keep plugins on CDN

@mtulio asked: "can we decrease dependencies or build together, preferably serving as static?"

Recommendation: Add SRI attributes now for security, consider bundling in future if deployment complexity is acceptable.

Would you like me to proceed with option 1 (SRI attributes) or option 2 (local bundling)?

The HasMetrics flag was being set to true even when Metrics.Process()
failed, which would advertise metrics assets that don't exist and
break the report flow.

Now only set HasMetrics=true after Process() succeeds.

Fixes CodeRabbit review comment on internal/opct/summary/result.go:515-523

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@mtulio

mtulio commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator Author

All CodeRabbit review comments addressed

Fixed in commit 6d0133d:

  • internal/opct/summary/result.go:515-523: Now only sets HasMetrics = true after successful Process(). On error, the flag remains false to avoid advertising non-existent metrics assets.

Already fixed in previous commits:

  • internal/openshift/mustgathermetrics/main.go:218-223: Chart write failures now return error immediately (commit 98f5816). Empty index validation added (lines 241-243).
  • data/templates/report/report.html Chart.js lifecycle: Added destroy() calls and render tokens to prevent memory leaks (commit 3e5abd8).
  • internal/openshift/mustgathermetrics/plotly.go: File deleted in refactor (commit 98f5816), comment no longer applies.

Pending user decision:

  • CDN dependencies (Chart.js) SRI attributes: Awaiting decision on approach (add SRI vs bundle locally).

🤖 Generated with Claude Code

Co-Authored-By: Claude Sonnet 4.5 noreply@anthropic.com

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/openshift/mustgathermetrics/metrics.html`:
- Around line 192-367: The grid and modal chart rendering code is duplicated;
extract shared helpers and use them from both places. Implement a
createDatasets(promResponse, config) that builds dataset objects (used where
datasets are created in the forEach block and in expandChart) and implement
createChartOptions(config, isModal=false) that returns the Chart.js options
(used instead of the duplicated options object in the Chart(...) calls), then
update the forEach chartInstances[config.id] creation and expandChart to call
axios, validate promResponse, call createDatasets(...) and
createChartOptions(..., true/false) and pass those into new Chart(...); keep
existing identifiers chartColors, chartInstances, expandChart and modalChart so
changes are local and preserve minor style differences via the isModal flag.
- Around line 161-168: The file currently hardcodes the chartConfigs array;
instead fetch and parse ./index.json on page load and use that response as
chartConfigs (preserving fields id, path, title, unit, convertToMs) before
invoking the existing chart/grid creation and chart-loading logic that expects
chartConfigs; update metrics.html to replace the static chartConfigs declaration
with an async fetch (axios or fetch) that falls back to an empty array or logs
an error if the request fails so the page degrades gracefully, and ensure any
code that reads chartConfigs (the chart/grid initialization code) runs only
after the fetch completes.
- Around line 7-11: Add Subresource Integrity (SRI) and crossorigin attributes
to the CDN resources referenced in the HTML (the <link> for bootstrap@5.3.3 and
the <script> tags for chart.js@4.4.9, chartjs-adapter-date-fns@3.0.0,
chartjs-plugin-zoom@2.2.0, and axios) by computing and inserting the correct
integrity hashes (e.g., sha384) for each URL and adding crossorigin="anonymous";
alternatively replace those CDN imports by bundling the libraries locally if you
prefer not to use CDN+SRI.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Enterprise

Run ID: 21e9aa05-e17b-4e70-9202-d8a865eee23a

📥 Commits

Reviewing files that changed from the base of the PR and between 98f5816 and 6d0133d.

📒 Files selected for processing (5)
  • data/templates/report/report.html
  • internal/opct/summary/result.go
  • internal/openshift/mustgathermetrics/index.html
  • internal/openshift/mustgathermetrics/main.go
  • internal/openshift/mustgathermetrics/metrics.html
✅ Files skipped from review due to trivial changes (1)
  • internal/openshift/mustgathermetrics/index.html
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/opct/summary/result.go
  • internal/openshift/mustgathermetrics/main.go
  • data/templates/report/report.html

Comment on lines +7 to +11
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.9/dist/chart.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom@2.2.0/dist/chartjs-plugin-zoom.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Add SRI hashes to CDN dependencies.

The CDN-loaded libraries lack Subresource Integrity (SRI) hashes, creating a supply-chain security risk. If a CDN is compromised or a MITM attack occurs, malicious code could be injected.

Per PR objectives, CDN security for report.html was left as a design decision. The same concern applies here. Consider adding SRI hashes with crossorigin="anonymous" to all CDN script/link tags, or bundle these dependencies locally.

🔒 Example with SRI hashes
-  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
-  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.9/dist/chart.umd.min.js"></script>
+  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" 
+        rel="stylesheet" 
+        integrity="sha384-..." 
+        crossorigin="anonymous">
+  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.9/dist/chart.umd.min.js"
+          integrity="sha384-..."
+          crossorigin="anonymous"></script>

Note: Generate actual SRI hashes using: curl -s URL | openssl dgst -sha384 -binary | openssl base64 -A

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/openshift/mustgathermetrics/metrics.html` around lines 7 - 11, Add
Subresource Integrity (SRI) and crossorigin attributes to the CDN resources
referenced in the HTML (the <link> for bootstrap@5.3.3 and the <script> tags for
chart.js@4.4.9, chartjs-adapter-date-fns@3.0.0, chartjs-plugin-zoom@2.2.0, and
axios) by computing and inserting the correct integrity hashes (e.g., sha384)
for each URL and adding crossorigin="anonymous"; alternatively replace those CDN
imports by bundling the libraries locally if you prefer not to use CDN+SRI.

Comment on lines +161 to +168
const chartConfigs = [
{ id: "chart-0", path: "./query_range-etcd-disk-fsync-wal-duration-p99.json.gz.json", title: "etcd fsync WAL p99", unit: "ms", convertToMs: true },
{ id: "chart-1", path: "./query_range-etcd-disk-fsync-db-duration-p99.json.gz.json", title: "etcd fsync DB p99", unit: "ms", convertToMs: true },
{ id: "chart-2", path: "./query_range-etcd-peer-round-trip-time.json.gz.json", title: "etcd peer round trip time", unit: "ms", convertToMs: true },
{ id: "chart-3", path: "./query_range-etcd-total-leader-elections-day.json.gz.json", title: "etcd total leader elections per day", unit: "count", convertToMs: false },
{ id: "chart-4", path: "./query_range-etcd-request-duration-p99.json.gz.json", title: "etcd request duration p99", unit: "ms", convertToMs: true },
{ id: "chart-5", path: "./query_range-api-kas-request-duration-p99.json.gz.json", title: "Kube API Server request duration p99", unit: "ms", convertToMs: true },
];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Fetch chart configurations dynamically from index.json.

The chart paths, titles, and metadata are hardcoded, but according to context snippet 1 (main.go:186-270), the server generates an index.json file with chart metadata. Hardcoding creates tight coupling with the server-side filename format and duplicates metadata that exists in charts-config.json on the server.

If the server changes the filename format or chart metadata, this file must be manually updated, increasing maintenance burden and the risk of inconsistencies.

Consider fetching index.json on page load to dynamically discover available charts and their configurations. This would make the dashboard resilient to server-side changes and eliminate duplication.

♻️ Proposed approach
  1. Extend index.json to include chart metadata (title, unit, convertToMs):
[
  {
    "id": "chart-0",
    "path": "./query_range-etcd-disk-fsync-wal-duration-p99.json.gz.json",
    "title": "etcd fsync WAL p99",
    "unit": "ms",
    "convertToMs": true
  },
  ...
]
  1. Fetch and parse index.json in metrics.html:
axios.get('./index.json')
  .then(resp => {
    const chartConfigs = resp.data;
    // Proceed with existing grid creation and chart loading logic
  });

This approach ensures the client and server stay in sync automatically.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/openshift/mustgathermetrics/metrics.html` around lines 161 - 168,
The file currently hardcodes the chartConfigs array; instead fetch and parse
./index.json on page load and use that response as chartConfigs (preserving
fields id, path, title, unit, convertToMs) before invoking the existing
chart/grid creation and chart-loading logic that expects chartConfigs; update
metrics.html to replace the static chartConfigs declaration with an async fetch
(axios or fetch) that falls back to an empty array or logs an error if the
request fails so the page degrades gracefully, and ensure any code that reads
chartConfigs (the chart/grid initialization code) runs only after the fetch
completes.

Comment on lines +192 to +367
chartConfigs.forEach(config => {
axios.get(config.path)
.then(resp => {
const promResponse = resp.data;
if (!promResponse.data || !promResponse.data.result) {
throw new Error('Invalid Prometheus response format');
}

const datasets = promResponse.data.result.map((result, i) => {
const labelKey = Object.keys(result.metric).find(k => k !== '__name__') || 'instance';
const labelValue = result.metric[labelKey] || 'unknown';

const dataPoints = result.values.map(v => ({
x: new Date(v[0] * 1000),
y: config.convertToMs ? parseFloat(v[1]) * 1000 : parseFloat(v[1]) // Convert seconds to ms
}));

return {
label: labelValue,
data: dataPoints,
borderColor: chartColors[i % chartColors.length],
backgroundColor: 'transparent',
borderWidth: 1.5,
pointRadius: 0,
tension: 0.3,
};
});

const canvas = document.getElementById(config.id);
if (!canvas) return;

chartInstances[config.id] = new Chart(canvas, {
type: 'line',
data: { datasets: datasets },
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: {
position: 'bottom',
labels: { boxWidth: 10, font: { size: 10 } }
},
tooltip: { mode: 'index', intersect: false },
zoom: {
zoom: {
drag: {
enabled: true,
backgroundColor: 'rgba(78,121,167,0.15)',
borderColor: '#4e79a7',
borderWidth: 1
},
mode: 'x'
},
pan: { enabled: true, mode: 'x' },
},
},
scales: {
x: {
type: 'time',
time: { tooltipFormat: 'yyyy-MM-dd HH:mm' },
ticks: { maxTicksAllowed: 6, font: { size: 9 } }
},
y: {
beginAtZero: false,
ticks: { font: { size: 9 } },
title: {
display: true,
text: config.unit,
font: { size: 10, weight: 'bold' }
}
},
},
},
});
})
.catch(err => {
console.error(`Failed to load ${config.title}:`, err);
const canvas = document.getElementById(config.id);
if (canvas) {
canvas.parentElement.innerHTML = '<div class="chart-unavailable">Chart not available</div>';
}
});
});

function expandChart(config) {
const modal = document.getElementById('chart-modal');
const modalTitle = document.getElementById('modal-chart-title');
const modalCanvas = document.getElementById('modal-chart-canvas');

modalTitle.textContent = config.title;
modal.classList.add('active');

// Destroy previous modal chart if exists
if (modalChart) {
modalChart.destroy();
}

// Load chart data
axios.get(config.path)
.then(resp => {
const promResponse = resp.data;
if (!promResponse.data || !promResponse.data.result) {
throw new Error('Invalid Prometheus response format');
}

const datasets = promResponse.data.result.map((result, i) => {
const labelKey = Object.keys(result.metric).find(k => k !== '__name__') || 'instance';
const labelValue = result.metric[labelKey] || 'unknown';

const dataPoints = result.values.map(v => ({
x: new Date(v[0] * 1000),
y: config.convertToMs ? parseFloat(v[1]) * 1000 : parseFloat(v[1]) // Convert seconds to ms
}));

return {
label: labelValue,
data: dataPoints,
borderColor: chartColors[i % chartColors.length],
backgroundColor: 'transparent',
borderWidth: 2,
pointRadius: 0,
tension: 0.3,
};
});

modalChart = new Chart(modalCanvas, {
type: 'line',
data: { datasets: datasets },
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: {
position: 'bottom',
labels: { boxWidth: 15, font: { size: 12 } }
},
tooltip: { mode: 'index', intersect: false },
zoom: {
zoom: {
drag: {
enabled: true,
backgroundColor: 'rgba(78,121,167,0.15)',
borderColor: '#4e79a7',
borderWidth: 1
},
mode: 'x'
},
pan: { enabled: true, mode: 'x' },
},
},
scales: {
x: {
type: 'time',
time: { tooltipFormat: 'yyyy-MM-dd HH:mm:ss' },
ticks: { font: { size: 11 } }
},
y: {
beginAtZero: false,
ticks: { font: { size: 11 } },
title: {
display: true,
text: config.unit,
font: { size: 12, weight: 'bold' }
}
},
},
},
});
})
.catch(err => {
console.error(`Failed to load expanded chart:`, err);
modalCanvas.parentElement.innerHTML = '<div class="chart-unavailable">Failed to load chart data</div>';
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Extract shared chart rendering logic to eliminate duplication.

The chart loading and rendering logic is duplicated between the grid view (lines 192-275) and modal view (lines 277-367). The dataset creation (lines 200-218 vs 298-316) and Chart.js configuration (lines 226-264 vs 321-360) are nearly identical, with only minor differences in styling.

This duplication violates DRY principles, increases maintenance burden (bug fixes must be applied twice), and makes it easy to introduce inconsistencies between the two views.

♻️ Proposed refactor

Extract a shared function to create datasets and chart options:

function createDatasets(promResponse, config) {
  return promResponse.data.result.map((result, i) => {
    const labelKey = Object.keys(result.metric).find(k => k !== '__name__') || 'instance';
    const labelValue = result.metric[labelKey] || 'unknown';
    
    const dataPoints = result.values.map(v => ({
      x: new Date(v[0] * 1000),
      y: config.convertToMs ? parseFloat(v[1]) * 1000 : parseFloat(v[1])
    }));
    
    return {
      label: labelValue,
      data: dataPoints,
      borderColor: chartColors[i % chartColors.length],
      backgroundColor: 'transparent',
      pointRadius: 0,
      tension: 0.3,
    };
  });
}

function createChartOptions(config, isModal = false) {
  const fontSize = isModal ? 11 : 9;
  const legendSize = isModal ? 12 : 10;
  
  return {
    responsive: true,
    maintainAspectRatio: false,
    // ... rest of options with conditional sizes
  };
}

Then use these helpers in both grid and modal rendering, passing style parameters as needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/openshift/mustgathermetrics/metrics.html` around lines 192 - 367,
The grid and modal chart rendering code is duplicated; extract shared helpers
and use them from both places. Implement a createDatasets(promResponse, config)
that builds dataset objects (used where datasets are created in the forEach
block and in expandChart) and implement createChartOptions(config,
isModal=false) that returns the Chart.js options (used instead of the duplicated
options object in the Chart(...) calls), then update the forEach
chartInstances[config.id] creation and expandChart to call axios, validate
promResponse, call createDatasets(...) and createChartOptions(..., true/false)
and pass those into new Chart(...); keep existing identifiers chartColors,
chartInstances, expandChart and modalChart so changes are local and preserve
minor style differences via the isModal flag.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/feature Categorizes issue or PR as related to a new feature. target-release/opct-v0.7

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant