Repository navigation
feat(web): add Dioxus web UI crate with streams browser - #4644
billy-doyle wants to merge 2 commits into
Conversation
The web UI is moving from SvelteKit to Rust so it builds with the same toolchain as the server and drops the npm dependency tree. This adds the Dioxus crate alongside the existing UI without changing what the server serves. It covers sign-in and sign-out, the dashboard shell, and browsing streams, topics, partitions and messages. The UI keeps its own serde mirrors of the HTTP JSON because iggy_common pulls tokio with net and does not build for wasm32-unknown-unknown. The app lives in a library target with a thin binary on top. Cargo ignores path dependencies that have no library target, so the license manifest would otherwise see none of the crate's dependencies. A 403 shows a permission message and keeps the session, where the Svelte UI signed the user out. The message pager follows the offsets the server returns, so it stops at the oldest retained message after retention. The build script clears dx's staging directory first, because dx copies every previously hashed asset into the bundle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for the PR. It is labeled If AI was used to create this PR, you must run the Slash commands (own line, regular comment) move it around the queue:
See CONTRIBUTING.md for details. |
hubcio
left a comment
There was a problem hiding this comment.
biggest issues: the messages pager can skip offsets and mixes stale topic stats with the poll reply, and session handling breaks when storage is blocked or across tabs. the rest is smaller stuff inline.
| button { | ||
| class: "secondary", | ||
| disabled: !has_newer, | ||
| onclick: move |_| requested_offset.set(Some(first_offset + page_size)), |
There was a problem hiding this comment.
warning: the newer button jumps to first_offset + page_size, so when the server returns a short page, the offsets after it never show up. start the next page at shown_last_offset + 1 instead.
There was a problem hiding this comment.
Fixed. Newer now starts at shown_last_offset + 1.
| .iter() | ||
| .find(|partition| partition.id == partition_id) | ||
| .ok_or(ApiError::NotFound)?; | ||
| let last_offset = (partition.messages_count > 0).then_some(partition.current_offset); |
There was a problem hiding this comment.
warning: last_offset comes from topic stats read before the poll, so it can disagree with the page - the range runs past the end, newer stays disabled, or it says "no messages". take it from PolledMessages::current_offset instead.
There was a problem hiding this comment.
Fixed. The range and the Newer button now use current_offset from the poll reply. Topic stats are only used to pick where the newest page starts. An offset past the end now shows "no messages from offset N" with a Latest button.
| // error to a 401 without remounting. | ||
| use_effect(use_reactive!(|unauthorized| { | ||
| if unauthorized { | ||
| Session::clear(); |
There was a problem hiding this comment.
warning: any 401 calls Session::clear(), which deletes the shared iggy_session key, so a stale tab wipes a newer login that another tab just saved. only delete it when the stored token is the one that failed.
also at auth.rs:50.
There was a problem hiding this comment.
Fixed. Session::clear() is gone. Session::forget(&self) only removes the stored session if it still holds the token that failed. The 401 path and sign-out both use it.
| impl Session { | ||
| /// Restores the stored session, dropping it if the token has expired. | ||
| pub fn restore() -> Option<Self> { | ||
| let session: Self = LocalStorage::get(SESSION_KEY).ok()?; |
There was a problem hiding this comment.
warning: when the browser blocks site storage, LocalStorage::raw() throws through expect_throw, so Session::restore kills the first render and the app never shows up. get window.local_storage() yourself and treat an error or None as no session.
also at lines 47, 51.
There was a problem hiding this comment.
Fixed. Storage goes through web_sys::window().local_storage(), and an error or None counts as no session. Same for persist and forget. Dropped gloo-storage.
| } | ||
| tbody { | ||
| for message in page.messages.iter().rev() { | ||
| MessageRow { key: "{message.header.offset}", message: message.clone() } |
There was a problem hiding this comment.
warning: the loop clones each Message into row props, so every payload sits in memory twice and gets cloned again on re-render. keep Vec<Rc<Message>> in MessagesPage, pass Rc<Message> to the row, and derive Eq on Message so memo compares pointers first.
There was a problem hiding this comment.
Done. The page holds Vec<Rc<IggyMessage>>, rows take Rc<IggyMessage>, and the message types derive Eq.
| IdentityInfo, LoginRequest, PolledMessages, Stream, StreamDetails, TopicDetails, UserDetails, | ||
| }; | ||
|
|
||
| /// Empty in the embedded build so requests go to the server that served the |
There was a problem hiding this comment.
nit: the crate has no README, so the dx workflow and IGGY_WEB_UI_API_URL are only documented in this comment. add a short README covering scripts/build-web-ui.sh, dx serve and IGGY_WEB_UI_API_URL.
There was a problem hiding this comment.
Added core/web_ui/README.md. It covers the build script, dx serve and IGGY_WEB_UI_API_URL.
|
|
||
| #[derive(Debug, Clone, PartialEq, Deserialize)] | ||
| pub struct PolledMessages { | ||
| pub partition_id: u32, |
There was a problem hiding this comment.
simplification: nothing reads PolledMessages::{partition_id, count}, MessageHeader::{checksum, origin_timestamp, user_headers_length}, OptionValue::{kind, explicit} or UserDetails::id, and serde skips unknown fields anyway. drop them, but keep PolledMessages::current_offset for the pager fix.
There was a problem hiding this comment.
Dropped them. Kept PolledMessages::current_offset for the pager.
| } | ||
| } | ||
|
|
||
| impl TryFrom<IdentityInfo> for Session { |
There was a problem hiding this comment.
simplification: issue_identity always sets the token and HttpJwtConfig can't turn JWT off, so MissingToken and this TryFrom guard a reply that never happens. make IdentityInfo::access_token a plain TokenInfo and convert with From.
There was a problem hiding this comment.
Done. access_token is a plain TokenInfo and Session uses From<IdentityInfo>.
| ) | ||
| } | ||
|
|
||
| fn text_or_hex(bytes: Vec<u8>) -> String { |
There was a problem hiding this comment.
simplification: text_or_hex is a one-line private helper with a single caller. inline String::from_utf8(bytes).unwrap_or_else(|error| hex(error.as_bytes())) into payload.
| use crate::auth::Session; | ||
| use crate::components::{api_client, use_session}; | ||
|
|
||
| const LOGO: Asset = asset!("/assets/iggy-apache-darkbg.svg"); |
There was a problem hiding this comment.
simplification: LOGO is declared here and again in sign_in.rs:26 for the same SVG. declare it once in lib.rs next to STYLESHEET and FAVICON.
There was a problem hiding this comment.
Moved it to lib.rs next to STYLESHEET and FAVICON.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #4644 +/- ##
============================================
- Coverage 90.77% 90.70% -0.08%
+ Complexity 2989 2987 -2
============================================
Files 1459 1472 +13
Lines 348165 348747 +582
Branches 292747 293331 +584
============================================
+ Hits 316042 316323 +281
- Misses 25485 25784 +299
- Partials 6638 6640 +2
🚀 New features to boost your workflow:
|
The messages pager computed its next page and range from topic stats read before the poll, so a short page skipped offsets and the range could disagree with the rows shown. It now pages from the offsets the poll returned, takes the newest offset from the poll reply, keeps the page offset in the URL so reloads and shared links land on it, and ignores pager clicks while a poll is in flight. Session storage went through gloo-storage, which throws when the browser blocks site storage and took down the first render. It now reads localStorage through web-sys and treats any failure as no session. A 401 or sign-out only removes the stored session when it holds the token that failed, so a stale tab no longer wipes a login another tab just saved. Responses are decoded from bytes instead of one JS string, which browsers cap below a page of large messages. Messages are shared with rows through Rc instead of cloned. The model mirrors now carry the server's type names and only the fields the UI reads, and host-only tests decode serialized iggy_common types into each mirror so a renamed server field fails in CI. The build script clears dx's staging directory under the real cargo target directory, and a crate README covers the build, dx serve and IGGY_WEB_UI_API_URL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
/ready |
Which issue does this PR address?
Relates to #4633
Part 1 of 10. The plan and the full series are in the issue.
Rationale
Rust web ui, one toolchain. Starts with a crate that ships alongside svelte until final removal.
What changed?
The web UI is a SvelteKit app, so changing it needs a second toolchain and npm dependency tree next
to the Rust workspace.
core/web_uiis a new Dioxus crate, built to WebAssembly withdx, coveringsign-in and sign-out, the dashboard shell, and browsing streams, topics, partitions and messages.
iggy-serverstill embeds the Svelte UI, so nothing changes for users until the switch-over PR.Two behaviors differ from the Svelte UI on purpose. A 403 shows a permission message and keeps the
session, where the Svelte UI signed the user out, so a user without global
read_streamscan stillsign in. The message pager follows the offsets the server returns, so after retention it stops at
the oldest message left instead of repeating pages.
The UI keeps its own serde types for the HTTP JSON, because
iggy_commonpulls tokio withnetanddoes not build for
wasm32-unknown-unknown.scripts/build-web-ui.shbuilds the bundle intocore/web_ui/distand clears dx's staging directory first, since dx copies every previously hashedasset into the bundle.
Local Execution
cargo test -p iggy-web-ui(10 tests), the bundle build, and headless-Chrome runs of thisPR's UI against a local
iggy-server. For those runs the server's embed was pointed at the Dioxusbundle locally. That change is not part of this PR. The runs covered:
signed-out redirect
I confirmed that each of the last four checks fails when its handling is removed.
prek run --from-ref origin/master --to-ref HEAD, plus the pre-pushcargo clippy --all-features --all-targetsandcargo machete.AI Usage