diff --git a/.gitignore b/.gitignore index 2dfad02764..6d9af9d641 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,7 @@ foreign/node/.npm __pycache__/ go.work core/bench/dashboard/frontend/dist +core/web_ui/dist core/integration/tests/connectors/postgres/cdc_restart_connectors/* !core/integration/tests/connectors/postgres/cdc_restart_connectors/config.toml LICENSE-binary diff --git a/Cargo.lock b/Cargo.lock index cefd525776..264deab350 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -185,7 +185,7 @@ dependencies = [ "bytes", "bytestring", "cfg-if", - "cookie", + "cookie 0.16.2", "derive_more", "encoding_rs", "foldhash", @@ -1178,7 +1178,7 @@ dependencies = [ "futures-core", "libc", "portable-atomic", - "rustc-hash", + "rustc-hash 2.1.3", "rustix 1.1.5", "tokio", "tokio-stream", @@ -1444,6 +1444,22 @@ dependencies = [ "syn 3.0.9", ] +[[package]] +name = "async-tungstenite" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee88b4c88ac8c9ea446ad43498955750a4bbe64c4392f21ccfe5d952865e318f" +dependencies = [ + "atomic-waker", + "futures-core", + "futures-io", + "futures-task", + "futures-util", + "log", + "pin-project-lite", + "tungstenite 0.27.0", +] + [[package]] name = "async-tungstenite" version = "0.34.1" @@ -2031,6 +2047,7 @@ dependencies = [ "matchit", "memchr", "mime", + "multer", "percent-encoding", "pin-project-lite", "serde_core", @@ -2114,6 +2131,12 @@ dependencies = [ "tokio", ] +[[package]] +name = "base16" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27c3610c36aee21ce8ac510e6224498de4228ad772a171ed65643a24693a5a8" + [[package]] name = "base16ct" version = "0.2.0" @@ -2274,7 +2297,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 2.1.3", "shlex 1.3.0", "syn 2.0.119", ] @@ -2803,6 +2826,9 @@ name = "bytes" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] [[package]] name = "bytes-utils" @@ -2924,6 +2950,12 @@ dependencies = [ "shlex 2.0.1", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cexpr" version = "0.6.0" @@ -2933,6 +2965,17 @@ dependencies = [ "nom 7.1.3", ] +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + [[package]] name = "cfg-if" version = "1.0.5" @@ -2988,6 +3031,16 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "charset" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f927b07c74ba84c7e5fe4db2baeb3e996ab2688992e39ac68ce3220a677c7e" +dependencies = [ + "base64 0.22.1", + "encoding_rs", +] + [[package]] name = "chrono" version = "0.4.45" @@ -3012,6 +3065,33 @@ dependencies = [ "phf 0.12.1", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "cipher" version = "0.4.4" @@ -3358,7 +3438,7 @@ dependencies = [ "futures-util", "libc", "quinn-proto", - "rustc-hash", + "rustc-hash 2.1.3", "rustls", "rustls-platform-verifier", "synchrony", @@ -3415,7 +3495,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0a9e5cbacce3ff2594cfd7321fd946d5f7afcf2dbc7537f6de7b42d798efb3a" dependencies = [ - "async-tungstenite", + "async-tungstenite 0.34.1", "compio-buf", "compio-io", "compio-log", @@ -3564,12 +3644,90 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "const-serialize" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad7154afa56de2f290e3c82c2c6dc4f5b282b6870903f56ef3509aba95866edc" +dependencies = [ + "const-serialize-macro 0.7.2", +] + +[[package]] +name = "const-serialize" +version = "0.8.0-alpha.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e42cd5aabba86f128b3763da1fec1491c0f728ce99245062cd49b6f9e6d235b" +dependencies = [ + "const-serialize 0.7.2", + "const-serialize-macro 0.8.0-alpha.3", + "serde", +] + +[[package]] +name = "const-serialize-macro" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f160aad86b4343e8d4e261fee9965c3005b2fd6bc117d172ab65948779e4acf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "const-serialize-macro" +version = "0.8.0-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7d372d7ea4c50890f188c72a08782d5c98bc8c7cc9dc4343b61d8ba8e20c26b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "const-str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0664d2867b4a32697dfe655557f5c3b187e9b605b38612a748e5ec99811d160" + +[[package]] +name = "const_format" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3decb4d6965448659923544a634a0571ba44cede4e11ef15a5a317126840454" +dependencies = [ + "const_format_proc_macros", + "konst", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941b570269a642506f08cb5b688c04bfa6cfd41a26f8bcdc49db8fd00c0ab659" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "constant_time_eq" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" +[[package]] +name = "content_disposition" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc14a88e1463ddd193906285abe5c360c7e8564e05ccc5d501755f7fbc9ca9c" +dependencies = [ + "charset", +] + [[package]] name = "convert_case" version = "0.6.0" @@ -3623,12 +3781,41 @@ dependencies = [ "version_check", ] +[[package]] +name = "cookie" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + [[package]] name = "cookie-factory" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" +[[package]] +name = "cookie_store" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206" +dependencies = [ + "cookie 0.18.2", + "document-features", + "idna", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -4569,126 +4756,599 @@ dependencies = [ name = "derive-new" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cdc8d50f426189eef89dac62fabfa0abb27d5cc008f25bf4156a0203325becc" +checksum = "2cdc8d50f426189eef89dac62fabfa0abb27d5cc008f25bf4156a0203325becc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "derive-syn-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65d7ce8132b7c0e54497a4d9a55a1c2a0912a0d786cf894472ba818fba45762" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "derive-where" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e2b94854e8576378ccda7c8de8a66ed8b4e8acbd2c50ec3418ea6c8aaf4b567" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.119", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case 0.10.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", + "unicode-xid", +] + +[[package]] +name = "des" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e" +dependencies = [ + "cipher 0.4.4", +] + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.6", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid 0.10.2", + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "dioxus" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9320593eda4f01858f046698b42441603d2af5fcb62a6c1f9046a0c2eebd6d9b" +dependencies = [ + "dioxus-asset-resolver", + "dioxus-cli-config", + "dioxus-config-macro", + "dioxus-config-macros", + "dioxus-core", + "dioxus-core-macro", + "dioxus-devtools", + "dioxus-document", + "dioxus-fullstack", + "dioxus-history", + "dioxus-hooks", + "dioxus-html", + "dioxus-logger", + "dioxus-router", + "dioxus-signals", + "dioxus-stores", + "dioxus-web", + "manganis", + "subsecond", + "warnings", +] + +[[package]] +name = "dioxus-asset-resolver" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84235ff0e272f15d4537603cdd50df6d45d50e51bb96326dbc0bdfd01d825226" +dependencies = [ + "dioxus-cli-config", + "http 1.5.0", + "infer", + "jni 0.21.1", + "js-sys", + "ndk", + "ndk-context", + "ndk-sys", + "percent-encoding", + "thiserror 2.0.21", + "tokio", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "dioxus-cli-config" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322d10f47effebd85ee0662a2cb083ff920d40dd6295a4afd8cfa4f9bc2e05c6" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "dioxus-config-macro" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f3a906c8219e94fa48189a793c760a4e3193cbb7791f799e9d37633fe41161d" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "dioxus-config-macros" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20de015bc89c20f4ffc7b4f849943420308365fa66f991250b5c075261990a21" + +[[package]] +name = "dioxus-core" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b232228ada232c0adc151e41ebd9ef70cc04683db097fa0dc6015979fa01722" +dependencies = [ + "anyhow", + "const_format", + "dioxus-core-types", + "futures-channel", + "futures-util", + "generational-box", + "longest-increasing-subsequence", + "rustc-hash 2.1.3", + "rustversion", + "serde", + "slab", + "slotmap", + "subsecond", + "tracing", +] + +[[package]] +name = "dioxus-core-macro" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6256a68462222f0600f5fd7d9542cbd9f299bc19355d900f7827dd3fc9e09fdb" +dependencies = [ + "convert_case 0.8.0", + "dioxus-rsx", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dioxus-core-types" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96007ed6cbad951dfed82d83c581aabd95bc591f351f5666d454ddbe7845b324" + +[[package]] +name = "dioxus-devtools" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434cebb282b3f820a341582ebfd37dab907d5a25c3eeafd87802468afcd2a959" +dependencies = [ + "dioxus-cli-config", + "dioxus-core", + "dioxus-devtools-types", + "dioxus-signals", + "serde", + "serde_json", + "subsecond", + "thiserror 2.0.21", + "tracing", + "tungstenite 0.28.0", +] + +[[package]] +name = "dioxus-devtools-types" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfd6f3475e38c93be245a664a9ce11679a8f2b99832620705cf7837f42aa39ce" +dependencies = [ + "dioxus-core", + "serde", + "subsecond-types", +] + +[[package]] +name = "dioxus-document" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f6fadd5c6d886b48b762820abae1a08142a2159f8a1136c0466b4b4ed9f547e" +dependencies = [ + "dioxus-core", + "dioxus-core-macro", + "dioxus-core-types", + "dioxus-html", + "futures-channel", + "futures-util", + "generational-box", + "lazy-js-bundle", + "serde", + "serde_json", + "tracing", +] + +[[package]] +name = "dioxus-fullstack" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a91d5acabd1470145bcf164cab9fd2edcf2e4fba0fa6fb0fd6dca23079fcc169" +dependencies = [ + "anyhow", + "async-stream", + "async-tungstenite 0.31.0", + "axum", + "axum-core", + "base64 0.22.1", + "bytes", + "ciborium", + "const-str", + "const_format", + "content_disposition", + "derive_more", + "dioxus-asset-resolver", + "dioxus-cli-config", + "dioxus-core", + "dioxus-fullstack-core", + "dioxus-fullstack-macro", + "dioxus-hooks", + "dioxus-html", + "dioxus-signals", + "form_urlencoded", + "futures", + "futures-channel", + "futures-util", + "gloo-net 0.6.0", + "headers", + "http 1.5.0", + "http-body", + "http-body-util", + "js-sys", + "mime", + "pin-project", + "reqwest 0.12.28", + "rustversion", + "send_wrapper", + "serde", + "serde_json", + "serde_qs", + "serde_urlencoded", + "thiserror 2.0.21", + "tokio-util", + "tracing", + "tungstenite 0.27.0", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.4.2", + "web-sys", + "xxhash-rust", +] + +[[package]] +name = "dioxus-fullstack-core" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0011e9bca8da2ae6bf02ba9ef93ba6c5fa539e402de3aeb316ad86ca77c59079" +dependencies = [ + "anyhow", + "axum-core", + "base64 0.22.1", + "ciborium", + "dioxus-core", + "dioxus-document", + "dioxus-history", + "dioxus-hooks", + "dioxus-signals", + "futures-channel", + "futures-util", + "generational-box", + "http 1.5.0", + "inventory", + "parking_lot", + "serde", + "serde_json", + "thiserror 2.0.21", + "tokio", + "tracing", +] + +[[package]] +name = "dioxus-fullstack-macro" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "681f39665de36af7bb724b1cc0ca9c2b7c36a2b93febf1b2911716479eac3db0" +dependencies = [ + "const_format", + "convert_case 0.8.0", + "proc-macro2", + "quote", + "syn 2.0.119", + "xxhash-rust", +] + +[[package]] +name = "dioxus-history" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2404b77d4441e694a5e93afb5c9a729efb26a3f6920f769cfe6cfedf5c7ac210" +dependencies = [ + "dioxus-core", + "tracing", +] + +[[package]] +name = "dioxus-hooks" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d3807a1bc039299cd37ad85f1c56ae7088210e8a38e3e877f01034d68c15fbb" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", + "dioxus-core", + "dioxus-signals", + "futures-channel", + "futures-util", + "generational-box", + "rustversion", + "slab", + "tracing", ] [[package]] -name = "derive-syn-parse" -version = "0.2.0" +name = "dioxus-html" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d65d7ce8132b7c0e54497a4d9a55a1c2a0912a0d786cf894472ba818fba45762" +checksum = "40b9a85679c4dfd8699407a9ad5041a6a4fe6be0ee386353951cd03b57a12030" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", + "async-trait", + "bytes", + "dioxus-core", + "dioxus-core-macro", + "dioxus-core-types", + "dioxus-hooks", + "dioxus-html-internal-macro", + "enumset", + "euclid", + "futures-channel", + "futures-util", + "generational-box", + "keyboard-types", + "lazy-js-bundle", + "rustversion", + "tracing", ] [[package]] -name = "derive-where" -version = "1.7.0" +name = "dioxus-html-internal-macro" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e2b94854e8576378ccda7c8de8a66ed8b4e8acbd2c50ec3418ea6c8aaf4b567" +checksum = "500282dad5b62e91f93e127cbf41c587bf1c457d52514a75f3ff2a55fd0ea9f6" dependencies = [ + "convert_case 0.8.0", "proc-macro2", "quote", - "syn 3.0.9", + "syn 2.0.119", ] [[package]] -name = "derive_builder" -version = "0.20.2" +name = "dioxus-interpreter-js" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +checksum = "faa22cc3431e7efdbae28069542a1763c977d5edbe4d4fbfb0b7667b9c9966e9" dependencies = [ - "derive_builder_macro", + "js-sys", + "lazy-js-bundle", + "rustc-hash 2.1.3", + "sledgehammer_bindgen", + "sledgehammer_utils", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] -name = "derive_builder_core" -version = "0.20.2" +name = "dioxus-logger" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +checksum = "0f36b2dac4df8c747431f77f2a6644dd033ea98e7f8ac80271ff47fd4ba8a548" dependencies = [ - "darling 0.20.11", - "proc-macro2", - "quote", - "syn 2.0.119", + "dioxus-cli-config", + "tracing", + "tracing-subscriber", + "tracing-wasm", ] [[package]] -name = "derive_builder_macro" -version = "0.20.2" +name = "dioxus-router" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +checksum = "e9652a5bcead34f687d7dbb12d5c8f6a7ecb342786273395ca0078e51b24320f" dependencies = [ - "derive_builder_core", - "syn 2.0.119", + "dioxus-cli-config", + "dioxus-core", + "dioxus-core-macro", + "dioxus-history", + "dioxus-hooks", + "dioxus-html", + "dioxus-router-macro", + "dioxus-signals", + "percent-encoding", + "rustversion", + "tracing", + "url", ] [[package]] -name = "derive_more" -version = "2.1.1" +name = "dioxus-router-macro" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +checksum = "56d20cf2173183946fdf8ab069b606a840a9d06e949050619355fc8cb97aafb6" dependencies = [ - "derive_more-impl", + "base16", + "digest 0.10.7", + "proc-macro2", + "quote", + "sha2 0.10.9", + "slab", + "syn 2.0.119", ] [[package]] -name = "derive_more-impl" -version = "2.1.1" +name = "dioxus-rsx" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +checksum = "aea662c9e13a91279d8acbc0d02a80430c7081704c20e8c791e4c4a151268596" dependencies = [ - "convert_case 0.10.0", "proc-macro2", + "proc-macro2-diagnostics", "quote", - "rustc_version", + "rustversion", "syn 2.0.119", - "unicode-xid", ] [[package]] -name = "des" -version = "0.8.1" +name = "dioxus-signals" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e" +checksum = "95d85e36fcaf7abdc986836801bf248776da7d5961f5fac65fdbf5ed491afb4f" dependencies = [ - "cipher 0.4.4", + "dioxus-core", + "futures-channel", + "futures-util", + "generational-box", + "parking_lot", + "rustc-hash 2.1.3", + "tracing", + "warnings", ] [[package]] -name = "difflib" -version = "0.4.0" +name = "dioxus-stores" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" +checksum = "15eaa9f2ccc5ce962d056515e61345c846a54b13a2b880cf7e8ac93faa53a178" +dependencies = [ + "dioxus-core", + "dioxus-signals", + "dioxus-stores-macro", + "generational-box", +] [[package]] -name = "digest" -version = "0.10.7" +name = "dioxus-stores-macro" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "a13bd8a693d7042544fa3ac79698a08783bbc698d1ed257e88c5d4059e9ac476" dependencies = [ - "block-buffer 0.10.4", - "const-oid 0.9.6", - "crypto-common 0.1.6", - "subtle", + "convert_case 0.8.0", + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] -name = "digest" -version = "0.11.3" +name = "dioxus-web" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" -dependencies = [ - "block-buffer 0.12.1", - "const-oid 0.10.2", - "crypto-common 0.2.2", - "ctutils", +checksum = "481093233d789c5ffbdde66e7970e803b01a199a7ab0c0f8ef84c65d30b7384f" +dependencies = [ + "dioxus-cli-config", + "dioxus-core", + "dioxus-core-types", + "dioxus-devtools", + "dioxus-document", + "dioxus-history", + "dioxus-html", + "dioxus-interpreter-js", + "dioxus-signals", + "futures-channel", + "futures-util", + "generational-box", + "gloo-timers 0.3.0", + "js-sys", + "lazy-js-bundle", + "rustc-hash 2.1.3", + "send_wrapper", + "serde", + "serde-wasm-bindgen", + "serde_json", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.4.2", + "web-sys", ] [[package]] @@ -4802,6 +5462,15 @@ dependencies = [ "serde_json", ] +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -5694,6 +6363,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "generational-box" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8b2dc9b873cd1a8adb80aa5dcef9a8205f781a8a313d5ffb867248cb3bcb764" +dependencies = [ + "parking_lot", + "tracing", +] + [[package]] name = "generator" version = "0.8.11" @@ -6044,6 +6723,27 @@ dependencies = [ "web-sys", ] +[[package]] +name = "gloo-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils 0.2.0", + "http 1.5.0", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "gloo-net" version = "0.7.0" @@ -6406,6 +7106,30 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "headers" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2700e3fe555c3310aa7286cac6167449f2c87e0eb58769c9208a1c58a1d106" +dependencies = [ + "base64 0.22.1", + "bytes", + "headers-core", + "http 1.5.0", + "httpdate", + "mime", + "sha1 0.10.7", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http 1.5.0", +] + [[package]] name = "heapless" version = "0.7.17" @@ -6459,7 +7183,7 @@ dependencies = [ "hickory-proto", "idna", "ipnet", - "jni", + "jni 0.22.4", "rand 0.10.3", "thiserror 2.0.21", "tinyvec", @@ -6477,7 +7201,7 @@ dependencies = [ "data-encoding", "idna", "ipnet", - "jni", + "jni 0.22.4", "once_cell", "prefix-trie", "rand 0.10.3", @@ -6500,7 +7224,7 @@ dependencies = [ "hickory-proto", "ipconfig", "ipnet", - "jni", + "jni 0.22.4", "moka", "ndk-context", "once_cell", @@ -7352,6 +8076,22 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "iggy-web-ui" +version = "0.1.0" +dependencies = [ + "base64 0.23.1", + "chrono", + "dioxus", + "gloo-net 0.7.0", + "iggy_common", + "js-sys", + "serde", + "serde_json", + "thiserror 2.0.21", + "web-sys", +] + [[package]] name = "iggy_binary_protocol" version = "0.11.1-edge.3" @@ -8039,6 +8779,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + [[package]] name = "inflections" version = "1.1.1" @@ -8321,6 +9070,22 @@ dependencies = [ "jiff-tzdb", ] +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + [[package]] name = "jni" version = "0.22.4" @@ -8330,7 +9095,7 @@ dependencies = [ "cfg-if", "combine", "jni-macros", - "jni-sys", + "jni-sys 0.4.1", "log", "simd_cesu8", "thiserror 2.0.21", @@ -8351,6 +9116,15 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + [[package]] name = "jni-sys" version = "0.4.1" @@ -8501,6 +9275,15 @@ dependencies = [ "cpufeatures 0.3.1", ] +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.13.2", +] + [[package]] name = "keyring-core" version = "1.0.1" @@ -8510,6 +9293,21 @@ dependencies = [ "log", ] +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + [[package]] name = "kqueue" version = "1.2.1" @@ -8565,6 +9363,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "lazy-js-bundle" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51cdaa5abc885e2d606e6985281ace220778bde190be20aab1ec2346ed8bd1fa" + [[package]] name = "lazy-regex" version = "3.6.2" @@ -8823,6 +9627,12 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" +[[package]] +name = "litrs" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4744e383959f0db86ede514b809b1c53251889093803c05267acc7d4e7030d70" + [[package]] name = "local-channel" version = "0.1.5" @@ -8893,6 +9703,12 @@ dependencies = [ "logos-codegen", ] +[[package]] +name = "longest-increasing-subsequence" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3bd0dd2cd90571056fdb71f6275fada10131182f84899f4b2a916e565d81d86" + [[package]] name = "loom" version = "0.7.2" @@ -8961,6 +9777,17 @@ dependencies = [ "twox-hash", ] +[[package]] +name = "macro-string" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "macro_magic" version = "0.5.1" @@ -9009,6 +9836,50 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "manganis" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ed2af894a724a0b6e521f9efe84f98cfdc74a4de6828efef77ffd3b1679fd52" +dependencies = [ + "const-serialize 0.7.2", + "const-serialize 0.8.0-alpha.0", + "jni 0.21.1", + "manganis-core", + "manganis-macro", + "ndk-context", + "objc2", + "thiserror 2.0.21", +] + +[[package]] +name = "manganis-core" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e258c71b137bf48deaffb48b8a8a2f994af9e5c3ca201daa20d41e26b379da11" +dependencies = [ + "const-serialize 0.7.2", + "const-serialize 0.8.0-alpha.0", + "dioxus-cli-config", + "dioxus-core-types", + "serde", + "winnow 0.7.15", +] + +[[package]] +name = "manganis-macro" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0abae8346bbf637e6d21ac3831863bd5a98657517f5cd2d69d651046b95e7ce9" +dependencies = [ + "dunce", + "macro-string", + "manganis-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "matchers" version = "0.2.0" @@ -9130,6 +10001,15 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "memfd" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57804b2c9b69967f1536a56f86297e367a33b19e98852ed624b84551cdbc0d90" +dependencies = [ + "rustix 1.1.5", +] + [[package]] name = "memmap2" version = "0.9.11" @@ -9446,6 +10326,23 @@ dependencies = [ "thiserror 2.0.21", ] +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http 1.5.0", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + [[package]] name = "multiversion_no_op" version = "1.0.0" @@ -9458,12 +10355,36 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9252111cf132ba0929b6f8e030cac2a24b507f3a4d6db6fb2896f27b354c714b" +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.2", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + [[package]] name = "ndk-context" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -9734,6 +10655,28 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.3.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "num_threads" version = "0.1.7" @@ -11292,6 +12235,12 @@ dependencies = [ "thiserror 2.0.21", ] +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + [[package]] name = "psm" version = "0.1.32" @@ -11302,6 +12251,16 @@ dependencies = [ "cc", ] +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna", + "psl-types", +] + [[package]] name = "pulp" version = "0.22.3" @@ -11408,7 +12367,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash", + "rustc-hash 2.1.3", "rustls", "socket2", "thiserror 2.0.21", @@ -11431,7 +12390,7 @@ dependencies = [ "rand 0.10.3", "rand_pcg", "ring", - "rustc-hash", + "rustc-hash 2.1.3", "rustls", "rustls-pki-types", "rustls-platform-verifier", @@ -11636,6 +12595,12 @@ dependencies = [ "bitflags 2.13.2", ] +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + [[package]] name = "rayon" version = "1.12.0" @@ -11855,6 +12820,8 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", + "cookie 0.18.2", + "cookie_store", "futures-core", "futures-util", "h2 0.4.20", @@ -11866,6 +12833,7 @@ dependencies = [ "hyper-util", "js-sys", "log", + "mime_guess", "percent-encoding", "pin-project-lite", "quinn", @@ -12181,7 +13149,7 @@ version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "249896ab27ed630590971738264baa8f722f18965d2e387c706c40a3c2a572cc" dependencies = [ - "async-tungstenite", + "async-tungstenite 0.34.1", "rustls-native-certs", "rustls-pki-types", "rustls-webpki", @@ -12196,7 +13164,7 @@ version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3767dde22a804ea806107d0f4331e33cb794d1b8468b11c86fb0c1366f07054" dependencies = [ - "async-tungstenite", + "async-tungstenite 0.34.1", "bytes", "fixedbitset", "flume", @@ -12221,7 +13189,7 @@ version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfa6ddcc7a7dd5688f9bf78d8f81cb94f367bce56c055d8d94cf81ecb0518bf" dependencies = [ - "async-tungstenite", + "async-tungstenite 0.34.1", "bytes", "fixedbitset", "flume", @@ -12338,6 +13306,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.3" @@ -12468,7 +13442,7 @@ checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", - "jni", + "jni 0.22.4", "log", "once_cell", "rustls", @@ -12854,6 +13828,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_qs" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3faaf9e727533a19351a43cc5a8de957372163c7d35cc48c90b75cdda13c352" +dependencies = [ + "percent-encoding", + "serde", + "thiserror 2.0.21", +] + [[package]] name = "serde_repr" version = "0.1.21" @@ -13344,12 +14329,42 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "sledgehammer_bindgen" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e83e178d176459c92bc129cfd0958afac3ced925471b889b3a75546cfc4133" +dependencies = [ + "sledgehammer_bindgen_macro", + "wasm-bindgen", +] + +[[package]] +name = "sledgehammer_bindgen_macro" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb251b407f50028476a600541542b605bb864d35d9ee1de4f6cab45d88475e6d" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "sledgehammer_utils" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdd4b83524961983cea3c55383b3910fd2f24fd13a188f5b091d2d504a61ae" +dependencies = [ + "rustc-hash 1.1.0", +] + [[package]] name = "slotmap" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" dependencies = [ + "serde", "version_check", ] @@ -13437,7 +14452,7 @@ dependencies = [ "data-encoding", "debugid", "if_chain", - "rustc-hash", + "rustc-hash 2.1.3", "serde", "serde_json", "unicode-id-start", @@ -13844,6 +14859,34 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "subsecond" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d350d5788fa94d560d92269266a50efc77b036bfca6675e420b64df7b3211f37" +dependencies = [ + "js-sys", + "libc", + "libloading", + "memfd", + "memmap2", + "serde", + "subsecond-types", + "thiserror 2.0.21", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "subsecond-types" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf32d66269b5fbb8558334e8d22b657b2f7af0dd2ef56c231541099a862e464" +dependencies = [ + "serde", +] + [[package]] name = "subtle" version = "2.6.1" @@ -14867,6 +15910,17 @@ dependencies = [ "tracing-serde", ] +[[package]] +name = "tracing-wasm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4575c663a174420fa2d78f4108ff68f65bf2fbb7dd89f33749b6e826b3626e07" +dependencies = [ + "tracing", + "tracing-subscriber", + "wasm-bindgen", +] + [[package]] name = "trait-variant" version = "0.1.3" @@ -14893,6 +15947,40 @@ dependencies = [ "core_maths", ] +[[package]] +name = "tungstenite" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d" +dependencies = [ + "bytes", + "data-encoding", + "http 1.5.0", + "httparse", + "log", + "rand 0.9.5", + "sha1 0.10.7", + "thiserror 2.0.21", + "utf-8", +] + +[[package]] +name = "tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" +dependencies = [ + "bytes", + "data-encoding", + "http 1.5.0", + "httparse", + "log", + "rand 0.9.5", + "sha1 0.10.7", + "thiserror 2.0.21", + "utf-8", +] + [[package]] name = "tungstenite" version = "0.29.0" @@ -15277,6 +16365,12 @@ dependencies = [ "xmlwriter", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8-width" version = "0.1.9" @@ -15498,6 +16592,28 @@ dependencies = [ "try-lock", ] +[[package]] +name = "warnings" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f68998838dab65727c9b30465595c6f7c953313559371ca8bf31759b3680ad" +dependencies = [ + "pin-project", + "tracing", + "warnings-macro", +] + +[[package]] +name = "warnings-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59195a1db0e95b920366d949ba5e0d3fc0e70b67c09be15ce5abb790106b0571" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -15965,6 +17081,15 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -16001,6 +17126,21 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -16052,6 +17192,12 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -16064,6 +17210,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -16076,6 +17228,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -16100,6 +17258,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -16112,6 +17276,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -16124,6 +17294,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -16136,6 +17312,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -16308,6 +17490,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" +[[package]] +name = "xxhash-rust" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "550a2b930b62486a393c52d5c3b84bff264b28aa437ed64694d31e93b1757af7" + [[package]] name = "y4m" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index f9528ebf60..da5c42b41e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,7 @@ members = [ "core/simulator", "core/system_stats", "core/tools", + "core/web_ui", "examples/rust", "gateways/kafka", "gateways/kafka/tools/kafka-tool", @@ -167,6 +168,7 @@ dashmap = "6.2.1" deltalake = { version = "1.1.1", features = ["azure", "gcs", "s3"] } derive_builder = "0.20.2" derive_more = { version = "2.1.1", features = ["full"] } +dioxus = { version = "0.7.10", features = ["router"] } dircpy = "0.3.21" dirs = "7.0.0" dlopen2 = "0.9.0" @@ -199,6 +201,7 @@ gcloud-googleapis = { version = "1.3.0", features = ["bigquery"] } getrandom = { version = "0.4", features = ["wasm_js"] } git2 = { version = "0.21.0", default-features = false, features = ["vendored-libgit2"] } gloo = "0.12" +gloo-net = { version = "0.7.0", default-features = false, features = ["http", "json"] } governor = "0.10.4" harness_derive = { path = "core/harness_derive" } hash32 = "1.0.0" diff --git a/core/web_ui/Cargo.toml b/core/web_ui/Cargo.toml new file mode 100644 index 0000000000..a50cc704c3 --- /dev/null +++ b/core/web_ui/Cargo.toml @@ -0,0 +1,44 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +[package] +name = "iggy-web-ui" +license = "Apache-2.0" +version = "0.1.0" +edition = "2024" +publish = false + +[features] +default = ["web"] +web = ["dioxus/web"] + +[dependencies] +base64 = { workspace = true } +chrono = { workspace = true } +dioxus = { workspace = true } +gloo-net = { workspace = true } +js-sys = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +web-sys = { workspace = true, features = ["Storage"] } + +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +iggy_common = { workspace = true } + +[lints] +workspace = true diff --git a/core/web_ui/Dioxus.toml b/core/web_ui/Dioxus.toml new file mode 100644 index 0000000000..8954ead77f --- /dev/null +++ b/core/web_ui/Dioxus.toml @@ -0,0 +1,26 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +[application] +name = "iggy-web-ui" +default_platform = "web" + +[web.app] +title = "Apache Iggy" +# The switch-over from the Svelte app will have the server embed the bundle +# and serve it under /ui. +base_path = "ui" diff --git a/core/web_ui/README.md b/core/web_ui/README.md new file mode 100644 index 0000000000..c98d3f448f --- /dev/null +++ b/core/web_ui/README.md @@ -0,0 +1,51 @@ +# Apache Iggy Web UI + +A [Dioxus](https://dioxuslabs.com/) single-page app for browsing an Iggy +server over its HTTP API. It compiles to `wasm32-unknown-unknown` and is +built with the Dioxus CLI, `dx`. + +## Prerequisites + +```bash +rustup target add wasm32-unknown-unknown +cargo install dioxus-cli --version 0.7.10 --locked +``` + +Keep the `dx` version equal to the `dioxus` version in `Cargo.lock`. + +## Release bundle + +```bash +./scripts/build-web-ui.sh +``` + +The bundle lands in `core/web_ui/dist`, with every asset path under `/ui`. +The script clears dx's staging directory first, because dx copies every +previously hashed asset into the bundle. + +## Development + +Start a server, then serve the UI with hot reload from this directory: + +```bash +cargo run --bin iggy-server -- --with-default-root-credentials +cd core/web_ui +IGGY_WEB_UI_API_URL=http://localhost:3000 dx serve +``` + +Open and sign in as `iggy` / `iggy`. + +`IGGY_WEB_UI_API_URL` is read at build time and prefixes every API request. +Left unset, requests go to the origin that served the UI, which is what an +embedded build wants. The server's default CORS settings allow any origin, so +the dev server on another port can reach it. + +## Tests + +```bash +cargo test -p iggy-web-ui +``` + +The tests run on the host. Besides the formatting helpers, they serialize +`iggy_common` types and decode them into the UI's mirrors in `src/models.rs`, +so a renamed server field fails here instead of in the browser. diff --git a/core/web_ui/assets/favicon.svg b/core/web_ui/assets/favicon.svg new file mode 100644 index 0000000000..7048812d45 --- /dev/null +++ b/core/web_ui/assets/favicon.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/web_ui/assets/iggy-apache-darkbg.svg b/core/web_ui/assets/iggy-apache-darkbg.svg new file mode 100644 index 0000000000..a03645fb3e --- /dev/null +++ b/core/web_ui/assets/iggy-apache-darkbg.svg @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/web_ui/assets/main.css b/core/web_ui/assets/main.css new file mode 100644 index 0000000000..552ba14f08 --- /dev/null +++ b/core/web_ui/assets/main.css @@ -0,0 +1,317 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +:root { + --bg: #1e1f22; + --surface: #2b2d31; + --surface-raised: #313338; + --border: #404249; + --text: #f2f3f5; + --text-muted: #949ba4; + --accent: #32ad84; + --accent-hover: #5abd9c; + --danger: #f23f43; + color-scheme: dark; + font-family: Inter, system-ui, -apple-system, 'Segoe UI', sans-serif; + font-size: 14px; + background: var(--bg); + color: var(--text); +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; +} + +a { + color: var(--accent); + text-decoration: none; +} + +a:hover { + color: var(--accent-hover); + text-decoration: underline; +} + +h1 { + margin: 0; + font-size: 1.6rem; + font-weight: 600; +} + +h2 { + margin: 2rem 0 0.75rem; + font-size: 1.1rem; + font-weight: 600; +} + +button { + padding: 0.55rem 1rem; + border: none; + border-radius: 6px; + background: var(--accent); + color: #fff; + font: inherit; + font-weight: 600; + cursor: pointer; +} + +button:hover:not(:disabled) { + background: var(--accent-hover); +} + +button:disabled { + opacity: 0.45; + cursor: default; +} + +button.secondary { + background: var(--surface-raised); + color: var(--text); + border: 1px solid var(--border); +} + +button.secondary:hover:not(:disabled) { + background: var(--border); +} + +input { + width: 100%; + margin-top: 0.35rem; + padding: 0.55rem 0.7rem; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg); + color: var(--text); + font: inherit; +} + +input:focus { + outline: 2px solid var(--accent); + outline-offset: -1px; +} + +.muted { + color: var(--text-muted); +} + +.mono { + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 0.85rem; +} + +.auth { + display: grid; + min-height: 100vh; + place-items: center; + padding: 1rem; +} + +.auth-card { + display: flex; + flex-direction: column; + gap: 1rem; + width: min(380px, 100%); + padding: 2rem; + border: 1px solid var(--border); + border-radius: 12px; + background: var(--surface); +} + +.auth-card label { + color: var(--text-muted); + font-size: 0.85rem; +} + +.auth-logo { + width: 70%; + margin: 0 auto 0.5rem; +} + +.form-error, +.error-box { + margin: 0; + color: var(--danger); +} + +.error-box { + padding: 0.75rem 1rem; + border: 1px solid var(--danger); + border-radius: 8px; +} + +.dashboard { + display: grid; + grid-template-columns: 220px 1fr; + min-height: 100vh; +} + +.sidebar { + display: flex; + flex-direction: column; + gap: 1.5rem; + padding: 1.25rem 1rem; + border-right: 1px solid var(--border); + background: var(--surface); +} + +.sidebar nav { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.sidebar-logo { + width: 100%; +} + +.sidebar-footer { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-top: auto; +} + +.nav-link { + padding: 0.5rem 0.75rem; + border-radius: 6px; + color: var(--text-muted); +} + +.nav-link:hover { + background: var(--surface-raised); + color: var(--text); + text-decoration: none; +} + +.nav-link.active { + background: var(--surface-raised); + color: var(--text); +} + +.content { + min-width: 0; + padding: 2rem 2.5rem; +} + +.page-header { + margin-bottom: 1.5rem; +} + +.breadcrumbs { + display: flex; + gap: 0.4rem; + min-height: 1.2rem; + margin-bottom: 0.4rem; + font-size: 0.85rem; +} + +.crumb-separator { + color: var(--text-muted); +} + +.stats { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 0.75rem; +} + +.stat { + display: flex; + flex-direction: column; + gap: 0.3rem; + padding: 0.85rem 1rem; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--surface); +} + +.stat-label { + color: var(--text-muted); + font-size: 0.8rem; +} + +.stat-value { + font-size: 1.05rem; + font-weight: 600; +} + +table { + width: 100%; + border-collapse: collapse; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--surface); +} + +th, +td { + padding: 0.6rem 0.85rem; + border-bottom: 1px solid var(--border); + text-align: left; + vertical-align: top; +} + +th { + color: var(--text-muted); + font-size: 0.8rem; + font-weight: 500; +} + +tbody tr:last-child td { + border-bottom: none; +} + +tbody tr:hover { + background: var(--surface-raised); +} + +.num { + text-align: right; + font-variant-numeric: tabular-nums; +} + +.pager { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 1rem; +} + +.pager .muted { + margin-right: auto; +} + +details summary { + cursor: pointer; +} + +details pre { + max-height: 320px; + margin: 0.5rem 0 0; + padding: 0.75rem; + overflow: auto; + border-radius: 6px; + background: var(--bg); + white-space: pre-wrap; + word-break: break-all; +} diff --git a/core/web_ui/src/api.rs b/core/web_ui/src/api.rs new file mode 100644 index 0000000000..3c294f96b2 --- /dev/null +++ b/core/web_ui/src/api.rs @@ -0,0 +1,188 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use gloo_net::http::{Request, RequestBuilder, Response}; +use serde::Deserialize; +use serde::de::DeserializeOwned; + +use crate::auth::Session; +use crate::models::{ + IdentityInfo, LoginUser, PolledMessages, Stream, StreamDetails, TopicDetails, UserInfoDetails, +}; + +/// Empty by default so requests go to the server that served the UI. Set at +/// build time to point `dx serve` at a separately running server, see the +/// crate README. +const API_BASE_URL: &str = match option_env!("IGGY_WEB_UI_API_URL") { + Some(url) => url, + None => "", +}; + +#[derive(Debug, Clone, PartialEq, thiserror::Error)] +pub enum ApiError { + /// The session is not valid any more. + #[error("session expired or not authorized")] + Unauthorized, + /// The account lacks a permission. The session stays valid. + #[error("you do not have permission to do this")] + Forbidden, + #[error("not found")] + NotFound, + /// The server rejected the request and said why. + #[error("{0}")] + Rejected(String), + #[error("server responded with {status}: {body}")] + Status { status: u16, body: String }, + #[error("request failed: {0}")] + Network(String), + #[error("unexpected response: {0}")] + Decode(String), +} + +/// Thin client for the server's HTTP API, scoped to one session. +#[derive(Debug, Clone, Default)] +pub struct ApiClient { + access_token: Option, +} + +impl ApiClient { + pub fn new(session: Option<&Session>) -> Self { + Self { + access_token: session.map(|session| session.access_token.clone()), + } + } + + pub async fn login(&self, credentials: &LoginUser) -> Result { + let request = self + .builder(Request::post(&url("/users/login"))) + .json(credentials) + .map_err(|error| ApiError::Network(error.to_string()))?; + let response = request + .send() + .await + .map_err(|error| ApiError::Network(error.to_string()))?; + decode(response).await + } + + pub async fn logout(&self) -> Result<(), ApiError> { + let response = self + .builder(Request::delete(&url("/users/logout"))) + .send() + .await + .map_err(|error| ApiError::Network(error.to_string()))?; + check_status(response).await.map(drop) + } + + pub async fn user(&self, user_id: u32) -> Result { + self.get(&format!("/users/{user_id}"), &[]).await + } + + pub async fn streams(&self) -> Result, ApiError> { + self.get("/streams", &[]).await + } + + pub async fn stream(&self, stream_id: u32) -> Result { + self.get(&format!("/streams/{stream_id}"), &[]).await + } + + pub async fn topic(&self, stream_id: u32, topic_id: u32) -> Result { + self.get(&format!("/streams/{stream_id}/topics/{topic_id}"), &[]) + .await + } + + /// Reads without committing, so browsing never moves a consumer offset. + pub async fn poll_messages( + &self, + stream_id: u32, + topic_id: u32, + partition_id: u32, + offset: u64, + count: u32, + ) -> Result { + let query = [ + ("kind", "offset".to_owned()), + ("value", offset.to_string()), + ("count", count.to_string()), + ("auto_commit", "false".to_owned()), + ("partition_id", partition_id.to_string()), + ]; + self.get( + &format!("/streams/{stream_id}/topics/{topic_id}/messages"), + &query, + ) + .await + } + + async fn get( + &self, + path: &str, + query: &[(&str, String)], + ) -> Result { + let builder = + Request::get(&url(path)).query(query.iter().map(|(key, value)| (*key, value.as_str()))); + let response = self + .builder(builder) + .send() + .await + .map_err(|error| ApiError::Network(error.to_string()))?; + decode(response).await + } + + fn builder(&self, builder: RequestBuilder) -> RequestBuilder { + match &self.access_token { + Some(token) => builder.header("Authorization", &format!("Bearer {token}")), + None => builder, + } + } +} + +fn url(path: &str) -> String { + format!("{API_BASE_URL}{path}") +} + +/// Reads the body as bytes, because `Response::json` goes through one JS +/// string, which browsers cap well below a page of large messages. +async fn decode(response: Response) -> Result { + let body = check_status(response) + .await? + .binary() + .await + .map_err(|error| ApiError::Network(error.to_string()))?; + serde_json::from_slice(&body).map_err(|error| ApiError::Decode(error.to_string())) +} + +async fn check_status(response: Response) -> Result { + match response.status() { + 200..=299 => Ok(response), + 401 => Err(ApiError::Unauthorized), + 403 => Err(ApiError::Forbidden), + 404 => Err(ApiError::NotFound), + status => { + let body = response.text().await.unwrap_or_default(); + Err(match serde_json::from_str::(&body) { + Ok(error) => ApiError::Rejected(error.reason), + Err(_) => ApiError::Status { status, body }, + }) + } + } +} + +/// The JSON body the server attaches to rejected requests. +#[derive(Deserialize)] +struct ErrorBody { + reason: String, +} diff --git a/core/web_ui/src/auth.rs b/core/web_ui/src/auth.rs new file mode 100644 index 0000000000..8bb64fb832 --- /dev/null +++ b/core/web_ui/src/auth.rs @@ -0,0 +1,92 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use serde::{Deserialize, Serialize}; +use web_sys::Storage; + +use crate::models::IdentityInfo; + +const SESSION_KEY: &str = "iggy_session"; + +/// The signed-in user's token, persisted so a reload keeps the session. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Session { + pub user_id: u32, + pub access_token: String, + /// Unix seconds, as returned by `/users/login`. + pub expiry: u64, +} + +impl Session { + /// Restores the stored session, dropping it if the token has expired. + /// Blocked or unreadable storage counts as no session. + pub fn restore() -> Option { + let storage = local_storage()?; + let stored = storage.get_item(SESSION_KEY).ok()??; + let session: Self = serde_json::from_str(&stored).ok()?; + if session.is_expired() { + let _ = storage.remove_item(SESSION_KEY); + return None; + } + Some(session) + } + + pub fn persist(&self) { + // A failed write only costs the user a sign-in after the next reload. + let (Some(storage), Ok(json)) = (local_storage(), serde_json::to_string(self)) else { + return; + }; + let _ = storage.set_item(SESSION_KEY, &json); + } + + /// Removes this session from storage. Tabs share the stored session, so + /// one that another tab saved after signing in again is left alone. + pub fn forget(&self) { + let Some(storage) = local_storage() else { + return; + }; + let stored = storage + .get_item(SESSION_KEY) + .ok() + .flatten() + .and_then(|stored| serde_json::from_str::(&stored).ok()); + if stored.is_none_or(|stored| stored.access_token == self.access_token) { + let _ = storage.remove_item(SESSION_KEY); + } + } + + fn is_expired(&self) -> bool { + let now_secs = (js_sys::Date::now() / 1000.0) as u64; + self.expiry <= now_secs + } +} + +impl From for Session { + fn from(identity: IdentityInfo) -> Self { + Self { + user_id: identity.user_id, + access_token: identity.access_token.token, + expiry: identity.access_token.expiry, + } + } +} + +/// `None` when the browser blocks site storage, where reading +/// `window.localStorage` throws. +fn local_storage() -> Option { + web_sys::window()?.local_storage().ok().flatten() +} diff --git a/core/web_ui/src/components.rs b/core/web_ui/src/components.rs new file mode 100644 index 0000000000..e6f7a9dae5 --- /dev/null +++ b/core/web_ui/src/components.rs @@ -0,0 +1,89 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use dioxus::prelude::*; + +use crate::Route; +use crate::api::{ApiClient, ApiError}; +use crate::auth::Session; + +pub fn use_session() -> Signal> { + use_context() +} + +pub fn api_client(session: Signal>) -> ApiClient { + ApiClient::new(session.read().as_ref()) +} + +#[component] +pub fn PageHeader(title: String, children: Element) -> Element { + rsx! { + header { class: "page-header", + nav { class: "breadcrumbs", {children} } + h1 { "{title}" } + } + } +} + +#[component] +pub fn Crumb(to: Route, label: String) -> Element { + rsx! { + Link { to, "{label}" } + span { class: "crumb-separator", "/" } + } +} + +#[component] +pub fn Stat(label: String, value: String) -> Element { + rsx! { + div { class: "stat", + span { class: "stat-label", "{label}" } + span { class: "stat-value", "{value}" } + } + } +} + +#[component] +pub fn Loading() -> Element { + rsx! { + p { class: "muted", "Loading…" } + } +} + +/// Renders a failed request. A 401 signs the user out, and the dashboard +/// layout then redirects to sign-in. A 403 only shows the message. +#[component] +pub fn ErrorView(error: ApiError) -> Element { + let mut session = use_session(); + let unauthorized = error == ApiError::Unauthorized; + + // Reactive on the prop, because a mounted view can change from another + // error to a 401 without remounting. + use_effect(use_reactive!(|unauthorized| { + if !unauthorized { + return; + } + let failed = session.write().take(); + if let Some(failed) = failed { + failed.forget(); + } + })); + + rsx! { + div { class: "error-box", "{error}" } + } +} diff --git a/core/web_ui/src/format.rs b/core/web_ui/src/format.rs new file mode 100644 index 0000000000..f01ed40008 --- /dev/null +++ b/core/web_ui/src/format.rs @@ -0,0 +1,234 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use std::fmt::Write; + +use base64::Engine; +use base64::engine::general_purpose::STANDARD; +use chrono::DateTime; + +use crate::models::ResourceOptions; + +const MICROS_PER_SECOND: u64 = 1_000_000; + +/// Formats a server timestamp (Unix microseconds) in UTC. +pub fn timestamp(micros: u64) -> String { + format_micros(micros, "%Y-%m-%d %H:%M:%S UTC") +} + +/// Like [`timestamp`], keeping the microseconds that order messages. +pub fn timestamp_precise(micros: u64) -> String { + format_micros(micros, "%Y-%m-%d %H:%M:%S%.6f UTC") +} + +/// Renders the `message_expiry` option (microseconds, 0 or `u64::MAX` means +/// never) as a short duration. +pub fn message_expiry(options: &ResourceOptions) -> String { + let micros = options + .get("message_expiry") + .and_then(|option| option.value.parse::().ok()) + .unwrap_or(0); + if micros == 0 || micros == u64::MAX { + return "never".to_owned(); + } + duration(micros / MICROS_PER_SECOND) +} + +/// Shows the payload as text when it is UTF-8 and as hex otherwise. +pub fn payload(base64_payload: &str) -> String { + match STANDARD.decode(base64_payload) { + Ok(bytes) => String::from_utf8(bytes).unwrap_or_else(|error| hex(error.as_bytes())), + Err(_) => base64_payload.to_owned(), + } +} + +/// Up to `max_chars` characters of the payload, decoding only as much of it +/// as that needs, so a list of large messages stays cheap. Text or hex is +/// picked from the decoded prefix alone, so a payload whose invalid UTF-8 +/// comes later previews as text while [`payload`] shows it as hex. +pub fn payload_preview(base64_payload: &str, max_chars: usize) -> String { + // A character is at most 4 bytes of UTF-8, and every 3 bytes take 4 + // base64 characters, so this prefix always holds `max_chars` characters. + let prefix_len = (max_chars * 4).div_ceil(3) * 4; + if base64_payload.len() <= prefix_len { + return truncate(&payload(base64_payload), max_chars); + } + let Ok(bytes) = STANDARD.decode(&base64_payload[..prefix_len]) else { + return truncate(base64_payload, max_chars); + }; + let preview = match String::from_utf8(bytes) { + Ok(text) => text, + // The cut can split the last character, which is still text. + Err(error) if error.utf8_error().error_len().is_none() => { + let valid = error.utf8_error().valid_up_to(); + let mut bytes = error.into_bytes(); + bytes.truncate(valid); + String::from_utf8(bytes).unwrap_or_default() + } + Err(error) => hex(error.as_bytes()), + }; + // The payload goes on past the prefix, so the preview always ends in "…". + match truncate(&preview, max_chars) { + cut if cut.ends_with('…') => cut, + whole => format!("{whole}…"), + } +} + +/// Message IDs are 128-bit and usually UUIDs, so they read best in that form. +pub fn message_id(id: u128) -> String { + let hex = format!("{id:032x}"); + format!( + "{}-{}-{}-{}-{}", + &hex[..8], + &hex[8..12], + &hex[12..16], + &hex[16..20], + &hex[20..] + ) +} + +fn hex(bytes: &[u8]) -> String { + let mut text = String::with_capacity(bytes.len() * 3); + for (index, byte) in bytes.iter().enumerate() { + if index > 0 { + text.push(' '); + } + // Writing into a String cannot fail. + let _ = write!(text, "{byte:02x}"); + } + text +} + +pub fn truncate(text: &str, max_chars: usize) -> String { + match text.char_indices().nth(max_chars) { + Some((cut, _)) => format!("{}…", &text[..cut]), + None => text.to_owned(), + } +} + +fn format_micros(micros: u64, pattern: &str) -> String { + i64::try_from(micros) + .ok() + .and_then(DateTime::from_timestamp_micros) + .map(|date| date.format(pattern).to_string()) + .unwrap_or_else(|| micros.to_string()) +} + +fn duration(total_seconds: u64) -> String { + const UNITS: [(u64, &str); 4] = [(86_400, "d"), (3_600, "h"), (60, "m"), (1, "s")]; + let mut remaining = total_seconds; + let parts: Vec = UNITS + .iter() + .filter_map(|&(unit_seconds, suffix)| { + let amount = remaining / unit_seconds; + remaining %= unit_seconds; + (amount > 0).then(|| format!("{amount}{suffix}")) + }) + .collect(); + if parts.is_empty() { + "0s".to_owned() + } else { + parts.join(" ") + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::OptionValue; + + fn expiry_options(micros: u64) -> ResourceOptions { + ResourceOptions::from([( + "message_expiry".to_owned(), + OptionValue { + value: micros.to_string(), + }, + )]) + } + + #[test] + fn given_zero_or_max_expiry_should_render_never() { + assert_eq!(message_expiry(&expiry_options(0)), "never"); + assert_eq!(message_expiry(&expiry_options(u64::MAX)), "never"); + assert_eq!(message_expiry(&ResourceOptions::new()), "never"); + } + + #[test] + fn given_expiry_in_micros_should_render_largest_units_first() { + let micros = (86_400 + 3_600 + 5) * MICROS_PER_SECOND; + assert_eq!(message_expiry(&expiry_options(micros)), "1d 1h 5s"); + } + + #[test] + fn given_utf8_payload_should_render_text() { + assert_eq!(payload(&STANDARD.encode("hello")), "hello"); + } + + #[test] + fn given_binary_payload_should_render_hex() { + assert_eq!(payload(&STANDARD.encode([0xff, 0x00, 0x10])), "ff 00 10"); + } + + #[test] + fn given_message_id_should_render_uuid_form() { + assert_eq!( + message_id(0x0123_4567_89ab_cdef_0123_4567_89ab_cdef), + "01234567-89ab-cdef-0123-456789abcdef" + ); + assert_eq!(message_id(1), "00000000-0000-0000-0000-000000000001"); + } + + #[test] + fn given_long_payload_should_preview_from_a_prefix() { + let text = "zażółć gęślą jaźń ".repeat(20); + let preview = payload_preview(&STANDARD.encode(&text), 10); + assert_eq!(preview, "zażółć gęś…"); + + let binary: Vec = (0xf8..=0xff).cycle().take(4096).collect(); + let preview = payload_preview(&STANDARD.encode(&binary), 11); + assert_eq!(preview, "f8 f9 fa fb…"); + } + + #[test] + fn given_short_payload_should_preview_all_of_it() { + assert_eq!(payload_preview(&STANDARD.encode("hello"), 60), "hello"); + assert_eq!(payload_preview(&STANDARD.encode([0xff, 0x00]), 60), "ff 00"); + } + + #[test] + fn given_prefix_cut_inside_a_character_should_keep_it_as_text() { + // 60 two-byte characters make a prefix cut inside a character likely. + let text = "ł".repeat(400); + let preview = payload_preview(&STANDARD.encode(&text), 60); + assert_eq!(preview, format!("{}…", "ł".repeat(60))); + } + + #[test] + fn given_long_text_should_truncate_on_char_boundary() { + assert_eq!(truncate("zażółć", 3), "zaż…"); + assert_eq!(truncate("short", 10), "short"); + } + + #[test] + fn given_micros_should_format_utc() { + assert_eq!(timestamp(1_700_000_000_123_456), "2023-11-14 22:13:20 UTC"); + assert_eq!( + timestamp_precise(1_700_000_000_123_456), + "2023-11-14 22:13:20.123456 UTC" + ); + } +} diff --git a/core/web_ui/src/lib.rs b/core/web_ui/src/lib.rs new file mode 100644 index 0000000000..54f5957824 --- /dev/null +++ b/core/web_ui/src/lib.rs @@ -0,0 +1,71 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +mod api; +mod auth; +mod components; +mod format; +mod models; +mod pages; + +use dioxus::prelude::*; + +use crate::auth::Session; +use crate::pages::{DashboardLayout, Messages, NotFound, SignIn, StreamPage, Streams, TopicPage}; + +const STYLESHEET: Asset = asset!("/assets/main.css"); +const FAVICON: Asset = asset!("/assets/favicon.svg"); +const LOGO: Asset = asset!("/assets/iggy-apache-darkbg.svg"); + +#[derive(Debug, Clone, PartialEq, Routable)] +#[rustfmt::skip] +pub enum Route { + #[redirect("/", || Route::SignIn {})] + #[route("/auth/sign-in")] + SignIn {}, + #[layout(DashboardLayout)] + #[redirect("/dashboard", || Route::Streams {})] + #[route("/dashboard/streams")] + Streams {}, + #[route("/dashboard/streams/:stream_id")] + StreamPage { stream_id: u32 }, + #[route("/dashboard/streams/:stream_id/topics/:topic_id")] + TopicPage { stream_id: u32, topic_id: u32 }, + #[route("/dashboard/streams/:stream_id/topics/:topic_id/partitions/:partition_id/messages?:offset")] + Messages { stream_id: u32, topic_id: u32, partition_id: u32, offset: Option }, + #[end_layout] + #[route("/:..segments")] + NotFound { segments: Vec }, +} + +/// Starts the app in the browser. The binary is a thin wrapper so that tools +/// such as the license manifest, which only follow library targets, see the +/// crate's dependencies. +pub fn launch() { + dioxus::launch(App); +} + +#[component] +fn App() -> Element { + use_context_provider(|| Signal::new(Session::restore())); + + rsx! { + document::Link { rel: "icon", r#type: "image/svg+xml", href: FAVICON } + document::Stylesheet { href: STYLESHEET } + Router:: {} + } +} diff --git a/core/web_ui/src/main.rs b/core/web_ui/src/main.rs new file mode 100644 index 0000000000..832ad45028 --- /dev/null +++ b/core/web_ui/src/main.rs @@ -0,0 +1,20 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +fn main() { + iggy_web_ui::launch(); +} diff --git a/core/web_ui/src/models.rs b/core/web_ui/src/models.rs new file mode 100644 index 0000000000..e332a5125e --- /dev/null +++ b/core/web_ui/src/models.rs @@ -0,0 +1,333 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//! Mirrors of the server's HTTP JSON shapes. +//! +//! `iggy_common` owns the real types but pulls tokio with `net`, which does +//! not build for `wasm32-unknown-unknown`, so the UI keeps its own copies of +//! the fields it renders, under the server's type names. Unknown fields are +//! ignored on purpose. The tests below decode the server's serialized types +//! into each mirror, so a renamed field fails on the host. + +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize)] +pub struct LoginUser { + pub username: String, + pub password: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct IdentityInfo { + pub user_id: u32, + pub access_token: TokenInfo, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct TokenInfo { + pub token: String, + pub expiry: u64, +} + +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct UserInfoDetails { + pub username: String, +} + +/// One entry of a resource's `options` map, as written by +/// `iggy_common::resource_options_json`. +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct OptionValue { + pub value: String, +} + +pub type ResourceOptions = BTreeMap; + +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct Stream { + pub id: u32, + pub created_at: u64, + pub name: String, + pub size: String, + pub messages_count: u64, + pub topics_count: u32, +} + +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct StreamDetails { + pub id: u32, + pub created_at: u64, + pub name: String, + pub size: String, + pub messages_count: u64, + pub topics_count: u32, + pub topics: Vec, +} + +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct Topic { + pub id: u32, + pub created_at: u64, + pub name: String, + pub size: String, + pub messages_count: u64, + pub partitions_count: u32, + #[serde(default)] + pub options: ResourceOptions, +} + +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct TopicDetails { + pub id: u32, + pub created_at: u64, + pub name: String, + pub size: String, + pub messages_count: u64, + pub partitions_count: u32, + pub partitions: Vec, + #[serde(default)] + pub options: ResourceOptions, +} + +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct Partition { + pub id: u32, + pub created_at: u64, + pub segments_count: u32, + pub current_offset: u64, + pub size: String, + pub messages_count: u64, +} + +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct PolledMessages { + /// Offset of the newest message in the partition when the poll ran. + pub current_offset: u64, + pub messages: Vec, +} + +/// `Eq` lets `Rc` props compare by pointer before comparing +/// payloads. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +pub struct IggyMessage { + pub header: IggyMessageHeader, + /// Standard padded base64. + pub payload: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +pub struct IggyMessageHeader { + pub id: u128, + pub offset: u64, + pub timestamp: u64, + pub payload_length: u32, +} + +#[cfg(all(test, not(target_arch = "wasm32")))] +mod tests { + use base64::Engine; + use base64::engine::general_purpose::STANDARD; + use iggy_common::{ + HeaderKey, HeaderValue, IggyByteSize, IggyTimestamp, OptionKey, PartitionContext, + UserStatus, + }; + use serde::de::DeserializeOwned; + + use super::*; + + const CREATED_AT: u64 = 1_700_000_000_000_000; + const MESSAGE_EXPIRY_MICROS: u64 = 86_400_000_000; + + #[test] + fn given_server_login_reply_should_decode_identity() { + let server = iggy_common::IdentityInfo { + user_id: 7, + access_token: Some(iggy_common::TokenInfo { + token: "secret".to_owned(), + expiry: 1_800_000_000, + }), + }; + let mirror: IdentityInfo = decode(&server); + assert_eq!(mirror.user_id, 7); + assert_eq!(mirror.access_token.token, "secret"); + assert_eq!(mirror.access_token.expiry, 1_800_000_000); + } + + #[test] + fn given_ui_login_should_decode_as_server_login_user() { + let mirror = LoginUser { + username: "iggy".to_owned(), + password: "password".to_owned(), + }; + let server: iggy_common::login_user::LoginUser = decode(&mirror); + assert_eq!(server.username, "iggy"); + } + + #[test] + fn given_server_user_should_decode_user_info_details() { + let server = iggy_common::UserInfoDetails { + id: 1, + created_at: IggyTimestamp::from(CREATED_AT), + status: UserStatus::Active, + username: "iggy".to_owned(), + permissions: None, + options: Default::default(), + }; + let mirror: UserInfoDetails = decode(&server); + assert_eq!(mirror.username, "iggy"); + } + + #[test] + fn given_server_streams_should_decode_stream_list() { + let server = vec![iggy_common::Stream { + id: 1, + created_at: IggyTimestamp::from(CREATED_AT), + name: "orders".to_owned(), + size: IggyByteSize::from(2_048), + messages_count: 10, + topics_count: 2, + options: Default::default(), + }]; + let mirror: Vec = decode(&server); + assert_eq!( + mirror, + [Stream { + id: 1, + created_at: CREATED_AT, + name: "orders".to_owned(), + size: "2 KiB".to_owned(), + messages_count: 10, + topics_count: 2, + }] + ); + } + + #[test] + fn given_server_stream_details_should_decode_topics() { + let server = iggy_common::StreamDetails { + id: 1, + created_at: IggyTimestamp::from(CREATED_AT), + name: "orders".to_owned(), + size: IggyByteSize::from(2_048), + messages_count: 10, + topics_count: 1, + topics: vec![server_topic()], + options: Default::default(), + }; + let mirror: StreamDetails = decode(&server); + assert_eq!(mirror.name, "orders"); + assert_eq!(mirror.topics.len(), 1); + let topic = &mirror.topics[0]; + assert_eq!((topic.id, topic.partitions_count), (3, 1)); + assert_eq!( + topic.options["message_expiry"].value, + MESSAGE_EXPIRY_MICROS.to_string() + ); + } + + #[test] + fn given_server_topic_details_should_decode_partitions() { + let topic = server_topic(); + let server = iggy_common::TopicDetails { + id: topic.id, + created_at: topic.created_at, + name: topic.name, + size: topic.size, + messages_count: topic.messages_count, + partitions_count: topic.partitions_count, + partitions: vec![iggy_common::Partition { + id: 0, + created_at: IggyTimestamp::from(CREATED_AT), + segments_count: 1, + current_offset: 9, + size: IggyByteSize::from(1_024), + messages_count: 10, + context: PartitionContext::default(), + }], + options: topic.options, + }; + let mirror: TopicDetails = decode(&server); + assert_eq!( + mirror.partitions, + [Partition { + id: 0, + created_at: CREATED_AT, + segments_count: 1, + current_offset: 9, + size: "1 KiB".to_owned(), + messages_count: 10, + }] + ); + assert_eq!( + mirror.options["message_expiry"].value, + MESSAGE_EXPIRY_MICROS.to_string() + ); + } + + #[test] + fn given_server_polled_messages_should_decode_payload_and_header() { + let mut message = iggy_common::IggyMessage::builder() + .id(42) + .payload("hello".into()) + .build() + .expect("valid message"); + message.header.offset = 5; + message.header.timestamp = CREATED_AT; + let mut server = iggy_common::PolledMessages::empty(); + server.current_offset = 9; + server.count = 1; + server.messages = vec![message]; + + let mirror: PolledMessages = decode(&server); + assert_eq!(mirror.current_offset, 9); + assert_eq!( + mirror.messages, + [IggyMessage { + header: IggyMessageHeader { + id: 42, + offset: 5, + timestamp: CREATED_AT, + payload_length: 5, + }, + payload: STANDARD.encode("hello"), + }] + ); + } + + fn server_topic() -> iggy_common::Topic { + let options = iggy_common::ResourceOptions::from([( + HeaderKey::from(OptionKey::MessageExpiry), + iggy_common::OptionValue::explicit(HeaderValue::from(MESSAGE_EXPIRY_MICROS)), + )]); + iggy_common::Topic { + id: 3, + created_at: IggyTimestamp::from(CREATED_AT), + name: "events".to_owned(), + size: IggyByteSize::from(1_024), + messages_count: 10, + partitions_count: 1, + options, + } + } + + fn decode(value: &impl Serialize) -> T { + let json = serde_json::to_string(value).expect("serializable"); + serde_json::from_str(&json).expect("the mirror decodes the server's JSON") + } +} diff --git a/core/web_ui/src/pages/layout.rs b/core/web_ui/src/pages/layout.rs new file mode 100644 index 0000000000..d91be9fc30 --- /dev/null +++ b/core/web_ui/src/pages/layout.rs @@ -0,0 +1,75 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use dioxus::prelude::*; + +use crate::components::{api_client, use_session}; +use crate::{LOGO, Route}; + +/// Shell for every signed-in page. Without a session it redirects to sign-in. +#[component] +pub fn DashboardLayout() -> Element { + let mut session = use_session(); + let navigator = navigator(); + + use_effect(move || { + if session.read().is_none() { + navigator.replace(Route::SignIn {}); + } + }); + + let user = use_resource(move || async move { + let user_id = session.read().as_ref()?.user_id; + api_client(session).user(user_id).await.ok() + }); + + let sign_out = move |_| async move { + // Logout does not revoke the bearer, so the token stays valid on the + // server until `expiry` whether or not this call succeeds. + let _ = api_client(session).logout().await; + let signed_out = session.write().take(); + if let Some(signed_out) = signed_out { + signed_out.forget(); + } + }; + + if session.read().is_none() { + return rsx! {}; + } + + let username = user + .read() + .as_ref() + .and_then(|user| user.as_ref().map(|user| user.username.clone())) + .unwrap_or_default(); + + rsx! { + div { class: "dashboard", + aside { class: "sidebar", + img { class: "sidebar-logo", src: LOGO, alt: "Apache Iggy" } + nav { + Link { class: "nav-link", active_class: "active", to: Route::Streams {}, "Streams" } + } + div { class: "sidebar-footer", + span { class: "muted", "{username}" } + button { class: "secondary", onclick: sign_out, "Sign out" } + } + } + main { class: "content", Outlet:: {} } + } + } +} diff --git a/core/web_ui/src/pages/messages.rs b/core/web_ui/src/pages/messages.rs new file mode 100644 index 0000000000..91ba50de4b --- /dev/null +++ b/core/web_ui/src/pages/messages.rs @@ -0,0 +1,206 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use std::rc::Rc; + +use dioxus::prelude::*; + +use crate::Route; +use crate::api::ApiError; +use crate::components::{Crumb, ErrorView, Loading, PageHeader, api_client, use_session}; +use crate::format; +use crate::models::IggyMessage; + +const PAGE_SIZE: u32 = 20; +const PAYLOAD_PREVIEW_CHARS: usize = 60; + +#[derive(Debug, Clone, PartialEq)] +struct MessagesPage { + topic_name: String, + /// Offset the page was polled from. Retention can make the first message + /// later than this. + requested_offset: u64, + /// Newest offset in the partition, taken from the poll reply so it always + /// agrees with `messages`. + current_offset: u64, + messages: Vec>, +} + +/// `offset` is the first offset of the page. `None` follows the newest page, +/// so reloading picks up new messages. +#[component] +pub fn Messages(stream_id: u32, topic_id: u32, partition_id: u32, offset: Option) -> Element { + let session = use_session(); + let navigator = navigator(); + + let mut page = use_resource(use_reactive!(|( + stream_id, + topic_id, + partition_id, + offset, + )| async move { + let client = api_client(session); + let topic = client.topic(stream_id, topic_id).await?; + let partition = topic + .partitions + .iter() + .find(|partition| partition.id == partition_id) + .ok_or(ApiError::NotFound)?; + let requested_offset = offset.unwrap_or_else(|| { + partition + .current_offset + .saturating_sub(u64::from(PAGE_SIZE) - 1) + }); + + let polled = client + .poll_messages( + stream_id, + topic_id, + partition_id, + requested_offset, + PAGE_SIZE, + ) + .await?; + Ok::<_, ApiError>(MessagesPage { + topic_name: topic.name, + requested_offset, + current_offset: polled.current_offset, + messages: polled.messages.into_iter().map(Rc::new).collect(), + }) + })); + + let mut show = move |target: Option| { + // A second click would restart the poll in flight and fetch the same + // page again. + if page.pending() { + return; + } + if target == offset { + page.restart(); + } else { + navigator.push(Route::Messages { + stream_id, + topic_id, + partition_id, + offset: target, + }); + } + }; + + let header = rsx! { + PageHeader { title: format!("Partition {partition_id} messages"), + Crumb { to: Route::Streams {}, label: "Streams" } + Crumb { to: Route::StreamPage { stream_id }, label: format!("Stream {stream_id}") } + Crumb { to: Route::TopicPage { stream_id, topic_id }, label: format!("Topic {topic_id}") } + } + }; + + let body = match &*page.read() { + None => rsx! { Loading {} }, + Some(Err(error)) => rsx! { ErrorView { error: error.clone() } }, + Some(Ok(loaded)) => match (loaded.messages.first(), loaded.messages.last()) { + (Some(first), Some(last)) => { + let first_offset = first.header.offset; + let shown_last_offset = last.header.offset; + let current_offset = loaded.current_offset; + // The server skipped ahead of the requested offset, so + // nothing older is left. + let at_oldest = first_offset > loaded.requested_offset; + let has_older = first_offset > 0 && !at_oldest; + let has_newer = shown_last_offset < current_offset; + rsx! { + div { class: "pager", + span { class: "muted", + "Topic {loaded.topic_name}, showing offsets {first_offset} to {shown_last_offset} of {current_offset}" + } + button { + class: "secondary", + disabled: !has_older, + onclick: move |_| show(Some(first_offset.saturating_sub(u64::from(PAGE_SIZE)))), + "Older" + } + button { + class: "secondary", + disabled: !has_newer, + onclick: move |_| show(Some(shown_last_offset + 1)), + "Newer" + } + button { class: "secondary", onclick: move |_| show(None), "Latest" } + } + table { + thead { + tr { + th { class: "num", "Offset" } + th { "Timestamp" } + th { "ID" } + th { class: "num", "Bytes" } + th { "Payload" } + } + } + tbody { + for message in loaded.messages.iter().rev() { + MessageRow { key: "{message.header.offset}", message: Rc::clone(message) } + } + } + } + } + } + _ if loaded.requested_offset == 0 => rsx! { + p { class: "muted", "Topic {loaded.topic_name} has no messages in this partition." } + }, + _ => rsx! { + div { class: "pager", + span { class: "muted", + "Topic {loaded.topic_name} has no messages from offset {loaded.requested_offset}." + } + button { class: "secondary", onclick: move |_| show(None), "Latest" } + } + }, + }, + }; + + rsx! { + {header} + {body} + } +} + +#[component] +fn MessageRow(message: Rc) -> Element { + // The full payload can be up to 64 MB, so it is decoded only once the + // row is opened. + let mut open = use_signal(|| false); + let preview = format::payload_preview(&message.payload, PAYLOAD_PREVIEW_CHARS); + let header = &message.header; + + rsx! { + tr { + td { class: "num", "{header.offset}" } + td { {format::timestamp_precise(header.timestamp)} } + td { class: "mono", {format::message_id(header.id)} } + td { class: "num", "{header.payload_length}" } + td { + details { ontoggle: move |_| open.toggle(), + summary { class: "mono", "{preview}" } + if open() { + pre { {format::payload(&message.payload)} } + } + } + } + } + } +} diff --git a/core/web_ui/src/pages/mod.rs b/core/web_ui/src/pages/mod.rs new file mode 100644 index 0000000000..6fd17b98c4 --- /dev/null +++ b/core/web_ui/src/pages/mod.rs @@ -0,0 +1,30 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +mod layout; +mod messages; +mod not_found; +mod sign_in; +mod streams; +mod topic; + +pub use layout::DashboardLayout; +pub use messages::Messages; +pub use not_found::NotFound; +pub use sign_in::SignIn; +pub use streams::{StreamPage, Streams}; +pub use topic::TopicPage; diff --git a/core/web_ui/src/pages/not_found.rs b/core/web_ui/src/pages/not_found.rs new file mode 100644 index 0000000000..083d3eb6c8 --- /dev/null +++ b/core/web_ui/src/pages/not_found.rs @@ -0,0 +1,34 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use dioxus::prelude::*; + +use crate::Route; + +#[component] +pub fn NotFound(segments: Vec) -> Element { + let path = segments.join("/"); + rsx! { + main { class: "auth", + div { class: "auth-card", + h1 { "Page not found" } + p { class: "muted", "/{path}" } + Link { to: Route::Streams {}, "Back to the dashboard" } + } + } + } +} diff --git a/core/web_ui/src/pages/sign_in.rs b/core/web_ui/src/pages/sign_in.rs new file mode 100644 index 0000000000..f46e41d89c --- /dev/null +++ b/core/web_ui/src/pages/sign_in.rs @@ -0,0 +1,93 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use dioxus::prelude::*; + +use crate::api::ApiClient; +use crate::auth::Session; +use crate::components::use_session; +use crate::models::LoginUser; +use crate::{LOGO, Route}; + +#[component] +pub fn SignIn() -> Element { + let mut session = use_session(); + let navigator = navigator(); + let mut username = use_signal(String::new); + let mut password = use_signal(String::new); + let mut error = use_signal(|| None::); + let mut submitting = use_signal(|| false); + + use_effect(move || { + if session.read().is_some() { + navigator.replace(Route::Streams {}); + } + }); + + let submit = move |event: FormEvent| async move { + event.prevent_default(); + submitting.set(true); + let credentials = LoginUser { + username: username(), + password: password(), + }; + let result = ApiClient::default().login(&credentials).await; + submitting.set(false); + + match result { + Ok(identity) => { + let new_session = Session::from(identity); + new_session.persist(); + session.set(Some(new_session)); + } + Err(failure) => error.set(Some(failure.to_string())), + } + }; + + rsx! { + main { class: "auth", + form { class: "auth-card", onsubmit: submit, + img { class: "auth-logo", src: LOGO, alt: "Apache Iggy" } + label { + "Username" + input { + name: "username", + autocomplete: "username", + required: true, + value: username, + oninput: move |event| username.set(event.value()), + } + } + label { + "Password" + input { + name: "password", + r#type: "password", + autocomplete: "current-password", + required: true, + value: password, + oninput: move |event| password.set(event.value()), + } + } + if let Some(message) = error() { + p { class: "form-error", "{message}" } + } + button { r#type: "submit", disabled: submitting(), "Sign in" } + } + } + } +} diff --git a/core/web_ui/src/pages/streams.rs b/core/web_ui/src/pages/streams.rs new file mode 100644 index 0000000000..37dc3df4ae --- /dev/null +++ b/core/web_ui/src/pages/streams.rs @@ -0,0 +1,135 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use dioxus::prelude::*; + +use crate::Route; +use crate::api::ApiError; +use crate::components::{Crumb, ErrorView, Loading, PageHeader, Stat, api_client, use_session}; +use crate::format; + +#[component] +pub fn Streams() -> Element { + let session = use_session(); + let streams = use_resource(move || async move { + let mut streams = api_client(session).streams().await?; + streams.sort_by_key(|stream| stream.id); + Ok::<_, ApiError>(streams) + }); + + let body = match &*streams.read() { + None => rsx! { Loading {} }, + Some(Err(error)) => rsx! { ErrorView { error: error.clone() } }, + Some(Ok(streams)) if streams.is_empty() => rsx! { p { class: "muted", "No streams yet." } }, + Some(Ok(streams)) => rsx! { + table { + thead { + tr { + th { "ID" } + th { "Name" } + th { class: "num", "Topics" } + th { class: "num", "Messages" } + th { class: "num", "Size" } + th { "Created" } + } + } + tbody { + for stream in streams { + tr { key: "{stream.id}", + td { "{stream.id}" } + td { + Link { to: Route::StreamPage { stream_id: stream.id }, "{stream.name}" } + } + td { class: "num", "{stream.topics_count}" } + td { class: "num", "{stream.messages_count}" } + td { class: "num", "{stream.size}" } + td { {format::timestamp(stream.created_at)} } + } + } + } + } + }, + }; + + rsx! { + PageHeader { title: "Streams" } + {body} + } +} + +#[component] +pub fn StreamPage(stream_id: u32) -> Element { + let session = use_session(); + let stream = use_resource(use_reactive!(|(stream_id,)| async move { + api_client(session).stream(stream_id).await + })); + + let body = match &*stream.read() { + None => rsx! { Loading {} }, + Some(Err(error)) => rsx! { ErrorView { error: error.clone() } }, + Some(Ok(stream)) => rsx! { + PageHeader { title: stream.name.clone(), + Crumb { to: Route::Streams {}, label: "Streams" } + } + div { class: "stats", + Stat { label: "ID", value: stream.id.to_string() } + Stat { label: "Topics", value: stream.topics_count.to_string() } + Stat { label: "Messages", value: stream.messages_count.to_string() } + Stat { label: "Size", value: stream.size.clone() } + Stat { label: "Created", value: format::timestamp(stream.created_at) } + } + h2 { "Topics" } + if stream.topics.is_empty() { + p { class: "muted", "No topics in this stream." } + } else { + table { + thead { + tr { + th { "ID" } + th { "Name" } + th { class: "num", "Partitions" } + th { class: "num", "Messages" } + th { class: "num", "Size" } + th { "Message expiry" } + th { "Created" } + } + } + tbody { + for topic in &stream.topics { + tr { key: "{topic.id}", + td { "{topic.id}" } + td { + Link { + to: Route::TopicPage { stream_id, topic_id: topic.id }, + "{topic.name}" + } + } + td { class: "num", "{topic.partitions_count}" } + td { class: "num", "{topic.messages_count}" } + td { class: "num", "{topic.size}" } + td { {format::message_expiry(&topic.options)} } + td { {format::timestamp(topic.created_at)} } + } + } + } + } + } + }, + }; + + rsx! { {body} } +} diff --git a/core/web_ui/src/pages/topic.rs b/core/web_ui/src/pages/topic.rs new file mode 100644 index 0000000000..428f90c4fd --- /dev/null +++ b/core/web_ui/src/pages/topic.rs @@ -0,0 +1,83 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use dioxus::prelude::*; + +use crate::Route; +use crate::components::{Crumb, ErrorView, Loading, PageHeader, Stat, api_client, use_session}; +use crate::format; + +#[component] +pub fn TopicPage(stream_id: u32, topic_id: u32) -> Element { + let session = use_session(); + let topic = use_resource(use_reactive!(|(stream_id, topic_id)| async move { + api_client(session).topic(stream_id, topic_id).await + })); + + let body = match &*topic.read() { + None => rsx! { Loading {} }, + Some(Err(error)) => rsx! { ErrorView { error: error.clone() } }, + Some(Ok(topic)) => rsx! { + PageHeader { title: topic.name.clone(), + Crumb { to: Route::Streams {}, label: "Streams" } + Crumb { to: Route::StreamPage { stream_id }, label: format!("Stream {stream_id}") } + } + div { class: "stats", + Stat { label: "ID", value: topic.id.to_string() } + Stat { label: "Partitions", value: topic.partitions_count.to_string() } + Stat { label: "Messages", value: topic.messages_count.to_string() } + Stat { label: "Size", value: topic.size.clone() } + Stat { label: "Message expiry", value: format::message_expiry(&topic.options) } + Stat { label: "Created", value: format::timestamp(topic.created_at) } + } + h2 { "Partitions" } + table { + thead { + tr { + th { "ID" } + th { class: "num", "Current offset" } + th { class: "num", "Segments" } + th { class: "num", "Messages" } + th { class: "num", "Size" } + th { "Created" } + th {} + } + } + tbody { + for partition in &topic.partitions { + tr { key: "{partition.id}", + td { "{partition.id}" } + td { class: "num", "{partition.current_offset}" } + td { class: "num", "{partition.segments_count}" } + td { class: "num", "{partition.messages_count}" } + td { class: "num", "{partition.size}" } + td { {format::timestamp(partition.created_at)} } + td { + Link { + to: Route::Messages { stream_id, topic_id, partition_id: partition.id, offset: None }, + "Browse messages" + } + } + } + } + } + } + }, + }; + + rsx! { {body} } +} diff --git a/scripts/build-web-ui.sh b/scripts/build-web-ui.sh new file mode 100755 index 0000000000..17b71aa532 --- /dev/null +++ b/scripts/build-web-ui.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +set -Eeuo pipefail + +# Builds the Dioxus web UI into core/web_ui/dist. iggy-server does not embed +# this bundle yet, the switch-over from the Svelte app will make it do so. + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +CRATE_DIR="${REPO_ROOT}/core/web_ui" + +# dx stages under the cargo target directory, which CARGO_TARGET_DIR or a +# cargo config can move, so ask cargo the same way dx does. Plain bash parses +# the JSON so the script needs no jq. +metadata="$(cargo metadata --format-version 1 --no-deps --manifest-path "${CRATE_DIR}/Cargo.toml")" +if [[ ! "${metadata}" =~ \"target_directory\":\"([^\"]+)\" ]]; then + echo "cargo metadata did not report a target_directory" >&2 + exit 1 +fi +TARGET_DIR="${BASH_REMATCH[1]}" + +# dx keeps every previously hashed asset in its staging directory and copies +# all of them into the bundle, so stale builds would end up in the bundle. +rm -rf "${TARGET_DIR}/dx/iggy-web-ui" "${CRATE_DIR}/dist" + +cd "${CRATE_DIR}" +dx bundle --platform web --release --out-dir dist