codify "platform" into resolution logic, server-fn/fullstack overhaul (#4596)

overhaul dioxus fullstack using a new implementation of server_fn

This PR is a complete rewrite of dioxus fullstack designed to improve compatibility with axum and to fix a number of compilation errors based on feature flag unification plumbing through macros.
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2025-10-03 18:26:19 -07:00
1 parent 8421c3aef0
commit f2b1c6d97f
424 files changed
+17089 -6429

No files matched your search

@@ -12,3 +12,6 @@ runs:
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/local/lib/android
sudo rm -rf /usr/share/swift
sudo docker image prune --all --force || true
+35 -154
View File
@@ -45,7 +45,7 @@ jobs:
check-msrv:
if: github.event.pull_request.draft == false
name: Check MSRV
runs-on: ubuntu-24.04
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@1.86.0
@@ -66,7 +66,7 @@ jobs:
test:
if: github.event.pull_request.draft == false
name: Test Suite
runs-on: ubuntu-24.04
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v5
- name: Free Disk Space
@@ -87,7 +87,7 @@ jobs:
release-test:
if: github.event.pull_request.draft == false
name: Test Suite with Optimizations
runs-on: ubuntu-24.04
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v5
- name: Free Disk Space
@@ -108,7 +108,7 @@ jobs:
fmt:
if: github.event.pull_request.draft == false
name: Rustfmt
runs-on: ubuntu-24.04
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@1.86.0
@@ -122,7 +122,7 @@ jobs:
docs:
if: github.event.pull_request.draft == false
name: Docs
runs-on: ubuntu-24.04
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v5
- uses: awalsh128/cache-apt-pkgs-action@latest
@@ -145,7 +145,7 @@ jobs:
test-docs:
if: github.event.pull_request.draft == false
name: Test Docs
runs-on: ubuntu-24.04
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v5
- uses: awalsh128/cache-apt-pkgs-action@latest
@@ -166,7 +166,7 @@ jobs:
check:
if: github.event.pull_request.draft == false
name: Check
runs-on: ubuntu-24.04
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v5
- uses: awalsh128/cache-apt-pkgs-action@latest
@@ -182,7 +182,7 @@ jobs:
clippy:
if: github.event.pull_request.draft == false
name: Clippy
runs-on: ubuntu-24.04
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v5
- uses: awalsh128/cache-apt-pkgs-action@latest
@@ -202,7 +202,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-24.04, macos-latest]
os: [blacksmith-4vcpu-ubuntu-2404, macos-latest]
steps:
- uses: actions/checkout@v5
- uses: nixbuild/nix-quick-install-action@master
@@ -220,7 +220,7 @@ jobs:
playwright:
if: github.event.pull_request.draft == false
name: Playwright Tests
runs-on: macos-latest
runs-on: blacksmith-8vcpu-ubuntu-2404
strategy:
matrix:
platform:
@@ -229,6 +229,9 @@ jobs:
steps:
# Do our best to cache the toolchain and node install steps
- uses: actions/checkout@v5
- name: Free Disk Space
if: ${{ matrix.platform.os == 'ubuntu-24.04' || matrix.platform.os == 'ubuntu-24.04-arm' || matrix.platform.os == 'blacksmith-4vcpu-ubuntu-2404' || matrix.platform.os == 'blacksmith-4vcpu-ubuntu-2404-arm' }}
uses: ./.github/actions/free-disk-space
- uses: actions/setup-node@v4
with:
node-version: 24
@@ -297,18 +300,9 @@ jobs:
args: "--package dioxus-desktop",
platform: "ios",
}
- {
target: x86_64-unknown-linux-gnu,
os: ubuntu-24.04,
toolchain: "1.86.0",
cross: false,
command: "build",
args: "--all --tests",
platform: "desktop",
}
- {
target: aarch64-unknown-linux-gnu,
os: ubuntu-24.04-arm,
os: blacksmith-4vcpu-ubuntu-2404-arm,
toolchain: "1.86.0",
cross: false,
command: "build",
@@ -317,16 +311,36 @@ jobs:
}
- {
target: aarch64-linux-android,
os: ubuntu-24.04,
os: blacksmith-4vcpu-ubuntu-2404,
toolchain: "1.86.0",
cross: true,
command: "build",
args: "--package dioxus-desktop",
platform: "android",
}
# commented out because it's having issues with space on the device, but we already test it above
# - {
# target: x86_64-unknown-linux-gnu,
# os: ubuntu-24.04,
# toolchain: "1.86.0",
# cross: false,
# command: "build",
# args: "--all --tests",
# platform: "desktop",
# }
steps:
- uses: actions/checkout@v5
- name: Free Disk Space
if: ${{ matrix.platform.os == 'ubuntu-24.04' || matrix.platform.os == 'ubuntu-24.04-arm' || matrix.platform.os == 'blacksmith-4vcpu-ubuntu-2404' || matrix.platform.os == 'blacksmith-4vcpu-ubuntu-2404-arm' }}
uses: ./.github/actions/free-disk-space
- uses: awalsh128/cache-apt-pkgs-action@latest
if: ${{ matrix.platform.os == 'ubuntu-24.04' || matrix.platform.os == 'ubuntu-24.04-arm' || matrix.platform.os == 'blacksmith-4vcpu-ubuntu-2404' || matrix.platform.os == 'blacksmith-4vcpu-ubuntu-2404-arm' }}
with:
packages: libwebkit2gtk-4.1-dev libgtk-3-dev libasound2-dev libudev-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev
version: ${{ matrix.platform.target }}-${{ matrix.platform.os }} # disambiguate since we're in a matrix and this caching action doesn't factor in these variables
- name: install stable
uses: dtolnay/rust-toolchain@master
with:
@@ -338,16 +352,6 @@ jobs:
if: ${{ matrix.platform.target == 'x86_64-pc-windows-msvc' }}
uses: ilammy/setup-nasm@v1
- name: Free Disk Space
if: ${{ matrix.platform.os == 'ubuntu-24.04' || matrix.platform.os == 'ubuntu-24.04-arm' }}
uses: ./.github/actions/free-disk-space
- uses: awalsh128/cache-apt-pkgs-action@latest
if: ${{ matrix.platform.os == 'ubuntu-24.04' || matrix.platform.os == 'ubuntu-24.04-arm' }}
with:
packages: libwebkit2gtk-4.1-dev libgtk-3-dev libasound2-dev libudev-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev
version: ${{ matrix.platform.target }}-${{ matrix.platform.os }} # disambiguate since we're in a matrix and this caching action doesn't factor in these variables
- name: Install cross
if: ${{ matrix.platform.cross == true }}
uses: taiki-e/install-action@cross
@@ -386,126 +390,3 @@ jobs:
working-directory: ${{ env.UV_WORKSPACE }}
run: |
cargo test --workspace --tests
# Only run semver checks if the PR is not a draft and does not have the breaking label
# Breaking PRs don't need to follow semver since they are breaking changes
# However, this means we won't attempt to backport them, so you should be careful about using this label, as it will
# likely make future backporting difficult
#
# todo: fix this so even if breaking changes have been merged, the fix can be backported
#
# This will stop working once the first breaking change has been merged, so we should really try to just backport the fix
# and *then* run the semver checks. Basically "would backporting this PR cause a breaking change on stable?"
#
# semver:
# if: github.event.pull_request.draft == false && !contains(github.event.pull_request.labels.*.name, 'breaking')
# name: Semver Check
# runs-on: ubuntu-24.04
# steps:
# - uses: actions/checkout@v5
# - uses: dtolnay/rust-toolchain@1.86.0
# - uses: Swatinem/rust-cache@v2
# with:
# cache-all-crates: "true"
# cache-on-failure: "true"
# - name: Check semver
# uses: obi1kenobi/cargo-semver-checks-action@v2
# with:
# manifest-path: ./Cargo.toml
# exclude: "dioxus-cli, dioxus-ext"
# Releases for the CLI on a per-branch basis
# nightly-cli:
# if: github.event.pull_request.draft == false
# runs-on: ${{ matrix.platform.os }}
# strategy:
# matrix:
# platform:
# - {
# target: x86_64-pc-windows-msvc,
# os: windows-latest
# }
# - {
# target: aarch64-pc-windows-msvc,
# os: windows-latest
# }
# - {
# target: x86_64-apple-darwin,
# os: macos-13
# }
# - {
# target: aarch64-apple-darwin,
# os: macos-latest
# }
# - {
# target: x86_64-unknown-linux-gnu,
# os: ubuntu-24.04
# }
# - {
# target: aarch64-unknown-linux-gnu,
# os: ubuntu-24.04-arm
# }
# - {
# target: x86_64-unknown-linux-musl,
# os: ubuntu-24.04
# }
# - {
# target: aarch64-unknown-linux-musl,
# os: ubuntu-24.04-arm
# }
# steps:
# - name: Checkout
# uses: actions/checkout@v5
# - name: Install openssl on macos
# if: matrix.platform.os == 'macos-latest'
# run: brew install openssl
# - name: Install nasm for windows (tls)
# if: ${{ matrix.platform.target == 'x86_64-pc-windows-msvc' }}
# uses: ilammy/setup-nasm@v1
# - name: Free Disk Space
# if: ${{ matrix.platform.os == 'ubuntu-24.04' || matrix.platform.os == 'ubuntu-24.04-arm' }}
# uses: ./.github/actions/free-disk-space
# - uses: awalsh128/cache-apt-pkgs-action@latest
# if: ${{ matrix.platform.os == 'ubuntu-24.04' || matrix.platform.os == 'ubuntu-24.04-arm' }}
# with:
# packages: libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev musl-tools
# version: 1.0
# - name: Install stable
# uses: dtolnay/rust-toolchain@master
# with:
# toolchain: "1.86.0"
# targets: ${{ matrix.platform.target }}
# - uses: Swatinem/rust-cache@v2
# with:
# cache-all-crates: "true"
# save-if: ${{ github.ref == 'refs/heads/main' }}
# - name: Free Disk Space
# uses: jlumbroso/free-disk-space@v1.3.1
# with: # speed things up a bit
# large-packages: false
# docker-images: false
# swap-storage: false
# # Todo: we want `cargo install dx` to actually just use a prebuilt binary instead of building it
# - name: Build CLI
# run: |
# cargo build --release --package dioxus-cli --target ${{ matrix.platform.target }}
# - name: Upload CLI binaries
# uses: actions/upload-artifact@v4
# if: always()
# with:
# name: dx-${{ matrix.platform.target }}-${{ github.event.number || github.ref_name }}
# path: ./target/${{ matrix.platform.target }}/release/dx
# retention-days: ${{ github.ref == 'refs/heads/main' && 90 || 30 }}
# overwrite: true
# - name: Output artifact ID
# run: echo 'Artifact ID is ${{ steps.artifact-upload-step.outputs.artifact-id }}'
+2 -2
View File
@@ -23,7 +23,7 @@ jobs:
# Attempt to backport a merged pull request to the latest stable release
backport:
name: Backport pull request
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2404
# Don't run on closed unmerged pull requests, or pull requests with the "breaking" label
if: github.event.pull_request.merged && !contains(github.event.pull_request.labels.*.name, 'breaking')
@@ -34,7 +34,7 @@ jobs:
# Upload nightly docs to the website
docs:
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: write
steps:
+1 -1
View File
@@ -22,7 +22,7 @@ permissions:
jobs:
promote:
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v5
- name: Publish the next pre-release
+2 -2
View File
@@ -58,9 +58,9 @@ jobs:
- target: aarch64-apple-darwin
os: macos-latest
- target: x86_64-unknown-linux-gnu
os: ubuntu-24.04
os: blacksmith-4vcpu-ubuntu-2404
- target: aarch64-unknown-linux-gnu
os: ubuntu-24.04-arm
os: blacksmith-4vcpu-ubuntu-2404
# - target: x86_64-unknown-linux-musl
# os: ubuntu-24.04
# - target: aarch64-unknown-linux-musl
+2 -2
View File
@@ -16,7 +16,7 @@ jobs:
typos:
if: github.event.pull_request.draft == false
name: Check for typos
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v5
- name: Check for typos
@@ -26,7 +26,7 @@ jobs:
link-check:
if: github.event.pull_request.draft == false
name: Check For Invalid Links
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v5
- name: Restore lychee cache
+1
View File
@@ -8,6 +8,7 @@
/dist
.DS_Store
/examples/assets/test_video.mp4
/examples/_assets/test_video.mp4
static
# new recommendation to keep the lockfile in for CI and reproducible builds
Generated
+569 -212
View File
File diff suppressed because it is too large. Load diff
+447 -143
View File
@@ -35,8 +35,9 @@ members = [
"packages/document",
"packages/extension",
"packages/fullstack",
"packages/fullstack-hooks",
"packages/fullstack-protocol",
"packages/fullstack-core",
"packages/fullstack-macro",
"packages/fullstack-server",
"packages/generational-box",
"packages/history",
"packages/hooks",
@@ -50,7 +51,6 @@ members = [
"packages/rsx-hotreload",
"packages/rsx-rosetta",
"packages/rsx",
"packages/server-macro",
"packages/signals",
"packages/stores",
"packages/stores-macro",
@@ -59,7 +59,6 @@ members = [
"packages/cli-config",
"packages/devtools",
"packages/devtools-types",
"packages/isrg",
"packages/rsx-hotreload",
"packages/const-serialize",
"packages/const-serialize-macro",
@@ -70,7 +69,9 @@ members = [
"packages/native-dom",
"packages/asset-resolver",
"packages/depinfo",
"packages/server",
# CLI harnesses, all included
"packages/cli-harnesses/*",
# Playwright tests
"packages/playwright-tests/liveview",
@@ -98,25 +99,27 @@ members = [
"packages/subsecond/subsecond-tests/cross-tls-test",
# Full project examples
"examples/fullstack-hackernews",
"examples/ecommerce-site",
"examples/bluetooth-scanner",
"examples/file-explorer",
"examples/hotdog",
"examples/01-app-demos/hackernews",
"examples/01-app-demos/ecommerce-site",
"examples/01-app-demos/bluetooth-scanner",
"examples/01-app-demos/file-explorer",
"examples/01-app-demos/hotdog",
# Simple examples that require a crate
"examples/tailwind",
"examples/pwa",
"examples/fullstack-hello-world",
"examples/fullstack-router",
"examples/fullstack-streaming",
"examples/fullstack-desktop",
"examples/fullstack-auth",
"examples/fullstack-websockets",
"examples/wgpu-texture",
"examples/native-headless",
"examples/native-headless-in-bevy",
"examples/bevy",
# Fullstack examples
"examples/07-fullstack/hello-world",
"examples/07-fullstack/router",
"examples/07-fullstack/desktop",
"examples/07-fullstack/auth",
"examples/07-fullstack/auth-with-fullstack",
"examples/07-fullstack/ssr-only",
# Integrations
"examples/10-integrations/tailwind",
"examples/10-integrations/pwa",
"examples/10-integrations/wgpu-texture",
"examples/10-integrations/native-headless",
"examples/10-integrations/native-headless-in-bevy",
"examples/10-integrations/bevy",
# Playwright tests
"packages/playwright-tests/liveview",
@@ -141,7 +144,6 @@ version = "0.7.0-rc.0"
# dependencies that are shared across packages
[workspace.dependencies]
dioxus = { path = "packages/dioxus", version = "0.7.0-rc.0" }
dioxus-lib = { path = "packages/dioxus-lib", version = "0.7.0-rc.0" }
dioxus-core = { path = "packages/core", version = "0.7.0-rc.0" }
dioxus-core-types = { path = "packages/core-types", version = "0.7.0-rc.0" }
dioxus-core-macro = { path = "packages/core-macro", version = "0.7.0-rc.0" }
@@ -154,10 +156,8 @@ dioxus-html = { path = "packages/html", version = "0.7.0-rc.0", default-features
dioxus-html-internal-macro = { path = "packages/html-internal-macro", version = "0.7.0-rc.0" }
dioxus-hooks = { path = "packages/hooks", version = "0.7.0-rc.0" }
dioxus-web = { path = "packages/web", version = "0.7.0-rc.0", default-features = false }
dioxus-isrg = { path = "packages/isrg", version = "0.7.0-rc.0" }
dioxus-ssr = { path = "packages/ssr", version = "0.7.0-rc.0", default-features = false }
dioxus-desktop = { path = "packages/desktop", version = "0.7.0-rc.0", default-features = false }
dioxus-mobile = { path = "packages/mobile", version = "0.7.0-rc.0" }
dioxus-interpreter-js = { path = "packages/interpreter", version = "0.7.0-rc.0" }
dioxus-liveview = { path = "packages/liveview", version = "0.7.0-rc.0" }
dioxus-autofmt = { path = "packages/autofmt", version = "0.7.0-rc.0" }
@@ -170,11 +170,10 @@ dioxus-stores = { path = "packages/stores", version = "0.7.0-rc.0" }
dioxus-stores-macro = { path = "packages/stores-macro", version = "0.7.0-rc.0" }
dioxus-devtools = { path = "packages/devtools", version = "0.7.0-rc.0" }
dioxus-devtools-types = { path = "packages/devtools-types", version = "0.7.0-rc.0" }
dioxus-server = { path = "packages/server", version = "0.7.0-rc.0" }
dioxus-fullstack = { path = "packages/fullstack", version = "0.7.0-rc.0" }
dioxus-fullstack-hooks = { path = "packages/fullstack-hooks", version = "0.7.0-rc.0" }
dioxus-fullstack-protocol = { path = "packages/fullstack-protocol", version = "0.7.0-rc.0" }
dioxus_server_macro = { path = "packages/server-macro", version = "0.7.0-rc.0", default-features = false }
dioxus-fullstack = { path = "packages/fullstack", version = "0.7.0-rc.0", default-features = false}
dioxus-fullstack-core = { path = "packages/fullstack-core", version = "0.7.0-rc.0", default-features = false}
dioxus-fullstack-macro = { path = "packages/fullstack-macro", version = "0.7.0-rc.0", default-features = false }
dioxus-server = { path = "packages/fullstack-server", version = "0.7.0-rc.0" }
dioxus-dx-wire-format = { path = "packages/dx-wire-format", version = "0.7.0-rc.0" }
dioxus-logger = { path = "packages/logger", version = "0.7.0-rc.0" }
dioxus-native = { path = "packages/native", version = "0.7.0-rc.0" }
@@ -207,7 +206,6 @@ manganis-macro = { path = "packages/manganis/manganis-macro", version = "0.7.0-r
wasm-splitter = { path = "packages/wasm-split/wasm-split", version = "0.7.0-rc.0" }
wasm-split-macro = { path = "packages/wasm-split/wasm-split-macro", version = "0.7.0-rc.0" }
wasm-split-cli = { path = "packages/wasm-split/wasm-split-cli", version = "0.7.0-rc.0" }
wasm-split-harness = { path = "packages/playwright-tests/wasm-split-harness", version = "0.7.0-rc.0" }
wasm-used = { path = "packages/wasm-split/wasm-used", version = "0.7.0-rc.0" }
depinfo = { path = "packages/depinfo", version = "0.7.0-rc.0" }
@@ -257,8 +255,7 @@ lru = "0.16.0"
async-trait = "0.1.88"
axum = { version = "0.8.4", default-features = false }
axum-server = { version = "0.7.2", default-features = false }
server_fn = { version = "=0.8.3", default-features = false }
server_fn_macro = { version = "=0.8.3" }
http-body = { version = "1.0"}
tower = "0.5.2"
http = "1.3.1"
notify = { version = "8.1.0" }
@@ -277,12 +274,13 @@ serde_json = "1.0.140"
serde = "1.0.219"
syn = "2.0"
quote = "1.0"
axum-core = "0.5"
proc-macro2 = "1.0.101"
axum_session = "0.16.0"
axum_session_auth = "0.16.0"
axum_session_sqlx = "0.5.0"
axum-extra = "0.10.1"
reqwest = "0.12.22"
reqwest = { version = "0.12.23", default-features = false }
owo-colors = "4.2.2"
ciborium = "0.2.2"
base64 = "0.22.1"
@@ -291,13 +289,10 @@ convert_case = "0.8.0"
tungstenite = { version = "0.27.0" }
tokio-tungstenite = { version = "0.27.0" }
gloo-timers = "0.3.0"
fluent-uri = { version = "0.3.2", features = ["serde"] }
internment = { version = "0.8.6" }
proc-macro2-diagnostics = { version = "0.10", default-features = false }
env_logger = "0.11.8"
chrono = { version = "0.4.39" }
gloo = { version = "0.11.0" }
gloo-utils = { version = "0.2.0" }
rustversion = "1.0.21"
rand = "0.9"
longest-increasing-subsequence = "0.1.0"
@@ -320,7 +315,6 @@ rayon = "1.10.0"
wasmparser = "0.235.0"
itertools = "0.14.0"
object = { version = "0.37.1" }
bincode = "1.3.3"
inventory = { version = "0.3" }
macro-string = "0.1.4"
walkdir = "2.5.0"
@@ -334,6 +328,16 @@ libloading = "0.8.8"
libc = "0.2.174"
memmap2 = "0.9.5"
memfd = "0.6.4"
xxhash-rust = {version = "0.8.15", default-features = false}
serde_qs = "0.15.0"
multer = "3.1.0"
const-str = "0.7.0"
bytes = "1.10"
send_wrapper = "0.6.0"
pin-project = { version = "1.1.10" }
postcard = { version = "1.1.3", default-features = false }
serde_urlencoded = "0.7"
form_urlencoded = "1.2.1"
# desktop
wry = { version = "0.52.1", default-features = false }
@@ -341,13 +345,7 @@ tao = { version = "0.34.0", features = ["rwh_05"] }
infer = "0.19.0"
dunce = "1.0.5"
percent-encoding = "2.3.1"
global-hotkey = "0.7.0"
rfd = { version = "0.15.3", default-features = false }
muda = "0.17.0"
cocoa = "0.26"
core-foundation = "0.10.1"
objc = { version = "0.2.7", features = ["exception"] }
objc_id = "0.1.1"
tray-icon = "0.21.0"
open = "5.3.2"
webbrowser = "1.0"
@@ -362,14 +360,11 @@ path-absolutize = "3.1"
crossterm = { version = "0.29.0" }
ratatui = { version = "0.29.0" }
shell-words = "1.1.0"
color-eyre = "0.6.5"
# native
keyboard-types = { version = "0.7", default-features = false }
winit = { version = "0.30.11", features = ["rwh_06"] }
[profile.dev]
# our release profile should be fast to compile and fast to run
# when we ship our CI builds, we turn on LTO which improves perf leftover by turning on incremental
[profile.release]
@@ -419,28 +414,40 @@ inherits = "dev"
[package]
name = "dioxus-examples"
authors = ["Jonathan Kelley"]
edition = "2021"
edition = "2024"
description = "Top level crate for the Dioxus repository"
license = "MIT OR Apache-2.0"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com"
documentation = "https://dioxuslabs.com"
keywords = ["dom", "ui", "gui", "react", "wasm"]
rust-version = "1.80.0"
rust-version = "1.85.0"
publish = false
version = "0.7.0-rc.0"
[dependencies]
reqwest = { workspace = true, features = ["json"], optional = true }
reqwest = { workspace = true, features = ["json"] }
ciborium = { workspace = true, optional = true }
base64 = { workspace = true, optional = true }
http-range = { version = "0.1.5", optional = true }
http-range = { version = "0.1.5" }
wgpu = { workspace = true, optional = true }
ouroboros = { version = "*", optional = true }
wasm-splitter = { workspace = true, package = "wasm-split" }
wasm-splitter = { workspace = true }
sqlx = { version = "0.8.6", features = [
"macros",
"migrate",
"postgres",
"sqlite",
"_unstable-all-types",
"tls-native-tls",
"runtime-tokio",
], optional = true }
wasm-streams = "0.4.2"
[dev-dependencies]
dioxus = { workspace = true, features = ["router"] }
dioxus = { workspace = true, features = ["router", "fullstack"] }
dioxus-html = { workspace = true, features = ["serialize"] }
dioxus-stores = { workspace = true }
dioxus-ssr = { workspace = true }
futures-util = { workspace = true }
@@ -451,10 +458,17 @@ rand = { workspace = true, features = ["small_rng"] }
form_urlencoded = "1.2.1"
async-std = "1.13.1"
web-time = "1.1.0"
anyhow = { workspace = true }
thiserror = { workspace = true }
bytes = { workspace = true }
futures = { workspace = true }
axum-core = { workspace = true }
uuid = { workspace = true, features = ["v4", "serde"] }
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
getrandom = { workspace = true, features = ["wasm_js"] }
tokio = { version = "1.46", default-features = false, features = ["sync", "macros", "io-util", "rt", "time"] }
uuid = { workspace = true, features = ["v4", "serde", "js"] }
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
tokio = { version = "1.46", features = ["full"] }
@@ -466,161 +480,451 @@ default = ["desktop"]
desktop = ["dioxus/desktop"]
native = ["dioxus/native"]
liveview = ["dioxus/liveview"]
fullstack = ["dioxus/fullstack"]
server = ["dioxus/server"]
mobile = ["dioxus/mobile"]
web = ["dioxus/web"]
http = ["dep:reqwest", "dep:http-range"]
gpu = ["dep:ouroboros", "dep:wgpu"]
[[example]]
name = "login_form"
required-features = ["http"]
doc-scrape-examples = true
[[example]]
name = "dog_app"
required-features = ["http"]
doc-scrape-examples = true
[[example]]
name = "video_stream"
required-features = ["http", "desktop"]
doc-scrape-examples = true
[[example]]
name = "suspense"
required-features = ["http", "desktop"]
name = "websocket_chat"
path = "examples/01-app-demos/websocket_chat.rs"
doc-scrape-examples = true
[[example]]
name = "weather_app"
required-features = ["http"]
path = "examples/01-app-demos/weather_app.rs"
doc-scrape-examples = true
[[example]]
name = "crm"
path = "examples/01-app-demos/crm.rs"
doc-scrape-examples = true
[[example]]
name = "image_generator_openai"
required-features = ["http"]
path = "examples/01-app-demos/image_generator_openai.rs"
doc-scrape-examples = true
[[example]]
name = "todomvc"
path = "examples/01-app-demos/todomvc.rs"
doc-scrape-examples = true
[[example]]
name = "calculator_mutable"
path = "examples/01-app-demos/calculator_mutable.rs"
doc-scrape-examples = true
[[example]]
name = "hello_world"
path = "examples/01-app-demos/hello_world.rs"
doc-scrape-examples = true
[[example]]
name = "counters"
path = "examples/01-app-demos/counters.rs"
doc-scrape-examples = true
[[example]]
name = "todomvc_store"
path = "examples/01-app-demos/todomvc_store.rs"
doc-scrape-examples = true
[[example]]
name = "dog_app"
path = "examples/01-app-demos/dog_app.rs"
doc-scrape-examples = true
[[example]]
name = "calculator"
path = "examples/01-app-demos/calculator.rs"
doc-scrape-examples = true
[[example]]
name = "repo_readme"
path = "examples/01-app-demos/repo_readme.rs"
doc-scrape-examples = true
[[example]]
name = "nested_listeners"
path = "examples/02-building-ui/nested_listeners.rs"
doc-scrape-examples = true
[[example]]
name = "disabled"
path = "examples/02-building-ui/disabled.rs"
doc-scrape-examples = true
[[example]]
name = "svg"
path = "examples/02-building-ui/svg.rs"
doc-scrape-examples = true
[[example]]
name = "custom_assets"
path = "examples/03-assets-styling/custom_assets.rs"
doc-scrape-examples = true
[[example]]
name = "dynamic_assets"
path = "examples/03-assets-styling/dynamic_assets.rs"
doc-scrape-examples = true
[[example]]
name = "meta"
path = "examples/03-assets-styling/meta.rs"
doc-scrape-examples = true
[[example]]
name = "meta_elements"
path = "examples/03-assets-styling/meta_elements.rs"
doc-scrape-examples = true
[[example]]
name = "reducer"
path = "examples/04-managing-state/reducer.rs"
doc-scrape-examples = true
[[example]]
name = "memo_chain"
path = "examples/04-managing-state/memo_chain.rs"
doc-scrape-examples = true
[[example]]
name = "global"
path = "examples/04-managing-state/global.rs"
doc-scrape-examples = true
[[example]]
name = "context_api"
path = "examples/04-managing-state/context_api.rs"
doc-scrape-examples = true
[[example]]
name = "signals"
path = "examples/04-managing-state/signals.rs"
doc-scrape-examples = true
[[example]]
name = "errors"
path = "examples/04-managing-state/error_handling.rs"
doc-scrape-examples = true
[[example]]
name = "backgrounded_futures"
path = "examples/05-using-async/backgrounded_futures.rs"
doc-scrape-examples = true
[[example]]
name = "clock"
path = "examples/05-using-async/clock.rs"
doc-scrape-examples = true
[[example]]
name = "streams"
path = "examples/05-using-async/streams.rs"
doc-scrape-examples = true
[[example]]
name = "suspense"
path = "examples/05-using-async/suspense.rs"
doc-scrape-examples = true
[[example]]
name = "future"
path = "examples/05-using-async/future.rs"
doc-scrape-examples = true
[[example]]
name = "simple_router"
path = "examples/06-routing/simple_router.rs"
doc-scrape-examples = true
[[example]]
name = "router_restore_scroll"
path = "examples/06-routing/router_restore_scroll.rs"
doc-scrape-examples = true
[[example]]
name = "link"
path = "examples/06-routing/link.rs"
doc-scrape-examples = true
[[example]]
name = "hash_fragment_state"
required-features = ["ciborium", "base64"]
path = "examples/06-routing/hash_fragment_state.rs"
doc-scrape-examples = true
[[example]]
name = "backgrounded_futures"
required-features = ["desktop"]
name = "router"
path = "examples/06-routing/router.rs"
doc-scrape-examples = true
[[example]]
name = "calculator_mutable"
required-features = ["desktop"]
name = "router_resource"
path = "examples/06-routing/router_resource.rs"
doc-scrape-examples = true
[[example]]
name = "calculator"
required-features = ["desktop"]
name = "query_segment_search"
path = "examples/06-routing/query_segment_search.rs"
doc-scrape-examples = true
[[example]]
name = "clock"
name = "flat_router"
path = "examples/06-routing/flat_router.rs"
doc-scrape-examples = true
[[example]]
name = "crm"
name = "header_map"
path = "examples/07-fullstack/header_map.rs"
doc-scrape-examples = true
[[example]]
name = "login_form"
path = "examples/07-fullstack/login_form.rs"
doc-scrape-examples = true
[[example]]
name = "handling_errors"
path = "examples/07-fullstack/handling_errors.rs"
doc-scrape-examples = true
[[example]]
name = "dog_app_self_hosted"
path = "examples/07-fullstack/dog_app_self_hosted.rs"
doc-scrape-examples = true
[[example]]
name = "through_reqwest"
path = "examples/07-fullstack/through_reqwest.rs"
doc-scrape-examples = true
[[example]]
name = "streaming_file_upload"
path = "examples/07-fullstack/streaming_file_upload.rs"
doc-scrape-examples = true
[[example]]
name = "multipart_form"
path = "examples/07-fullstack/multipart_form.rs"
doc-scrape-examples = true
[[example]]
name = "full_request_access"
path = "examples/07-fullstack/full_request_access.rs"
doc-scrape-examples = true
[[example]]
name = "fullstack_hello_world"
path = "examples/07-fullstack/fullstack_hello_world.rs"
doc-scrape-examples = true
[[example]]
name = "server_sent_events"
path = "examples/07-fullstack/server_sent_events.rs"
doc-scrape-examples = true
[[example]]
name = "server_state"
path = "examples/07-fullstack/server_state.rs"
doc-scrape-examples = true
required-features = ["sqlx"]
[[example]]
name = "streaming"
path = "examples/07-fullstack/streaming.rs"
doc-scrape-examples = true
[[example]]
name = "custom_axum_serve"
path = "examples/07-fullstack/custom_axum_serve.rs"
doc-scrape-examples = true
[[example]]
name = "websocket"
path = "examples/07-fullstack/websocket.rs"
doc-scrape-examples = true
[[example]]
name = "control_focus"
path = "examples/08-apis/control_focus.rs"
doc-scrape-examples = true
[[example]]
name = "window_popup"
required-features = ["desktop"]
path = "examples/08-apis/window_popup.rs"
doc-scrape-examples = true
[[example]]
name = "custom_html"
required-features = ["desktop"]
path = "examples/08-apis/custom_html.rs"
doc-scrape-examples = true
[[example]]
name = "custom_menu"
name = "multiwindow_with_tray_icon"
required-features = ["desktop"]
doc-scrape-examples = true
[[example]]
name = "dynamic_asset"
required-features = ["desktop"]
doc-scrape-examples = true
[[example]]
name = "errors"
required-features = ["desktop"]
doc-scrape-examples = true
[[example]]
name = "future"
doc-scrape-examples = true
[[example]]
name = "hydration"
required-features = ["desktop"]
doc-scrape-examples = true
[[example]]
name = "multiwindow"
required-features = ["desktop"]
doc-scrape-examples = true
[[example]]
name = "overlay"
required-features = ["desktop"]
doc-scrape-examples = true
[[example]]
name = "popup"
required-features = ["desktop"]
doc-scrape-examples = true
[[example]]
name = "read_size"
required-features = ["desktop"]
doc-scrape-examples = true
[[example]]
name = "shortcut"
required-features = ["desktop"]
doc-scrape-examples = true
[[example]]
name = "streams"
doc-scrape-examples = true
[[example]]
name = "visible"
path = "examples/08-apis/multiwindow_with_tray_icon.rs"
doc-scrape-examples = true
[[example]]
name = "window_event"
required-features = ["desktop"]
path = "examples/08-apis/window_event.rs"
doc-scrape-examples = true
[[example]]
name = "read_size"
path = "examples/08-apis/read_size.rs"
doc-scrape-examples = true
[[example]]
name = "logging"
path = "examples/08-apis/logging.rs"
doc-scrape-examples = true
[[example]]
name = "overlay"
required-features = ["desktop"]
path = "examples/08-apis/overlay.rs"
doc-scrape-examples = true
[[example]]
name = "ssr"
path = "examples/08-apis/ssr.rs"
doc-scrape-examples = true
[[example]]
name = "video_stream"
required-features = ["desktop"]
path = "examples/08-apis/video_stream.rs"
doc-scrape-examples = true
[[example]]
name = "title"
path = "examples/08-apis/title.rs"
doc-scrape-examples = true
[[example]]
name = "file_upload"
path = "examples/08-apis/file_upload.rs"
doc-scrape-examples = true
[[example]]
name = "window_focus"
required-features = ["desktop"]
path = "examples/08-apis/window_focus.rs"
doc-scrape-examples = true
[[example]]
name = "eval"
path = "examples/08-apis/eval.rs"
doc-scrape-examples = true
[[example]]
name = "shortcut"
required-features = ["desktop"]
path = "examples/08-apis/shortcut.rs"
doc-scrape-examples = true
[[example]]
name = "scroll_to_offset"
path = "examples/08-apis/scroll_to_offset.rs"
doc-scrape-examples = true
[[example]]
name = "scroll_to_top"
path = "examples/08-apis/scroll_to_top.rs"
doc-scrape-examples = true
[[example]]
name = "wgpu_child_window"
path = "examples/08-apis/wgpu_child_window.rs"
required-features = ["gpu", "desktop"]
doc-scrape-examples = true
[[example]]
name = "multiwindow"
required-features = ["desktop"]
path = "examples/08-apis/multiwindow.rs"
doc-scrape-examples = true
[[example]]
name = "window_zoom"
required-features = ["desktop"]
path = "examples/08-apis/window_zoom.rs"
doc-scrape-examples = true
[[example]]
name = "wgpu_child_window"
required-features = ["desktop", "gpu"]
[[example]]
name = "control_focus"
name = "custom_menu"
required-features = ["desktop"]
path = "examples/08-apis/custom_menu.rs"
doc-scrape-examples = true
[[example]]
name = "eval"
name = "on_resize"
path = "examples/08-apis/on_resize.rs"
doc-scrape-examples = true
[[example]]
name = "logging"
name = "form"
path = "examples/08-apis/form.rs"
doc-scrape-examples = true
[[example]]
name = "on_visible"
path = "examples/08-apis/on_visible.rs"
doc-scrape-examples = true
[[example]]
name = "all_events"
path = "examples/09-reference/all_events.rs"
doc-scrape-examples = true
[[example]]
name = "xss_safety"
path = "examples/09-reference/xss_safety.rs"
doc-scrape-examples = true
[[example]]
name = "web_component"
path = "examples/09-reference/web_component.rs"
doc-scrape-examples = true
[[example]]
name = "generic_component"
path = "examples/09-reference/generic_component.rs"
doc-scrape-examples = true
[[example]]
name = "shorthand"
path = "examples/09-reference/shorthand.rs"
doc-scrape-examples = true
[[example]]
name = "simple_list"
path = "examples/09-reference/simple_list.rs"
doc-scrape-examples = true
[[example]]
name = "optional_props"
path = "examples/09-reference/optional_props.rs"
doc-scrape-examples = true
[[example]]
name = "rsx_usage"
path = "examples/09-reference/rsx_usage.rs"
doc-scrape-examples = true
[[example]]
name = "spread"
path = "examples/09-reference/spread.rs"
doc-scrape-examples = true
[[example]]
name = "__scrape_example_list"
path = "examples/scripts/scrape_examples.rs"
@@ -1,7 +1,7 @@
[package]
name = "bluetooth-scanner"
version = "0.1.1"
edition = "2018"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -0,0 +1,90 @@
use dioxus::prelude::*;
fn main() {
dioxus::launch(app)
}
fn app() -> Element {
let mut scan = use_action(|| async {
use btleplug::api::{Central, Manager as _, Peripheral, ScanFilter};
let manager = btleplug::platform::Manager::new().await?;
// get the first bluetooth adapter
let adapters = manager.adapters().await?;
let central = adapters
.into_iter()
.next()
.context("No Bluetooth adapter found")?;
// start scanning for devices
central.start_scan(ScanFilter::default()).await?;
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
// Return the list of peripherals after scanning
let mut devices = vec![];
for p in central.peripherals().await? {
if let Some(p) = p.properties().await? {
devices.push(p);
}
}
// Sort them by RSSI (signal strength)
devices.sort_by_key(|p| p.rssi.unwrap_or(-100));
dioxus::Ok(devices)
});
rsx! {
Stylesheet { href: asset!("/assets/tailwind.css") }
div {
div { class: "py-8 px-6",
div { class: "container px-4 mx-auto",
h2 { class: "text-2xl font-bold", "Scan for Bluetooth Devices" }
button {
class: "inline-block w-full md:w-auto px-6 py-3 font-medium text-white bg-indigo-500 hover:bg-indigo-600 rounded transition duration-200",
disabled: scan.is_pending(),
onclick: move |_| {
scan.call();
},
if scan.is_pending() { "Scanning" } else { "Scan" }
}
}
}
section { class: "py-8",
div { class: "container px-4 mx-auto",
div { class: "p-4 mb-6 bg-white shadow rounded overflow-x-auto",
table { class: "table-auto w-full",
thead {
tr { class: "text-xs text-gray-500 text-left",
th { class: "pl-6 pb-3 font-medium", "Strength" }
th { class: "pb-3 font-medium", "Network" }
th { class: "pb-3 font-medium", "Channel" }
th { class: "pb-3 px-2 font-medium", "Security" }
}
}
match scan.result() {
None if scan.is_pending() => rsx! { "Scanning..." },
None => rsx! { "Press Scan to start scanning" },
Some(Err(_err)) => rsx! { "Failed to scan" },
Some(Ok(peripherals)) => rsx! {
tbody {
for peripheral in peripherals.read().iter().rev() {
tr { class: "text-xs bg-gray-50",
td { class: "py-5 px-6 font-medium", "{peripheral.rssi.unwrap_or(-100)}" }
td { class: "flex py-3 font-medium", "{peripheral.local_name.clone().unwrap_or_default()}" }
td { span { class: "inline-block py-1 px-2 text-white bg-green-500 rounded-full", "{peripheral.address}" } }
td { span { class: "inline-block py-1 px-2 text-purple-500 bg-purple-50 rounded-full", "{peripheral.tx_power_level.unwrap_or_default()}" } }
}
}
}
}
}
}
}
}
}
}
}
}
@@ -54,7 +54,7 @@ fn app() -> Element {
};
rsx! {
document::Link { rel: "stylesheet", href: STYLE }
Stylesheet { href: STYLE }
div { id: "wrapper",
div { class: "app",
div { class: "calculator", tabindex: "0", onkeydown: handle_key_down_event,
@@ -8,8 +8,8 @@
use dioxus::desktop::tao::dpi::LogicalSize;
use dioxus::desktop::{Config, WindowBuilder};
use dioxus::html::input_data::keyboard_types::Key;
use dioxus::html::MouseEvent;
use dioxus::html::input_data::keyboard_types::Key;
use dioxus::prelude::*;
fn main() {
@@ -29,10 +29,7 @@ fn app() -> Element {
let mut state = use_signal(Calculator::new);
rsx! {
document::Link {
rel: "stylesheet",
href: asset!("/examples/assets/calculator.css"),
}
Stylesheet { href: asset!("/examples/assets/calculator.css") }
div { id: "wrapper",
div { class: "app",
div {
@@ -16,7 +16,7 @@ fn app() -> Element {
let sum = use_memo(move || counters.read().iter().copied().sum::<i32>());
rsx! {
document::Link { rel: "stylesheet", href: STYLE }
Stylesheet { href: STYLE }
div { id: "controls",
button { onclick: move |_| counters.write().push(0), "Add counter" }
@@ -20,20 +20,12 @@ fn main() {
}))
.launch(|| {
rsx! {
document::Link {
rel: "stylesheet",
Stylesheet {
href: "https://unpkg.com/purecss@2.0.6/build/pure-min.css",
integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5",
crossorigin: "anonymous",
}
document::Link {
rel: "stylesheet",
href: asset!("/examples/assets/crm.css"),
}
document::Link {
rel: "stylesheet",
href: asset!("/examples/assets/crm.css"),
}
Stylesheet { href: asset!("/examples/assets/crm.css") }
h1 { "Dioxus CRM Example" }
Router::<Route> {}
}
+70
View File
@@ -0,0 +1,70 @@
//! This example demonstrates a simple app that fetches a list of dog breeds and displays a random dog.
//!
//! This app combines `use_loader` and `use_action` to fetch data from the Dog API.
//! - `use_loader` automatically fetches the list of dog breeds when the component mounts.
//! - `use_action` fetches a random dog image whenever the `.dispatch` method is called.
use dioxus::prelude::*;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
fn main() {
dioxus::launch(app);
}
fn app() -> Element {
// Fetch the list of breeds from the Dog API, using the `?` syntax to suspend or throw errors
let breed_list = use_loader(move || async move {
#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
struct ListBreeds {
message: HashMap<String, Vec<String>>,
}
reqwest::get("https://dog.ceo/api/breeds/list/all")
.await?
.json::<ListBreeds>()
.await
})?;
// Whenever this action is called, it will re-run the future and return the result.
let mut breed = use_action(move |breed| async move {
#[derive(Deserialize, Serialize, Debug, PartialEq)]
struct DogApi {
message: String,
}
reqwest::get(format!("https://dog.ceo/api/breed/{breed}/images/random"))
.await
.unwrap()
.json::<DogApi>()
.await
});
rsx! {
h1 { "Doggo selector" }
div { width: "400px",
for cur_breed in breed_list.read().message.keys().take(20).cloned() {
button {
onclick: move |_| {
breed.call(cur_breed.clone());
},
"{cur_breed}"
}
}
}
div {
match breed.result() {
None => rsx! { div { "Click the button to fetch a dog!" } },
Some(Err(_e)) => rsx! { div { "Failed to fetch a dog, please try again." } },
Some(Ok(res)) => rsx! {
img {
max_width: "500px",
max_height: "500px",
src: "{res.read().message}"
}
},
}
}
}
}
@@ -1,7 +1,7 @@
[package]
name = "ecommerce-site"
version = "0.1.1"
edition = "2018"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -1,9 +1,27 @@
#![allow(unused)]
use chrono::{DateTime, Utc};
use dioxus::prelude::Result;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
// Cache up to 100 requests, invalidating them after 60 seconds
pub(crate) async fn fetch_product(product_id: usize) -> Result<Product> {
Ok(
reqwest::get(format!("https://fakestoreapi.com/products/{product_id}"))
.await?
.json()
.await?,
)
}
// Cache up to 100 requests, invalidating them after 60 seconds
pub(crate) async fn fetch_products(count: usize, sort: Sort) -> Result<Vec<Product>> {
Ok(reqwest::get(format!(
"https://fakestoreapi.com/products/?sort={sort}&limit={count}"
))
.await?
.json()
.await?)
}
#[derive(Serialize, Deserialize, PartialEq, Clone, Debug, Default)]
pub(crate) struct Product {
@@ -54,48 +72,8 @@ impl Display for Sort {
}
}
// Cache up to 100 requests, invalidating them after 60 seconds
pub(crate) async fn fetch_user_carts(user_id: usize) -> Result<Vec<Cart>, reqwest::Error> {
reqwest::get(format!(
"https://fakestoreapi.com/carts/user/{user_id}?startdate=2019-12-10&enddate=2023-01-01"
))
.await?
.json()
.await
}
// Cache up to 100 requests, invalidating them after 60 seconds
pub(crate) async fn fetch_user(user_id: usize) -> dioxus::Result<Product> {
Ok(
reqwest::get(format!("https://fakestoreapi.com/users/{user_id}"))
.await?
.json()
.await?,
)
}
// Cache up to 100 requests, invalidating them after 60 seconds
pub(crate) async fn fetch_product(product_id: usize) -> dioxus::Result<Product> {
Ok(
reqwest::get(format!("https://fakestoreapi.com/products/{product_id}"))
.await?
.json()
.await?,
)
}
// Cache up to 100 requests, invalidating them after 60 seconds
pub(crate) async fn fetch_products(count: usize, sort: Sort) -> dioxus::Result<Vec<Product>> {
Ok(reqwest::get(format!(
"https://fakestoreapi.com/products/?sort={sort}&limit={count}"
))
.await?
.json()
.await?)
}
#[derive(Serialize, Deserialize)]
pub(crate) struct User {
struct User {
id: usize,
email: String,
username: String,
@@ -104,14 +82,6 @@ pub(crate) struct User {
phone: String,
}
impl User {
async fn fetch_most_recent_cart(&self) -> Result<Option<Cart>, reqwest::Error> {
let all_carts = fetch_user_carts(self.id).await?;
Ok(all_carts.into_iter().max_by_key(|cart| cart.date))
}
}
#[derive(Serialize, Deserialize)]
struct FullName {
firstname: String,
@@ -119,7 +89,7 @@ struct FullName {
}
#[derive(Serialize, Deserialize, Clone)]
pub(crate) struct Cart {
struct Cart {
id: usize,
#[serde(rename = "userId")]
user_id: usize,
@@ -128,29 +98,9 @@ pub(crate) struct Cart {
date: DateTime<Utc>,
}
impl Cart {
async fn update_database(&mut self) -> Result<(), reqwest::Error> {
let id = self.id;
let client = reqwest::Client::new();
*self = client
.put(format!("https://fakestoreapi.com/carts/{id}"))
.send()
.await?
.json()
.await?;
Ok(())
}
}
#[derive(Serialize, Deserialize, Clone)]
pub(crate) struct ProductInCart {
struct ProductInCart {
#[serde(rename = "productId")]
product_id: usize,
quantity: usize,
}
impl ProductInCart {
pub async fn fetch_product(&self) -> Result<Product, dioxus::CapturedError> {
fetch_product(self.product_id).await
}
}
@@ -0,0 +1,23 @@
// The homepage is statically rendered, so we don't need to a persistent websocket connection.
use crate::{
api::{fetch_products, Sort},
components::nav::Nav,
components::product_item::ProductItem,
};
use dioxus::prelude::*;
pub(crate) fn Home() -> Element {
let products = use_loader(|| fetch_products(10, Sort::Ascending))?;
rsx! {
Nav {}
section { class: "p-10",
for product in products.iter() {
ProductItem {
product: product.clone()
}
}
}
}
}
@@ -0,0 +1,12 @@
use dioxus::prelude::*;
#[component]
pub(crate) fn ChildrenOrLoading(children: Element) -> Element {
rsx! {
Stylesheet { href: asset!("/public/loading.css") }
SuspenseBoundary {
fallback: |_| rsx! { div { class: "spinner", } },
{children}
}
}
}
@@ -1,6 +1,7 @@
use dioxus::prelude::*;
pub fn nav() -> Element {
#[component]
pub fn Nav() -> Element {
rsx! {
section { class: "relative",
nav { class: "flex justify-between border-b",
@@ -3,7 +3,7 @@ use dioxus::prelude::*;
use crate::api::Product;
#[component]
pub(crate) fn product_item(product: Product) -> Element {
pub(crate) fn ProductItem(product: Product) -> Element {
let Product {
id,
title,
@@ -3,44 +3,11 @@ use std::{fmt::Display, str::FromStr};
use crate::api::{fetch_product, Product};
use dioxus::prelude::*;
#[derive(Default)]
enum Size {
Small,
#[default]
Medium,
Large,
}
impl Display for Size {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Size::Small => "small".fmt(f),
Size::Medium => "medium".fmt(f),
Size::Large => "large".fmt(f),
}
}
}
impl FromStr for Size {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
use Size::*;
match s.to_lowercase().as_str() {
"small" => Ok(Small),
"medium" => Ok(Medium),
"large" => Ok(Large),
_ => Err(()),
}
}
}
#[component]
pub fn product_page(product_id: ReadSignal<usize>) -> Element {
pub fn ProductPage(product_id: ReadSignal<usize>) -> Element {
let mut quantity = use_signal(|| 1);
let mut size = use_signal(Size::default);
let product = use_server_future(move || fetch_product(product_id()))?;
let product = use_loader(move || fetch_product(product_id()))?;
let Product {
title,
@@ -50,7 +17,7 @@ pub fn product_page(product_id: ReadSignal<usize>) -> Element {
image,
rating,
..
} = product().unwrap()?;
} = product();
rsx! {
section { class: "py-20",
@@ -216,6 +183,38 @@ pub fn product_page(product_id: ReadSignal<usize>) -> Element {
}
}
#[derive(Default)]
enum Size {
Small,
#[default]
Medium,
Large,
}
impl Display for Size {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Size::Small => "small".fmt(f),
Size::Medium => "medium".fmt(f),
Size::Large => "large".fmt(f),
}
}
}
impl FromStr for Size {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
use Size::*;
match s.to_lowercase().as_str() {
"small" => Ok(Small),
"medium" => Ok(Medium),
"large" => Ok(Large),
_ => Err(()),
}
}
}
mod icons {
use super::*;
@@ -43,8 +43,8 @@ enum Route {
fn Details(product_id: usize) -> Element {
rsx! {
div {
components::nav::nav {}
components::product_page::product_page {
components::nav::Nav {}
components::product_page::ProductPage {
product_id
}
}
@@ -8,25 +8,19 @@
use std::env::current_dir;
use std::path::PathBuf;
use dioxus::desktop::{Config, WindowBuilder};
use dioxus::prelude::*;
fn main() {
dioxus::LaunchBuilder::desktop()
.with_cfg(Config::new().with_window(WindowBuilder::new().with_resizable(true)))
.launch(app)
dioxus::launch(app);
}
fn app() -> Element {
let mut files = use_signal(Files::new);
rsx! {
document::Link {
rel: "stylesheet",
href: asset!("/assets/fileexplorer.css")
}
Stylesheet { href: asset!("/assets/fileexplorer.css") }
Stylesheet { href: "https://fonts.googleapis.com/icon?family=Material+Icons" }
div {
document::Link { href: "https://fonts.googleapis.com/icon?family=Material+Icons", rel: "stylesheet" }
header {
i { class: "material-icons icon-menu", "menu" }
h1 { "Files: " {files.read().current()} }
@@ -13,45 +13,37 @@ use svg_attributes::to;
fn main() {
LaunchBuilder::new()
.with_cfg(server_only! {
dioxus::fullstack::ServeConfig::builder().enable_out_of_order_streaming()
dioxus::server::ServeConfig::builder().enable_out_of_order_streaming()
})
.launch(|| rsx! { Router::<Route> {} });
.launch(|| {
rsx! {
Stylesheet { href: asset!("/assets/hackernews.css") }
Router::<Route> {}
}
});
}
#[derive(Clone, Routable)]
enum Route {
#[redirect("/", || Route::Homepage { story: PreviewState { active_story: None } })]
#[route("/:story")]
Homepage { story: PreviewState },
}
pub fn App() -> Element {
rsx! {
Router::<Route> {}
}
#[route("/story&:story")]
StoryPreview { story: Option<i64> },
}
#[component]
fn Homepage(story: ReadSignal<PreviewState>) -> Element {
fn StoryPreview(story: ReadSignal<Option<i64>>) -> Element {
rsx! {
document::Link { rel: "stylesheet", href: asset!("/assets/hackernews.css") }
div { display: "flex", flex_direction: "row", width: "100%",
div {
width: "50%",
SuspenseBoundary {
fallback: |context: SuspenseContext| rsx! {
"Loading..."
},
div { width: "50%",
SuspenseBoundary { fallback: |context| rsx! { "Loading..." },
Stories {}
}
}
div { width: "50%",
SuspenseBoundary {
fallback: |context: SuspenseContext| rsx! {
"Loading preview..."
},
Preview {
story
SuspenseBoundary { fallback: |context| rsx! { "Loading preview..." },
if let Some(story) = story() {
Preview { story_id: story }
} else {
div { padding: "0.5rem", "Select a story to preview" }
}
}
}
@@ -61,32 +53,31 @@ fn Homepage(story: ReadSignal<PreviewState>) -> Element {
#[component]
fn Stories() -> Element {
let stories: Resource<dioxus::Result<Vec<i64>>> = use_server_future(move || async move {
let url = format!("{}topstories.json", BASE_API_URL);
let mut stories_ids = reqwest::get(&url).await?.json::<Vec<i64>>().await?;
stories_ids.truncate(30);
Ok(stories_ids)
let stories = use_loader(move || async move {
let stories_ids = reqwest::get(&format!("{}topstories.json", BASE_API_URL))
.await?
.json::<Vec<i64>>()
.await?
.into_iter()
.take(30)
.collect::<Vec<i64>>();
dioxus::Ok(stories_ids)
})?;
match stories().unwrap() {
Ok(list) => rsx! {
rsx! {
div {
for story in list {
ChildrenOrLoading {
key: "{story}",
for story in stories() {
ChildrenOrLoading { key: "{story}",
StoryListing { story }
}
}
}
},
Err(err) => rsx! {"An error occurred while fetching stories {err}"},
}
}
#[component]
fn StoryListing(story: ReadSignal<i64>) -> Element {
let story = use_server_future(move || get_story(story()))?;
let story = use_loader(move || get_story(story()))?;
let StoryItem {
title,
url,
@@ -96,7 +87,7 @@ fn StoryListing(story: ReadSignal<i64>) -> Element {
kids,
id,
..
} = story().unwrap()?.item;
} = story().item;
let url = url.as_deref().unwrap_or_default();
let hostname = url
@@ -121,7 +112,7 @@ fn StoryListing(story: ReadSignal<i64>) -> Element {
position: "relative",
div { font_size: "1.5rem",
Link {
to: Route::Homepage { story: PreviewState { active_story: Some(id) } },
to: Route::StoryPreview { story: Some(id) },
"{title}"
}
a {
@@ -141,44 +132,9 @@ fn StoryListing(story: ReadSignal<i64>) -> Element {
}
}
#[derive(Clone, Debug, Default)]
struct PreviewState {
active_story: Option<i64>,
}
impl FromStr for PreviewState {
type Err = ParseIntError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let state = i64::from_str(s)?;
Ok(PreviewState {
active_story: Some(state),
})
}
}
impl Display for PreviewState {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if let Some(id) = &self.active_story {
write!(f, "{id}")?;
}
Ok(())
}
}
#[component]
fn Preview(story: ReadSignal<PreviewState>) -> Element {
let PreviewState {
active_story: Some(id),
} = story()
else {
return rsx! {"Click on a story to preview it here"};
};
let story = use_server_future(use_reactive!(|id| get_story(id)))?;
let story = story().unwrap()?;
fn Preview(story_id: ReadSignal<i64>) -> Element {
let story = use_loader(move || get_story(story_id()))?.cloned();
rsx! {
div { padding: "0.5rem",
div { font_size: "1.5rem", a { href: story.item.url, "{story.item.title}" } }
@@ -194,13 +150,14 @@ fn Preview(story: ReadSignal<PreviewState>) -> Element {
}
#[component]
fn Comment(comment: i64) -> Element {
let comment: Resource<dioxus::Result<CommentData>> =
use_server_future(use_reactive!(|comment| async move {
let url = format!("{}{}{}.json", BASE_API_URL, ITEM_API, comment);
let mut comment = reqwest::get(&url).await?.json::<CommentData>().await?;
Ok(comment)
}))?;
fn Comment(comment: ReadSignal<i64>) -> Element {
let comment = use_loader(move || async move {
let mut comment = reqwest::get(&format!("{}{}{}.json", BASE_API_URL, ITEM_API, comment))
.await?
.json::<CommentData>()
.await?;
dioxus::Ok(comment)
})?;
let CommentData {
by,
@@ -209,7 +166,7 @@ fn Comment(comment: i64) -> Element {
id,
kids,
..
} = comment().unwrap()?;
} = comment();
rsx! {
div { padding: "0.5rem",
@@ -272,33 +229,21 @@ pub struct StoryItem {
pub r#type: String,
}
pub async fn get_story(id: i64) -> dioxus::Result<StoryPageData> {
let url = format!("{}{}{}.json", BASE_API_URL, ITEM_API, id);
Ok(reqwest::get(&url).await?.json::<StoryPageData>().await?)
pub async fn get_story(id: i64) -> Result<StoryPageData> {
Ok(
reqwest::get(&format!("{}{}{}.json", BASE_API_URL, ITEM_API, id))
.await?
.json::<StoryPageData>()
.await?,
)
}
#[component]
fn ChildrenOrLoading(children: Element) -> Element {
rsx! {
SuspenseBoundary {
fallback: |context: SuspenseContext| {
rsx! {
if let Some(placeholder) = context.suspense_placeholder() {
{placeholder}
} else {
LoadingIndicator {}
}
}
},
fallback: |_| rsx! { div { class: "spinner", } },
children
}
}
}
fn LoadingIndicator() -> Element {
rsx! {
div {
class: "spinner",
}
}
}
File renamed without changes.
@@ -11,9 +11,10 @@ reqwest = { workspace = true, features = ["json"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
rusqlite = { version = "0.32.0", optional = true }
anyhow = { workspace = true }
[features]
default = ["web"]
default = ["web", "server"]
web = ["dioxus/web"]
desktop = ["dioxus/desktop"]
mobile = ["dioxus/mobile"]
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,41 @@
use anyhow::Result;
use dioxus::prelude::*;
#[cfg(feature = "server")]
thread_local! {
static DB: std::sync::LazyLock<rusqlite::Connection> = std::sync::LazyLock::new(|| {
let conn = rusqlite::Connection::open("hotdogdb/hotdog.db").expect("Failed to open database");
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS dogs (
id INTEGER PRIMARY KEY,
url TEXT NOT NULL
);",
)
.unwrap();
conn
});
}
#[get("/api/dogs")]
pub async fn list_dogs() -> Result<Vec<(usize, String)>> {
DB.with(|db| {
Ok(db
.prepare("SELECT id, url FROM dogs ORDER BY id DESC LIMIT 10")?
.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?
.collect::<Result<Vec<(usize, String)>, rusqlite::Error>>()?)
})
}
#[delete("/api/dogs/{id}")]
pub async fn remove_dog(id: usize) -> Result<()> {
DB.with(|db| db.execute("DELETE FROM dogs WHERE id = ?1", [id]))?;
Ok(())
}
#[post("/api/dogs")]
pub async fn save_dog(image: String) -> Result<()> {
DB.with(|db| db.execute("INSERT INTO dogs (url) VALUES (?1)", [&image]))?;
Ok(())
}
@@ -0,0 +1,71 @@
use dioxus::prelude::*;
use crate::{
backend::{list_dogs, remove_dog, save_dog},
Route,
};
#[component]
pub fn Favorites() -> Element {
let mut favorites = use_loader(list_dogs)?;
rsx! {
div { id: "favorites",
for (id , url) in favorites.cloned() {
div { class: "favorite-dog", key: "{id}",
img { src: "{url}" }
button {
onclick: move |_| async move {
_ = remove_dog(id).await;
favorites.restart();
},
"❌"
}
}
}
}
}
}
#[component]
pub fn NavBar() -> Element {
rsx! {
div { id: "title",
span {}
Link { to: Route::DogView, h1 { "🌭 HotDog! " } }
Link { to: Route::Favorites, id: "heart", "♥️" }
}
Outlet::<Route> {}
}
}
#[component]
pub fn DogView() -> Element {
let mut img_src = use_loader(|| async move {
anyhow::Ok(
reqwest::get("https://dog.ceo/api/breeds/image/random")
.await?
.json::<serde_json::Value>()
.await?["message"]
.to_string(),
)
})?;
rsx! {
div { id: "dogview",
img { id: "dogimg", src: "{img_src}" }
}
div { id: "buttons",
button {
id: "skip",
onclick: move |_| img_src.restart(),
"skip"
}
button {
id: "save",
onclick: move |_| async move { _ = save_dog(img_src()).await },
"save!"
}
}
}
}
@@ -1,8 +1,8 @@
mod backend;
mod components;
mod frontend;
use components::{DogView, Favorites, NavBar};
use dioxus::prelude::*;
use frontend::*;
#[derive(Routable, PartialEq, Clone)]
enum Route {
@@ -16,14 +16,14 @@ enum Route {
fn main() {
#[cfg(not(feature = "server"))]
server_fn::client::set_server_url("https://hot-dog.fly.dev");
dioxus::fullstack::set_server_url("https://hot-dog.fly.dev");
dioxus::launch(app);
}
fn app() -> Element {
rsx! {
document::Stylesheet { href: asset!("/assets/main.css") }
Stylesheet { href: asset!("/assets/main.css") }
Router::<Route> {}
}
}
@@ -0,0 +1,97 @@
use dioxus::prelude::*;
fn main() {
dioxus::launch(app)
}
fn app() -> Element {
let mut api_key = use_signal(|| "".to_string());
let mut prompt = use_signal(|| "".to_string());
let mut num_images = use_signal(|| 1.to_string());
let mut image = use_action(move || async move {
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Props, Clone, Default)]
struct ImageResponse {
created: i32,
data: Vec<UrlImage>,
}
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Props, Clone)]
struct UrlImage {
url: String,
}
if api_key.peek().is_empty() || prompt.peek().is_empty() || num_images.peek().is_empty() {
return dioxus::Ok(ImageResponse::default());
}
let res = reqwest::Client::new()
.post("https://api.openai.com/v1/images/generations")
.json(&serde_json::json!({
"prompt": prompt.cloned(),
"n": num_images.cloned().parse::<i32>().unwrap_or(1),
"size":"1024x1024",
}))
.bearer_auth(api_key)
.send()
.await?
.json::<ImageResponse>()
.await?;
Ok(res)
});
rsx! {
Stylesheet { href: "https://unpkg.com/bulma@0.9.0/css/bulma.min.css" }
div { class: "container",
div { class: "columns",
div { class: "column",
input { class: "input is-primary mt-4",
value: "{api_key}",
r#type: "text",
placeholder: "Your OpenAI API Key",
oninput: move |evt| api_key.set(evt.value()),
}
input { class: "input is-primary mt-4",
placeholder: "MAX 1000 Dgts",
r#type: "text",
value:"{prompt}",
oninput: move |evt| prompt.set(evt.value())
}
input { class: "input is-primary mt-4",
r#type: "number",
min:"1",
max:"10",
value:"{num_images}",
oninput: move |evt| num_images.set(evt.value()),
}
}
}
button {
class: "button is-primary",
class: if image.is_pending() { "is-loading" },
onclick: move |_| {
image.call();
},
"Generate image"
}
if let Some(image) = image.value() {
for image in image.read().data.as_slice() {
section { class: "is-flex",
div { class: "container is-fluid",
div { class: "container has-text-centered",
div { class: "is-justify-content-center",
div { class: "level",
div { class: "level-item",
figure { class: "image", img { alt: "", src: "{image.url}", } }
}
}
}
}
}
}
}
}
}
}
}
File renamed without changes.
@@ -63,7 +63,7 @@ fn app() -> Element {
};
rsx! {
document::Link { rel: "stylesheet", href: STYLE }
Stylesheet { href: STYLE }
section { class: "todoapp",
TodoHeader { todos }
section { class: "main",
@@ -124,7 +124,7 @@ fn app() -> Element {
};
rsx! {
document::Link { rel: "stylesheet", href: STYLE }
Stylesheet { href: STYLE }
section { class: "todoapp",
TodoHeader { todos }
section { class: "main",
@@ -1,7 +1,8 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus::{fullstack::Loading, prelude::*};
use serde::{Deserialize, Serialize};
use std::fmt::Display;
fn main() {
dioxus::launch(app);
@@ -16,10 +17,10 @@ fn app() -> Element {
id: 2950159,
});
let current_weather = use_resource(move || async move { get_weather(&country()).await });
let current_weather = use_loader(move || get_weather(country()));
rsx! {
document::Link { rel: "stylesheet", href: "https://unpkg.com/tailwindcss@^2.0/dist/tailwind.min.css" }
Stylesheet { href: "https://unpkg.com/tailwindcss@^2.0/dist/tailwind.min.css" }
div { class: "mx-auto p-4 bg-gray-100 h-screen flex justify-center",
div { class: "flex items-center justify-center flex-row",
div { class: "flex items-start justify-center flex-row",
@@ -27,14 +28,20 @@ fn app() -> Element {
div { class: "flex flex-wrap w-full px-2",
div { class: "bg-gray-900 text-white relative min-w-0 break-words rounded-lg overflow-hidden shadow-sm mb-4 w-full dark:bg-gray-600",
div { class: "px-6 py-6 relative",
if let Some(Ok(weather)) = current_weather.read().as_ref() {
match current_weather {
Ok(weather) => rsx! {
CountryData {
country: country.read().clone(),
weather: weather.clone(),
weather: weather.cloned(),
}
Forecast { weather: weather.cloned() }
},
Err(Loading::Pending(_)) => rsx! {
div { "Loading weather data..." }
},
Err(Loading::Failed(_)) => rsx! {
div { "Failed to load weather data." }
}
Forecast { weather: weather.clone() }
} else {
p { "Loading.." }
}
}
}
@@ -110,8 +117,7 @@ fn Forecast(weather: WeatherResponse) -> Element {
#[component]
fn SearchBox(mut country: WriteSignal<WeatherLocation>) -> Element {
let mut input = use_signal(|| "".to_string());
let locations = use_resource(move || async move { get_locations(&input()).await });
let locations = use_resource(move || get_locations(input()));
rsx! {
div {
@@ -197,7 +203,7 @@ struct SearchResponse {
results: WeatherLocations,
}
async fn get_locations(input: &str) -> reqwest::Result<WeatherLocations> {
async fn get_locations(input: impl Display) -> reqwest::Result<WeatherLocations> {
let res = reqwest::get(&format!(
"https://geocoding-api.open-meteo.com/v1/search?name={input}"
))
@@ -226,13 +232,9 @@ struct DailyWeather {
temperature_2m_max: Vec<f32>,
}
async fn get_weather(location: &WeatherLocation) -> reqwest::Result<WeatherResponse> {
let res = reqwest::get(&format!("https://api.open-meteo.com/v1/forecast?latitude={}&longitude={}&hourly=temperature_2m&daily=temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min&timezone=GMT", location.latitude, location.longitude))
.await
?
async fn get_weather(location: WeatherLocation) -> reqwest::Result<WeatherResponse> {
reqwest::get(&format!("https://api.open-meteo.com/v1/forecast?latitude={}&longitude={}&hourly=temperature_2m&daily=temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min&timezone=GMT", location.latitude, location.longitude))
.await?
.json::<WeatherResponse>()
.await
?;
Ok(res)
}
+162
View File
@@ -0,0 +1,162 @@
//! A websocket chat demo using Dioxus' built-in websocket support.
//!
//! We setup an endpoint at `/api/chat` that accepts a `name` and `user_id` query parameter.
//! Each client connects to that endpoint, and we use a `tokio::broadcast` channel
//! to send messages to all connected clients.
//!
//! In practice, you'd use a distributed messaging system (Redis PubSub / Kafka / etc) to coordinate
//! between multiple server instances and an additional database to persist chat history.
use dioxus::fullstack::{WebSocketOptions, Websocket, use_websocket};
use dioxus::prelude::*;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
fn main() {
println!("IP and PORT: {:#?}", std::env::var("IP"));
println!("IP and PORT: {:#?}", std::env::var("PORT"));
dioxus::launch(app);
}
fn app() -> Element {
// store the user's current input
let mut input = use_signal(|| "".to_string());
// Select a unique id for the user, and then use that entropy to pick a random name
let user_id = use_signal(uuid::Uuid::new_v4);
let user_name = use_signal(|| {
match user_id.read().as_bytes()[0] % 7 {
0 => "Alice",
1 => "Bob",
2 => "Eve",
3 => "Mallory",
4 => "Trent",
5 => "Peggy",
6 => "Victor",
_ => "Charlie",
}
.to_string()
});
// Store the messages we've received from the server
let mut message_list = use_signal(Vec::<ChatMessage>::new);
// Connect to the websocket endpoint
let mut socket =
use_websocket(move || uppercase_ws(user_name(), user_id(), Default::default()));
use_future(move || async move {
while let Ok(msg) = socket.recv().await {
match msg {
ServerEvent::ReceiveMessage(message) => message_list.write().push(message),
ServerEvent::Connected { messages } => message_list.set(messages),
}
}
});
rsx! {
h1 { "WebSocket Chat" }
p { "Connection status: {socket.status():?} as {user_name}" }
input {
placeholder: "Type a message",
value: "{input}",
oninput: move |e| async move { input.set(e.value()) },
onkeydown: move |e| async move {
if e.key() == Key::Enter {
_ = socket.send(ClientEvent::SendMessage(input.read().clone())).await;
input.set("".to_string());
}
}
}
div {
for message in message_list.read().iter().rev() {
pre { "{message.name}: {message.message}" }
}
}
}
}
/// The events that the client can send to the server
#[derive(Serialize, Deserialize, Debug)]
enum ClientEvent {
SendMessage(String),
}
/// The events that the server can send to the client
#[derive(Serialize, Deserialize, Debug)]
enum ServerEvent {
Connected { messages: Vec<ChatMessage> },
ReceiveMessage(ChatMessage),
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
struct ChatMessage {
user_id: Uuid,
name: String,
message: String,
}
#[get("/api/chat?name&user_id")]
async fn uppercase_ws(
name: String,
user_id: Uuid,
options: WebSocketOptions,
) -> Result<Websocket<ClientEvent, ServerEvent>> {
use std::sync::LazyLock;
use tokio::sync::{
Mutex,
broadcast::{self, Sender},
};
// Every chat app needs a chat room! For this demo, we just use a tokio broadcast channel and a mutex-protected
// list of messages to store chat history.
//
// We place these types in the body of this serverfn since they're not used on the client, only the server.
static MESSAGES: LazyLock<Mutex<Vec<ChatMessage>>> = LazyLock::new(|| Mutex::new(Vec::new()));
static BROADCAST: LazyLock<Sender<ChatMessage>> = LazyLock::new(|| broadcast::channel(100).0);
Ok(options.on_upgrade(move |mut socket| async move {
// Send back all the messages from the room to the new client
let messages = MESSAGES.lock().await.clone();
_ = socket.send(ServerEvent::Connected { messages }).await;
// Subscriber to the broadcast channel
let sender = BROADCAST.clone();
let mut broadcast = sender.subscribe();
// Announce that we've joined
let _ = sender.send(ChatMessage {
message: format!("{name} has connected."),
user_id,
name: "[CONSOLE]".to_string(),
});
// Loop poll the broadcast receiver and the websocket for new messages
// If we receive a message from the broadcast channel, send it to the client
// If we receive a message from the client, broadcast it to all other clients and save it to the message list
loop {
tokio::select! {
Ok(msg) = broadcast.recv() => {
let _ = socket.send(ServerEvent::ReceiveMessage(msg)).await;
}
Ok(ClientEvent::SendMessage(message)) = socket.recv() => {
let chat_message = ChatMessage {
user_id,
name: name.clone(),
message,
};
let _ = sender.send(chat_message.clone());
MESSAGES.lock().await.push(chat_message.clone());
},
else => break,
}
}
_ = sender.send(ChatMessage {
name: "[CONSOLE]".to_string(),
message: format!("{name} has disconnected."),
user_id,
});
}))
}
@@ -12,8 +12,9 @@ fn app() -> Element {
let mut disabled = use_signal(|| false);
rsx! {
div { style: "text-align: center; margin: 20px; display: flex; flex-direction: column; align-items: center;",
button { onclick: move |_| disabled.toggle(),
div { text_align: "center", margin: "20px", display: "flex", flex_direction: "column", align_items: "center",
button {
onclick: move |_| disabled.toggle(),
"click to "
if disabled() { "enable" } else { "disable" }
" the lower button"
File renamed without changes.
@@ -7,7 +7,7 @@
//! If you `go-to-definition` on the `svg` element, you'll see its custom namespace.
use dioxus::prelude::*;
use rand::{rng, Rng};
use rand::{Rng, rng};
fn main() {
dioxus::launch(|| {
@@ -45,7 +45,7 @@ fn Dice() -> Element {
value.set(rng().random_range(1..=6))
},
rect { x: -1000, y: -1000, width: 2000, height: 2000, rx: 200, fill: "#aaa" }
for ((x, y), _) in DOTS.iter().zip(active_dots.read().iter()).filter(|(_, &active)| active) {
for ((x, y), _) in DOTS.iter().zip(active_dots.read().iter()).filter(|(_, active)| **active) {
circle {
cx: *x * 600,
cy: *y * 600,
File renamed without changes.
@@ -20,11 +20,11 @@ fn app() -> Element {
return;
}
response.respond(Response::new(include_bytes!("./assets/logo.png").to_vec()));
response.respond(Response::new(include_bytes!("../assets/logo.png").to_vec()));
});
rsx! {
document::Link { rel: "stylesheet", href: STYLE }
Stylesheet { href: STYLE }
h1 { "Dynamic Assets" }
img { src: "/logos/logo.png" }
}
File renamed without changes.
@@ -0,0 +1,20 @@
//! This example shows how to add metadata to the page with the Meta component
use dioxus::prelude::*;
fn main() {
dioxus::launch(app);
}
fn app() -> Element {
rsx! {
// You can use the Meta component to render a meta tag into the head of the page
// Meta tags are useful to provide information about the page to search engines and social media sites
// This example sets up meta tags for the open graph protocol for social media previews
Meta { property: "og:title", content: "My Site" }
Meta { property: "og:type", content: "website" }
Meta { property: "og:url", content: "https://www.example.com" }
Meta { property: "og:image", content: "https://example.com/image.jpg" }
Meta { name: "description", content: "My Site is a site" }
}
}
@@ -21,7 +21,7 @@ fn app() -> Element {
use_context_provider(|| Signal::new(Theme::Light));
rsx! {
document::Link { rel: "stylesheet", href: STYLE }
Stylesheet { href: STYLE }
main {
class: "main-container",
@@ -54,9 +54,20 @@ fn Show() -> Element {
ErrorBoundary {
handle_error: |errors: ErrorContext| {
rsx! {
for error in errors.errors() {
if let Some(error) = error.show() {
{error}
for error in errors.error() {
// You can downcast the error to see if it's a specific type and render something specific for it
if let Some(_error) = error.downcast_ref::<std::num::ParseIntError>() {
div {
background_color: "red",
border: "black",
border_width: "2px",
border_radius: "5px",
p { "Failed to parse data" }
Link {
to: Route::Home {},
"Go back to the homepage"
}
}
} else {
pre {
color: "red",
@@ -79,24 +90,8 @@ fn ParseNumberWithShow() -> Element {
button {
onclick: move |_| {
let request_data = "0.5";
let data: i32 = request_data.parse()
// You can attach rsx to results that can be displayed in the Error Boundary
.show(|_| rsx! {
div {
background_color: "red",
border: "black",
border_width: "2px",
border_radius: "5px",
p { "Failed to parse data" }
Link {
to: Route::Home {},
"Go back to the homepage"
}
}
})?;
let data: i32 = request_data.parse()?;
println!("parsed {data}");
Ok(())
},
"Click to throw an error"
@@ -18,7 +18,7 @@ fn main() {
fn app() -> Element {
rsx! {
document::Link { rel: "stylesheet", href: STYLE }
Stylesheet { href: STYLE }
Increment {}
Decrement {}
Reset {}
File renamed without changes.
@@ -17,7 +17,7 @@ fn app() -> Element {
let mut state = use_signal(|| PlayerState { is_playing: false });
rsx!(
document::Link { rel: "stylesheet", href: STYLE }
Stylesheet { href: STYLE }
h1 {"Select an option"}
// Add some cute animations if the radio is playing!
@@ -68,7 +68,7 @@ fn app() -> Element {
}
// We can also use the signal value as a slice
if let [ref first, .., ref last] = saved_values.read().as_slice() {
if let [first, .., last] = saved_values.read().as_slice() {
li { "First and last: {first}, {last}" }
} else {
"No saved values"
@@ -12,7 +12,7 @@ fn app() -> Element {
let mut millis = use_signal(|| 0);
use_future(move || async move {
// Save our initial timea
// Save our initial time
let start = Instant::now();
loop {
File renamed without changes.
@@ -2,7 +2,7 @@
use async_std::task::sleep;
use dioxus::prelude::*;
use futures_util::{future, stream, Stream, StreamExt};
use futures_util::{Stream, StreamExt, future, stream};
fn main() {
dioxus::launch(app);
+62
View File
@@ -0,0 +1,62 @@
//! Suspense in Dioxus
//!
//! Suspense allows components to bubble up loading states to parent components, simplifying data fetching.
use dioxus::prelude::*;
fn main() {
dioxus::launch(app)
}
fn app() -> Element {
rsx! {
div {
h1 { "Dogs are very important" }
p {
"The dog or domestic dog (Canis familiaris[4][5] or Canis lupus familiaris[5])"
"is a domesticated descendant of the wolf which is characterized by an upturning tail."
"The dog derived from an ancient, extinct wolf,[6][7] and the modern grey wolf is the"
"dog's nearest living relative.[8] The dog was the first species to be domesticated,[9][8]"
"by hunter–gatherers over 15,000 years ago,[7] before the development of agriculture.[1]"
}
h3 { "Illustrious Dog Photo" }
ErrorBoundary { handle_error: |_| rsx! { p { "Error loading doggos" } },
SuspenseBoundary { fallback: move |_| rsx! { "Loading doggos..." },
Doggo {}
}
}
}
}
}
#[component]
fn Doggo() -> Element {
// `use_loader` returns a Result<Loader<T>, Loading>. Loading can either be "Pending" or "Failed".
// When we use the `?` operator, the pending/error state will be thrown to the nearest Suspense or Error boundary.
//
// During SSR, `use_loader` will serialize the contents of the fetch, and during hydration, the client will
// use the pre-fetched data instead of re-fetching to render.
let mut dog = use_loader(move || async move {
#[derive(serde::Deserialize, serde::Serialize, PartialEq)]
struct DogApi {
message: String,
}
reqwest::get("https://dog.ceo/api/breeds/image/random/")
.await
.unwrap()
.json::<DogApi>()
.await
})?;
rsx! {
button { onclick: move |_| dog.restart(), "Click to fetch another doggo" }
div {
img {
max_width: "500px",
max_height: "500px",
src: "{dog.read().message}"
}
}
}
}
@@ -14,7 +14,7 @@ const STYLE: Asset = asset!("/examples/assets/flat_router.css");
fn main() {
dioxus::launch(|| {
rsx! {
document::Link { rel: "stylesheet", href: STYLE }
Stylesheet { href: STYLE }
Router::<Route> {}
}
})
File renamed without changes.
@@ -16,7 +16,7 @@ fn main() {
fn app() -> Element {
rsx! (
document::Link { rel: "stylesheet", href: STYLE }
Stylesheet { href: STYLE }
Router::<Route> {}
)
}
File renamed without changes.
@@ -13,7 +13,7 @@ const STYLE: Asset = asset!("/examples/assets/router.css");
fn main() {
dioxus::launch(|| {
rsx! {
document::Link { rel: "stylesheet", href: STYLE }
Stylesheet { href: STYLE }
Router::<Route> {}
}
});
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,61 @@
[package]
name = "fullstack-auth-example-with-fullstack"
version = "0.1.0"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus-web = { workspace = true, features = ["hydrate"], optional = true }
dioxus = { features = ["fullstack"], workspace = true }
axum = { workspace = true, optional = true, features = ["macros"]}
tokio = { workspace = true, features = ["full"], optional = true }
tower-http = { workspace = true, features = ["auth"], optional = true }
async-trait = { version = "0.1.88", optional = true }
sqlx = { version = "0.8.6", features = [
"macros",
"migrate",
"postgres",
"sqlite",
"_unstable-all-types",
"tls-native-tls",
"runtime-tokio",
], optional = true }
http = { workspace = true, optional = true }
tower = { workspace = true, optional = true }
execute = "0.2.13"
serde = { workspace = true }
anyhow = { workspace = true }
[dependencies.axum_session]
workspace = true
optional = true
[dependencies.axum_session_auth]
workspace = true
optional = true
[dependencies.axum_session_sqlx]
workspace = true
features = ["sqlite"]
optional = true
[features]
default = ["web", "server"]
server = [
"dioxus/server",
"dep:axum",
"dep:tokio",
"dep:tower-http",
"dep:async-trait",
"dep:sqlx",
"dep:axum_session",
"dep:axum_session_auth",
"dep:axum_session_sqlx",
"dep:http",
"dep:tower",
]
web = ["dioxus/web", "dep:dioxus-web"]
@@ -0,0 +1,81 @@
//! The code here is pulled from the `axum-session-auth` crate examples, requiring little to no
//! modification to work with dioxus fullstack.
use async_trait::async_trait;
use axum_session_auth::*;
use axum_session_sqlx::SessionSqlitePool;
use serde::{Deserialize, Serialize};
use sqlx::sqlite::SqlitePool;
use std::collections::HashSet;
pub(crate) type Session = axum_session_auth::AuthSession<User, i64, SessionSqlitePool, SqlitePool>;
pub(crate) type AuthLayer =
axum_session_auth::AuthSessionLayer<User, i64, SessionSqlitePool, SqlitePool>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct User {
pub id: i32,
pub anonymous: bool,
pub username: String,
pub permissions: HashSet<String>,
}
#[derive(sqlx::FromRow, Clone)]
pub(crate) struct SqlPermissionTokens {
pub token: String,
}
#[async_trait]
impl Authentication<User, i64, SqlitePool> for User {
async fn load_user(userid: i64, pool: Option<&SqlitePool>) -> Result<User, anyhow::Error> {
let db = pool.unwrap();
#[derive(sqlx::FromRow, Clone)]
struct SqlUser {
id: i32,
anonymous: bool,
username: String,
}
let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE id = $1")
.bind(userid)
.fetch_one(db)
.await
.unwrap();
//lets just get all the tokens the user can use, we will only use the full permissions if modifying them.
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
"SELECT token FROM user_permissions WHERE user_id = $1;",
)
.bind(userid)
.fetch_all(db)
.await
.unwrap();
Ok(User {
id: sqluser.id,
anonymous: sqluser.anonymous,
username: sqluser.username,
permissions: sql_user_perms.into_iter().map(|x| x.token).collect(),
})
}
fn is_authenticated(&self) -> bool {
!self.anonymous
}
fn is_active(&self) -> bool {
!self.anonymous
}
fn is_anonymous(&self) -> bool {
self.anonymous
}
}
#[async_trait]
impl HasPermission<SqlitePool> for User {
async fn has(&self, perm: &str, _pool: &Option<&SqlitePool>) -> bool {
self.permissions.contains(perm)
}
}
@@ -0,0 +1,158 @@
//! This example showcases how to use the `axum-session-auth` crate with Dioxus fullstack.
//! We add the `auth::Session` extractor to our server functions to get access to the current user session.
//!
//! To initialize the axum router, we use `dioxus::serve` to spawn a custom axum server that creates
//! our database, session store, and authentication layer.
//!
//! The `.serve_dioxus_application` method is used to mount our Dioxus app as a fallback service to
//! handle HTML rendering and static assets.
//!
//! We easily share the "permissions" between the server and client by using a `HashSet<String>`
//! which is serialized to/from JSON automatically by the server function system.
use std::collections::HashSet;
use dioxus::prelude::*;
#[cfg(feature = "server")]
mod auth;
fn main() {
// On the client, we simply launch the app as normal, taking over the main thread
#[cfg(not(feature = "server"))]
dioxus::launch(app);
// On the server, we can use `dioxus::serve` to create a server that serves our app.
//
// The `serve` function takes a closure that returns a `Future` which resolves to an `axum::Router`.
//
// We return a `Router` such that dioxus sets up logging, hot-reloading, devtools, and wires up the
// IP and PORT environment variables to our server.
#[cfg(feature = "server")]
dioxus::serve(|| async {
use crate::auth::*;
use axum_session::{SessionConfig, SessionLayer, SessionStore};
use axum_session_auth::AuthConfig;
use axum_session_sqlx::SessionSqlitePool;
use sqlx::{sqlite::SqlitePoolOptions, Executor};
// Create an in-memory SQLite database and set up our tables
let db = SqlitePoolOptions::new()
.max_connections(20)
.connect_with("sqlite::memory:".parse()?)
.await?;
// Create the tables (sessions, users)
db.execute(r#"CREATE TABLE IF NOT EXISTS users ( "id" INTEGER PRIMARY KEY, "anonymous" BOOLEAN NOT NULL, "username" VARCHAR(256) NOT NULL )"#,)
.await?;
db.execute(r#"CREATE TABLE IF NOT EXISTS user_permissions ( "user_id" INTEGER NOT NULL, "token" VARCHAR(256) NOT NULL)"#,)
.await?;
// Insert in some test data for two users (one anonymous, one normal)
db.execute(r#"INSERT INTO users (id, anonymous, username) SELECT 1, true, 'Guest' ON CONFLICT(id) DO UPDATE SET anonymous = EXCLUDED.anonymous, username = EXCLUDED.username"#,)
.await?;
db.execute(r#"INSERT INTO users (id, anonymous, username) SELECT 2, false, 'Test' ON CONFLICT(id) DO UPDATE SET anonymous = EXCLUDED.anonymous, username = EXCLUDED.username"#,)
.await?;
// Make sure our test user has the ability to view categories
db.execute(r#"INSERT INTO user_permissions (user_id, token) SELECT 2, 'Category::View'"#)
.await?;
// Create an axum router that dioxus will attach the app to
Ok(dioxus::server::router(app)
.layer(
AuthLayer::new(Some(db.clone()))
.with_config(AuthConfig::<i64>::default().with_anonymous_user_id(Some(1))),
)
.layer(SessionLayer::new(
SessionStore::<SessionSqlitePool>::new(
Some(db.into()),
SessionConfig::default().with_table_name("test_table"),
)
.await?,
)))
});
}
/// The UI for our app - is just a few buttons to call our server functions and display the results.
fn app() -> Element {
let mut login = use_action(login);
let mut user_name = use_action(get_user_name);
let mut permissions = use_action(get_permissions);
let mut logout = use_action(logout);
let fetch_new = move |_| async move {
user_name.call().await;
permissions.call().await;
};
rsx! {
div {
button {
onclick: move |_| async move {
login.call().await;
},
"Login Test User"
}
button {
onclick: move |_| async move {
logout.call().await;
},
"Logout"
}
button {
onclick: fetch_new,
"Fetch User Info"
}
pre { "Logged in: {login.result():?}" }
pre { "User name: {user_name.result():?}" }
pre { "Permissions: {permissions.result():?}" }
}
}
}
/// We use the `auth::Session` extractor to get access to the current user session.
/// This lets us modify the user session, log in/out, and access the current user.
#[post("/api/user/login", auth: auth::Session)]
pub async fn login() -> Result<()> {
auth.login_user(2);
Ok(())
}
/// Just like `login`, but this time we log out the user.
#[post("/api/user/logout", auth: auth::Session)]
pub async fn logout() -> Result<()> {
auth.logout_user();
Ok(())
}
/// We can access the current user via `auth.current_user`.
/// We can have both anonymous user (id 1) and a logged in user (id 2).
///
/// Logged-in users will have more permissions which we can modify.
#[post("/api/user/name", auth: auth::Session)]
pub async fn get_user_name() -> Result<String> {
Ok(auth.current_user.unwrap().username)
}
/// Get the current user's permissions, guarding the endpoint with the `Auth` validator.
/// If this returns false, we use the `or_unauthorized` extension to return a 401 error.
#[get("/api/user/permissions", auth: auth::Session)]
pub async fn get_permissions() -> Result<HashSet<String>> {
use crate::auth::User;
use axum_session_auth::{Auth, Rights};
let user = auth.current_user.unwrap();
Auth::<User, i64, sqlx::SqlitePool>::build([axum::http::Method::GET], false)
.requires(Rights::any([
Rights::permission("Category::View"),
Rights::permission("Admin::View"),
]))
.validate(&user, &axum::http::Method::GET, None)
.await
.or_unauthorized("You do not have permission to view categories")?;
Ok(user.permissions)
}
@@ -10,8 +10,6 @@ publish = false
[dependencies]
dioxus-web = { workspace = true, features = ["hydrate"], optional = true }
dioxus = { features = ["fullstack"], workspace = true }
dioxus-fullstack = { workspace = true }
dioxus-cli-config = { workspace = true, optional = true }
axum = { workspace = true, optional = true }
tokio = { workspace = true, features = ["full"], optional = true }
tower-http = { workspace = true, features = ["auth"], optional = true }
@@ -49,7 +47,6 @@ optional = true
default = []
server = [
"dioxus/server",
"dep:dioxus-cli-config",
"dep:axum",
"dep:tokio",
"dep:tower-http",
@@ -1,23 +1,15 @@
use async_trait::async_trait;
use axum::{
http::Method,
response::{IntoResponse, Response},
routing::get,
Router,
};
use axum_session::{SessionConfig, SessionLayer, SessionStore};
use axum_session_auth::*;
use axum_session_sqlx::SessionSqlitePool;
use core::pin::Pin;
use dioxus::prelude::*;
use serde::{Deserialize, Serialize};
use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions};
use std::error::Error;
use std::future::Future;
use std::{collections::HashSet, net::SocketAddr, str::FromStr};
use sqlx::sqlite::SqlitePool;
use std::collections::HashSet;
pub(crate) type Session =
axum_session_auth::AuthSession<User, i64, SessionSqlitePool, sqlx::SqlitePool>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub(crate) struct User {
pub id: i32,
pub anonymous: bool,
pub username: String,
@@ -25,25 +17,10 @@ pub struct User {
}
#[derive(sqlx::FromRow, Clone)]
pub struct SqlPermissionTokens {
pub(crate) struct SqlPermissionTokens {
pub token: String,
}
impl Default for User {
fn default() -> Self {
let mut permissions = HashSet::new();
permissions.insert("Category::View".to_owned());
Self {
id: 1,
anonymous: true,
username: "Guest".into(),
permissions,
}
}
}
#[async_trait]
impl Authentication<User, i64, SqlitePool> for User {
async fn load_user(userid: i64, pool: Option<&SqlitePool>) -> Result<User, anyhow::Error> {
@@ -76,6 +53,13 @@ impl HasPermission<SqlitePool> for User {
impl User {
pub async fn get_user(id: i64, pool: &SqlitePool) -> Option<Self> {
#[derive(sqlx::FromRow, Clone)]
struct SqlUser {
id: i32,
anonymous: bool,
username: String,
}
let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE id = $1")
.bind(id)
.fetch_one(pool)
@@ -91,10 +75,15 @@ impl User {
.await
.ok()?;
Some(sqluser.into_user(Some(sql_user_perms)))
Some(User {
id: sqluser.id,
anonymous: sqluser.anonymous,
username: sqluser.username,
permissions: sql_user_perms.into_iter().map(|x| x.token).collect(),
})
}
pub async fn create_user_tables(pool: &SqlitePool) {
pub async fn create_user_tables(pool: &SqlitePool) -> Result<(), sqlx::Error> {
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS users (
@@ -105,20 +94,17 @@ impl User {
"#,
)
.execute(pool)
.await
.unwrap();
.await?;
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS user_permissions (
"user_id" INTEGER NOT NULL,
"token" VARCHAR(256) NOT NULL
)
"#,
)"#,
)
.execute(pool)
.await
.unwrap();
.await?;
sqlx::query(
r#"
@@ -130,8 +116,7 @@ impl User {
"#,
)
.execute(pool)
.await
.unwrap();
.await?;
sqlx::query(
r#"
@@ -143,8 +128,7 @@ impl User {
"#,
)
.execute(pool)
.await
.unwrap();
.await?;
sqlx::query(
r#"
@@ -153,51 +137,8 @@ impl User {
"#,
)
.execute(pool)
.await
.unwrap();
.await?;
Ok(())
}
}
#[derive(sqlx::FromRow, Clone)]
pub struct SqlUser {
pub id: i32,
pub anonymous: bool,
pub username: String,
}
impl SqlUser {
pub fn into_user(self, sql_user_perms: Option<Vec<SqlPermissionTokens>>) -> User {
User {
id: self.id,
anonymous: self.anonymous,
username: self.username,
permissions: if let Some(user_perms) = sql_user_perms {
user_perms
.into_iter()
.map(|x| x.token)
.collect::<HashSet<String>>()
} else {
HashSet::<String>::new()
},
}
}
}
pub async fn connect_to_database() -> SqlitePool {
let connect_opts = SqliteConnectOptions::from_str("sqlite::memory:").unwrap();
SqlitePoolOptions::new()
.max_connections(5)
.connect_with(connect_opts)
.await
.unwrap()
}
pub type Session =
axum_session_auth::AuthSession<crate::auth::User, i64, SessionSqlitePool, sqlx::SqlitePool>;
pub async fn get_session() -> ServerFnResult<Session> {
extract::<Session, _>()
.await
.map_err(|_| ServerFnError::new("AuthSessionLayer was not found"))
}
+129
View File
@@ -0,0 +1,129 @@
#[cfg(feature = "server")]
mod auth;
use dioxus::prelude::*;
fn main() {
#[cfg(feature = "server")]
dioxus::serve(|| async {
use crate::auth::*;
use axum_session::{SessionConfig, SessionLayer, SessionStore};
use axum_session_auth::AuthConfig;
use axum_session_sqlx::SessionSqlitePool;
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
use std::str::FromStr;
let pool = SqlitePoolOptions::new()
.max_connections(5)
.connect_with(SqliteConnectOptions::from_str("sqlite::memory:").unwrap())
.await?;
// This Defaults as normal Cookies.
// To enable Private cookies for integrity, and authenticity please check the next Example.
let session_config = SessionConfig::default().with_table_name("test_table");
let auth_config = AuthConfig::<i64>::default().with_anonymous_user_id(Some(1));
let session_store =
SessionStore::<SessionSqlitePool>::new(Some(pool.clone().into()), session_config)
.await
.unwrap();
User::create_user_tables(&pool).await?;
// build our application with some routes
Ok(
dioxus::server::router(app)
.layer(
axum_session_auth::AuthSessionLayer::<
User,
i64,
SessionSqlitePool,
sqlx::SqlitePool,
>::new(Some(pool))
.with_config(auth_config),
)
.layer(SessionLayer::new(session_store)),
)
});
dioxus::launch(app);
}
fn app() -> Element {
let mut user_name = use_action(get_user_name);
let mut permissions = use_action(get_permissions);
let mut login = use_action(login);
rsx! {
div {
button {
onclick: move |_| {
login.call();
},
"Login Test User"
}
}
div {
button {
onclick: move |_| {
user_name.call();
},
"Get User Name"
}
"User name: {user_name.value().unwrap_or_default()}"
}
div {
button {
onclick: move |_| {
permissions.call();
},
"Get Permissions"
}
"Permissions: {permissions.value().unwrap_or_default()}"
}
}
}
#[get("/api/user/name", auth: auth::Session)]
pub async fn get_user_name() -> Result<String> {
Ok(auth
.current_user
.context("UNAUTHORIZED")?
.username
.to_string())
}
#[post("/api/user/login", auth: auth::Session)]
pub async fn login() -> Result<()> {
auth.login_user(2);
Ok(())
}
#[get("/api/user/permissions", auth: auth::Session)]
pub async fn get_permissions() -> Result<String> {
use crate::auth::User;
use axum_session_auth::{Auth, Rights};
let user = auth.current_user.as_ref().context("UNAUTHORIZED")?;
// lets check permissions only and not worry about if they are anon or not
let has_permissions =
Auth::<User, i64, sqlx::SqlitePool>::build([axum::http::Method::POST], false)
.requires(Rights::any([
Rights::permission("Category::View"),
Rights::permission("Admin::View"),
]))
.validate(user, &axum::http::Method::GET, None)
.await;
if !has_permissions {
dioxus::core::bail!(
"User {}, Does not have permissions needed to view this page please login",
user.username
);
}
Ok(format!(
"User has Permissions needed. {:?}",
user.permissions
))
}
@@ -0,0 +1,46 @@
//! This example demonstrates how to use `dioxus::serve` with a custom Axum router.
//!
//! By default, `dioxus::launch` takes over the main thread and runs the Dioxus application.
//! However, if you want to integrate Dioxus into an existing web server or use a custom router,
//! you can use `dioxus::serve` to create a server that serves your Dioxus application alongside
//! other routes.
//!
//! `dioxus::serve` sets up an async runtime, logging, hot-reloading, crash handling, and more.
//! You can then use the `.serve_dioxus_application` method on your router to serve the Dioxus app.
//!
//! `dioxus::serve` is most useful for customizing the server setup, such as adding middleware,
//! custom routes, or integrating with existing axum backend code.
//!
//! Note that `dioxus::serve` is accepts a Router from `axum`. Dioxus will use the IP and PORT
//! environment variables to determine where to bind the server. To customize the port, use environment
//! variables or a `.env` file.
//!
//! On other platforms (like desktop or mobile), you'll want to use `dioxus::launch` instead and then
//! handle async loading of data through hooks like `use_future` or `use_resource` and give the user
//! a loading state while data is being fetched.
use dioxus::prelude::*;
fn main() {
// On the client we just launch the app as normal.
#[cfg(not(feature = "server"))]
dioxus::launch(app);
// On the server, we can use `dioxus::serve` and `.serve_dioxus_application` to serve our app with routing.
// The `dioxus::server::router` function creates a new axum Router with the necessary routes to serve the Dioxus app.
#[cfg(feature = "server")]
dioxus::serve(|| async move {
use dioxus::server::axum::routing::{get, post};
Ok(dioxus::server::router(app)
.route("/submit", post(|| async { "Form submitted!" }))
.route("/about", get(|| async { "About us" }))
.route("/contact", get(|| async { "Contact us" })))
});
}
fn app() -> Element {
rsx! {
div { "Hello from Dioxus!" }
}
}
Loaded 100 of 424 files, more files were not shown because too many files have changed in this diff. Show more