Skip to content

Commit d6f2d75

Browse files
Close remaining React parity gaps in the Go SSR website
Follow-up fixes after comparing the server-rendered site against the original React app. - Drop the column-visibility ("View") toggle from the History and Macro Queries tables, matching the toggle-free Status/PR tables. - Restore searchable command-palette pickers on the Foreign Keys page (workloads + commit ref, grouped Branches/Releases), mirroring Compare; resolve the ref name to a SHA and show the friendly label. - Standardize all table headers on font-semibold text-foreground. - Re-add Google Analytics (GA4 gtag, G-QCJ7MJ5CPX) in the base layout, skipped on localhost so dev traffic doesn't pollute the property. - Theme the Daily/Status chart tooltips and restore the Status 7-day bar chart's Y-axis ticks + horizontal gridlines. - Restore the Execution Queue filter toolbar (free-text + Source/Workload facets) with client-side pagination (default 10/page). - Colorize the Macro Queries plan JSON with a monokai theme. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Arthur Schreiber <arthur@planetscale.com>
1 parent 854e6bb commit d6f2d75

9 files changed

Lines changed: 574 additions & 129 deletions

File tree

go/server/static/web/app.js

Lines changed: 230 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,84 @@ function compareForm() {
122122
};
123123
}
124124

125+
// fkForm backs the Foreign Keys page's pickers, mirroring the React FK hero's
126+
// WorkloadsCommand (old/new workload) + VitessRefsCommand (commit) palettes. Each
127+
// of the three fields is a button opening one shared command dialog: the two
128+
// workload fields list the page's TPCC workloads, the commit field lists the
129+
// vitess refs grouped into Branches/Releases (or accepts a pasted SHA). The
130+
// chosen values feed hidden inputs the form submits (server resolves ref name ->
131+
// SHA). Data (refs, workloads, initial values) is read from data-* attributes.
132+
function fkForm() {
133+
return {
134+
refs: [],
135+
workloads: [],
136+
oldWorkload: "",
137+
newWorkload: "",
138+
shaVal: "",
139+
open: false, // false | "oldWorkload" | "newWorkload" | "sha"
140+
query: "",
141+
init() {
142+
try {
143+
this.refs = JSON.parse(this.$el.dataset.refs || "[]");
144+
} catch (e) {
145+
this.refs = [];
146+
}
147+
try {
148+
this.workloads = JSON.parse(this.$el.dataset.workloads || "[]");
149+
} catch (e) {
150+
this.workloads = [];
151+
}
152+
this.oldWorkload = this.$el.dataset.oldWorkload || "";
153+
this.newWorkload = this.$el.dataset.newWorkload || "";
154+
this.shaVal = this.$el.dataset.sha || "";
155+
},
156+
show(field) {
157+
this.open = field;
158+
this.query = "";
159+
this.$nextTick(
160+
function () {
161+
if (this.$refs.search) this.$refs.search.focus();
162+
}.bind(this)
163+
);
164+
},
165+
workloadMatches() {
166+
var q = this.query.trim().toLowerCase();
167+
return this.workloads.filter(function (w) {
168+
return !q || w.toLowerCase().indexOf(q) !== -1;
169+
});
170+
},
171+
matches(kind) {
172+
var q = this.query.trim().toLowerCase();
173+
return this.refs.filter(function (r) {
174+
return r.kind === kind && (!q || r.name.toLowerCase().indexOf(q) !== -1);
175+
});
176+
},
177+
branches() {
178+
return this.matches("branch");
179+
},
180+
releases() {
181+
return this.matches("release");
182+
},
183+
commit(value) {
184+
if (this.open === "oldWorkload") this.oldWorkload = value;
185+
else if (this.open === "newWorkload") this.newWorkload = value;
186+
else this.shaVal = value;
187+
this.open = false;
188+
},
189+
select(name) {
190+
this.commit(name);
191+
},
192+
enter() {
193+
var q = this.query.trim();
194+
if (!q) {
195+
this.open = false;
196+
return;
197+
}
198+
this.commit(q);
199+
},
200+
};
201+
}
202+
125203
// cssHSL resolves a Tailwind design-token variable (e.g. "--primary", stored as
126204
// "24.6 95% 53.1%") into a CSS hsl() color usable by Chart.js.
127205
function cssHSL(varName, fallback) {
@@ -130,6 +208,24 @@ function cssHSL(varName, fallback) {
130208
return "hsl(" + v.replace(/,/g, " ") + ")";
131209
}
132210

211+
// themedTooltip returns Chart.js tooltip options styled to match the site's
212+
// surface (light/dark) instead of Chart.js's default dark bubble, mirroring the
213+
// React custom tooltips. Pass per-chart title/label callbacks.
214+
function themedTooltip(callbacks) {
215+
return {
216+
enabled: true,
217+
backgroundColor: cssHSL("--background", "#fff"),
218+
titleColor: cssHSL("--foreground", "#000"),
219+
bodyColor: cssHSL("--foreground", "#000"),
220+
borderColor: cssHSL("--border", "#ddd"),
221+
borderWidth: 1,
222+
padding: 8,
223+
cornerRadius: 6,
224+
usePointStyle: true,
225+
callbacks: callbacks || {},
226+
};
227+
}
228+
133229
// initSparklines draws a minimal QPS line chart into every uninitialized
134230
// [data-sparkline] canvas, reading its series from the data-qps attribute.
135231
function initSparklines(root) {
@@ -201,14 +297,35 @@ function initBarCharts(root) {
201297
backgroundColor: color,
202298
borderColor: color,
203299
borderWidth: 1,
300+
maxBarThickness: 10,
204301
},
205302
],
206303
},
207304
options: {
208305
responsive: true,
209306
maintainAspectRatio: false,
210-
plugins: { legend: { display: false }, tooltip: { enabled: true } },
211-
scales: { x: { display: false }, y: { display: false } },
307+
plugins: {
308+
legend: { display: false },
309+
// Value only, no x-axis (day) label — mirrors React's hideLabel tooltip.
310+
tooltip: themedTooltip({
311+
title: function () {
312+
return "";
313+
},
314+
label: function (ctx) {
315+
return String(ctx.parsed.y);
316+
},
317+
}),
318+
},
319+
// Y axis with ticks + horizontal gridlines; X axis without tick labels or
320+
// vertical gridlines (mirrors recharts YAxis / XAxis tick=false /
321+
// CartesianGrid vertical=false).
322+
scales: {
323+
x: { ticks: { display: false }, grid: { display: false } },
324+
y: {
325+
ticks: { color: cssHSL("--muted-foreground", "#888") },
326+
grid: { color: cssHSL("--border", "#ddd") },
327+
},
328+
},
212329
},
213330
});
214331
});
@@ -252,7 +369,16 @@ function initLineCharts(root) {
252369
interaction: { mode: "index", intersect: false },
253370
plugins: {
254371
legend: { display: true, position: "bottom", labels: { color: axisColor, usePointStyle: true } },
255-
tooltip: { enabled: true },
372+
// "Commit: <ref>" header + integer-rounded per-series values, mirroring
373+
// the React DailyCharts CustomTooltip.
374+
tooltip: themedTooltip({
375+
title: function (items) {
376+
return items.length ? "Commit: " + items[0].label : "";
377+
},
378+
label: function (ctx) {
379+
return ctx.dataset.label + ": " + Math.round(ctx.parsed.y);
380+
},
381+
}),
256382
},
257383
scales: {
258384
x: { ticks: { color: axisColor }, grid: { color: gridColor } },
@@ -289,11 +415,39 @@ function copyCompareMarkdown(btn) {
289415
});
290416
}
291417

418+
// highlightJSON turns an (already indented) JSON string into HTML with per-token
419+
// spans, so the query-plan dialog can render it with the monokai colors defined
420+
// in tailwind.css (.json-pretty .*), mirroring the React react-json-pretty view.
421+
// HTML metacharacters in the source are escaped before the spans are added, so
422+
// plan content can't inject markup when rendered via x-html.
423+
function highlightJSON(jsonStr) {
424+
if (!jsonStr) return "";
425+
var s = String(jsonStr)
426+
.replace(/&/g, "&amp;")
427+
.replace(/</g, "&lt;")
428+
.replace(/>/g, "&gt;");
429+
return s.replace(
430+
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false)\b|\bnull\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
431+
function (match) {
432+
var cls = "json-num";
433+
if (/^"/.test(match)) {
434+
cls = /:$/.test(match) ? "json-key" : "json-string";
435+
} else if (/^(true|false)$/.test(match)) {
436+
cls = "json-bool";
437+
} else if (match === "null") {
438+
cls = "json-null";
439+
}
440+
return '<span class="' + cls + '">' + match + "</span>";
441+
}
442+
);
443+
}
444+
292445
// queryPlansTable is the Alpine.js data factory for the Compare Query Plans
293446
// table (see templates/web/pages/macro_queries_compare.html). It reads the row
294447
// data from the host element's data-rows attribute and handles the text/operator
295448
// filtering, exec-time sorting, pagination, and per-row plan dialog client-side,
296-
// mirroring the React TanStack table + dialog.
449+
// mirroring the React TanStack table + dialog. (Column-visibility toggles were
450+
// dropped to keep all tables consistent — see go/server/MIGRATION_PARITY.md.)
297451
function queryPlansTable() {
298452
return {
299453
rows: [],
@@ -304,7 +458,6 @@ function queryPlansTable() {
304458
pageSize: 10,
305459
modalOpen: false,
306460
current: {},
307-
showCol: { query: true, execTime: true },
308461
init() {
309462
try {
310463
this.rows = JSON.parse(this.$el.dataset.rows || "[]");
@@ -375,7 +528,7 @@ function queryPlansTable() {
375528
// historyTable is the Alpine.js data factory for the History table (see
376529
// templates/web/pages/history.html). Like queryPlansTable it reads its rows from
377530
// the host element's data-rows attribute and handles the text filter, source
378-
// faceted filter, column visibility, and pagination client-side. The data-initial
531+
// faceted filter, and pagination client-side. The data-initial
379532
// attribute seeds the text filter from the ?gitRef= query param so a "Benchmarks
380533
// History" row action deep-links to a pre-filtered table.
381534
function historyTable() {
@@ -385,7 +538,6 @@ function historyTable() {
385538
sources: [],
386539
page: 0,
387540
pageSize: 10,
388-
showCol: { sha: true, source: true, workloads: true, started: true },
389541
init() {
390542
try {
391543
this.rows = JSON.parse(this.$el.dataset.rows || "[]");
@@ -433,6 +585,77 @@ function historyTable() {
433585
};
434586
}
435587

588+
// queueTable is the Alpine.js data factory for the Status page's Execution Queue
589+
// table. The queue is small in-memory data, so it reads its rows from the host
590+
// element's data-rows attribute and filters client-side: a free-text match on the
591+
// SHA plus Source/Workload faceted filters, mirroring the React queue toolbar.
592+
// (No pagination — the queue is short; that matches the restored toolbar's scope.)
593+
function queueTable() {
594+
return {
595+
rows: [],
596+
query: "",
597+
sources: [],
598+
workloads: [],
599+
page: 0,
600+
pageSize: 10,
601+
init() {
602+
try {
603+
this.rows = JSON.parse(this.$el.dataset.rows || "[]");
604+
} catch (e) {
605+
this.rows = [];
606+
}
607+
},
608+
toggleSource(src) {
609+
var idx = this.sources.indexOf(src);
610+
if (idx === -1) {
611+
this.sources.push(src);
612+
} else {
613+
this.sources.splice(idx, 1);
614+
}
615+
this.page = 0;
616+
},
617+
toggleWorkload(w) {
618+
var idx = this.workloads.indexOf(w);
619+
if (idx === -1) {
620+
this.workloads.push(w);
621+
} else {
622+
this.workloads.splice(idx, 1);
623+
}
624+
this.page = 0;
625+
},
626+
reset() {
627+
this.query = "";
628+
this.sources = [];
629+
this.workloads = [];
630+
this.page = 0;
631+
},
632+
filtered() {
633+
var q = this.query.trim().toLowerCase();
634+
var srcs = this.sources;
635+
var wls = this.workloads;
636+
return this.rows.filter(function (r) {
637+
if (q && (r.sha || "").toLowerCase().indexOf(q) === -1) return false;
638+
if (srcs.length && srcs.indexOf(r.source) === -1) return false;
639+
if (wls.length && wls.indexOf(r.workload) === -1) return false;
640+
return true;
641+
});
642+
},
643+
pageCount() {
644+
return Math.ceil(this.filtered().length / this.pageSize);
645+
},
646+
paged() {
647+
var start = this.page * this.pageSize;
648+
return this.filtered().slice(start, start + this.pageSize);
649+
},
650+
prevPage() {
651+
if (this.page > 0) this.page--;
652+
},
653+
nextPage() {
654+
if (this.page + 1 < this.pageCount()) this.page++;
655+
},
656+
};
657+
}
658+
436659
// tooltip is a small Alpine.js data factory for a hover tooltip, modeled on the
437660
// React shadcn/Radix Tooltip used on the Status page timestamps. Following Radix
438661
// (@radix-ui/react-tooltip + react-popper):

go/server/tailwind.css

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,28 @@
2525
@apply absolute top-0 left-0 w-full h-full;
2626
}
2727
}
28+
29+
/*
30+
* Syntax highlighting for the query-plan JSON in the Macro Queries dialog,
31+
* ported from react-json-pretty's "monikai" theme (monikai.css). Like that
32+
* theme it always renders a dark monokai surface, independent of light/dark
33+
* mode. Token spans are produced client-side by highlightJSON() in app.js.
34+
*/
35+
.json-pretty {
36+
color: #66d9ef;
37+
background: #272822;
38+
line-height: 1.3;
39+
}
40+
.json-pretty .json-key {
41+
color: #f92672;
42+
}
43+
.json-pretty .json-string {
44+
color: #fd971f;
45+
}
46+
.json-pretty .json-num {
47+
color: #a6e22e;
48+
}
49+
.json-pretty .json-bool,
50+
.json-pretty .json-null {
51+
color: #ac81fe;
52+
}

go/server/templates/web/base.html

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,28 @@
2121
})();
2222
</script>
2323

24+
<!--
25+
Google Analytics (GA4) — ports the React App.tsx `ReactGA.initialize("G-QCJ7MJ5CPX")`.
26+
gtag's default config sends a page_view; because each SSR page is a full
27+
navigation, every page is tracked (the React SPA only tracked the first load).
28+
Skipped on localhost so local/docker dev traffic doesn't pollute the property.
29+
-->
30+
<script>
31+
(function () {
32+
var h = location.hostname;
33+
if (h === "localhost" || h === "127.0.0.1" || h === "::1" || h.endsWith(".local")) return;
34+
var id = "G-QCJ7MJ5CPX";
35+
var s = document.createElement("script");
36+
s.async = true;
37+
s.src = "https://www.googletagmanager.com/gtag/js?id=" + id;
38+
document.head.appendChild(s);
39+
window.dataLayer = window.dataLayer || [];
40+
window.gtag = function () { dataLayer.push(arguments); };
41+
gtag("js", new Date());
42+
gtag("config", id);
43+
})();
44+
</script>
45+
2446
<!-- Open Sans font -->
2547
<link rel="preconnect" href="https://fonts.googleapis.com" />
2648
<link

0 commit comments

Comments
 (0)