@@ -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.
127205function 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.
135231function 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, "&" )
427+ . replace ( / < / g, "<" )
428+ . replace ( / > / g, ">" ) ;
429+ return s . replace (
430+ / ( " ( \\ u [ a - z A - Z 0 - 9 ] { 4 } | \\ [ ^ u ] | [ ^ \\ " ] ) * " ( \s * : ) ? | \b ( t r u e | f a l s e ) \b | \b n u l l \b | - ? \d + (?: \. \d * ) ? (?: [ e E ] [ + \- ] ? \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 ( / ^ ( t r u e | f a l s e ) $ / . 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.)
297451function 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.
381534function 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):
0 commit comments