-
This is not a viable "zero-refactor" change. OneDev's UI is Apache Wicket: each screen is a server-side page instance with its own component tree, Ajax callback URLs, header contributions (JS/CSS), and WebSocket observers. Replacing DOM from another page's HTML leaves the browser showing one screen while Wicket still owns another.
Why the interceptor cannot work
-
There is no global
#contentpanel to swap.LayoutPageputs each page's body in<wicket:child>inside.main.wicket:id="content"is used on many inner panels, not as a layout-level main region. -
innerHTMLswapping breaks Wicket. The new markup has different component ids and Ajax URLs. Scripts from the fetched page (OnDomReady, resource references, widget init) are not executed byinnerHTML. Existing listeners (CodeMirror, Tippy, PerfectScrollbar,Wicket.Ajax) stay attached to detached nodes. Later clicks hit the original page's component tree or fail. That also fails the "no conflicts with Wicket's existing JavaScript event handlers" criterion. -
The chrome is not static. Sidebar menus, active items, and topbar title are rendered per page (
SidebarMenuItem.isActive(), page title). Project, administration, and user pages have different menus. Keeping the old shell would show stale navigation. -
WebSockets are page-scoped.
WebSocketService.observe(BasePage)registers change observables by Wicket page id. Live updates for issues, PRs, and builds would remain bound to the previous page. -
Server cost is not reduced. The proposal still requests a fully rendered page and extracts a fragment in the browser. Wicket still builds the whole page, including sidebar.
-
AbstractAjaxBehaviorcannot render an arbitrary other page's panel on the current request cycle. Doing that would require one mega-page that hosts every screen — a large rewrite, not a small interceptor.
X-Requested-With: XMLHttpRequestis also not how Wicket Ajax works here. Wicket Ajax uses theWicket-Ajaxheader (see existing callers such as blob folder view and markdown). A plainfetch()of a bookmarkable URL is a new page request, not a partial update of the current page.What OneDev already does
Intra-page navigation already uses the History API without a full reload:
BasePage.pushState/onPopStateandonedev.server.historyinbase.js- Many screens (blob browser, issue/PR/build lists, boards, and others) update via Wicket Ajax plus
pushState #ajax-loading-indicatoralready shows feedback during Ajax requests
Bookmarkable links between different page classes correctly do a full navigation so Wicket can construct a new page, load that page's resources, and attach WebSockets.
Recommendation
Do not add a global fetch-and-swap navigation interceptor.
If the flash on full page loads is the concern, Wicket-compatible options are:
- Keep expanding Ajax +
pushStatewithin a page (the existing pattern) - Cosmetic only: keep the chrome painted during a normal load, or show a top progress bar on full navigations, without swapping another page's DOM
A true SPA across all screens would mean replacing Wicket, which is a rewrite rather than a drop-in interceptor.
-
-
Evaluated as not feasible.
-
Previous Value Current Value Open
Closed
-
Discussion: What if we implement this using Cross-Document View Transitions?
-
Previous Value Current Value Closed
Open
-
-
@zaakiy Cross-document view transitions are a different mechanism from the interceptor, and they are compatible with Wicket in a way fetch-and-swap is not. They still do not meet the original SPA acceptance criteria, and I would not use them to implement that design.
Why they do not break Wicket
The browser snapshots the outgoing document, performs a normal same-origin navigation, snapshots the incoming document, then animates between those snapshots (
@view-transition { navigation: auto; }on both pages).That full navigation is what OneDev needs: a new Wicket page instance, new Ajax callback URLs, header contributions, and WebSocket observers via
WebSocketService.observe(BasePage). Nothing isinnerHTML-swapped into the old page. Unsupported browsers ignore the at-rule and keep today's behavior.Why they are not the original proposal
The original criteria were: no full-page reloads, extract/swap
#content, keep the shell, History API as the router, andAbstractAjaxBehaviorfor other pages.View transitions still do a full reload. The server still renders the whole page, including sidebar. Chrome is not kept as a live Wicket tree; it is only painted over during the swap. Intra-page Ajax +
onedev.server.history.pushState(blob browser, issue/PR/build lists, boards, and others) is same-document, so the cross-document at-rule never runs there.Practical limits in this UI
-
Browser coverage. Chromium 126+ and Safari 18.2+. Firefox still does not ship cross-document transitions (same-document work is separate). Progressive enhancement is fine; it is not a uniform SPA feel.
-
Only real document navigations. Sidebar
BookmarkablePageLink/ViewStateAwarePageLinkclicks and back/forward between page classes can animate. Wicket Ajax,window.locationfallbacks, POSTs, and many redirects typically will not (navigation: autois conservative). -
Default effect is a whole-document crossfade, which can still read as a flash. Pinning chrome with
view-transition-nameon.sidebar/.topbaris possible because those classes are stable inLayoutPage, but:- Sidebar content changes across project, administration, user, and my pages (
SidebarMenuItem.isActive(), different menus). Morphing mismatched menus looks wrong. - List pages nest another
.maininsideLayoutPage's.main.autofit. A naive.main { view-transition-name: ... }would duplicate names and skip the transition. SimplePage(login and similar) has no sidebar/topbar.
- Sidebar content changes across project, administration, user, and my pages (
-
Incoming snapshot vs Wicket init. OnDomReady work (CodeMirror, PerfectScrollbar, Tippy, and similar) often runs after first paint. The new-page snapshot can be of uninitialized widgets. Heavy pages (large diffs/lists) can also miss Chrome's ~4s "renderable" timeout and skip silently.
-
Overlays and existing motion. Open dropdowns, floating panels, modals, and
#ajax-loading-indicatorcan be captured in the old snapshot.layout.cssalready transitions.sidebarwithtransition: all 0.3s ease, which can fight view transitions.prefers-reduced-motionwould need to turn this off.
Recommendation
Do not implement the original SPA interceptor via cross-document view transitions.
If the remaining concern is only the flash on full loads, Wicket-safe options remain:
- Keep expanding Ajax +
pushStatewithin a page (already the pattern) - A top progress bar on full document navigations (works in all browsers, no snapshot QA)
- Optionally a later, separate experiment: opt-in
@view-transitionas progressive enhancement, with named chrome and reduced-motion — not as a substitute for SPA navigation
Does this answer the view-transition question, or do you want that cosmetic experiment tracked separately?
-
-
Default effect is a whole-document crossfade
Rather than implement it as a whole-of-document crossfades, what we can do is tie together the ID of elements that can confidently be considered to be the same between different pages.
This would require adding IDs to divs like ones that have the following classes:
- side-info
- topbar
- sidebar
@servicedesk what do you think about this?
-
@zaakiy Named pairing is how you avoid a whole-document crossfade, but matching is done with CSS
view-transition-name, not HTMLids. I would not add ids on.side-info/.topbar/.sidebarfor this.How matching actually works
Cross-document view transitions pair elements that share the same
view-transition-nameon both documents. An HTMLidonly participates if you also setview-transition-name: auto(Level 2).match-elementis same-document only and does not match across pages..sidebarand.topbaralready exist once onLayoutPage. Explicit names are enough:.sidebar { view-transition-name: sidebar; } .topbar { view-transition-name: topbar; }No markup ids are required. Wicket ids would also be a poor key:
setOutputMarkupId(true)generates per-page ids (id1,id2, …) that do not match across navigations.SideInfoPanelalready does that..side-infois not layout chromeThat class is not a stable shell region:
- Two different widgets share it.
a.side-infois the “more info” trigger;div.side-infois theSideInfoPanelslide-out. Naming the class would try to pair a button with a panel. - Duplicates on one page. Issue, PR, and build detail pages render both
moreInfoandmoreInfoDockwithclass="side-info". Duplicateview-transition-namevalues skip the entire transition. - Most screens do not have it (lists, blob, admin, login). A named element with no counterpart just enters/exits; it does not pin chrome.
- Panel content is page-specific (issue vs PR vs build). Morphing those panels together would look wrong even when both sides exist.
Named
.sidebar/.topbarstill morph the wrong contentPinning those boxes is possible, but the boxes are shared, not the contents:
- Sidebar menus change across project, administration, user, and my pages (
SidebarMenuItem.isActive(), different item lists). The snapshot morphs one menu into another. - Topbar title changes on every screen.
.sidebaralready hastransition: all 0.3s easeinlayout.css, which fights view-transition snapshots.SimplePage(login and similar) has neither sidebar nor topbar, so those names vanish and the default root crossfade remains.- Naming
.mainis unsafe: list and detail pages nest another.maininsideLayoutPage’s.main. Duplicate names skip the transition.
The visual result is still a full document navigation with a nicer paint of the chrome. Wicket still builds a new page, new Ajax URLs, and new WebSocket observers. Firefox still has no cross-document view transitions; Chromium 126+ and Safari 18.2+ only.
Recommendation
Do not add ids (or names) on
.side-info, and do not treat named chrome as the original SPA interceptor.If the remaining goal is only “less flash on full loads”, the Wicket-safe options are unchanged:
- Keep expanding Ajax +
pushStatewithin a page - A top progress bar on full document navigations (all browsers)
- Optionally a later, separate experiment:
view-transition-nameon.sidebarand.topbaronly, plusprefers-reduced-motion— not.side-info, and not as SPA navigation
Does that answer the named-element question, or do you want that narrower chrome experiment tracked separately?
- Two different widgets share it.
-
I'm happy with using the CSS classes instead of the ID-based named elements. In fact, this is the approach I prefer. I just didn't know that it could be done without IDs.
Optionally a later, separate experiment: view-transition-name on .sidebar and .topbar only, plus prefers-reduced-motion — not .side-info, and not as SPA navigation
I really like this idea, but it should not be optional. We should try to do it right now.
-
@zaakiy CSS
view-transition-nameon.sidebarand.topbar(not HTML ids, and not.side-info), plusprefers-reduced-motion, is the right scope. It is still a full document navigation with a nicer chrome paint, not the original SPA interceptor.@robin Zaakiy wants that chrome experiment implemented now rather than as a later optional follow-up. Can you take the implementation?
-
@servicedesk please redraft the requirements into a new issue based on the above discussion to ensure that it is not polluted with implementation ideas that are not feasible
-
@zaakiy Opened #3008 with the feasible chrome work only: cross-document view transitions, CSS
view-transition-nameon.sidebarand.topbar, andprefers-reduced-motion. The original SPA interceptor is not in that issue.Please comment on #3008 if the wording needs a change. Closing this evaluation as answered.
-
Previous Value Current Value Open
Closed
| Type |
Question
|
| Priority |
Normal
|
| Assignee | |
| Labels |
No labels
|
Executive Summary
As OneDev continues to solidify its position as a highly capable Git management and CI/CD platform, the user interface's responsiveness plays a critical role in developer satisfaction. Currently, standard navigation triggers a full-page reload, leading to redundant rendering overhead and a visual "flash" that interrupts user focus.
I propose a high-ROI, low-risk frontend enhancement: an AJAX-driven navigation interceptor that achieves an instant, Single Page Application (SPA) feel. By leveraging Apache Wicket's built-in AJAX components to fetch and swap only the main content panel while keeping the global navigation shell intact, we can drastically improve perceived performance and visual ergonomics — without any architectural refactoring.
Motivation
Proposed Solution
Introduce a lightweight JavaScript interceptor that:
AbstractAjaxBehaviororAjaxLink.history.pushStateto maintain back/forward button support.Sequence Diagram
Implementation Details
1. JavaScript Navigation Interceptor
2. CSS Loading Bar
3. Server-Side: Wicket AJAX Behavior
Add a
WicketBehaviororAbstractAjaxBehaviorthat can be attached to the main content panel container. This behavior will handle AJAX requests for content swapping and return only the rendered panel markup.Exclusions and Fallbacks
window.location.href.popstateevent listener.Performance Considerations
Acceptance Criteria