Repository navigation
Replace SammyJS and bump jQuery + Flot - #17760
Conversation
grok-99s
left a comment
There was a problem hiding this comment.
Thorough review of the cumulative final state at b4779cb (WIP, so no per-commit notes). Compared against merge-base bd3d3701c5 with both UIs talking to the same make run-broker instance. Almost everything matches base behaviour, but there are 2 blockers and several should-fix items.
(Posted as a COMMENT review — this account can't REQUEST_CHANGES on rabbitmq/rabbitmq-server.)
Blockers
-
Page-out-of-range recovery is broken (
main.js:1677, not in the PR diff) —check_bad_responsestill callsSammy.log(...), but the shim only exposesApplication(see inline onrouter.js), so it throws beforeupdate_pages(..., 1). The out-of-range page number stays in localStorage; every refresh hits 400 again and the list never redraws. Verified: after forcing page 7 on exchanges, PR kept page 7; base reset to 1. Hits when a list shrinks under the user (vhost switch, deletes, filters). Fix:console.log/debug, or addlogto the shim. -
Chart y-axes no longer start at 0 — Flot 4's default
autoScale: "loose"ignoresyaxis.min: 0. A flat backlog of 1000 becomes 1000–1000.01 with five identical "1.0 k" ticks; 100→120 becomes 95–125; rates 0.15/0.2 become 0.14–0.21.autoScale: "none"leaves the axis at 0–1. A fix that restored base ranges in testing: push an invisible zero series inrender_chart, e.g.out_data.push({data:[[x0,0]], lines:{show:false}}).
Should-fix
-
Start page loads twice →
getFullYearTypeError — when the URL has no#(normal after login / logout→login / reload of/),start_appsetsurl + '#/'(queues hashchange), thenapp.run()immediately runs#/and starts/api/overview; the hashchange runs the same route again, aborts the first request (status 0), andupdate_status('error')throws becauselast_successful_connectis still undefined (fmt_date0→getFullYearatmain.js:1504). Sammy skipped re-running the same location; the new router does not. Verified 3/3 PR logins vs 0/3 base; also duplicates/api/overview. Same path for the OAuth#token_typeredirect by reading. Fix: remember last routed hash and skip same-hash hashchange; also don't treat aborted requests as "Error", and hardenupdate_status. -
Dark mode: chart labels nearly invisible — Flot 4 draws axis labels as SVG
<text>with black fill on a dark background (base used grey HTML labels). Y-axis label also sits ~8px left of the chart and gets clipped (light mode too). Font 10px → 12px. Likely fix:.flot-tick-label { fill: currentColor }or axisfont.color, plus left margin. (Screenshots compared PR vs base locally; happy to share another way if useful.) -
OAuth2 callback still loads deleted files (
js/oidc-oauth/login-callback.html:7-8, not in the PR diff) — still referencesjquery-3.5.1.min.jsandbase64.js(both 404). Not fatal on the success path ofoauth_completeLogin(doesn't use$), but needs updating tojquery-3.7.1.min.jsand dropping the base64 script. -
Third-party plugins: one unsupported Sammy call kills the whole UI — in-repo plugins are fine (management, stream, top, shovel, federation, tracing: only
get/put/post/delwith string routes,this.params,this.title). External risk: a regex route throws (path.replace is not a function);this.redirect/partial/before/params.toHashalso throw. The throw is insidedispatcher(), sostart_appnever runs and the entire UI is dead. Wrap eachdispatcher_modules[i](this)in try/catch, accept regex routes, and mention the Sammy removal in release notes.
Minor
- Forms with no
method: router always setslocation.hash = path + '?' + qs. The OAuthoauth2-resource-formhas neither method nor action → URL becomes#?(Sammy did nothing). Clash with non-SP-initiatedlocation.hrefredirect untested (no IdP). - Idle charts lose the top "1.0" tick (
fmt_y_axismax==1 hack). #/queues?hasOwnProperty=1throws and the page doesn't render (base rendered Queues) — useObject.prototype.hasOwnProperty.call.- Form with unknown method throws at
routes.lengthafterpreventDefault(harmless). - Tests: router suite never exercises
run()+ hashchange (would have caught #3);charts.test.jsonly checks a config constant (misses #2); nothing greps leftoverSammy.*;js-testisn't in CI and exits 0 when node is missing;engines >=21but tests pass on Node 20.
Nits
jQuery.inArraystill atglobal.js:791-793andformatters.js:475,487(works; the replace pass is incomplete).LICENSEstill lists ExplorerCanvas / base64.js / Sammy; theirLICENSE-*files remain.jquery.flot-4.2.6.jsis byte-identical to.min.js..gitignoreadds/.playwright-mcp/(author tooling).- Router helpers are globals;
:paramnames now include-(Sammy stopped at\w).
Clean / verified OK
- jQuery 3.7.1 byte-identical to code.jquery.com; no removed APIs in core, plugins, or templates;
indexOf/.trim()replacements match. - Flot bundle byte-identical to npm
flot@4.2.6(dist/es5); time plugin included; time labels fine. - CSP: no eval/inline script; tested with the broker's CSP header.
- Removed files have no remaining callers except finding 5.
- Route params / query decoding match Sammy; queue
q a/b#c?d+e%fand vhostv h/#?went through create/open/publish/get/purge/delete same as base. - Login, failed login, logout, rate-options auto-submit, back/forward, titles,
#/topand#/tracesredirects match. - Packaging:
test/not in the plugin package; no build scripts reference removed files. make -C deps/rabbitmq_management js-test: 58/58 on Node 20.19.2. Plugin suites (still on old fake Sammy): federation 3/3, shovel 3/3, stream 4/4.
Not checked
Real OAuth2/UAA (no IdP), Firefox/Safari/touch, actual third-party plugins.
grok-99s
left a comment
There was a problem hiding this comment.
Fresh full review of the cumulative state (merge-base bd3d3701c5..c1433b6). I compared against the merge-base UI in headless Chrome, with the API served by a live make run-broker node and with the core, stream, top, shovel, federation and tracing plugins enabled.
No blockers and nothing to fix before merge. Every route, form and plugin page I exercised behaves the same as on the merge-base.
Previous issues: all 7 fixed and verified against merge-base
- The
Sammy.logcall incheck_bad_responseis gone. A page that is out of range gets a 400 and falls back to page 1 with no exception. - The y-axis starts at 0 again: data at 100–122 gives a 0–150 axis, the same as on the merge-base. All overview and queue chart axes and ticks match.
- Logging in sends one page request, with no aborted XHR and no duplicate
#/run. When/api/overviewfails before any success, the merge-base throwsgetFullYearof undefined. The PR shows "Error: could not connect to server. Will retry at …". - Chart labels in dark mode are readable (
#bbbon dark,#666on light). oidc-oauth/login-callback.htmlloadsjquery-3.7.1.min.js. All of its scripts return 200, with no errors.- A GET form with no fields now goes to the plain action, and filled fields give the same query string as on the merge-base.
- A query key such as
hasOwnPropertyno longer throws (covered by a test).
What I exercised (merge-base vs PR, identical results)
- About 45 routes, including every plugin route. I used a vhost
rv2/v h+xand queue names containing/ % + # ? & =and unicode, plus a query string, an unknown route and a malformed%. A malformed%raises the same URIError in both. - Real tab and link clicks, back/forward, reload on a hash, and login, bad login and logout.
- Every in-repo PUT/POST/DELETE form produces the same API calls: queues, exchanges, bindings, publish/get, purge, users, permissions, policies, limits, cluster name, super streams, federation upstreams, dynamic shovels, traces, and the auto-submitted column/rate options.
- A plugin that calls an unsupported Sammy API keeps the routes it registered before that call, and the other plugins keep working.
- I injected jQuery Migrate and browsed every page. Core, plugin and template code make no deprecated or removed jQuery calls. The only warnings come from Flot 4.2.6 itself (
$.isFunction,.unbind,.click()/.dblclick()shorthands). They are harmless on 3.7.1 but will need attention for a future jQuery 4. jquery-3.7.1(.min).jsandjquery.flot-4.2.6.min.jsare byte-identical to the npm releases. Sammy, base64, json2, excanvas,title.jsandevil.cssall return 404, and nothing references them anymore.- I found no
evalornew Function, and the broker's CSP logged no violations.test/is not part of the packaged plugin.
Minor (inline comments below)
router.js:109: a regex route with thegflag only matches every other time, becauseexeckeepslastIndex.router.js:125: exceptions thrown by route callbacks now propagate. Sammy caught and logged them.router.js:62-66:use()andhelper()silently do nothing.charts.js:257-261: the lines are now 1px instead of 2px, andtickColorno longer sets the grid line colour.charts.js:267-268: an empty series now gets a 0–0.01 axis with five "0.0" labels.
Nits (not on diff lines)
put_parameter/put_cast_paramsinmain.js:mandatory_keys.indexOf(...)throws when a caller passesundefined, wherejQuery.inArrayreturned -1. All in-repo callers pass arrays, so this only matters for third-party extensions that call these globals.- The
js-testtarget duplicates the existingnpm-testtarget (rabbit_common/mk/rabbitmq-npm-tests.mk), which already runs as part ofmake tests.npm-testneeds Node ≥ 21 for its glob;js-testworks on Node 20. - The unminified
jquery-3.7.1.jsstill ships but nothing loads it, while the unminified Flot was dropped.LICENSE-MIT-jQuerystill carries the 2011 John Resig copyright; upstream now says "OpenJS Foundation and other contributors".rabbitmq_federation_management/LICENSE*still lists Sammy, base64.js and ExplorerCanvas (this predates the PR). - The router exposes a global named
Application, which is quite generic. - The release note could mention point 2 above.
Tests and CI
make -C deps/rabbitmq_management js-teston Node 20: 83/83 pass. With Node 22 andnode --test 'test/**/*.test.js': management 83/83, federation_management 3/3, shovel_management 3/3, stream_management 4/4.- CI on
c1433b6: everything is green, including "Test Management UI with Selenium for PRs" (basic auth, management prefix, OAuth with Keycloak/UAA). - Coverage gaps: nothing tests
unload(), regex routes withg, or empty series. The router plugin tests cover stream and top, but not shovel, federation or tracing routing.
Not checked
- End-to-end OAuth against a real IdP. I relied on the Selenium job for that.
- Multi-node clusters, and browsers other than Chrome.
|
The nits from #17760 (review):
I left the |
grok-99s
left a comment
There was a problem hiding this comment.
All points from the previous review are addressed. Two remaining notes:
- Minor,
router.js:63:use()now throws. A third-party extension callingthis.use('Title')before its routes (a common Sammy idiom) will lose every route defined after that call, sincedispatcher()catches the error (main.js:97-101). Suggest makinguse()a no-op with aconsole.warn(keephelper()throwing), or listinguseas unsupported in the release notes. - Nit,
charts.js:321-322:getComputedStyleruns on every chart render; the grid colour could be read once per refresh.
|
@michaelklishin I just saw this PR marked for 5.0. I'm fine with that, just FYI this PR modified 4.4.0 release notes. |
|
@Zerpet a backport can one day be backported to |
|
@Zerpet the 🧞♂️ and I are reviewing this. I'll open a PR against |
base64.js has zero call sites. excanvas, json2, and evil.css only existed behind an IE 8 conditional comment that modern browsers skip entirely.
Use native Array.prototype.indexOf and String.prototype.trim instead of the jQuery wrappers deprecated since jQuery 3.0.
Flot 4.x changed the default timeBase from milliseconds to seconds. Add timeBase: "milliseconds" to chart_chrome.xaxis since RabbitMQ timestamps are millisecond-epoch values. The ES5 bundle replaces the separate core and time plugin files.
Verify the timeBase, mode, and timezone settings are correct after the Flot upgrade.
The custom router provides the same API: hash-based routing with named parameters, form submission interception for put/delete/post verbs, and a title helper. The dispatcher_add plugin extension point and all five management UI plugins continue to work without changes.
Test route pattern compilation, named parameter extraction, verb routing, title helper, first-match-wins ordering, and params merging.
The sandbox is created once and shared across tests; only the Application instance is fresh per test.
Exercises run()'s submit delegation end to end: method="delete" maps to the del route table, getAttribute is used instead of the normalising method property, and duplicate field names fold into an array.
`jQuery.inArray` returned -1 for a missing array, while `indexOf` on `undefined` throws. The in-repo callers always pass arrays, but `put_parameter` and `put_cast_params` are globals that extensions can call.
Flot 4 draws 1px lines by default where Flot 0.8 drew 2px ones, and it colours grid lines with `grid.tickColor` rather than the per-axis `tickColor`. The axis options had no effect, and the default grid colour was almost invisible in dark mode. The grid colour now comes from the active theme.
A series with no points leaves the y range unbounded, and Flot 4 then picks a 0 to 0.01 axis. An idle series already gets 0 to 1.
`npm-test` from `rabbitmq-npm-tests.mk` already runs the same tests as part of `make tests`.
Nothing loads it. The jQuery licence now carries the copyright line of the `3.7.1` release.
The plugin ships only its own JavaScript and templates. jQuery, EJS and Flot ship with the management plugin, and Sammy, base64.js and ExplorerCanvas are no longer used at all.
The change will be released in 5.0.0, not in 4.4.0. The `use` and `helper` functions are listed as well, since they now throw.
Flot 0.8 hid the vertical grid lines with `xaxis.tickColor`. Flot 4 strokes both directions with a single `grid.tickColor` and draws grid lines for the innermost axis unless told otherwise, so every chart gained a vertical grid. It also measures only interior tick labels by default, which leaves a two-tick axis with no room for the two labels it still draws. The grid colour is read once per refresh rather than once per chart, with a fallback for when neither theme stylesheet applies.
A query string or a form field can name a parameter `hasOwnProperty`, which shadows the method with a string, so calling it raises a `TypeError`. `router.js` reads its own parameter map through `Object.prototype` and `router.test.js` asserts that such a parameter survives routing, so the three places that read the map afterwards do the same.
`check_bad_response` passes `update_status` a `timeout` status for HTTP 408, and `css/main.css` styles `.status-timeout`, but the function had no branch for it and threw instead. The throw escaped the `XMLHttpRequest` handler after `update` had cleared the refresh timer, so the page stopped refreshing until the next navigation.
`jQuery.inArray` answers -1 for a missing array, and the plain `indexOf` that replaced it raises instead. `fmt_plugins_small` checked `applications` but not `enabled_plugins`, and `get_plugins_list` checked neither. Both keys reach the browser from `augment_nodes`, while `all_nodes_raw` lists a node with only its name, type, running flag and drain state, so a node that is down carries neither key.
`run()` had no guard against running twice, and overwrote the fields `unload()` reads, so a second call left a submit handler bound for the lifetime of the page. The submit handler read `e.target` rather than the form jQuery matched, so a submit raised on a field inside the form fell through to the browser. A form `method` was not trimmed, so one with surrounding whitespace missed the rename to `del`. An unmatched verb or action returned silently after `preventDefault()`, leaving a click with no effect and nothing in the console. Sammy logged a 404 here, so this does too. `decodeURIComponent` raises `URIError` for a malformed escape, which reached the `hashchange` listener and stopped the route from running at all. `_checkLocation` records the hash before dispatching, so the same URL could not be retried.
`router-plugins.test.js` read `stream.js` and `top.js` out of two packages that depend on `rabbitmq_management`, so editing either plugin failed the `rabbitmq_management` suite, and that suite could not load at all against a `deps` tree without them. Each plugin now tests its own routes against the router it depends on. `rabbitmq_top` gains the `package.json` that makes `npm-test` pick the tests up, and joins the `tests` matrix, which already covered `rabbitmq_stream_management`. Its `rabbit_top_util_SUITE` has never run there either and passes.
`sync_get` answers false when the request fails, `JSON.parse` passes that straight through, and reading `name` off the first element of a boolean raises. Sammy caught what a route callback raised and logged it; nothing does now, so visiting `#/top` or `#/traces` during a brief API outage leaves a blank page. An empty node list raised in the same place, so both callbacks now check for one before navigating. `rabbitmq_tracing` gains the `package.json` that makes `npm-test` run its new tests; it is already in the `tests` matrix.
`npm-test` skipped with a zero exit code when npm was missing, and no workflow installed Node, so the only coverage for `router.js` could have stopped running without turning anything red. `node --test` also exits 0 when its glob matches nothing, so an npm that works is not on its own evidence that any test ran. CI now pins a Node version and passes `REQUIRE_NPM_TESTS=1`, which turns both a missing npm and an empty test list into a failure. A local build without npm still skips, as before.
`update_status/1` built the same retry date in both error branches.
The top, tracing and stream management route tests each carried the same sandbox that loads a plugin against `router.js`. It now lives next to the router in `rabbitmq_management`, which all three plugins depend on.
Run the tests through `npm test` again when npm is available, so `npm install` still runs for a `package.json` that gains dependencies. Without npm, `node --test` runs them directly. With neither, `REQUIRE_NPM_TESTS` decides whether to fail, as before.
|
I replied in #17862, posting the same here. Thank you @lukebakken for the review and suggestions! My 🧞 disagreed with your 🧞♂️ in two points. Everything else looked very good. I cherry-picked your commits onto this branch, and applied some suggestions from 🧞 |
grok-99s
left a comment
There was a problem hiding this comment.
Pre-existing on main, not introduced here (main.js:115-126, start_app_login): with OAuth enabled, app.run() is only called when there are no stored credentials. If stored credentials exist but check_login() fails, the login page is re-rendered with the Basic auth form while the router is not bound. Submitting it then falls back to a native GET (browsers don't know method="put"), so the browser goes to /?username=…&password=…#/login and the password lands in server logs and browser history.
Fix: use the same condition as the non-OAuth branch:
if (!has_auth_credentials() || !check_login()) app.run();| if (charts.length === 0) { | ||
| return; | ||
| } | ||
| chart_chrome.grid.tickColor = |
There was a problem hiding this comment.
Nit: the grid colour is read only when charts render, so after a theme switch the grid lines keep the old colour until the next refresh (or the next navigation if auto-refresh is off). The tick labels switch right away because they use CSS.
There was a problem hiding this comment.
Leaving this one as is. On main the grid colour doesn't follow the theme at all; here it catches up at the next refresh. Recolouring the existing plots right away would mean reaching into Flot's options after the plot has been drawn, since the chart data is discarded once a chart is rendered.
🤖
With OAuth 2 enabled and stored credentials that `check_login/0` rejects, the login page was rendered again without running the router. Submitting the Basic auth form then fell back to a native GET that put the username and password in the URL. Use the same condition as when OAuth 2 is disabled.
Both plugins redirect to the first node in the same way. Move the helper to `main.js` so that it is no longer a global defined by the Top plugin, and test it once.
The source map is not shipped, so browser developer tools logged a 404 for it.
|
🧞 says the oauth2 bug is also present in 4.4 and 4.3. Worth considering backporting this PR. |
grok-99s
left a comment
There was a problem hiding this comment.
Two pre-existing issues on main in $(document).ready (deps/rabbitmq_management/priv/www/js/main.js). These lines aren't in this PR's diff, so they go here.
1. Medium: on the ?error= landing page, the Basic form submits natively and the password ends up in the URL (main.js:24-31)
The if (error) branch renders login_oauth with the warning but never calls start_app_login(), so no router is bound. With OAuth enabled and oauth_disable_basic_auth: false, the Basic form is shown (notAuthorized is false). Submitting <form action="#/login" method="put"> then falls back to a native GET:
/?username=guest&password=guest#/login
The password ends up in broker access logs, browser history, proxy logs and the address bar. This branch is reached through:
oauth_redirectToLogin(error)inoidc-oauth/helper.js:290-294(SP-initiated callback errors);rabbit_mgmt_login:replyWithError/3(IdP-initiatedPOST /loginfailures);- any link to
/?error=....
I reproduced it in headless Chrome at 3d18f2c, both SP- and IdP-initiated. It's the same class of bug that 1ecf32c fixes in start_app_login.
Suggested fix (tested; afterwards the login goes through login() and no password reaches the URL):
if (error) {
clear_pref("oauth-idp-pending");
clear_pref("oauth-return-to");
if (oauth.enabled) {
renderWarningMessageInLoginStatus(oauth, fmt_escape_html(error));
start_app_login(true);
}
} else {function start_app_login (skip_stored_credentials) {
// ...
if (skip_stored_credentials || !has_auth_credentials() || !check_login()) {
app.run();
}
}Optionally, also drop ?error= once it has been read, so it doesn't stay in the URL after a successful login. Otherwise a reload shows the error page again:
history.replaceState(null, '', location.pathname + location.hash);2. Nit: ?error= with OAuth disabled renders a blank page (main.js:28-30)
Only the oauth.enabled case renders anything, so with OAuth off the page stays empty.
There was a problem hiding this comment.
Nit: these overwrite Application, oauth, has_auth_credentials and check_login on the shared sandbox and never restore them. Any later test that relies on the real ones would silently get these stubs. Save the originals and restore them in after().
lhoguin
left a comment
There was a problem hiding this comment.
We can merge this. Remaining findings are for issues that already exist in main, or nits. We can fix them here or elsewhere. Merging early ensures we can test it over a long period of time.

Proposed Changes
Upgrade management UI dependencies and replace SammyJS with a custom router.
This is the bare minimum we can do without impacting the current user experience. SammyJS is abandonware, and we really just used it for routing. In the days we live in, it's cheap to replace such library with our own custom solution for the time being. The next-gen management will scrap all this code anyway. This is a good interim solution to move away from old software.
This PR also bumps jQuery to the latest 3.x version and Flot to latest major. Flot is not very active, although it deceives in terms of releases. They stopped using GitHub releases in favour of NPM releases. Latest version is 4.2.6 in NPM, not 0.8.x (latest in github). We can consider changing Flot for ChartJS, but that's a more intrusive change.