diff --git a/changelog.d/fixed-10673.md b/changelog.d/fixed-10673.md new file mode 100644 index 0000000000..eb2c945b35 --- /dev/null +++ b/changelog.d/fixed-10673.md @@ -0,0 +1 @@ +- dashboard: overview window pills overlap tiles use in-flow toolbar like throughput (#10673) diff --git a/src/pkg/dashboard/overview_window_toolbar_test.go b/src/pkg/dashboard/overview_window_toolbar_test.go new file mode 100644 index 0000000000..1d82fa4188 --- /dev/null +++ b/src/pkg/dashboard/overview_window_toolbar_test.go @@ -0,0 +1,63 @@ +package dashboard + +import ( + "os/exec" + "strings" + "testing" +) + +func TestOverviewWindowPillsUseSharedInFlowToolbar(t *testing.T) { + html := indexHTML(t) + for _, snippet := range []string{ + `.window-pills-toolbar { grid-column: 1 / -1; justify-self: end; max-width: 100%; flex-wrap: wrap; justify-content: flex-end; }`, + `.window-pills-toolbar { justify-self: start; justify-content: flex-start; }`, + `function overviewKPIWindowControls()`, + `class="gov-pr-models-toggle window-pills-toolbar overview-window-toolbar"`, + `data-action="setOverviewKPIWindow"`, + `
`, + `
`, + } { + if !strings.Contains(html, snippet) { + t.Fatalf("Overview window toolbar missing snippet %q", snippet) + } + } + for _, banned := range []string{ + `.overview-kpi-range { position: absolute;`, + `.overview-kpis { position: relative;`, + } { + if strings.Contains(html, banned) { + t.Fatalf("Overview KPI window controls must be in normal flow, found %q", banned) + } + } + render := jsFunc(t, html, "renderOverviewKPIs") + toolbar := strings.Index(render, "${overviewKPIWindowControls()}") + grid := strings.Index(render, `
`) + if toolbar < 0 || grid < 0 || toolbar > grid { + t.Fatalf("Overview KPI toolbar must render before the tile grid: toolbar=%d grid=%d", toolbar, grid) + } +} + +func TestOverviewWindowControlsPreserveBehavior(t *testing.T) { + node, err := exec.LookPath("node") + if err != nil { + t.Skip("node unavailable: Overview window controls were not executed") + } + html := indexHTML(t) + script := `const assert = require('node:assert/strict'); +const OVERVIEW_KPI_WINDOWS = { '24h': 24 * 3600e3, '7d': 7 * 24 * 3600e3 }; +let _overviewKPIWindow = '7d'; +function esc(v) { return String(v).replace(/[&<>"]/g, ch => ({'&':'&','<':'<','>':'>','"':'"'}[ch])); } +` + jsFunc(t, html, "overviewKPIWindowControls") + ` +const out = overviewKPIWindowControls(); +assert.match(out, /class="gov-pr-models-toggle window-pills-toolbar overview-window-toolbar"/); +assert.match(out, /data-stop="1"/); +assert.match(out, /data-action="setOverviewKPIWindow"/); +assert.match(out, /data-arg0="24h"[^>]*>24h<\/button>/); +assert.match(out, /class="active"[^>]*data-action="setOverviewKPIWindow"[^>]*data-arg0="7d"/); +assert.match(out, /aria-pressed="true"[^>]*>7d<\/button>/); +` + out, err := exec.Command(node, "-e", script).CombinedOutput() + if err != nil { + t.Fatalf("node Overview window controls failed: %v\n%s", err, strings.TrimSpace(string(out))) + } +} diff --git a/src/pkg/dashboard/pr_throughput_ui_test.go b/src/pkg/dashboard/pr_throughput_ui_test.go index e6cc5f0ed8..7c2107f439 100644 --- a/src/pkg/dashboard/pr_throughput_ui_test.go +++ b/src/pkg/dashboard/pr_throughput_ui_test.go @@ -94,8 +94,8 @@ func TestPRThroughputCollapsedHeadlineKeepsWindowPill(t *testing.T) { } html := indexHTML(t) for _, snippet := range []string{ - `.prt-window-controls { grid-column: 1 / -1; justify-self: end; max-width: 100%; flex-wrap: wrap; justify-content: flex-end; }`, - `.prt-window-controls { justify-self: start; justify-content: flex-start; }`, + `.window-pills-toolbar { grid-column: 1 / -1; justify-self: end; max-width: 100%; flex-wrap: wrap; justify-content: flex-end; }`, + `.window-pills-toolbar { justify-self: start; justify-content: flex-start; }`, `.prt-share-tile .prt-role-select { position:absolute; right:var(--sp-3); bottom:var(--sp-3);`, `.prt-share-tile .prt-tile-spark { max-width: calc(100% - 6.4rem); overflow: hidden; }`, } { @@ -126,7 +126,7 @@ const PRT_WINDOWS = [[1, '1h'], [6, '6h'], [12, '12h'], [24, '24h'], [48, '48h'] let prtHours = 168; ` + jsFunc(t, html, "prtWindowControls") + ` const out = prtWindowControls(); -assert.match(out, /class="gov-pr-models-toggle prt-window-controls"/); +assert.match(out, /class="gov-pr-models-toggle window-pills-toolbar prt-window-controls"/); assert.match(out, /data-stop="1"/); assert.match(out, /data-action="setPRThroughputHours"/); assert.match(out, /data-arg0="168"[^>]*>7d<\/button>/); diff --git a/src/pkg/dashboard/static/index.html b/src/pkg/dashboard/static/index.html index 072a36da66..a28a66d156 100644 --- a/src/pkg/dashboard/static/index.html +++ b/src/pkg/dashboard/static/index.html @@ -1427,7 +1427,7 @@ .prt-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; margin-bottom: var(--sp-4); } .prt-controls { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; } .prt-headline { position: relative; } - .prt-window-controls { grid-column: 1 / -1; justify-self: end; max-width: 100%; flex-wrap: wrap; justify-content: flex-end; } + .window-pills-toolbar { grid-column: 1 / -1; justify-self: end; max-width: 100%; flex-wrap: wrap; justify-content: flex-end; } .prt-repo-select { max-width: 220px; background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: var(--r-sm); padding: var(--sp-1) var(--sp-3); font-size: var(--fs-xs); } .lc-fleet-stat .val.opened { color: var(--blue); } .lc-fleet-stat .val.closed { color: var(--red); } @@ -1473,7 +1473,7 @@ .prt-caption { color:var(--muted); font-size:var(--fs-xs); margin-top:var(--sp-2); } @media (max-width: 800px) { .prt-attribution { grid-template-columns:1fr; } } @media (max-width: 900px) { .lc-fleet.prt-headline { grid-template-columns: repeat(2, minmax(0, 1fr)); } } - @media (max-width: 640px) { .lc-fleet.prt-headline { grid-template-columns: 1fr; } .prt-window-controls { justify-self: start; justify-content: flex-start; } } + @media (max-width: 640px) { .lc-fleet.prt-headline { grid-template-columns: 1fr; } .window-pills-toolbar { justify-self: start; justify-content: flex-start; } } .lc-journeys { list-style: none; margin: var(--sp-0); padding: var(--sp-0); display: flex; flex-direction: column; gap: var(--sp-2); max-height: 420px; overflow-y: auto; } .lc-journey { font-size: 0.78rem; border-radius: var(--r); background: var(--bg); border: 1px solid var(--border); overflow: hidden; } @@ -1784,8 +1784,9 @@ .overview-settings-field input, .overview-settings-field select, .overview-chart-select { min-width: 0; border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--bg); color: var(--text); padding: var(--sp-1) var(--sp-2); font: inherit; } .overview-repo-filter { display: grid; gap: var(--sp-2); max-height: min(16rem, 32vh); overflow: auto; padding: var(--sp-2); border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--bg); } .overview-settings-links { display: flex; flex-wrap: wrap; gap: var(--sp-2); } - .overview-kpis { position: relative; display: grid; grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr)); gap: var(--sp-3); margin-bottom: var(--sp-4); padding-top: var(--sp-6); } - .overview-kpi-range { position: absolute; top: 0; right: 0; display: inline-flex; gap: var(--sp-1); } + .overview-headline { display: grid; gap: var(--sp-3); margin-bottom: var(--sp-4); } + .overview-kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr)); gap: var(--sp-3); } + .overview-kpi-range { display: inline-flex; gap: var(--sp-1); } .overview-kpi-range-btn { min-width: 2rem; height: 1.25rem; border: 1px solid var(--border); border-radius: var(--r-pill); background: var(--bg); color: var(--muted); font-size: var(--fs-2xs); font-weight: 800; cursor: pointer; } .overview-kpi-range-btn.active { border-color: var(--accent); color: var(--accent); } .overview-kpi { display: grid; gap: var(--sp-1); text-align: left; border: 1px solid var(--border); border-radius: var(--r); background: var(--bg); color: var(--text); padding: var(--sp-3); min-height: 5.9rem; cursor: pointer; } @@ -13279,7 +13280,7 @@

What sh })}`; } function overviewKPIWindowControls() { - return `${Object.keys(OVERVIEW_KPI_WINDOWS).map(win => ``).join('')}`; + return `
${Object.keys(OVERVIEW_KPI_WINDOWS).map(win => ``).join('')}
`; } function saveOverviewChartsState(state) { @@ -13627,13 +13628,13 @@

What sh { key: 'median-age', field: 'overviewMedianAgeSec', label: `Median actionable age (${state.timeBasis === 'created' ? 'created' : 'updated'})`, value: age, rawValue: ageSec, color: 'var(--purple)', sparkFmt: fmtDurationFromSeconds, tip: `Median time since ${state.timeBasis === 'created' ? 'created' : 'updated'} for actionable open issues and PRs in the selected repos (not MTTR, which measures time-to-resolve for closed items).` } ]; overviewKPIRecordLocal(overviewKPICurrentSample(cards)); - return `
${overviewKPIWindowControls()}${cards.map(card => { + return `
${overviewKPIWindowControls()}
${cards.map(card => { const value = card.value; const n = Number(card.rawValue); const attrs = Number.isFinite(n) ? ` data-overview-kpi-key="${esc(card.key)}" data-overview-kpi-value="${n}" data-overview-kpi-display="${esc(String(value ?? '—'))}"` : ''; const subline = card.subline ? `${esc(card.subline)}` : ''; return ``; - }).join('')}
`; + }).join('')}
`; } function overviewRepoFilterHTML(repos) { @@ -17452,7 +17453,7 @@

What sh } function prtWindowControls() { const buttons = PRT_WINDOWS.map(([h, label]) => ``).join(''); - return `
${buttons}
`; + return `
${buttons}
`; } function renderPRThroughput(dto) {