mirror of
https://github.com/juspay/omnix.git
synced 2024-11-30 23:44:17 +03:00
Leptos -> Dioxus Desktop (#79)
Switch from Leptos to Dioxus, and convert this to be a desktop. Consequently, the following changes have been made: - Remove e2e testing (we may have to probably reintroduce this latter after figuring out how to test desktop apps; the code exists in `leptos` branch) - Remove bunch of server/cli code that are no longer necessary - Remove `leptos_extra` crate - `nix_rs`: no longer needs "ssr" feature flags
This commit is contained in:
parent
3882f9a5c6
commit
99086207c9
2
.envrc
2
.envrc
@ -1,2 +1,2 @@
|
|||||||
use flake
|
|
||||||
nix_direnv_watch_file */*.nix *.nix
|
nix_direnv_watch_file */*.nix *.nix
|
||||||
|
use flake
|
||||||
|
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
flake.lock linguist-generated=true
|
||||||
|
assets/tailwind.css linguist-generated=true
|
18
.github/workflows/ci.yaml
vendored
18
.github/workflows/ci.yaml
vendored
@ -1,18 +0,0 @@
|
|||||||
name: "CI"
|
|
||||||
on:
|
|
||||||
# Run only when pushing to master branch, and making PRs
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Install Nix
|
|
||||||
uses: DeterminateSystems/nix-installer-action@main
|
|
||||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
|
||||||
- run: nix --accept-flake-config build --no-link --print-out-paths .#e2e-playwright-test
|
|
||||||
- name: E2E tests
|
|
||||||
run: nix --accept-flake-config run .#e2e-playwright-test
|
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -13,6 +13,9 @@
|
|||||||
"rust": "html",
|
"rust": "html",
|
||||||
"*.rs": "html"
|
"*.rs": "html"
|
||||||
},
|
},
|
||||||
|
"tailwindCSS.experimental.classRegex": [
|
||||||
|
"class: \"(.*)\""
|
||||||
|
],
|
||||||
"files.associations": {
|
"files.associations": {
|
||||||
"*.css": "tailwindcss"
|
"*.css": "tailwindcss"
|
||||||
}
|
}
|
||||||
|
3688
Cargo.lock
generated
3688
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
86
Cargo.toml
86
Cargo.toml
@ -10,22 +10,16 @@ repository = "https://github.com/juspay/nix-browser"
|
|||||||
cfg-if = "1"
|
cfg-if = "1"
|
||||||
clap = { version = "4.3", features = ["derive", "env"] }
|
clap = { version = "4.3", features = ["derive", "env"] }
|
||||||
human-panic = "1.1.5"
|
human-panic = "1.1.5"
|
||||||
leptos = { version = "0.4", features = ["serde", "nightly"] }
|
|
||||||
leptos_meta = { version = "0.4", features = ["nightly"] }
|
|
||||||
leptos_router = { version = "0.4", features = ["nightly"] }
|
|
||||||
leptos_query = "0.2"
|
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
tracing-subscriber-wasm = "0.1"
|
|
||||||
wasm-bindgen = "=0.2.87" # The version here must match the pinned stuff in Nix flakes.
|
|
||||||
nix_rs = { version = "0.2", path = "./crates/nix_rs" }
|
nix_rs = { version = "0.2", path = "./crates/nix_rs" }
|
||||||
nix_health = { path = "./crates/nix_health" }
|
nix_health = { path = "./crates/nix_health" }
|
||||||
leptos_extra = { path = "./crates/leptos_extra" }
|
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_with = { version = "3.2", features = ["json"] }
|
serde_with = { version = "3.2", features = ["json"] }
|
||||||
bytesize = { version = "1.3.0", features = ["serde"] }
|
bytesize = { version = "1.3.0", features = ["serde"] }
|
||||||
|
anyhow = "1.0.75"
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@ -37,89 +31,29 @@ homepage = "https://github.com/juspay/nix-browser"
|
|||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
|
|
||||||
[lib]
|
|
||||||
crate-type = ["cdylib", "rlib"]
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = { version = "0.6", features = ["json", "tokio"], optional = true }
|
anyhow.workspace = true
|
||||||
axum-macros = { version = "0.3", optional = true }
|
|
||||||
cfg-if.workspace = true
|
cfg-if.workspace = true
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
console_error_panic_hook = "0.1"
|
console_error_panic_hook = "0.1"
|
||||||
console_log = { version = "1" }
|
console_log = "1"
|
||||||
http = { version = "0.2", optional = true }
|
http = "0.2"
|
||||||
human-panic.workspace = true
|
human-panic.workspace = true
|
||||||
hyper = { version = "0.14", features = ["server"], optional = true }
|
|
||||||
leptos.workspace = true
|
|
||||||
leptos_axum = { version = "0.4", optional = true }
|
|
||||||
leptos_meta.workspace = true
|
|
||||||
leptos_router.workspace = true
|
|
||||||
leptos_query.workspace = true
|
|
||||||
regex = "1.9.3"
|
regex = "1.9.3"
|
||||||
open = { version = "5.0", optional = true }
|
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
serde_with.workspace = true
|
serde_with.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
tokio = { version = "1.29", features = ["full"], optional = true }
|
tokio = { version = "1", features = ["full"] }
|
||||||
tower = { version = "0.4", optional = true }
|
|
||||||
tower-http = { version = "0.4", features = ["full"], optional = true }
|
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
tracing-subscriber-wasm.workspace = true
|
|
||||||
uuid = { version = "1.3.0", features = ["serde", "v4", "js"] }
|
uuid = { version = "1.3.0", features = ["serde", "v4", "js"] }
|
||||||
wasm-bindgen.workspace = true
|
|
||||||
nix_rs.workspace = true
|
nix_rs.workspace = true
|
||||||
nix_health.workspace = true
|
nix_health.workspace = true
|
||||||
leptos_extra.workspace = true
|
dioxus = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "459f24d5e9ef0e03a3bbd037342c60bbde409dbf" }
|
||||||
|
dioxus-desktop = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "459f24d5e9ef0e03a3bbd037342c60bbde409dbf" }
|
||||||
[features]
|
dioxus-router = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "459f24d5e9ef0e03a3bbd037342c60bbde409dbf" }
|
||||||
default = [
|
dioxus-signals = { git = "https://github.com/DioxusLabs/dioxus.git", rev = "459f24d5e9ef0e03a3bbd037342c60bbde409dbf" }
|
||||||
"ssr",
|
dioxus-std = { version = "0.4.0", features = ["clipboard", "utils"] }
|
||||||
] # Unfortunately, leptos_query won't compile (in `nix build`) without this
|
|
||||||
hydrate = [
|
|
||||||
"leptos/hydrate",
|
|
||||||
"leptos_meta/hydrate",
|
|
||||||
"leptos_query/hydrate",
|
|
||||||
"leptos_router/hydrate",
|
|
||||||
"leptos_extra/hydrate",
|
|
||||||
]
|
|
||||||
ssr = [
|
|
||||||
"dep:axum-macros",
|
|
||||||
"dep:axum",
|
|
||||||
"dep:http",
|
|
||||||
"dep:hyper",
|
|
||||||
"dep:leptos_axum",
|
|
||||||
"dep:open",
|
|
||||||
"dep:tokio",
|
|
||||||
"dep:tower-http",
|
|
||||||
"dep:tower",
|
|
||||||
"leptos/ssr",
|
|
||||||
"leptos_meta/ssr",
|
|
||||||
"leptos_query/ssr",
|
|
||||||
"leptos_router/ssr",
|
|
||||||
"leptos_extra/ssr",
|
|
||||||
"nix_rs/ssr",
|
|
||||||
"nix_health/ssr",
|
|
||||||
]
|
|
||||||
|
|
||||||
# Defines a size-optimized profile for the WASM bundle in release mode
|
|
||||||
[profile.wasm-release]
|
|
||||||
inherits = "release"
|
|
||||||
opt-level = 'z'
|
|
||||||
lto = true
|
|
||||||
codegen-units = 1
|
|
||||||
panic = "abort"
|
|
||||||
|
|
||||||
[package.metadata.leptos]
|
|
||||||
site-addr = "127.0.0.1:3000"
|
|
||||||
tailwind-input-file = "css/input.css"
|
|
||||||
assets-dir = "assets"
|
|
||||||
bin-features = ["ssr"]
|
|
||||||
lib-features = ["hydrate"]
|
|
||||||
# The profile to use for the lib target when compiling for release
|
|
||||||
#
|
|
||||||
# Optional. Defaults to "release".
|
|
||||||
lib-profile-release = "wasm-release"
|
|
||||||
|
45
Dioxus.toml
Normal file
45
Dioxus.toml
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
[application]
|
||||||
|
name = "nix-browser"
|
||||||
|
default_platform = "desktop"
|
||||||
|
out_dir = "dist"
|
||||||
|
asset_dir = "assets"
|
||||||
|
|
||||||
|
[web.app]
|
||||||
|
title = "nix-browser | 🌍"
|
||||||
|
|
||||||
|
[web.watcher]
|
||||||
|
# when watcher trigger, regenerate the `index.html`
|
||||||
|
reload_html = true
|
||||||
|
# which files or dirs will be watcher monitoring
|
||||||
|
watch_path = ["src", "assets"]
|
||||||
|
|
||||||
|
[web.resource]
|
||||||
|
# CSS style file
|
||||||
|
style = ["tailwind.css"]
|
||||||
|
# Javascript code file
|
||||||
|
script = []
|
||||||
|
|
||||||
|
[web.resource.dev]
|
||||||
|
# CSS style file
|
||||||
|
style = []
|
||||||
|
# Javascript code file
|
||||||
|
script = []
|
||||||
|
|
||||||
|
# FIXME: Need to `cd assets` before running `dx bundle` due to https://github.com/DioxusLabs/dioxus/issues/1283
|
||||||
|
[bundle]
|
||||||
|
name = "Nix Browser"
|
||||||
|
identifier = "in.juspay.nix-browser"
|
||||||
|
icon = ["images/128x128.png"] # ["32x32.png", "128x128.png", "128x128@2x.png"]
|
||||||
|
version = "1.0.0"
|
||||||
|
# TODO: Must add these files
|
||||||
|
resources = ["**/tailwind.css", "images/**/*.png"] # , "secrets/public_key.txt"]
|
||||||
|
copyright = "Copyright (c) Juspay 2023. All rights reserved."
|
||||||
|
category = "Developer Tool"
|
||||||
|
short_description = "WIP: nix-browser"
|
||||||
|
long_description = """
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
|
||||||
|
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
|
||||||
|
enim ad minim veniam, quis nostrud exercitation ullamco laboris
|
||||||
|
nisi ut aliquip ex ea commodo consequat.
|
||||||
|
"""
|
||||||
|
osx_frameworks = []
|
16
README.md
16
README.md
@ -1,6 +1,6 @@
|
|||||||
# nix-browser
|
# nix-browser
|
||||||
|
|
||||||
🚧 This project is a work in progress. The ultimate goal is to create something that inspires people towards [Nix](https://zero-to-flakes.com/).
|
🚧 This project is a work in progress. The ultimate goal is to create a GUI app that inspires people towards using [Nix](https://zero-to-flakes.com/).
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
@ -15,12 +15,17 @@ This will automatically activate the nix develop shell. Open VSCode and install
|
|||||||
|
|
||||||
## Running locally
|
## Running locally
|
||||||
|
|
||||||
In nix shell,
|
We should run two watchers: one for generating Tailwind CSS (`just tw`) and another running the cargo package (`just watch`). In nix shell,
|
||||||
|
|
||||||
```
|
```
|
||||||
|
# In one terminal,
|
||||||
just watch
|
just watch
|
||||||
|
# In another,
|
||||||
|
just tw
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`just watch` runs `dx serve` (with hot reload disabled) that will restart the desktop app after compilation.
|
||||||
|
|
||||||
## Nix workflows
|
## Nix workflows
|
||||||
|
|
||||||
Inside the nix develop shell (activated by direnv) you can use any of the `cargo` or `rustc` commands, as well as [`just`](https://just.systems/) workflows. Nix specific commands can also be used to work with the project:
|
Inside the nix develop shell (activated by direnv) you can use any of the `cargo` or `rustc` commands, as well as [`just`](https://just.systems/) workflows. Nix specific commands can also be used to work with the project:
|
||||||
@ -38,14 +43,13 @@ nix run
|
|||||||
- When you are done with your changes, run `just fmt` to **autoformat** the source tree; the CI checks for this.
|
- When you are done with your changes, run `just fmt` to **autoformat** the source tree; the CI checks for this.
|
||||||
- Add tests if relevant, and run them:
|
- Add tests if relevant, and run them:
|
||||||
- Run `just test` to run the **unit tests**.
|
- Run `just test` to run the **unit tests**.
|
||||||
- Run `just e2e` (requires `just watch` to be running) or `just e2e-release` to run the **end-to-end tests**
|
|
||||||
- Add documentation wherever useful. To preview the **docs**, run `just doc`.
|
- Add documentation wherever useful. To preview the **docs**, run `just doc`.
|
||||||
|
|
||||||
## Frontend tech
|
## Tech
|
||||||
|
|
||||||
### Rust wasm
|
### Rust desktop app
|
||||||
|
|
||||||
We use [Leptos](https://leptos.dev/). With sufficient knowledge of Rust, you can 🎓 read the [Leptos Book](https://leptos-rs.github.io/leptos/) to get familiar with reactive frontend programming in Rust.
|
We use [Dioxus](https://dioxuslabs.com/) to build the desktop app using web technologies. The yet to be released [dioxus-signals](https://github.com/DioxusLabs/dioxus/tree/master/packages/signals) package is also used for data reactivity.
|
||||||
|
|
||||||
### Styling
|
### Styling
|
||||||
|
|
||||||
|
BIN
assets/images/128x128.png
Normal file
BIN
assets/images/128x128.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.6 KiB |
1155
assets/tailwind.css
generated
Normal file
1155
assets/tailwind.css
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,19 +0,0 @@
|
|||||||
[package]
|
|
||||||
edition = "2021"
|
|
||||||
name = "leptos_extra"
|
|
||||||
version = "0.1.0"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
crate-type = ["cdylib", "rlib"]
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
cfg-if.workspace = true
|
|
||||||
leptos.workspace = true
|
|
||||||
leptos_query.workspace = true
|
|
||||||
tracing.workspace = true
|
|
||||||
|
|
||||||
[features]
|
|
||||||
hydrate = ["leptos/hydrate", "leptos_query/hydrate"]
|
|
||||||
ssr = ["leptos/ssr", "leptos_query/ssr"]
|
|
@ -1,3 +0,0 @@
|
|||||||
//! Extra modules for [leptos]
|
|
||||||
pub mod query;
|
|
||||||
pub mod signal;
|
|
@ -1,104 +0,0 @@
|
|||||||
//! [leptos_query] helpers for working with [server] fns, and useful widgets.
|
|
||||||
use cfg_if::cfg_if;
|
|
||||||
use leptos::*;
|
|
||||||
use leptos_query::*;
|
|
||||||
use std::{future::Future, hash::Hash};
|
|
||||||
use tracing::instrument;
|
|
||||||
|
|
||||||
/// The result type of Leptos [server] function returning a `T`
|
|
||||||
pub type ServerFnResult<T> = Result<T, ServerFnError>;
|
|
||||||
|
|
||||||
/// Sensible [QueryOptions] defaults for an app
|
|
||||||
pub fn query_options<V>() -> QueryOptions<V> {
|
|
||||||
QueryOptions {
|
|
||||||
// Disable staleness so the query is not refetched on every route switch.
|
|
||||||
stale_time: None,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Like [use_query] but specifically meant for server functions, does logging
|
|
||||||
/// via [tracing] and uses [query_options] always.
|
|
||||||
///
|
|
||||||
/// In order to be able to log the name of the server fns, we unfortunately must
|
|
||||||
/// require them to be 1-ary functions taking tuples, due to a limitation with
|
|
||||||
/// Rust type system around Fn trait.
|
|
||||||
///
|
|
||||||
/// Arguments
|
|
||||||
/// * `k`: The argument to the server fn
|
|
||||||
/// * `fetcher`: The server fn to call
|
|
||||||
#[instrument(
|
|
||||||
name = "use_server_query",
|
|
||||||
skip(cx, k, fetcher),
|
|
||||||
fields(
|
|
||||||
fetcher = std::any::type_name::<F>(),
|
|
||||||
render_mode=LEPTOS_MODE
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
pub fn use_server_query<K, V, F, Fu>(
|
|
||||||
cx: Scope,
|
|
||||||
k: impl Fn() -> K + 'static,
|
|
||||||
fetcher: F,
|
|
||||||
) -> QueryResult<ServerFnResult<V>, impl RefetchFn>
|
|
||||||
where
|
|
||||||
K: Hash + Eq + Clone + std::fmt::Debug + 'static,
|
|
||||||
ServerFnResult<V>: Clone + Serializable + 'static,
|
|
||||||
Fu: Future<Output = ServerFnResult<V>> + 'static,
|
|
||||||
F: Fn(K) -> Fu + 'static,
|
|
||||||
{
|
|
||||||
let span = tracing::Span::current();
|
|
||||||
tracing::debug!("use_query");
|
|
||||||
leptos_query::use_query(
|
|
||||||
cx,
|
|
||||||
k,
|
|
||||||
move |k| {
|
|
||||||
let _enter = span.enter();
|
|
||||||
tracing::info!("calling server fn");
|
|
||||||
fetcher(k)
|
|
||||||
},
|
|
||||||
query_options::<ServerFnResult<V>>(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const LEPTOS_MODE: &str = {
|
|
||||||
cfg_if! { if #[cfg(feature="ssr")] {
|
|
||||||
"ssr"
|
|
||||||
} else if #[cfg(feature="hydrate")] {
|
|
||||||
"hydrate"
|
|
||||||
} else {
|
|
||||||
compile_error!("Either ssr or hydrate feature must be enabled");
|
|
||||||
}}
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Button component to refresh the given [leptos_query] query.
|
|
||||||
///
|
|
||||||
/// Arguments
|
|
||||||
/// * `result`: The query result to refresh
|
|
||||||
/// * `query`: The value to pass to [invalidate_query]
|
|
||||||
#[component]
|
|
||||||
pub fn RefetchQueryButton<K, V, R, F>(
|
|
||||||
cx: Scope,
|
|
||||||
result: QueryResult<ServerFnResult<V>, R>,
|
|
||||||
query: F,
|
|
||||||
) -> impl IntoView
|
|
||||||
where
|
|
||||||
K: Hash + Eq + Clone + std::fmt::Debug + 'static,
|
|
||||||
ServerFnResult<V>: Clone + Serializable + 'static,
|
|
||||||
R: RefetchFn,
|
|
||||||
F: Fn() -> K + 'static,
|
|
||||||
{
|
|
||||||
view! { cx,
|
|
||||||
<button
|
|
||||||
class="p-1 text-white shadow border-1 bg-primary-700 disabled:bg-base-400 disabled:text-black"
|
|
||||||
disabled=move || result.is_fetching.get()
|
|
||||||
on:click=move |_| {
|
|
||||||
let k = query();
|
|
||||||
tracing::debug!("Invalidating query");
|
|
||||||
use_query_client(cx).invalidate_query::<K, ServerFnResult<V>>(k);
|
|
||||||
}
|
|
||||||
>
|
|
||||||
|
|
||||||
{move || if result.is_fetching.get() { "Fetching..." } else { "Re-fetch" }}
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,78 +0,0 @@
|
|||||||
//! [Signal] related helpers for Leptos
|
|
||||||
use leptos::*;
|
|
||||||
use tracing::instrument;
|
|
||||||
|
|
||||||
/// [provide_context] a new signal of type `T` in the current scope
|
|
||||||
pub fn provide_signal<T: 'static>(cx: Scope, default: T) {
|
|
||||||
let sig = create_rw_signal(cx, default);
|
|
||||||
provide_context(cx, sig);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// [use_context] the signal of type `T` in the current scope
|
|
||||||
///
|
|
||||||
/// If the signal was not provided in a top-level scope (via [provide_signal])
|
|
||||||
/// this method will panic after tracing an error.
|
|
||||||
#[instrument(name = "use_signal")]
|
|
||||||
pub fn use_signal<T>(cx: Scope) -> RwSignal<T> {
|
|
||||||
use_context(cx)
|
|
||||||
.ok_or_else(|| {
|
|
||||||
// This happens if the dev forgets to call `provide_signal::<T>` in
|
|
||||||
// the parent scope
|
|
||||||
let msg = format!(
|
|
||||||
"no signal provided for type: {}",
|
|
||||||
std::any::type_name::<T>()
|
|
||||||
);
|
|
||||||
tracing::error!(msg);
|
|
||||||
msg
|
|
||||||
})
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extends [SignalWith] to add a `with_result` method that operates on the
|
|
||||||
/// inner value, avoiding the need to clone it.
|
|
||||||
pub trait SignalWithResult<T, E>: SignalWith<Option<Result<T, E>>> {
|
|
||||||
/// Like [SignalWith::with] but operates on the inner [Result] value without cloning it.
|
|
||||||
fn with_result<U>(&self, f: impl Fn(&T) -> U + 'static) -> Option<Result<U, E>>
|
|
||||||
where
|
|
||||||
E: Clone,
|
|
||||||
{
|
|
||||||
self.with(move |d| d.map_option_result(f))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, E> SignalWithResult<T, E> for Signal<Option<Result<T, E>>> {}
|
|
||||||
|
|
||||||
/// Functions unique to [Option] of [Result] values
|
|
||||||
pub trait OptionResult<T, E> {
|
|
||||||
/// Map the value inside a nested [Option]-of-[Result]
|
|
||||||
///
|
|
||||||
/// This function is efficient in that the inner value is not cloned.
|
|
||||||
fn map_option_result<U>(&self, f: impl Fn(&T) -> U + 'static) -> Option<Result<U, E>>
|
|
||||||
where
|
|
||||||
E: Clone;
|
|
||||||
|
|
||||||
/// Like [[Option::unwrap_or]] but unwraps the nested value
|
|
||||||
fn unwrap_option_result_value_or(&self, default: T) -> T
|
|
||||||
where
|
|
||||||
T: Clone;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, E> OptionResult<T, E> for Option<Result<T, E>> {
|
|
||||||
fn map_option_result<U>(&self, f: impl Fn(&T) -> U + 'static) -> Option<Result<U, E>>
|
|
||||||
where
|
|
||||||
E: Clone,
|
|
||||||
{
|
|
||||||
self.as_ref()
|
|
||||||
.map(|r| r.as_ref().map(f).map_err(Clone::clone))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unwrap_option_result_value_or(&self, default: T) -> T
|
|
||||||
where
|
|
||||||
T: Clone,
|
|
||||||
{
|
|
||||||
self.as_ref()
|
|
||||||
.and_then(|r| r.as_ref().ok())
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or(default)
|
|
||||||
}
|
|
||||||
}
|
|
@ -12,33 +12,22 @@ crate-type = ["cdylib", "rlib"]
|
|||||||
[[bin]]
|
[[bin]]
|
||||||
name = "nix-health"
|
name = "nix-health"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
required-features = ["ssr"]
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
cfg-if.workspace = true
|
cfg-if.workspace = true
|
||||||
clap = { workspace = true, optional = true }
|
clap = { workspace = true }
|
||||||
regex = "1.9.3"
|
regex = "1.9.3"
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
serde_with.workspace = true
|
serde_with.workspace = true
|
||||||
tokio = { version = "1.29", features = ["full"], optional = true }
|
tokio = { version = "1.29", features = ["full"] }
|
||||||
url = { version = "2.4", features = ["serde"] }
|
url = { version = "2.4", features = ["serde"] }
|
||||||
nix_rs.workspace = true
|
nix_rs.workspace = true
|
||||||
human-panic.workspace = true
|
human-panic.workspace = true
|
||||||
anyhow = { version = "1.0.75", optional = true }
|
anyhow = { version = "1.0.75" }
|
||||||
colored = { version = "2.0", optional = true }
|
colored = { version = "2.0" }
|
||||||
which = { version = "4.4.2", optional = true }
|
which = { version = "4.4.2" }
|
||||||
bytesize.workspace = true
|
bytesize.workspace = true
|
||||||
|
|
||||||
[features]
|
|
||||||
ssr = [
|
|
||||||
"dep:clap",
|
|
||||||
"dep:tokio",
|
|
||||||
"dep:anyhow",
|
|
||||||
"dep:colored",
|
|
||||||
"dep:which",
|
|
||||||
"nix_rs/ssr",
|
|
||||||
]
|
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
#[cfg(feature = "ssr")]
|
|
||||||
use nix_rs::{env, info};
|
use nix_rs::{env, info};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
use crate::traits::*;
|
use crate::traits::*;
|
||||||
|
|
||||||
/// Check that [nix_rs::config::NixConfig::substituters] is set to a good value.
|
/// Check that [nix_rs::config::NixConfig::substituters] is set to a good value.
|
||||||
@ -22,7 +20,6 @@ impl Default for Caches {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
impl Checkable for Caches {
|
impl Checkable for Caches {
|
||||||
fn check(&self, nix_info: &info::NixInfo, nix_env: &env::NixEnv) -> Vec<Check> {
|
fn check(&self, nix_info: &info::NixInfo, nix_env: &env::NixEnv) -> Vec<Check> {
|
||||||
let val = &nix_info.nix_config.substituters.value;
|
let val = &nix_info.nix_config.substituters.value;
|
||||||
|
@ -2,9 +2,8 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
use crate::traits::{Check, CheckResult, Checkable};
|
use crate::traits::{Check, CheckResult, Checkable};
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
use nix_rs::{env, info};
|
use nix_rs::{env, info};
|
||||||
|
|
||||||
/// Check if direnv is installed
|
/// Check if direnv is installed
|
||||||
@ -25,7 +24,6 @@ impl Default for Direnv {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
impl Checkable for Direnv {
|
impl Checkable for Direnv {
|
||||||
fn check(&self, _nix_info: &info::NixInfo, nix_env: &env::NixEnv) -> Vec<Check> {
|
fn check(&self, _nix_info: &info::NixInfo, nix_env: &env::NixEnv) -> Vec<Check> {
|
||||||
let mut checks = vec![];
|
let mut checks = vec![];
|
||||||
@ -47,7 +45,7 @@ impl Checkable for Direnv {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// [Check] that direnv was installed.
|
/// [Check] that direnv was installed.
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
fn install_check(required: bool) -> Check {
|
fn install_check(required: bool) -> Check {
|
||||||
let suggestion = "Install direnv <https://zero-to-flakes.com/direnv/#setup>".to_string();
|
let suggestion = "Install direnv <https://zero-to-flakes.com/direnv/#setup>".to_string();
|
||||||
let direnv_install = DirenvInstall::detect();
|
let direnv_install = DirenvInstall::detect();
|
||||||
@ -67,7 +65,7 @@ fn install_check(required: bool) -> Check {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// [Check] that direnv was activated on the local flake
|
/// [Check] that direnv was activated on the local flake
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
fn activation_check(local_flake: &std::path::Path, required: bool) -> Check {
|
fn activation_check(local_flake: &std::path::Path, required: bool) -> Check {
|
||||||
let suggestion = format!("Run `direnv allow` under `{}`", local_flake.display());
|
let suggestion = format!("Run `direnv allow` under `{}`", local_flake.display());
|
||||||
Check {
|
Check {
|
||||||
@ -103,7 +101,7 @@ pub struct DirenvInstall {
|
|||||||
|
|
||||||
impl DirenvInstall {
|
impl DirenvInstall {
|
||||||
/// Detect user's direnv installation
|
/// Detect user's direnv installation
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
pub fn detect() -> anyhow::Result<Self> {
|
pub fn detect() -> anyhow::Result<Self> {
|
||||||
let bin_path = which::which("direnv")?;
|
let bin_path = which::which("direnv")?;
|
||||||
let output = std::process::Command::new(&bin_path)
|
let output = std::process::Command::new(&bin_path)
|
||||||
@ -136,7 +134,7 @@ impl DirenvInstall {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Check if direnv was already activated in [project_dir]
|
/// Check if direnv was already activated in [project_dir]
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
pub fn is_direnv_active_on(project_dir: &std::path::Path) -> anyhow::Result<bool> {
|
pub fn is_direnv_active_on(project_dir: &std::path::Path) -> anyhow::Result<bool> {
|
||||||
let output = std::process::Command::new("direnv")
|
let output = std::process::Command::new("direnv")
|
||||||
.arg("status")
|
.arg("status")
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
#[cfg(feature = "ssr")]
|
|
||||||
use nix_rs::{env, info};
|
use nix_rs::{env, info};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
use crate::traits::*;
|
use crate::traits::*;
|
||||||
|
|
||||||
/// Check that [nix_rs::config::NixConfig::experimental_features] is set to a good value.
|
/// Check that [nix_rs::config::NixConfig::experimental_features] is set to a good value.
|
||||||
@ -10,7 +8,6 @@ use crate::traits::*;
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct FlakeEnabled {}
|
pub struct FlakeEnabled {}
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
impl Checkable for FlakeEnabled {
|
impl Checkable for FlakeEnabled {
|
||||||
fn check(&self, nix_info: &info::NixInfo, _nix_env: &env::NixEnv) -> Vec<Check> {
|
fn check(&self, nix_info: &info::NixInfo, _nix_env: &env::NixEnv) -> Vec<Check> {
|
||||||
let val = &nix_info.nix_config.experimental_features.value;
|
let val = &nix_info.nix_config.experimental_features.value;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
#[cfg(feature = "ssr")]
|
|
||||||
use nix_rs::{env, info};
|
use nix_rs::{env, info};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
use crate::traits::*;
|
use crate::traits::*;
|
||||||
|
|
||||||
/// Check that [nix_rs::config::NixConfig::max_jobs] is set to a good value.
|
/// Check that [nix_rs::config::NixConfig::max_jobs] is set to a good value.
|
||||||
@ -10,7 +8,6 @@ use crate::traits::*;
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct MaxJobs {}
|
pub struct MaxJobs {}
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
impl Checkable for MaxJobs {
|
impl Checkable for MaxJobs {
|
||||||
fn check(&self, nix_info: &info::NixInfo, nix_env: &env::NixEnv) -> Vec<Check> {
|
fn check(&self, nix_info: &info::NixInfo, nix_env: &env::NixEnv) -> Vec<Check> {
|
||||||
let max_jobs = nix_info.nix_config.max_jobs.value;
|
let max_jobs = nix_info.nix_config.max_jobs.value;
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
use nix_rs::version::NixVersion;
|
use nix_rs::version::NixVersion;
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
use nix_rs::{env, info};
|
use nix_rs::{env, info};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
use crate::traits::*;
|
use crate::traits::*;
|
||||||
|
|
||||||
/// Check that [nix_rs::version::NixVersion] is set to a good value.
|
/// Check that [nix_rs::version::NixVersion] is set to a good value.
|
||||||
@ -25,7 +24,6 @@ impl Default for MinNixVersion {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
impl Checkable for MinNixVersion {
|
impl Checkable for MinNixVersion {
|
||||||
fn check(&self, nix_info: &info::NixInfo, _nix_env: &env::NixEnv) -> Vec<Check> {
|
fn check(&self, nix_info: &info::NixInfo, _nix_env: &env::NixEnv) -> Vec<Check> {
|
||||||
let val = &nix_info.nix_version;
|
let val = &nix_info.nix_version;
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
#[cfg(feature = "ssr")]
|
|
||||||
use nix_rs::{
|
use nix_rs::{
|
||||||
env::{self, AppleEmulation, MacOSArch, OS},
|
env::{self, AppleEmulation, MacOSArch, OS},
|
||||||
info,
|
info,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
use crate::traits::{Check, CheckResult, Checkable};
|
use crate::traits::{Check, CheckResult, Checkable};
|
||||||
|
|
||||||
/// Check if Nix is being run under rosetta emulation
|
/// Check if Nix is being run under rosetta emulation
|
||||||
@ -27,7 +25,6 @@ impl Default for Rosetta {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
impl Checkable for Rosetta {
|
impl Checkable for Rosetta {
|
||||||
fn check(&self, _nix_info: &info::NixInfo, nix_env: &env::NixEnv) -> Vec<Check> {
|
fn check(&self, _nix_info: &info::NixInfo, nix_env: &env::NixEnv) -> Vec<Check> {
|
||||||
let mut checks = vec![];
|
let mut checks = vec![];
|
||||||
@ -52,7 +49,7 @@ impl Checkable for Rosetta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Return [AppleEmulation]. Return None if not an ARM mac.
|
/// Return [AppleEmulation]. Return None if not an ARM mac.
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
fn get_apple_emulation(system: &OS) -> Option<AppleEmulation> {
|
fn get_apple_emulation(system: &OS) -> Option<AppleEmulation> {
|
||||||
match system {
|
match system {
|
||||||
OS::MacOS {
|
OS::MacOS {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
use bytesize::ByteSize;
|
use bytesize::ByteSize;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
use crate::traits::{Check, CheckResult, Checkable};
|
use crate::traits::{Check, CheckResult, Checkable};
|
||||||
|
|
||||||
/// Check if the system has enough resources
|
/// Check if the system has enough resources
|
||||||
@ -29,7 +28,6 @@ impl Default for System {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
impl Checkable for System {
|
impl Checkable for System {
|
||||||
fn check(
|
fn check(
|
||||||
&self,
|
&self,
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
use crate::traits::*;
|
use crate::traits::*;
|
||||||
|
|
||||||
/// Check that [crate::nix::config::NixConfig::trusted_users] is set to a good value.
|
/// Check that [crate::nix::config::NixConfig::trusted_users] is set to a good value.
|
||||||
@ -8,7 +7,6 @@ use crate::traits::*;
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct TrustedUsers {}
|
pub struct TrustedUsers {}
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
impl Checkable for TrustedUsers {
|
impl Checkable for TrustedUsers {
|
||||||
fn check(&self, nix_info: &nix_rs::info::NixInfo, nix_env: &nix_rs::env::NixEnv) -> Vec<Check> {
|
fn check(&self, nix_info: &nix_rs::info::NixInfo, nix_env: &nix_rs::env::NixEnv) -> Vec<Check> {
|
||||||
let val = &nix_info.nix_config.trusted_users.value;
|
let val = &nix_info.nix_config.trusted_users.value;
|
||||||
|
@ -30,7 +30,6 @@ pub struct NixHealth {
|
|||||||
pub direnv: Direnv,
|
pub direnv: Direnv,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
impl<'a> IntoIterator for &'a NixHealth {
|
impl<'a> IntoIterator for &'a NixHealth {
|
||||||
type Item = &'a dyn traits::Checkable;
|
type Item = &'a dyn traits::Checkable;
|
||||||
type IntoIter = std::vec::IntoIter<Self::Item>;
|
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||||
@ -51,7 +50,6 @@ impl<'a> IntoIterator for &'a NixHealth {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
impl NixHealth {
|
impl NixHealth {
|
||||||
/// Create [NixHealth] using configuration from the given flake
|
/// Create [NixHealth] using configuration from the given flake
|
||||||
///
|
///
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// Types that can do specific "health check" for Nix
|
/// Types that can do specific "health check" for Nix
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
pub trait Checkable {
|
pub trait Checkable {
|
||||||
/// Run and create the health check
|
/// Run and create the health check
|
||||||
///
|
///
|
||||||
/// NOTE: Some checks may perform impure actions (IO, etc.). Returning an
|
/// NOTE: Some checks may perform impure actions (IO, etc.). Returning an
|
||||||
/// empty vector indicates that the check is skipped on this environment.
|
/// empty vector indicates that the check is skipped on this environment.
|
||||||
|
/// TODO: This should be async!
|
||||||
fn check(&self, nix_info: &nix_rs::info::NixInfo, nix_env: &nix_rs::env::NixEnv) -> Vec<Check>;
|
fn check(&self, nix_info: &nix_rs::info::NixInfo, nix_env: &nix_rs::env::NixEnv) -> Vec<Check>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,14 +20,11 @@ thiserror.workspace = true
|
|||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
serde_with.workspace = true
|
serde_with.workspace = true
|
||||||
tokio = { version = "1.29", features = ["full"], optional = true }
|
tokio = { version = "1.29", features = ["full"] }
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
url = { version = "2.4", features = ["serde"] }
|
url = { version = "2.4", features = ["serde"] }
|
||||||
colored = { version = "2.0", optional = true }
|
colored = { version = "2.0" }
|
||||||
shell-words = { version = "1.1.0", optional = true }
|
shell-words = { version = "1.1.0" }
|
||||||
is_proc_translated = { version = "0.1.1", optional = true }
|
is_proc_translated = { version = "0.1.1" }
|
||||||
sysinfo.version = "0.29.10"
|
sysinfo.version = "0.29.10"
|
||||||
bytesize.workspace = true
|
bytesize.workspace = true
|
||||||
|
|
||||||
[features]
|
|
||||||
ssr = ["dep:tokio", "dep:colored", "dep:shell-words", "dep:is_proc_translated"]
|
|
||||||
|
@ -12,9 +12,9 @@ use std::fmt::{self, Display};
|
|||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
/// The `nix` command's global options.
|
/// The `nix` command's global options.
|
||||||
@ -54,14 +54,13 @@ impl Default for NixCmd {
|
|||||||
///
|
///
|
||||||
/// The command will be highlighted to distinguish it (for copying) from the
|
/// The command will be highlighted to distinguish it (for copying) from the
|
||||||
/// rest of the instrumentation parameters.
|
/// rest of the instrumentation parameters.
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
#[instrument(name = "command")]
|
#[instrument(name = "command")]
|
||||||
pub fn trace_cmd(cmd: &tokio::process::Command) {
|
pub fn trace_cmd(cmd: &tokio::process::Command) {
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
tracing::info!("🐚 {}️", to_cli(cmd).bright_blue());
|
tracing::info!("🐚 {}️", to_cli(cmd).bright_blue());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
impl NixCmd {
|
impl NixCmd {
|
||||||
/// Return a [Command] for this [NixCmd] configuration
|
/// Return a [Command] for this [NixCmd] configuration
|
||||||
pub fn command(&self) -> Command {
|
pub fn command(&self) -> Command {
|
||||||
@ -72,7 +71,7 @@ impl NixCmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Run nix with given args, interpreting stdout as JSON, parsing into `T`
|
/// Run nix with given args, interpreting stdout as JSON, parsing into `T`
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
pub async fn run_with_args_expecting_json<T>(&self, args: &[&str]) -> Result<T, NixCmdError>
|
pub async fn run_with_args_expecting_json<T>(&self, args: &[&str]) -> Result<T, NixCmdError>
|
||||||
where
|
where
|
||||||
T: serde::de::DeserializeOwned,
|
T: serde::de::DeserializeOwned,
|
||||||
@ -83,7 +82,7 @@ impl NixCmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Run nix with given args, interpreting parsing stdout, via [std::str::FromStr], into `T`
|
/// Run nix with given args, interpreting parsing stdout, via [std::str::FromStr], into `T`
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
pub async fn run_with_args_expecting_fromstr<T>(&self, args: &[&str]) -> Result<T, NixCmdError>
|
pub async fn run_with_args_expecting_fromstr<T>(&self, args: &[&str]) -> Result<T, NixCmdError>
|
||||||
where
|
where
|
||||||
T: std::str::FromStr,
|
T: std::str::FromStr,
|
||||||
@ -96,7 +95,7 @@ impl NixCmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Run nix with given args, returning stdout.
|
/// Run nix with given args, returning stdout.
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
pub async fn run_with_args_returning_stdout(
|
pub async fn run_with_args_returning_stdout(
|
||||||
&self,
|
&self,
|
||||||
args: &[&str],
|
args: &[&str],
|
||||||
@ -132,7 +131,7 @@ impl NixCmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a Command to user-copyable CLI string
|
/// Convert a Command to user-copyable CLI string
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
fn to_cli(cmd: &tokio::process::Command) -> String {
|
fn to_cli(cmd: &tokio::process::Command) -> String {
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
let program = cmd.as_std().get_program().to_string_lossy().to_string();
|
let program = cmd.as_std().get_program().to_string_lossy().to_string();
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
//! Rust module for `nix show-config`
|
//! Rust module for `nix show-config`
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
@ -35,7 +35,7 @@ pub struct ConfigVal<T> {
|
|||||||
|
|
||||||
impl NixConfig {
|
impl NixConfig {
|
||||||
/// Get the output of `nix show-config`
|
/// Get the output of `nix show-config`
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
#[instrument(name = "show-config")]
|
#[instrument(name = "show-config")]
|
||||||
pub async fn from_nix(
|
pub async fn from_nix(
|
||||||
nix_cmd: &super::command::NixCmd,
|
nix_cmd: &super::command::NixCmd,
|
||||||
@ -47,7 +47,6 @@ impl NixConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_nix_config() -> Result<(), crate::command::NixCmdError> {
|
async fn test_nix_config() -> Result<(), crate::command::NixCmdError> {
|
||||||
let v = NixConfig::from_nix(&crate::command::NixCmd::default()).await?;
|
let v = NixConfig::from_nix(&crate::command::NixCmd::default()).await?;
|
||||||
|
@ -26,23 +26,27 @@ pub struct NixEnv {
|
|||||||
|
|
||||||
impl NixEnv {
|
impl NixEnv {
|
||||||
/// Determine [NixEnv] on the user's system
|
/// Determine [NixEnv] on the user's system
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
pub async fn detect(current_flake: Option<FlakeUrl>) -> Result<NixEnv, NixEnvError> {
|
pub async fn detect(current_flake: Option<FlakeUrl>) -> Result<NixEnv, NixEnvError> {
|
||||||
use sysinfo::{DiskExt, SystemExt};
|
use sysinfo::{DiskExt, SystemExt};
|
||||||
let current_user = std::env::var("USER")?;
|
|
||||||
let os = OS::detect().await;
|
let os = OS::detect().await;
|
||||||
let sys = sysinfo::System::new_with_specifics(
|
tokio::task::spawn_blocking(|| {
|
||||||
sysinfo::RefreshKind::new().with_disks_list().with_memory(),
|
let current_user = std::env::var("USER")?;
|
||||||
);
|
let sys = sysinfo::System::new_with_specifics(
|
||||||
let total_disk_space = to_bytesize(get_nix_disk(&sys)?.total_space());
|
sysinfo::RefreshKind::new().with_disks_list().with_memory(),
|
||||||
let total_memory = to_bytesize(sys.total_memory());
|
);
|
||||||
Ok(NixEnv {
|
let total_disk_space = to_bytesize(get_nix_disk(&sys)?.total_space());
|
||||||
current_user,
|
let total_memory = to_bytesize(sys.total_memory());
|
||||||
current_flake,
|
Ok(NixEnv {
|
||||||
os,
|
current_user,
|
||||||
total_disk_space,
|
current_flake,
|
||||||
total_memory,
|
os,
|
||||||
|
total_disk_space,
|
||||||
|
total_memory,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return [NixEnv::current_flake] as a local path if it is one
|
/// Return [NixEnv::current_flake] as a local path if it is one
|
||||||
@ -54,7 +58,7 @@ impl NixEnv {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get the disk where /nix exists
|
/// Get the disk where /nix exists
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
fn get_nix_disk(sys: &sysinfo::System) -> Result<&sysinfo::Disk, NixEnvError> {
|
fn get_nix_disk(sys: &sysinfo::System) -> Result<&sysinfo::Disk, NixEnvError> {
|
||||||
use sysinfo::{DiskExt, SystemExt};
|
use sysinfo::{DiskExt, SystemExt};
|
||||||
let by_mount_point: std::collections::HashMap<&Path, &sysinfo::Disk> = sys
|
let by_mount_point: std::collections::HashMap<&Path, &sysinfo::Disk> = sys
|
||||||
@ -98,7 +102,6 @@ pub enum AppleEmulation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AppleEmulation {
|
impl AppleEmulation {
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
use is_proc_translated::is_proc_translated;
|
use is_proc_translated::is_proc_translated;
|
||||||
if is_proc_translated() {
|
if is_proc_translated() {
|
||||||
@ -109,7 +112,6 @@ impl AppleEmulation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
impl Default for AppleEmulation {
|
impl Default for AppleEmulation {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new()
|
Self::new()
|
||||||
@ -117,7 +119,6 @@ impl Default for AppleEmulation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MacOSArch {
|
impl MacOSArch {
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
pub fn from(os_arch: Option<&str>) -> MacOSArch {
|
pub fn from(os_arch: Option<&str>) -> MacOSArch {
|
||||||
match os_arch {
|
match os_arch {
|
||||||
Some("arm64") => MacOSArch::Arm64(AppleEmulation::new()),
|
Some("arm64") => MacOSArch::Arm64(AppleEmulation::new()),
|
||||||
@ -146,7 +147,6 @@ impl Display for OS {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl OS {
|
impl OS {
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
pub async fn detect() -> Self {
|
pub async fn detect() -> Self {
|
||||||
let os_info = tokio::task::spawn_blocking(os_info::get).await.unwrap();
|
let os_info = tokio::task::spawn_blocking(os_info::get).await.unwrap();
|
||||||
let os_type = os_info.os_type();
|
let os_type = os_info.os_type();
|
||||||
@ -188,7 +188,7 @@ impl OS {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Errors while trying to fetch [NixEnv]
|
/// Errors while trying to fetch [NixEnv]
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum NixEnvError {
|
pub enum NixEnvError {
|
||||||
#[error("Failed to fetch ENV: {0}")]
|
#[error("Failed to fetch ENV: {0}")]
|
||||||
@ -201,7 +201,7 @@ pub enum NixEnvError {
|
|||||||
/// Convert bytes to a closest [ByteSize]
|
/// Convert bytes to a closest [ByteSize]
|
||||||
///
|
///
|
||||||
/// Useful for displaying disk space and memory which are typically in GBs / TBs
|
/// Useful for displaying disk space and memory which are typically in GBs / TBs
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
fn to_bytesize(bytes: u64) -> ByteSize {
|
fn to_bytesize(bytes: u64) -> ByteSize {
|
||||||
let kb = bytes / 1024;
|
let kb = bytes / 1024;
|
||||||
let mb = kb / 1024;
|
let mb = kb / 1024;
|
||||||
@ -218,7 +218,7 @@ fn to_bytesize(bytes: u64) -> ByteSize {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Test for [to_bytesize]
|
/// Test for [to_bytesize]
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_to_bytesize() {
|
fn test_to_bytesize() {
|
||||||
assert_eq!(to_bytesize(0), ByteSize::b(0));
|
assert_eq!(to_bytesize(0), ByteSize::b(0));
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
//! Rust module for Nix flakes
|
//! Rust module for Nix flakes
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
pub mod eval;
|
pub mod eval;
|
||||||
pub mod outputs;
|
pub mod outputs;
|
||||||
pub mod schema;
|
pub mod schema;
|
||||||
@ -7,11 +7,11 @@ pub mod system;
|
|||||||
pub mod url;
|
pub mod url;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
use self::{outputs::FlakeOutputs, schema::FlakeSchema, system::System, url::FlakeUrl};
|
use self::{outputs::FlakeOutputs, schema::FlakeSchema, system::System, url::FlakeUrl};
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
use crate::command::NixCmdError;
|
use crate::command::NixCmdError;
|
||||||
|
|
||||||
/// All the information about a Nix flake
|
/// All the information about a Nix flake
|
||||||
@ -28,7 +28,7 @@ pub struct Flake {
|
|||||||
|
|
||||||
impl Flake {
|
impl Flake {
|
||||||
/// Get [Flake] info for the given flake url
|
/// Get [Flake] info for the given flake url
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
#[instrument(name = "flake", skip(nix_cmd))]
|
#[instrument(name = "flake", skip(nix_cmd))]
|
||||||
pub async fn from_nix(
|
pub async fn from_nix(
|
||||||
nix_cmd: &crate::command::NixCmd,
|
nix_cmd: &crate::command::NixCmd,
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
//! Nix flake outputs
|
//! Nix flake outputs
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::{btree_map::Entry, BTreeMap};
|
use std::{
|
||||||
|
collections::{btree_map::Entry, BTreeMap},
|
||||||
|
fmt::Display,
|
||||||
|
};
|
||||||
|
|
||||||
/// Represents the "outputs" of a flake
|
/// Represents the "outputs" of a flake
|
||||||
///
|
///
|
||||||
@ -15,7 +18,7 @@ pub enum FlakeOutputs {
|
|||||||
|
|
||||||
impl FlakeOutputs {
|
impl FlakeOutputs {
|
||||||
/// Run `nix flake show` on the given flake url
|
/// Run `nix flake show` on the given flake url
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
#[tracing::instrument(name = "flake-show")]
|
#[tracing::instrument(name = "flake-show")]
|
||||||
pub async fn from_nix(
|
pub async fn from_nix(
|
||||||
nix_cmd: &crate::command::NixCmd,
|
nix_cmd: &crate::command::NixCmd,
|
||||||
@ -113,3 +116,9 @@ impl Type {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Display for Type {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(&format!("{:?}", self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -13,7 +13,7 @@ pub struct NixInfo {
|
|||||||
|
|
||||||
impl NixInfo {
|
impl NixInfo {
|
||||||
/// Determine [NixInfo] on the user's system
|
/// Determine [NixInfo] on the user's system
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
pub async fn from_nix(
|
pub async fn from_nix(
|
||||||
nix_cmd: &crate::command::NixCmd,
|
nix_cmd: &crate::command::NixCmd,
|
||||||
) -> Result<NixInfo, crate::command::NixCmdError> {
|
) -> Result<NixInfo, crate::command::NixCmdError> {
|
||||||
|
@ -3,11 +3,11 @@ use regex::Regex;
|
|||||||
use serde_with::{DeserializeFromStr, SerializeDisplay};
|
use serde_with::{DeserializeFromStr, SerializeDisplay};
|
||||||
use std::{fmt, str::FromStr};
|
use std::{fmt, str::FromStr};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
/// Nix version as parsed from `nix --version`
|
/// Nix version as parsed from `nix --version`
|
||||||
#[derive(Clone, PartialOrd, PartialEq, Eq, Debug, SerializeDisplay, DeserializeFromStr)]
|
#[derive(Clone, Copy, PartialOrd, PartialEq, Eq, Debug, SerializeDisplay, DeserializeFromStr)]
|
||||||
pub struct NixVersion {
|
pub struct NixVersion {
|
||||||
pub major: u32,
|
pub major: u32,
|
||||||
pub minor: u32,
|
pub minor: u32,
|
||||||
@ -48,7 +48,7 @@ impl FromStr for NixVersion {
|
|||||||
|
|
||||||
impl NixVersion {
|
impl NixVersion {
|
||||||
/// Get the output of `nix --version`
|
/// Get the output of `nix --version`
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
#[instrument(name = "version")]
|
#[instrument(name = "version")]
|
||||||
pub async fn from_nix(
|
pub async fn from_nix(
|
||||||
nix_cmd: &super::command::NixCmd,
|
nix_cmd: &super::command::NixCmd,
|
||||||
@ -66,7 +66,6 @@ impl fmt::Display for NixVersion {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_run_nix_version() {
|
async fn test_run_nix_version() {
|
||||||
let nix_version = NixVersion::from_nix(&crate::command::NixCmd::default())
|
let nix_version = NixVersion::from_nix(&crate::command::NixCmd::default())
|
||||||
@ -75,7 +74,6 @@ async fn test_run_nix_version() {
|
|||||||
println!("Nix version: {}", nix_version);
|
println!("Nix version: {}", nix_version);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_parse_nix_version() {
|
async fn test_parse_nix_version() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -1,7 +1,3 @@
|
|||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
a[aria-current="page"] {
|
|
||||||
@apply font-bold bg-white text-primary-600;
|
|
||||||
}
|
|
4
e2e/.gitignore
vendored
4
e2e/.gitignore
vendored
@ -1,4 +0,0 @@
|
|||||||
node_modules
|
|
||||||
/test-results/
|
|
||||||
/playwright-report/
|
|
||||||
/playwright/.cache/
|
|
@ -1,6 +0,0 @@
|
|||||||
# Playwright end-to-end tests
|
|
||||||
|
|
||||||
We use [Playwright](https://playwright.dev/dotnet/) to test our application.
|
|
||||||
|
|
||||||
- All e2e test are nixified, simply run `nix run .#e2e-playwright-test` from project root (there are `just` targets as well)
|
|
||||||
- The nix shell creates a `node_modules` symlink which in turn provide IDE support in VSCode for editing `tests/*`
|
|
@ -1,52 +0,0 @@
|
|||||||
{ lib, ... }:
|
|
||||||
{
|
|
||||||
perSystem = { config, self', pkgs, system, ... }: {
|
|
||||||
# e2e test service using playwright
|
|
||||||
process-compose.e2e-playwright-test =
|
|
||||||
let
|
|
||||||
TEST_PORT = "5000";
|
|
||||||
in
|
|
||||||
{
|
|
||||||
tui = false;
|
|
||||||
settings.processes = {
|
|
||||||
start-app = {
|
|
||||||
command = "${lib.getExe self'.packages.default} --site-addr=127.0.0.1:${TEST_PORT} --no-open";
|
|
||||||
readiness_probe = {
|
|
||||||
exec.command = "${lib.getExe pkgs.curl} --fail 127.0.0.1:${TEST_PORT}";
|
|
||||||
initial_delay_seconds = 2;
|
|
||||||
period_seconds = 10;
|
|
||||||
timeout_seconds = 4;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
test = {
|
|
||||||
environment = {
|
|
||||||
inherit TEST_PORT;
|
|
||||||
};
|
|
||||||
command = pkgs.writeShellApplication {
|
|
||||||
name = "e2e-playwright";
|
|
||||||
runtimeInputs = with pkgs; [ nodejs playwright-test ];
|
|
||||||
text = ''
|
|
||||||
cd e2e
|
|
||||||
playwright test --project chromium
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
depends_on."start-app".condition = "process_healthy";
|
|
||||||
availability.exit_on_end = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
devShells.e2e-playwright = pkgs.mkShell {
|
|
||||||
buildInputs = with pkgs; [
|
|
||||||
nodejs
|
|
||||||
playwright-test
|
|
||||||
];
|
|
||||||
shellHook = ''
|
|
||||||
export NODE_PATH=${pkgs.playwright-test}/lib/node_modules
|
|
||||||
# VSCode disrespects NODE_PATH https://github.com/microsoft/TypeScript/issues/8760
|
|
||||||
# So we must manually create ./node_modules
|
|
||||||
just node_modules NODE_PATH=$NODE_PATH
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
1
e2e/node_modules
Symbolic link
1
e2e/node_modules
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
/nix/store/ffsk31ifmhjxzf5nvagljxhqylsqzfxv-_at_playwright_slash_test-1.38.0/lib/node_modules
|
@ -1,68 +0,0 @@
|
|||||||
import { defineConfig, devices } from '@playwright/test';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read environment variables from file.
|
|
||||||
* https://github.com/motdotla/dotenv
|
|
||||||
*/
|
|
||||||
// require('dotenv').config();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @see https://playwright.dev/docs/test-configuration
|
|
||||||
*/
|
|
||||||
export default defineConfig({
|
|
||||||
testDir: './tests',
|
|
||||||
/* Run tests in files in parallel */
|
|
||||||
fullyParallel: true,
|
|
||||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
|
||||||
forbidOnly: !!process.env.CI,
|
|
||||||
/* Retry on CI only */
|
|
||||||
retries: process.env.CI ? 2 : 0,
|
|
||||||
/* Opt out of parallel tests on CI. */
|
|
||||||
workers: process.env.CI ? 1 : undefined,
|
|
||||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
|
||||||
reporter: 'line',
|
|
||||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
|
||||||
use: {
|
|
||||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
|
||||||
baseURL: `http://127.0.0.1:${process.env.TEST_PORT}`,
|
|
||||||
|
|
||||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
|
||||||
trace: 'on-first-retry',
|
|
||||||
},
|
|
||||||
|
|
||||||
/* Configure projects for major browsers */
|
|
||||||
projects: [
|
|
||||||
{
|
|
||||||
name: 'chromium',
|
|
||||||
use: { ...devices['Desktop Chrome'] },
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Test against mobile viewports. */
|
|
||||||
// {
|
|
||||||
// name: 'Mobile Chrome',
|
|
||||||
// use: { ...devices['Pixel 5'] },
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: 'Mobile Safari',
|
|
||||||
// use: { ...devices['iPhone 12'] },
|
|
||||||
// },
|
|
||||||
|
|
||||||
/* Test against branded browsers. */
|
|
||||||
// {
|
|
||||||
// name: 'Microsoft Edge',
|
|
||||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: 'Google Chrome',
|
|
||||||
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
|
||||||
// },
|
|
||||||
],
|
|
||||||
|
|
||||||
/* Run your local dev server before starting the tests */
|
|
||||||
// webServer: {
|
|
||||||
// command: 'npm run start',
|
|
||||||
// url: 'http://127.0.0.1:3000',
|
|
||||||
// reuseExistingServer: !process.env.CI,
|
|
||||||
// },
|
|
||||||
});
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
|||||||
import { test, expect } from "@playwright/test";
|
|
||||||
|
|
||||||
test('check nix version', async ({ page }) => {
|
|
||||||
await page.goto('/info');
|
|
||||||
const nixVersion = await page.locator(":text('Nix Version') + div").textContent();
|
|
||||||
await expect(nixVersion).toBeTruthy();
|
|
||||||
});
|
|
34
flake.lock
generated
34
flake.lock
generated
@ -38,6 +38,22 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dioxus-desktop-template": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1696953583,
|
||||||
|
"narHash": "sha256-g+8wFUOSVLVN63DAlcj1OhYjOD3pkSFH5BCt6Gs0y6I=",
|
||||||
|
"owner": "srid",
|
||||||
|
"repo": "dioxus-desktop-template",
|
||||||
|
"rev": "a3c76b656c9fbddbde423c800bcab46876b5026e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "srid",
|
||||||
|
"repo": "dioxus-desktop-template",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"flake-compat": {
|
"flake-compat": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
@ -108,22 +124,6 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"leptos-fullstack": {
|
|
||||||
"flake": false,
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1694027158,
|
|
||||||
"narHash": "sha256-XzDBUA5jzTFDiLJtgvknAb9MuXwEGXmAd/ZoTqVUg+o=",
|
|
||||||
"owner": "srid",
|
|
||||||
"repo": "leptos-fullstack",
|
|
||||||
"rev": "4203055c17476616bec375cd7e471e91f70d26d0",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "srid",
|
|
||||||
"repo": "leptos-fullstack",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1696234590,
|
"lastModified": 1696234590,
|
||||||
@ -193,8 +193,8 @@
|
|||||||
"inputs": {
|
"inputs": {
|
||||||
"cargo-doc-live": "cargo-doc-live",
|
"cargo-doc-live": "cargo-doc-live",
|
||||||
"crane": "crane",
|
"crane": "crane",
|
||||||
|
"dioxus-desktop-template": "dioxus-desktop-template",
|
||||||
"flake-parts": "flake-parts",
|
"flake-parts": "flake-parts",
|
||||||
"leptos-fullstack": "leptos-fullstack",
|
|
||||||
"nixpkgs": "nixpkgs",
|
"nixpkgs": "nixpkgs",
|
||||||
"process-compose-flake": "process-compose-flake",
|
"process-compose-flake": "process-compose-flake",
|
||||||
"rust-overlay": "rust-overlay_2",
|
"rust-overlay": "rust-overlay_2",
|
||||||
|
@ -18,8 +18,8 @@
|
|||||||
process-compose-flake.url = "github:Platonic-Systems/process-compose-flake";
|
process-compose-flake.url = "github:Platonic-Systems/process-compose-flake";
|
||||||
cargo-doc-live.url = "github:srid/cargo-doc-live";
|
cargo-doc-live.url = "github:srid/cargo-doc-live";
|
||||||
|
|
||||||
leptos-fullstack.url = "github:srid/leptos-fullstack";
|
dioxus-desktop-template.url = "github:srid/dioxus-desktop-template";
|
||||||
leptos-fullstack.flake = false;
|
dioxus-desktop-template.flake = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = inputs:
|
outputs = inputs:
|
||||||
@ -30,9 +30,8 @@
|
|||||||
inputs.treefmt-nix.flakeModule
|
inputs.treefmt-nix.flakeModule
|
||||||
inputs.process-compose-flake.flakeModule
|
inputs.process-compose-flake.flakeModule
|
||||||
inputs.cargo-doc-live.flakeModule
|
inputs.cargo-doc-live.flakeModule
|
||||||
(inputs.leptos-fullstack + /nix/flake-module.nix)
|
(inputs.dioxus-desktop-template + /nix/flake-module.nix)
|
||||||
./rust.nix
|
./rust.nix
|
||||||
./e2e/flake-module.nix
|
|
||||||
];
|
];
|
||||||
|
|
||||||
flake = {
|
flake = {
|
||||||
@ -63,7 +62,6 @@
|
|||||||
programs = {
|
programs = {
|
||||||
nixpkgs-fmt.enable = true;
|
nixpkgs-fmt.enable = true;
|
||||||
rustfmt.enable = true;
|
rustfmt.enable = true;
|
||||||
leptosfmt.enable = true;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -72,7 +70,6 @@
|
|||||||
inputsFrom = [
|
inputsFrom = [
|
||||||
config.treefmt.build.devShell
|
config.treefmt.build.devShell
|
||||||
self'.devShells.rust
|
self'.devShells.rust
|
||||||
self'.devShells.e2e-playwright
|
|
||||||
];
|
];
|
||||||
packages = with pkgs; [
|
packages = with pkgs; [
|
||||||
just
|
just
|
||||||
|
28
justfile
28
justfile
@ -6,33 +6,37 @@ default:
|
|||||||
# Auto-format the source tree
|
# Auto-format the source tree
|
||||||
fmt:
|
fmt:
|
||||||
treefmt
|
treefmt
|
||||||
|
# Due a bug in dx fmt, we cannot run it on files using macros
|
||||||
|
find src/app/ -name \*.rs | grep -v state.rs | grep -v state/ | xargs -n1 sh -c 'echo "📔 $1"; dx fmt -f $1' sh
|
||||||
|
|
||||||
alias f := fmt
|
alias f := fmt
|
||||||
|
|
||||||
|
# CI=true for https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208)
|
||||||
|
bundle $CI="true":
|
||||||
|
# HACK (change PWD): Until https://github.com/DioxusLabs/dioxus/issues/1283
|
||||||
|
cd assets && dx bundle
|
||||||
|
nix run nixpkgs#eza -- -T ./dist/bundle/macos/nix-browser.app
|
||||||
|
|
||||||
# Run the project locally
|
# Run the project locally
|
||||||
watch $RUST_BACKTRACE="1":
|
watch $RUST_BACKTRACE="1":
|
||||||
cargo leptos watch
|
# XXX: hot reload doesn't work with tailwind
|
||||||
|
# dx serve --hot-reload
|
||||||
|
dx serve
|
||||||
|
|
||||||
alias w := watch
|
alias w := watch
|
||||||
|
|
||||||
|
tw:
|
||||||
|
tailwind -i ./css/input.css -o ./assets/tailwind.css --watch
|
||||||
|
|
||||||
# Run 'cargo run' for nix-health CLI in watch mode. Example: just watch-nix-health github:nammayatri/nammayatri
|
# Run 'cargo run' for nix-health CLI in watch mode. Example: just watch-nix-health github:nammayatri/nammayatri
|
||||||
watch-nix-health *ARGS:
|
watch-nix-health *ARGS:
|
||||||
cargo watch -- cargo run --bin nix-health --features=ssr -- {{ARGS}}
|
cargo watch -- cargo run --bin nix-health -- {{ARGS}}
|
||||||
|
|
||||||
alias wh := watch-nix-health
|
alias wh := watch-nix-health
|
||||||
|
|
||||||
# Run tests (backend & frontend)
|
# Run tests
|
||||||
test:
|
test:
|
||||||
cargo test
|
cargo test
|
||||||
cargo leptos test
|
|
||||||
|
|
||||||
# Run end-to-end tests against release server
|
|
||||||
e2e-release:
|
|
||||||
nix run .#e2e-playwright-test
|
|
||||||
|
|
||||||
# Run end-to-end tests against `just watch` server
|
|
||||||
e2e:
|
|
||||||
cd e2e && TEST_PORT=3000 playwright test --project chromium
|
|
||||||
|
|
||||||
# Run docs server (live reloading)
|
# Run docs server (live reloading)
|
||||||
doc:
|
doc:
|
||||||
|
117
rust.nix
117
rust.nix
@ -1,83 +1,48 @@
|
|||||||
# Nix module for the Rust part of the project
|
# Nix module for the Rust part of the project
|
||||||
#
|
#
|
||||||
# This uses https://github.com/srid/leptos-fullstack/blob/master/nix/flake-module.nix
|
# This uses https://github.com/srid/dioxus-desktop-template/blob/master/nix/flake-module.nix
|
||||||
{
|
{
|
||||||
perSystem = { config, self', pkgs, lib, system, ... }:
|
perSystem = { config, self', pkgs, lib, system, ... }: {
|
||||||
let
|
dioxus-desktop = {
|
||||||
rustBuildInputs = lib.optionals pkgs.stdenv.isDarwin (with pkgs.darwin.apple_sdk.frameworks; [
|
overrideCraneArgs = oa: {
|
||||||
IOKit
|
nativeBuildInputs = (oa.nativeBuildInputs or [ ]) ++ [
|
||||||
# For when we start using Tauri
|
pkgs.nix # cargo tests need nix
|
||||||
Carbon
|
|
||||||
WebKit
|
|
||||||
]);
|
|
||||||
in
|
|
||||||
{
|
|
||||||
leptos-fullstack.overrideCraneArgs = oa:
|
|
||||||
let
|
|
||||||
# 'cargo leptos test' doesn't run tests for all crates in the
|
|
||||||
# workspace. We do it here.
|
|
||||||
run-test = pkgs.writeShellApplication {
|
|
||||||
name = "run-test";
|
|
||||||
text = ''
|
|
||||||
set -xe
|
|
||||||
|
|
||||||
${oa.cargoTestCommand}
|
|
||||||
|
|
||||||
# Disable tests on macOS for https://github.com/garnix-io/issues/issues/69
|
|
||||||
# If/when we move to Jenkins, this won't be necessary.
|
|
||||||
${if !pkgs.stdenv.isDarwin
|
|
||||||
then ''
|
|
||||||
# Run `cargo test` using the same settings as `cargo leptos test`
|
|
||||||
# In particular: target-dir and features
|
|
||||||
cargo test --target-dir=target/server --no-default-features --features=ssr
|
|
||||||
cargo test --target-dir=target/front --no-default-features --features=hydrate
|
|
||||||
''
|
|
||||||
else ""
|
|
||||||
}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
nativeBuildInputs = (oa.nativeBuildInputs or [ ]) ++ [
|
|
||||||
pkgs.nix # cargo tests need nix
|
|
||||||
];
|
|
||||||
buildInputs = (oa.buildInputs or [ ]) ++ rustBuildInputs;
|
|
||||||
cargoTestCommand = lib.getExe run-test;
|
|
||||||
meta.description = "WIP: nix-browser";
|
|
||||||
};
|
|
||||||
|
|
||||||
packages = {
|
|
||||||
default = self'.packages.nix-browser;
|
|
||||||
nix-health = config.leptos-fullstack.craneLib.buildPackage {
|
|
||||||
inherit (config.leptos-fullstack) src;
|
|
||||||
pname = "nix-health";
|
|
||||||
nativeBuildInputs = [
|
|
||||||
pkgs.nix # cargo tests need nix
|
|
||||||
];
|
|
||||||
buildInputs = rustBuildInputs;
|
|
||||||
cargoExtraArgs = "-p nix_health --features ssr";
|
|
||||||
# Disable tests on macOS for https://github.com/garnix-io/issues/issues/69
|
|
||||||
# If/when we move to Jenkins, this won't be necessary.
|
|
||||||
doCheck = !pkgs.stdenv.isDarwin;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
devShells.rust = pkgs.mkShell {
|
|
||||||
inputsFrom = [
|
|
||||||
self'.devShells.nix-browser
|
|
||||||
];
|
];
|
||||||
packages = with pkgs; [
|
meta.description = "WIP: nix-browser";
|
||||||
cargo-watch
|
|
||||||
cargo-expand
|
|
||||||
cargo-nextest
|
|
||||||
config.process-compose.cargo-doc-live.outputs.package
|
|
||||||
];
|
|
||||||
buildInputs = rustBuildInputs;
|
|
||||||
shellHook = ''
|
|
||||||
echo
|
|
||||||
echo "🍎🍎 Run 'just <recipe>' to get started"
|
|
||||||
just
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
packages = {
|
||||||
|
default = self'.packages.nix-browser;
|
||||||
|
nix-health = config.dioxus-desktop.craneLib.buildPackage {
|
||||||
|
inherit (config.dioxus-desktop) src;
|
||||||
|
pname = "nix-health";
|
||||||
|
nativeBuildInputs = [
|
||||||
|
pkgs.nix # cargo tests need nix
|
||||||
|
];
|
||||||
|
buildInputs = config.dioxus-desktop.rustBuildInputs;
|
||||||
|
cargoExtraArgs = "-p nix_health";
|
||||||
|
# Disable tests on macOS for https://github.com/garnix-io/issues/issues/69
|
||||||
|
# If/when we move to Jenkins, this won't be necessary.
|
||||||
|
doCheck = !pkgs.stdenv.isDarwin;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
devShells.rust = pkgs.mkShell {
|
||||||
|
inputsFrom = [
|
||||||
|
self'.devShells.nix-browser
|
||||||
|
];
|
||||||
|
packages = with pkgs; [
|
||||||
|
cargo-watch
|
||||||
|
cargo-expand
|
||||||
|
cargo-nextest
|
||||||
|
config.process-compose.cargo-doc-live.outputs.package
|
||||||
|
];
|
||||||
|
shellHook = ''
|
||||||
|
echo
|
||||||
|
echo "🍎🍎 Run 'just <recipe>' to get started"
|
||||||
|
just
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
360
src/app/flake.rs
360
src/app/flake.rs
@ -2,203 +2,162 @@
|
|||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use leptos::*;
|
use dioxus::prelude::*;
|
||||||
use leptos_extra::{
|
use dioxus_router::prelude::Link;
|
||||||
query::{self, RefetchQueryButton},
|
use nix_rs::flake::{
|
||||||
signal::{use_signal, SignalWithResult},
|
outputs::{FlakeOutputs, Type, Val},
|
||||||
};
|
schema::FlakeSchema,
|
||||||
use leptos_meta::*;
|
url::FlakeUrl,
|
||||||
use leptos_router::*;
|
Flake,
|
||||||
use nix_rs::{
|
|
||||||
command::Refresh,
|
|
||||||
flake::{
|
|
||||||
outputs::{FlakeOutputs, Type, Val},
|
|
||||||
schema::FlakeSchema,
|
|
||||||
url::FlakeUrl,
|
|
||||||
Flake,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::widget::*;
|
use crate::{
|
||||||
|
app::widget::RefreshButton,
|
||||||
|
app::{state::AppState, Route},
|
||||||
|
};
|
||||||
|
|
||||||
/// Nix flake dashboard
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn NixFlakeRoute(cx: Scope) -> impl IntoView {
|
pub fn Flake(cx: Scope) -> Element {
|
||||||
let suggestions = FlakeUrl::suggestions();
|
let state = AppState::use_state(cx);
|
||||||
let url = use_signal::<FlakeUrl>(cx);
|
let fut = use_future(cx, (), |_| async move { state.update_flake().await });
|
||||||
let refresh = use_signal::<Refresh>(cx);
|
let flake = state.flake.read();
|
||||||
let query = move || (url(), refresh());
|
let busy = (*flake).is_loading_or_refreshing();
|
||||||
let result = query::use_server_query(cx, query, get_flake);
|
render! {
|
||||||
view! { cx,
|
h1 { class: "text-5xl font-bold", "Flake dashboard" }
|
||||||
<Title text="Nix Flake"/>
|
div { class: "p-2 my-1",
|
||||||
<h1 class="text-5xl font-bold">{"Nix Flake"}</h1>
|
input {
|
||||||
<TextInput id="nix-flake-input" label="Load a Nix Flake" val=url suggestions/>
|
class: "w-full p-1 mb-4 font-mono",
|
||||||
<RefetchQueryButton result query/>
|
id: "nix-flake-input",
|
||||||
<Outlet/>
|
"type": "text",
|
||||||
|
value: "{state.flake_url}",
|
||||||
|
disabled: busy,
|
||||||
|
onchange: move |ev| {
|
||||||
|
let url: FlakeUrl = ev.value.clone().into();
|
||||||
|
tracing::info!("setting flake url set to {}", & url);
|
||||||
|
state.flake_url.set(url);
|
||||||
|
fut.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RefreshButton { busy: busy, handler: move |_| { fut.restart() } }
|
||||||
|
flake.render_with(cx, |v| render! { FlakeView { flake: v.clone() } })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn NixFlakeHomeRoute(cx: Scope) -> impl IntoView {
|
pub fn FlakeRaw(cx: Scope) -> Element {
|
||||||
let url = use_signal::<FlakeUrl>(cx);
|
let state = AppState::use_state(cx);
|
||||||
let refresh = use_signal::<Refresh>(cx);
|
use_future(cx, (), |_| async move { state.update_flake().await });
|
||||||
let query = move || (url(), refresh());
|
let flake = state.flake.read();
|
||||||
let result = query::use_server_query(cx, query, get_flake);
|
render! {
|
||||||
let data = result.data;
|
div {
|
||||||
view! { cx,
|
Link { to: Route::Flake {}, "⬅ Back" }
|
||||||
<div class="p-2 my-1">
|
div { class: "px-4 py-2 font-mono text-xs text-left text-gray-500 border-2 border-black",
|
||||||
<SuspenseWithErrorHandling>
|
flake.render_with(cx, |v| render! { FlakeOutputsRawView { outs: v.output.clone() } } )
|
||||||
{move || {
|
}
|
||||||
data.with_result(move |flake| {
|
}
|
||||||
view! { cx, <FlakeView flake/> }
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
|
|
||||||
</SuspenseWithErrorHandling>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn NixFlakeRawRoute(cx: Scope) -> impl IntoView {
|
pub fn FlakeView(cx: Scope, flake: Flake) -> Element {
|
||||||
let url = use_signal::<FlakeUrl>(cx);
|
render! {
|
||||||
let refresh = use_signal::<Refresh>(cx);
|
div { class: "flex flex-col my-4",
|
||||||
let query = move || (url(), refresh());
|
h3 { class: "text-lg font-bold", flake.url.to_string() }
|
||||||
let result = query::use_server_query(cx, query, get_flake);
|
div { class: "text-sm italic text-gray-600",
|
||||||
let data = result.data;
|
Link { to: Route::FlakeRaw {}, "View raw output" }
|
||||||
view! { cx,
|
}
|
||||||
<div>
|
div { FlakeSchemaView { schema: &flake.schema } }
|
||||||
<A href="/flake">"< Back"</A>
|
}
|
||||||
</div>
|
|
||||||
<div class="px-4 py-2 font-mono text-xs text-left text-gray-500 border-2 border-black">
|
|
||||||
<SuspenseWithErrorHandling>
|
|
||||||
{move || {
|
|
||||||
data.with_result(move |r| {
|
|
||||||
view! { cx, <FlakeOutputsRawView outs=&r.output/> }
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
|
|
||||||
</SuspenseWithErrorHandling>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn FlakeView<'a>(cx: Scope, flake: &'a Flake) -> impl IntoView {
|
pub fn SectionHeading(cx: Scope, title: &'static str) -> Element {
|
||||||
view! { cx,
|
render! {
|
||||||
<div class="flex flex-col my-4">
|
h3 { class: "p-2 mt-4 mb-2 font-bold bg-gray-300 border-b-2 border-l-2 border-black text-l",
|
||||||
<h3 class="text-lg font-bold">{flake.url.to_string()}</h3>
|
"{title}"
|
||||||
<div class="text-sm italic text-gray-600">
|
}
|
||||||
<A href="/flake/raw" exact=true>
|
|
||||||
"View raw output"
|
|
||||||
</A>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<FlakeSchemaView schema=&flake.schema/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn SectionHeading(cx: Scope, title: &'static str) -> impl IntoView {
|
pub fn FlakeSchemaView<'a>(cx: Scope, schema: &'a FlakeSchema) -> Element {
|
||||||
view! { cx,
|
let system = schema.system.clone();
|
||||||
<h3 class="p-2 mt-4 mb-2 font-bold bg-gray-300 border-b-2 border-l-2 border-black text-l">
|
render! {
|
||||||
{title}
|
div {
|
||||||
</h3>
|
h2 { class: "my-2",
|
||||||
|
div { class: "text-xl font-bold text-primary-600", "{system.human_readable()}" }
|
||||||
|
span { class: "font-mono text-xs text-gray-500", "(", "{system }", ")" }
|
||||||
|
}
|
||||||
|
div { class: "text-left",
|
||||||
|
BtreeMapView { title: "Packages", tree: &schema.packages }
|
||||||
|
BtreeMapView { title: "Legacy Packages", tree: &schema.legacy_packages }
|
||||||
|
BtreeMapView { title: "Dev Shells", tree: &schema.devshells }
|
||||||
|
BtreeMapView { title: "Checks", tree: &schema.checks }
|
||||||
|
BtreeMapView { title: "Apps", tree: &schema.apps }
|
||||||
|
SectionHeading { title: "Formatter" }
|
||||||
|
match schema.formatter.as_ref() {
|
||||||
|
Some(v) => {
|
||||||
|
let k = v.name.clone().unwrap_or("formatter".to_string());
|
||||||
|
render! { FlakeValView { k: k.clone(), v: v.clone() } }
|
||||||
|
},
|
||||||
|
None => render! { "" }
|
||||||
|
},
|
||||||
|
SectionHeading { title: "Other" }
|
||||||
|
match &schema.other {
|
||||||
|
Some(v) => render! { FlakeOutputsRawView { outs: FlakeOutputs::Attrset(v.clone()) } },
|
||||||
|
None => render! { "" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn FlakeSchemaView<'a>(cx: Scope, schema: &'a FlakeSchema) -> impl IntoView {
|
pub fn BtreeMapView<'a>(
|
||||||
let system = &schema.system.clone();
|
|
||||||
view! { cx,
|
|
||||||
<div>
|
|
||||||
<h2 class="my-2 ">
|
|
||||||
<div class="text-xl font-bold text-primary-600">{system.human_readable()}</div>
|
|
||||||
" "
|
|
||||||
<span class="font-mono text-xs text-gray-500">"(" {system.to_string()} ")"</span>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div class="text-left">
|
|
||||||
<BTreeMapView title="Packages" tree=&schema.packages/>
|
|
||||||
<BTreeMapView title="Legacy Packages" tree=&schema.legacy_packages/>
|
|
||||||
<BTreeMapView title="Dev Shells" tree=&schema.devshells/>
|
|
||||||
<BTreeMapView title="Checks" tree=&schema.checks/>
|
|
||||||
<BTreeMapView title="Apps" tree=&schema.apps/>
|
|
||||||
<SectionHeading title="Formatter"/>
|
|
||||||
{schema
|
|
||||||
.formatter
|
|
||||||
.as_ref()
|
|
||||||
.map(|v| {
|
|
||||||
let default = "formatter".to_string();
|
|
||||||
let k = v.name.as_ref().unwrap_or(&default);
|
|
||||||
view! { cx, <FlakeValView k v/> }
|
|
||||||
})}
|
|
||||||
|
|
||||||
<SectionHeading title="Other"/>
|
|
||||||
{schema
|
|
||||||
.other
|
|
||||||
.as_ref()
|
|
||||||
.map(|v| {
|
|
||||||
// TODO: Use a non-recursive rendering component?
|
|
||||||
view! { cx, <FlakeOutputsRawView outs=&FlakeOutputs::Attrset(v.clone())/> }
|
|
||||||
})}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
fn BTreeMapView<'a>(
|
|
||||||
cx: Scope,
|
cx: Scope,
|
||||||
title: &'static str,
|
title: &'static str,
|
||||||
tree: &'a BTreeMap<String, Val>,
|
tree: &'a BTreeMap<String, Val>,
|
||||||
) -> impl IntoView {
|
) -> Element {
|
||||||
(!tree.is_empty()).then(move || {
|
render! {
|
||||||
view! { cx,
|
div {
|
||||||
<SectionHeading title/>
|
SectionHeading { title: title }
|
||||||
<BTreeMapBodyView tree/>
|
BtreeMapBodyView { tree: tree }
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
fn BTreeMapBodyView<'a>(cx: Scope, tree: &'a BTreeMap<String, Val>) -> impl IntoView {
|
|
||||||
view! { cx,
|
|
||||||
<div class="flex flex-wrap justify-start">
|
|
||||||
{tree.iter().map(|(k, v)| view! { cx, <FlakeValView k v/> }).collect_view(cx)}
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn FlakeValView<'a>(cx: Scope, k: &'a String, v: &'a Val) -> impl IntoView {
|
pub fn BtreeMapBodyView<'a>(cx: Scope, tree: &'a BTreeMap<String, Val>) -> Element {
|
||||||
view! { cx,
|
render! {
|
||||||
<div
|
div { class: "flex flex-wrap justify-start",
|
||||||
title=format!("{:?}", v.type_)
|
for (k , v) in tree.iter() {
|
||||||
class="flex flex-col p-2 my-2 mr-2 space-y-2 bg-white border-4 border-gray-300 rounded hover:border-gray-400"
|
FlakeValView { k: k.clone(), v: v.clone() }
|
||||||
>
|
}
|
||||||
<div class="flex flex-row justify-start space-x-2 font-bold text-primary-500">
|
}
|
||||||
<div>{v.type_.to_icon()}</div>
|
}
|
||||||
<div>{k}</div>
|
}
|
||||||
</div>
|
|
||||||
{v
|
|
||||||
.name
|
|
||||||
.as_ref()
|
|
||||||
.map(|v| {
|
|
||||||
view! { cx, <div class="font-mono text-xs text-gray-500">{v}</div> }
|
|
||||||
})}
|
|
||||||
|
|
||||||
{v
|
#[component]
|
||||||
.description
|
pub fn FlakeValView(cx: Scope, k: String, v: Val) -> Element {
|
||||||
.as_ref()
|
render! {
|
||||||
.map(|v| {
|
div {
|
||||||
view! { cx, <div class="font-light">{v}</div> }
|
title: "{v.type_}",
|
||||||
})}
|
class: "flex flex-col p-2 my-2 mr-2 space-y-2 bg-white border-4 border-gray-300 rounded hover:border-gray-400",
|
||||||
|
div { class: "flex flex-row justify-start space-x-2 font-bold text-primary-500",
|
||||||
</div>
|
div { v.type_.to_icon() }
|
||||||
|
div { "{k}" }
|
||||||
|
}
|
||||||
|
match &v.name {
|
||||||
|
Some(name_val) => render! { div { class: "font-mono text-xs text-gray-500", "{name_val}" } },
|
||||||
|
None => render! { "" } // No-op for None
|
||||||
|
},
|
||||||
|
match &v.description {
|
||||||
|
Some(desc_val) => render! { div { class: "font-light", "{desc_val}" } },
|
||||||
|
None => render! { "" } // No-op for None
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,65 +166,46 @@ fn FlakeValView<'a>(cx: Scope, k: &'a String, v: &'a Val) -> impl IntoView {
|
|||||||
///
|
///
|
||||||
/// WARNING: This may cause performance problems if the tree is large.
|
/// WARNING: This may cause performance problems if the tree is large.
|
||||||
#[component]
|
#[component]
|
||||||
fn FlakeOutputsRawView<'a>(cx: Scope, outs: &'a FlakeOutputs) -> impl IntoView {
|
pub fn FlakeOutputsRawView(cx: Scope, outs: FlakeOutputs) -> Element {
|
||||||
fn view_val<'b>(cx: Scope, val: &'b Val) -> View {
|
#[component]
|
||||||
view! { cx,
|
fn ValView<'a>(cx: Scope, val: &'a Val) -> Element {
|
||||||
<span>
|
render! {
|
||||||
<b>{val.name.clone()}</b>
|
span {
|
||||||
|
b { val.name.clone() }
|
||||||
" ("
|
" ("
|
||||||
<TypeView type_=&val.type_/>
|
TypeView { type_: &val.type_ }
|
||||||
") "
|
") "
|
||||||
<em>{val.description.clone()}</em>
|
em { val.description.clone() }
|
||||||
</span>
|
}
|
||||||
}
|
}
|
||||||
.into_view(cx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn TypeView<'b>(cx: Scope, type_: &'b Type) -> impl IntoView {
|
pub fn TypeView<'a>(cx: Scope, type_: &'a Type) -> Element {
|
||||||
view! { cx,
|
render! {
|
||||||
<span>
|
span {
|
||||||
{match type_ {
|
match type_ {
|
||||||
Type::NixosModule => "nixosModule ❄️",
|
Type::NixosModule => "nixosModule ❄️",
|
||||||
Type::Derivation => "derivation 📦",
|
Type::Derivation => "derivation 📦",
|
||||||
Type::App => "app 📱",
|
Type::App => "app 📱",
|
||||||
Type::Template => "template 🏗️",
|
Type::Template => "template 🏗️",
|
||||||
Type::Unknown => "unknown ❓",
|
Type::Unknown => "unknown ❓",
|
||||||
}}
|
}
|
||||||
|
}
|
||||||
</span>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match outs {
|
match outs {
|
||||||
FlakeOutputs::Val(v) => view_val(cx, v),
|
FlakeOutputs::Val(v) => render! { ValView { val: v } },
|
||||||
FlakeOutputs::Attrset(v) => view! { cx,
|
FlakeOutputs::Attrset(v) => render! {
|
||||||
<ul class="list-disc">
|
ul { class: "list-disc",
|
||||||
{v
|
for (k , v) in v.iter() {
|
||||||
.iter()
|
li { class: "ml-4",
|
||||||
.map(|(k, v)| {
|
span { class: "px-2 py-1 font-bold text-primary-500", "{k}" }
|
||||||
view! { cx,
|
FlakeOutputsRawView { outs: v.clone() }
|
||||||
<li class="ml-4">
|
}
|
||||||
<span class="px-2 py-1 font-bold text-primary-500">{k}</span>
|
}
|
||||||
<FlakeOutputsRawView outs=v/>
|
}
|
||||||
</li>
|
},
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect_view(cx)}
|
|
||||||
</ul>
|
|
||||||
}
|
|
||||||
.into_view(cx),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get [Flake] info for the given flake url
|
|
||||||
#[server(GetFlake, "/api")]
|
|
||||||
pub async fn get_flake(args: (FlakeUrl, Refresh)) -> Result<Flake, ServerFnError> {
|
|
||||||
use nix_rs::command::NixCmd;
|
|
||||||
let (url, refresh) = args;
|
|
||||||
let nix_cmd = &NixCmd {
|
|
||||||
refresh,
|
|
||||||
..NixCmd::default()
|
|
||||||
};
|
|
||||||
let v = Flake::from_nix(nix_cmd, url).await?;
|
|
||||||
Ok(v)
|
|
||||||
}
|
|
||||||
|
@ -1,100 +1,71 @@
|
|||||||
//! Nix health check UI
|
//! Nix health check UI
|
||||||
|
|
||||||
use leptos::*;
|
use dioxus::prelude::*;
|
||||||
use leptos_extra::query::{self, RefetchQueryButton};
|
|
||||||
use leptos_meta::*;
|
|
||||||
use nix_health::traits::{Check, CheckResult};
|
use nix_health::traits::{Check, CheckResult};
|
||||||
use tracing::instrument;
|
|
||||||
|
|
||||||
use crate::widget::*;
|
use crate::{app::state::AppState, app::widget::RefreshButton};
|
||||||
|
|
||||||
/// Nix health checks
|
/// Nix health checks
|
||||||
#[component]
|
pub fn Health(cx: Scope) -> Element {
|
||||||
pub fn NixHealthRoute(cx: Scope) -> impl IntoView {
|
let state = AppState::use_state(cx);
|
||||||
|
let health_checks = state.health_checks.read();
|
||||||
let title = "Nix Health";
|
let title = "Nix Health";
|
||||||
let result = query::use_server_query(cx, || (), get_nix_health);
|
render! {
|
||||||
let data = result.data;
|
h1 { class: "text-5xl font-bold", title }
|
||||||
view! { cx,
|
RefreshButton {
|
||||||
<Title text=title/>
|
busy: (*health_checks).is_loading_or_refreshing(),
|
||||||
<h1 class="text-5xl font-bold">{title}</h1>
|
handler: move |_event| {
|
||||||
<RefetchQueryButton result query=|| ()/>
|
cx.spawn(async move {
|
||||||
<div class="my-1">
|
state.update_health_checks().await;
|
||||||
<SuspenseWithErrorHandling>
|
});
|
||||||
<div class="flex flex-col items-stretch justify-start space-y-8 text-left">
|
}
|
||||||
<For
|
}
|
||||||
each=move || data.get().unwrap_or(Ok(vec![])).unwrap_or(vec![])
|
health_checks.render_with(cx, |checks| render! {
|
||||||
key=|check| check.title.clone()
|
div { class: "flex flex-col items-stretch justify-start space-y-8 text-left",
|
||||||
view=move |cx, check| {
|
for check in checks {
|
||||||
view! { cx, <ViewCheck check/> }
|
ViewCheck { check: check.clone() }
|
||||||
}
|
}
|
||||||
/>
|
}
|
||||||
|
})
|
||||||
</div>
|
|
||||||
|
|
||||||
</SuspenseWithErrorHandling>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn ViewCheck(cx: Scope, check: Check) -> impl IntoView {
|
fn ViewCheck(cx: Scope, check: Check) -> Element {
|
||||||
view! { cx,
|
render! {
|
||||||
<div class="contents">
|
div { class: "contents",
|
||||||
<details
|
details {
|
||||||
open=check.result != CheckResult::Green
|
open: check.result != CheckResult::Green,
|
||||||
class="my-2 bg-white border-2 rounded-lg cursor-pointer hover:bg-primary-100 border-base-300"
|
class: "my-2 bg-white border-2 rounded-lg cursor-pointer hover:bg-primary-100 border-base-300",
|
||||||
>
|
summary { class: "p-4 text-xl font-bold",
|
||||||
<summary class="p-4 text-xl font-bold">
|
CheckResultSummaryView { green: check.result.green() }
|
||||||
<CheckResultSummaryView green=check.result.green()/>
|
" "
|
||||||
{" "}
|
check.title.clone()
|
||||||
{check.title}
|
}
|
||||||
</summary>
|
div { class: "p-4",
|
||||||
<div class="p-4">
|
div { class: "p-2 my-2 font-mono text-sm bg-black text-base-100", check.info.clone() }
|
||||||
<div class="p-2 my-2 font-mono text-sm bg-black text-base-100">
|
div { class: "flex flex-col justify-start space-y-4",
|
||||||
{check.info}
|
match check.result.clone() {
|
||||||
</div>
|
CheckResult::Green => render! { "" },
|
||||||
<div class="flex flex-col justify-start space-y-4">
|
CheckResult::Red { msg, suggestion } => render! {
|
||||||
{match check.result {
|
h3 { class: "my-2 font-bold text-l" }
|
||||||
CheckResult::Green => view! { cx, "" }.into_view(cx),
|
div { class: "p-2 bg-red-400 rounded bg-border", msg }
|
||||||
CheckResult::Red { msg, suggestion } => {
|
h3 { class: "my-2 font-bold text-l" }
|
||||||
view! { cx,
|
div { class: "p-2 bg-blue-400 rounded bg-border", suggestion }
|
||||||
<h3 class="my-2 font-bold text-l"></h3>
|
|
||||||
<div class="p-2 bg-red-400 rounded bg-border">{msg}</div>
|
|
||||||
<h3 class="my-2 font-bold text-l"></h3>
|
|
||||||
<div class="p-2 bg-blue-400 rounded bg-border">
|
|
||||||
{suggestion}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
.into_view(cx)
|
|
||||||
}
|
}
|
||||||
}}
|
}
|
||||||
|
}
|
||||||
</div>
|
}
|
||||||
</div>
|
}
|
||||||
</details>
|
}
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn CheckResultSummaryView(cx: Scope, green: bool) -> impl IntoView {
|
pub fn CheckResultSummaryView(cx: Scope, green: bool) -> Element {
|
||||||
if green {
|
if *green {
|
||||||
view! { cx, <span class="text-green-500">{"✓"}</span> }
|
render! { span { class: "text-green-500", "✓" } }
|
||||||
} else {
|
} else {
|
||||||
view! { cx, <span class="text-red-500">{"✗"}</span> }
|
render! { span { class: "text-red-500", "✗" } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get [NixHealth] information
|
|
||||||
#[instrument(name = "nix-health")]
|
|
||||||
#[server(GetNixHealth, "/api")]
|
|
||||||
pub async fn get_nix_health(_unit: ()) -> Result<Vec<nix_health::traits::Check>, ServerFnError> {
|
|
||||||
use nix_health::NixHealth;
|
|
||||||
use nix_rs::{env, info};
|
|
||||||
let nix_info = info::NixInfo::from_nix(&nix_rs::command::NixCmd::default()).await?;
|
|
||||||
// TODO: Use Some(flake_url)? With what UX?
|
|
||||||
let nix_env = env::NixEnv::detect(None).await?;
|
|
||||||
let health = NixHealth::default();
|
|
||||||
let checks = health.run_checks(&nix_info, &nix_env);
|
|
||||||
Ok(checks)
|
|
||||||
}
|
|
||||||
|
188
src/app/info.rs
188
src/app/info.rs
@ -2,128 +2,104 @@
|
|||||||
|
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
use leptos::*;
|
use dioxus::prelude::*;
|
||||||
use leptos_extra::query::{self, RefetchQueryButton};
|
use nix_rs::{config::NixConfig, info::NixInfo, version::NixVersion};
|
||||||
use leptos_extra::signal::SignalWithResult;
|
|
||||||
use leptos_meta::*;
|
|
||||||
use nix_rs::{
|
|
||||||
config::{ConfigVal, NixConfig},
|
|
||||||
info::NixInfo,
|
|
||||||
version::NixVersion,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::widget::*;
|
use crate::{app::state::AppState, app::widget::RefreshButton};
|
||||||
|
|
||||||
/// Nix information
|
/// Nix information
|
||||||
#[component]
|
#[component]
|
||||||
pub fn NixInfoRoute(cx: Scope) -> impl IntoView {
|
pub fn Info(cx: Scope) -> Element {
|
||||||
let title = "Nix Info";
|
let title = "Nix Info";
|
||||||
let result = query::use_server_query(cx, || (), get_nix_info);
|
let state = AppState::use_state(cx);
|
||||||
let data = result.data;
|
let nix_info = state.nix_info.read();
|
||||||
view! { cx,
|
render! {
|
||||||
<Title text=title/>
|
h1 { class: "text-5xl font-bold", title }
|
||||||
<h1 class="text-5xl font-bold">{title}</h1>
|
RefreshButton {
|
||||||
<RefetchQueryButton result query=|| ()/>
|
busy: (*nix_info).is_loading_or_refreshing(),
|
||||||
<div class="my-1 text-left">
|
handler: move |_event| {
|
||||||
<SuspenseWithErrorHandling>
|
cx.spawn(async move {
|
||||||
{move || {
|
state.update_nix_info().await;
|
||||||
data.with_result(move |info| {
|
});
|
||||||
view! { cx, <NixInfoView info/> }
|
}
|
||||||
})
|
}
|
||||||
}}
|
nix_info.render_with(cx, |v| render! { NixInfoView { info: v.clone() } })
|
||||||
|
|
||||||
</SuspenseWithErrorHandling>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn NixInfoView<'a>(cx: Scope, info: &'a NixInfo) -> impl IntoView {
|
fn NixInfoView(cx: Scope, info: NixInfo) -> Element {
|
||||||
view! { cx,
|
render! {
|
||||||
<div class="flex flex-col p-4 space-y-8 bg-white border-2 rounded border-base-400">
|
div { class: "flex flex-col p-4 space-y-8 bg-white border-2 rounded border-base-400",
|
||||||
<div>
|
div {
|
||||||
<b>Nix Version</b>
|
b { "Nix Version" }
|
||||||
<div class="p-1 my-1 rounded bg-primary-50">
|
div { class: "p-1 my-1 rounded bg-primary-50", NixVersionView { version: info.nix_version } }
|
||||||
<NixVersionView version=&info.nix_version/>
|
}
|
||||||
</div>
|
div {
|
||||||
</div>
|
b { "Nix Config" }
|
||||||
<div>
|
NixConfigView { config: info.nix_config.clone() }
|
||||||
<b>Nix Config</b>
|
}
|
||||||
<NixConfigView config=info.nix_config.clone()/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
fn NixVersionView<'a>(cx: Scope, version: &'a NixVersion) -> impl IntoView {
|
|
||||||
view! { cx,
|
|
||||||
<a href=nix_rs::refs::RELEASE_HISTORY class="font-mono hover:underline" target="_blank">
|
|
||||||
{format!("{}", version)}
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
fn NixConfigView(cx: Scope, config: NixConfig) -> impl IntoView {
|
|
||||||
#[component]
|
|
||||||
fn ConfigRow(cx: Scope, key: &'static str, title: String, children: Children) -> impl IntoView {
|
|
||||||
view! { cx,
|
|
||||||
// TODO: Use a nice Tailwind tooltip here, instead of "title"
|
|
||||||
// attribute.
|
|
||||||
<tr title=title>
|
|
||||||
<td class="px-4 py-2 font-semibold text-base-700">{key}</td>
|
|
||||||
<td class="px-4 py-2 text-left">
|
|
||||||
<code>{children(cx)}</code>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
view! { cx,
|
|
||||||
<div class="py-1 my-1 rounded bg-primary-50">
|
|
||||||
<table class="text-right">
|
|
||||||
// FIXME: so many clones
|
|
||||||
<tbody>
|
|
||||||
<ConfigRow key="Local System" title=config.system.description>
|
|
||||||
{config.system.value.to_string()}
|
|
||||||
</ConfigRow>
|
|
||||||
<ConfigRow key="Max Jobs" title=config.max_jobs.description.clone()>
|
|
||||||
{config.max_jobs.value}
|
|
||||||
</ConfigRow>
|
|
||||||
<ConfigRow key="Cores per build" title=config.cores.description>
|
|
||||||
{config.cores.value}
|
|
||||||
</ConfigRow>
|
|
||||||
<ConfigRow key="Nix Caches" title=config.substituters.clone().description>
|
|
||||||
<ConfigValListView cfg=config.substituters.clone()/>
|
|
||||||
</ConfigRow>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
.into_view(cx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ConfigValListView<T>(cx: Scope, cfg: ConfigVal<Vec<T>>) -> impl IntoView
|
fn NixVersionView(cx: Scope, version: NixVersion) -> Element {
|
||||||
|
render! {a { href: nix_rs::refs::RELEASE_HISTORY, class: "font-mono hover:underline", target: "_blank", "{version}" }}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn NixConfigView(cx: Scope, config: NixConfig) -> Element {
|
||||||
|
let config_row = |key: &'static str, title: String, children: Element<'a>| {
|
||||||
|
render! {
|
||||||
|
tr { title: "{title}",
|
||||||
|
td { class: "px-4 py-2 font-semibold text-base-700", "{key}" }
|
||||||
|
td { class: "px-4 py-2 text-left",
|
||||||
|
code { children }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
render! {
|
||||||
|
div { class: "py-1 my-1 rounded bg-primary-50",
|
||||||
|
table { class: "text-right",
|
||||||
|
tbody {
|
||||||
|
config_row (
|
||||||
|
"Local System",
|
||||||
|
config.system.description.clone(),
|
||||||
|
render! { "{config.system.value}" }
|
||||||
|
),
|
||||||
|
config_row (
|
||||||
|
"Max Jobs",
|
||||||
|
config.max_jobs.description.clone(),
|
||||||
|
render! {"{config.max_jobs.value}"}
|
||||||
|
),
|
||||||
|
config_row (
|
||||||
|
"Cores per build",
|
||||||
|
config.cores.description.clone(),
|
||||||
|
render! { "{config.cores.value}" }
|
||||||
|
),
|
||||||
|
config_row (
|
||||||
|
"Nix Caches",
|
||||||
|
config.substituters.clone().description,
|
||||||
|
render! { ConfigValList { items: &config.substituters.value } }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn ConfigValList<T, 'a>(cx: Scope, items: &'a Vec<T>) -> Element
|
||||||
where
|
where
|
||||||
T: Display,
|
T: Display,
|
||||||
{
|
{
|
||||||
view! { cx,
|
render! {
|
||||||
// Render a list of T items in the list 'self'
|
div { class: "flex flex-col space-y-4",
|
||||||
<div class="flex flex-col space-y-4">
|
for item in items {
|
||||||
{cfg
|
li { class: "list-disc", "{item}" }
|
||||||
.value
|
}
|
||||||
.into_iter()
|
}
|
||||||
.map(|item| view! { cx, <li class="list-disc">{item.to_string()}</li> })
|
|
||||||
.collect_view(cx)}
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
.into_view(cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Determine [NixInfo] on the user's system
|
|
||||||
#[server(GetNixInfo, "/api")]
|
|
||||||
pub async fn get_nix_info(_unit: ()) -> Result<NixInfo, ServerFnError> {
|
|
||||||
let v = NixInfo::from_nix(&nix_rs::command::NixCmd::default()).await?;
|
|
||||||
Ok(v)
|
|
||||||
}
|
}
|
||||||
|
226
src/app/mod.rs
226
src/app/mod.rs
@ -6,141 +6,139 @@
|
|||||||
mod flake;
|
mod flake;
|
||||||
mod health;
|
mod health;
|
||||||
mod info;
|
mod info;
|
||||||
|
mod state;
|
||||||
|
mod widget;
|
||||||
|
|
||||||
use leptos::*;
|
use dioxus::prelude::*;
|
||||||
use leptos_extra::{
|
use dioxus_router::prelude::*;
|
||||||
query::{self},
|
use nix_rs::flake::url::FlakeUrl;
|
||||||
signal::{provide_signal, SignalWithResult},
|
|
||||||
|
use crate::app::{
|
||||||
|
flake::{Flake, FlakeRaw},
|
||||||
|
health::Health,
|
||||||
|
info::Info,
|
||||||
|
state::AppState,
|
||||||
|
widget::Loader,
|
||||||
};
|
};
|
||||||
use leptos_meta::*;
|
|
||||||
use leptos_query::*;
|
|
||||||
use leptos_router::*;
|
|
||||||
use nix_rs::{command::Refresh, flake::url::FlakeUrl};
|
|
||||||
|
|
||||||
use crate::{app::flake::*, app::health::*, app::info::*, widget::*};
|
#[derive(Routable, PartialEq, Debug, Clone)]
|
||||||
|
#[rustfmt::skip]
|
||||||
|
enum Route {
|
||||||
|
#[layout(Wrapper)]
|
||||||
|
#[route("/")]
|
||||||
|
Dashboard {},
|
||||||
|
#[route("/about")]
|
||||||
|
About {},
|
||||||
|
#[route("/flake")]
|
||||||
|
Flake {},
|
||||||
|
#[route("/flake/raw")]
|
||||||
|
FlakeRaw {},
|
||||||
|
#[route("/health")]
|
||||||
|
Health {},
|
||||||
|
#[route("/info")]
|
||||||
|
Info {},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn Wrapper(cx: Scope) -> Element {
|
||||||
|
render! {
|
||||||
|
Nav {}
|
||||||
|
Outlet::<Route> {}
|
||||||
|
footer { class: "flex flex-row justify-center w-full p-4", img { src: "images/128x128.png", width: "32", height: "32" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Main frontend application container
|
/// Main frontend application container
|
||||||
#[component]
|
pub fn App(cx: Scope) -> Element {
|
||||||
pub fn App(cx: Scope) -> impl IntoView {
|
AppState::provide_state(cx);
|
||||||
provide_meta_context(cx);
|
use_shared_state_provider(cx, || {
|
||||||
provide_query_client(cx);
|
|
||||||
provide_signal::<FlakeUrl>(
|
|
||||||
cx,
|
|
||||||
FlakeUrl::suggestions()
|
FlakeUrl::suggestions()
|
||||||
.first()
|
.first()
|
||||||
.map(Clone::clone)
|
.map(Clone::clone)
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default()
|
||||||
);
|
});
|
||||||
provide_signal::<Refresh>(cx, false.into()); // refresh flag is unused, but we may add it to UI later.
|
render! {
|
||||||
|
body {
|
||||||
|
// Can't do this, because Tauri window has its own scrollbar. :-/
|
||||||
|
// class: "overflow-y-scroll",
|
||||||
|
div { class: "flex justify-center w-full min-h-screen bg-center bg-cover bg-base-200",
|
||||||
|
div { class: "flex flex-col items-stretch mx-auto sm:container sm:max-w-screen-md",
|
||||||
|
main { class: "flex flex-col px-2 mb-8 space-y-3 text-center", Router::<Route> {} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
view! { cx,
|
// Home page
|
||||||
<Stylesheet id="leptos" href="/pkg/nix-browser.css"/>
|
fn Dashboard(cx: Scope) -> Element {
|
||||||
<Title formatter=|s| format!("{s} ― nix-browser")/>
|
tracing::debug!("Rendering Dashboard page");
|
||||||
<Router fallback=|cx| {
|
let state = AppState::use_state(cx);
|
||||||
view! { cx, <NotFound/> }
|
let health_checks = state.health_checks.read();
|
||||||
}>
|
// A Card component
|
||||||
<Body class="overflow-y-scroll"/>
|
#[component]
|
||||||
<div class="flex justify-center w-full min-h-screen bg-center bg-cover bg-base-200">
|
fn Card<'a>(cx: Scope, href: Route, children: Element<'a>) -> Element<'a> {
|
||||||
<div class="flex flex-col items-stretch mx-auto sm:container sm:max-w-screen-md">
|
render! {
|
||||||
<Nav/>
|
Link {
|
||||||
<main class="flex flex-col px-2 mb-8 space-y-3 text-center">
|
to: "{href}",
|
||||||
<Routes>
|
class: "flex items-center justify-center w-48 h-48 p-2 m-2 border-2 rounded-lg shadow border-base-400 active:shadow-none bg-base-100 hover:bg-primary-200",
|
||||||
<Route path="" view=Dashboard/>
|
span { class: "text-3xl text-base-800", children }
|
||||||
<Route path="/flake" view=NixFlakeRoute>
|
}
|
||||||
<Route path="" view=NixFlakeHomeRoute/>
|
}
|
||||||
<Route path="raw" view=NixFlakeRawRoute/>
|
}
|
||||||
</Route>
|
render! {
|
||||||
<Route path="/health" view=NixHealthRoute/>
|
h1 { class: "text-5xl font-bold", "Dashboard" }
|
||||||
<Route path="/info" view=NixInfoRoute/>
|
div { id: "cards", class: "flex flex-row flex-wrap",
|
||||||
<Route path="/about" view=About/>
|
Card { href: Route::Health {},
|
||||||
</Routes>
|
"Nix Health Check "
|
||||||
</main>
|
match (*health_checks).current_value() {
|
||||||
</div>
|
Some(Ok(checks)) => render! {
|
||||||
</div>
|
if checks.iter().all(|check| check.result.green()) {
|
||||||
</Router>
|
"✅"
|
||||||
|
} else {
|
||||||
|
"❌"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Some(Err(err)) => render! { "{err}" },
|
||||||
|
None => render! { Loader {} },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Card { href: Route::Info {}, "Nix Info ℹ️" }
|
||||||
|
Card { href: Route::Flake {}, "Flake Dashboard ❄️️" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Navigation bar
|
/// Navigation bar
|
||||||
///
|
///
|
||||||
/// TODO Switch to breadcrumbs, as it simplifes the design overall.
|
/// TODO Switch to breadcrumbs, as it simplifes the design overall.
|
||||||
#[component]
|
fn Nav(cx: Scope) -> Element {
|
||||||
fn Nav(cx: Scope) -> impl IntoView {
|
|
||||||
let class = "px-3 py-2";
|
let class = "px-3 py-2";
|
||||||
view! { cx,
|
let active_class = "bg-white text-primary-800 font-bold";
|
||||||
<nav class="flex flex-row w-full mb-8 text-white md:rounded-b bg-primary-800">
|
render! {
|
||||||
<A exact=true href="/" class=class>
|
nav { class: "flex flex-row w-full mb-8 text-white md:rounded-b bg-primary-800",
|
||||||
"Dashboard"
|
Link { to: Route::Dashboard {}, class: class, active_class: active_class, "Dashboard" }
|
||||||
</A>
|
Link { to: Route::Flake {}, class: class, active_class: active_class, "Flake" }
|
||||||
<A exact=false href="/flake" class=class>
|
Link { to: Route::Health {}, class: class, active_class: active_class, "Nix Health" }
|
||||||
"Flake"
|
Link { to: Route::Info {}, class: class, active_class: active_class, "Nix Info" }
|
||||||
</A>
|
Link { to: Route::About {}, class: class, active_class: active_class, "About" }
|
||||||
<A exact=true href="/health" class=class>
|
div { class: "flex-grow font-bold text-end {class}", "🌍 nix-browser" }
|
||||||
"Nix Health"
|
|
||||||
</A>
|
|
||||||
<A exact=true href="/info" class=class>
|
|
||||||
"Nix Info"
|
|
||||||
</A>
|
|
||||||
<A exact=true href="/about" class=class>
|
|
||||||
"About"
|
|
||||||
</A>
|
|
||||||
<div class=format!("flex-grow font-bold text-end {}", class)>"🌍 nix-browser"</div>
|
|
||||||
</nav>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Home page
|
|
||||||
#[component]
|
|
||||||
fn Dashboard(cx: Scope) -> impl IntoView {
|
|
||||||
tracing::debug!("Rendering Dashboard page");
|
|
||||||
let result = query::use_server_query(cx, || (), get_nix_health);
|
|
||||||
let data = result.data;
|
|
||||||
let healthy = Signal::derive(cx, move || {
|
|
||||||
data.with_result(|checks| checks.iter().all(|check| check.result.green()))
|
|
||||||
});
|
|
||||||
// A Card component
|
|
||||||
#[component]
|
|
||||||
fn Card(cx: Scope, href: &'static str, children: Children) -> impl IntoView {
|
|
||||||
view! { cx,
|
|
||||||
<A
|
|
||||||
href=href
|
|
||||||
class="flex items-center justify-center w-48 h-48 p-2 m-2 border-2 rounded-lg shadow border-base-400 active:shadow-none bg-base-100 hover:bg-primary-200"
|
|
||||||
>
|
|
||||||
<span class="text-3xl text-base-800">{children(cx)}</span>
|
|
||||||
</A>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
view! { cx,
|
|
||||||
<Title text="Dashboard"/>
|
|
||||||
<h1 class="text-5xl font-bold">"Dashboard"</h1>
|
|
||||||
<div id="cards" class="flex flex-row flex-wrap">
|
|
||||||
<SuspenseWithErrorHandling>
|
|
||||||
<Card href="/health">
|
|
||||||
"Nix Health Check "
|
|
||||||
{move || {
|
|
||||||
healthy
|
|
||||||
.with_result(move |green| {
|
|
||||||
view! { cx, <CheckResultSummaryView green=*green/> }
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
|
|
||||||
</Card>
|
|
||||||
</SuspenseWithErrorHandling>
|
|
||||||
<Card href="/info">"Nix Info ℹ️"</Card>
|
|
||||||
<Card href="/flake">"Flake Overview ❄️️"</Card>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// About page
|
/// About page
|
||||||
#[component]
|
fn About(cx: Scope) -> Element {
|
||||||
fn About(cx: Scope) -> impl IntoView {
|
render! {
|
||||||
view! { cx,
|
h1 { class: "text-5xl font-bold", "About" }
|
||||||
<Title text="About"/>
|
p {
|
||||||
<h1 class="text-5xl font-bold">"About"</h1>
|
|
||||||
<p>
|
|
||||||
"nix-browser is still work in progress. Track its development "
|
"nix-browser is still work in progress. Track its development "
|
||||||
<LinkExternal link="https://github.com/juspay/nix-browser" text="on Github"/>
|
a {
|
||||||
</p>
|
href: "https://github.com/juspay/nix-browser",
|
||||||
|
class: "underline text-primary-500 hover:no-underline",
|
||||||
|
rel: "external",
|
||||||
|
target: "_blank",
|
||||||
|
"on Github"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
161
src/app/state.rs
Normal file
161
src/app/state.rs
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
//! Application state
|
||||||
|
|
||||||
|
mod datum;
|
||||||
|
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use dioxus::prelude::{use_context, use_context_provider, use_future, Scope};
|
||||||
|
use dioxus_signals::Signal;
|
||||||
|
use nix_health::NixHealth;
|
||||||
|
use nix_rs::{
|
||||||
|
command::NixCmdError,
|
||||||
|
flake::{url::FlakeUrl, Flake},
|
||||||
|
};
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
|
use self::datum::Datum;
|
||||||
|
|
||||||
|
/// Our dioxus application state is a struct of [Signal]
|
||||||
|
///
|
||||||
|
/// They use [Datum] which is a glorified [Option] to distinguis between initial
|
||||||
|
/// loading and subsequent refreshing.
|
||||||
|
#[derive(Default, Clone, Copy, Debug)]
|
||||||
|
pub struct AppState {
|
||||||
|
pub nix_info: Signal<Datum<Result<nix_rs::info::NixInfo, SystemError>>>,
|
||||||
|
pub nix_env: Signal<Datum<Result<nix_rs::env::NixEnv, SystemError>>>,
|
||||||
|
pub health_checks: Signal<Datum<Result<Vec<nix_health::traits::Check>, SystemError>>>,
|
||||||
|
|
||||||
|
pub flake_url: Signal<FlakeUrl>,
|
||||||
|
pub flake: Signal<Datum<Result<Flake, NixCmdError>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
pub async fn initialize(&self) {
|
||||||
|
tracing::info!("Initializing app state");
|
||||||
|
// Initializing health checks automatially initializes other signals.
|
||||||
|
self.update_health_checks().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(name = "update-nix-info", skip(self))]
|
||||||
|
pub async fn update_nix_info(&self) {
|
||||||
|
tracing::debug!("Updating nix info ...");
|
||||||
|
Datum::refresh_with(self.nix_info, async {
|
||||||
|
// NOTE: Without tokio::spawn, this will run in main desktop thread,
|
||||||
|
// and will hang at some point.
|
||||||
|
let nix_info = tokio::spawn(async move {
|
||||||
|
nix_rs::info::NixInfo::from_nix(&nix_rs::command::NixCmd::default())
|
||||||
|
.await
|
||||||
|
.map_err(|e| SystemError {
|
||||||
|
message: format!("Error getting nix info: {:?}", e),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
tracing::debug!("Got nix info, about to mut");
|
||||||
|
nix_info
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(name = "update-nix-env", skip(self))]
|
||||||
|
pub async fn update_nix_env(&self) {
|
||||||
|
tracing::debug!("Updating nix env ...");
|
||||||
|
Datum::refresh_with(self.nix_env, async {
|
||||||
|
let nix_env = tokio::spawn(async move {
|
||||||
|
nix_rs::env::NixEnv::detect(None)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string().into())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
tracing::debug!("Got nix env, about to mut");
|
||||||
|
nix_env
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(name = "update-health-checks", skip(self))]
|
||||||
|
pub async fn update_health_checks(&self) {
|
||||||
|
tracing::debug!("Updating health checks ...");
|
||||||
|
Datum::refresh_with(self.health_checks, async {
|
||||||
|
// Update depenencies
|
||||||
|
self.update_nix_info().await;
|
||||||
|
self.update_nix_env().await;
|
||||||
|
let get_nix_health = move || -> Result<Vec<nix_health::traits::Check>, SystemError> {
|
||||||
|
let nix_env = self.nix_env.read();
|
||||||
|
let nix_env: &nix_rs::env::NixEnv = nix_env
|
||||||
|
.current_value()
|
||||||
|
.unwrap()
|
||||||
|
.as_ref()
|
||||||
|
.map_err(|e| Into::<SystemError>::into(e.to_string()))?;
|
||||||
|
let nix_info = self.nix_info.read();
|
||||||
|
let nix_info: &nix_rs::info::NixInfo =
|
||||||
|
nix_info
|
||||||
|
.current_value()
|
||||||
|
.unwrap()
|
||||||
|
.as_ref()
|
||||||
|
.map_err(|e| Into::<SystemError>::into(e.to_string()))?;
|
||||||
|
let health_checks = NixHealth::default().run_checks(nix_info, nix_env);
|
||||||
|
Ok(health_checks)
|
||||||
|
};
|
||||||
|
let health_checks = get_nix_health();
|
||||||
|
tracing::debug!("Got health checks, about to mut");
|
||||||
|
health_checks
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(name = "set-flake-url", skip(self))]
|
||||||
|
pub async fn set_flake_url(&self, url: &FlakeUrl) {
|
||||||
|
// TODO: Can we use derived signals here?
|
||||||
|
self.flake_url.set(url.clone());
|
||||||
|
self.update_flake().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(name = "update-flake", skip(self))]
|
||||||
|
pub async fn update_flake(&self) {
|
||||||
|
tracing::debug!("Updating flake ...");
|
||||||
|
Datum::refresh_with(self.flake, async {
|
||||||
|
let flake_url = self.flake_url.read().clone();
|
||||||
|
let flake = tokio::spawn(async move {
|
||||||
|
Flake::from_nix(&nix_rs::command::NixCmd::default(), flake_url.clone()).await
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
tracing::debug!("Got flake, about to mut");
|
||||||
|
flake
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the [AppState] from context
|
||||||
|
pub fn use_state(cx: Scope) -> Self {
|
||||||
|
*use_context(cx).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn provide_state(cx: Scope) {
|
||||||
|
use_context_provider(cx, AppState::default);
|
||||||
|
let state = AppState::use_state(cx);
|
||||||
|
use_future(cx, (), |_| async move {
|
||||||
|
state.initialize().await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Catch all error to use in UI components
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct SystemError {
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for SystemError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(&self.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for SystemError {
|
||||||
|
fn from(message: String) -> Self {
|
||||||
|
Self { message }
|
||||||
|
}
|
||||||
|
}
|
109
src/app/state/datum.rs
Normal file
109
src/app/state/datum.rs
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
use std::{fmt::Display, future::Future};
|
||||||
|
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_signals::Signal;
|
||||||
|
|
||||||
|
/// Represent loading/refreshing state of UI data
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum Datum<T> {
|
||||||
|
#[default]
|
||||||
|
Loading,
|
||||||
|
Available {
|
||||||
|
value: T,
|
||||||
|
refreshing: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Datum<T> {
|
||||||
|
pub fn is_loading_or_refreshing(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
Datum::Loading
|
||||||
|
| Datum::Available {
|
||||||
|
value: _,
|
||||||
|
refreshing: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the inner value if available
|
||||||
|
pub fn current_value(&self) -> Option<&T> {
|
||||||
|
match self {
|
||||||
|
Datum::Loading => None,
|
||||||
|
Datum::Available {
|
||||||
|
value: x,
|
||||||
|
refreshing: _,
|
||||||
|
} => Some(x),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the datum value
|
||||||
|
///
|
||||||
|
/// Use [refresh_with] if the value is produced by a long-running task.
|
||||||
|
fn set_value(&mut self, value: T) {
|
||||||
|
tracing::debug!("🍒 Setting {} datum value", std::any::type_name::<T>());
|
||||||
|
*self = Datum::Available {
|
||||||
|
value,
|
||||||
|
refreshing: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark the datum is being-refreshed
|
||||||
|
///
|
||||||
|
/// Do this just prior to doing a long-running task that will provide a
|
||||||
|
/// value to be set using [set_value]
|
||||||
|
fn mark_refreshing(&mut self) {
|
||||||
|
if let Datum::Available {
|
||||||
|
value: _,
|
||||||
|
refreshing,
|
||||||
|
} = self
|
||||||
|
{
|
||||||
|
if *refreshing {
|
||||||
|
panic!("Cannot refresh already refreshing data");
|
||||||
|
}
|
||||||
|
tracing::debug!(
|
||||||
|
"🍒 Marking {} datum as refreshing",
|
||||||
|
std::any::type_name::<T>()
|
||||||
|
);
|
||||||
|
*refreshing = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Refresh the datum [Signal] using the given function
|
||||||
|
///
|
||||||
|
/// Refresh state is automatically set.
|
||||||
|
pub async fn refresh_with<F>(signal: Signal<Self>, f: F)
|
||||||
|
where
|
||||||
|
F: Future<Output = T>,
|
||||||
|
{
|
||||||
|
signal.with_mut(move |x| {
|
||||||
|
x.mark_refreshing();
|
||||||
|
});
|
||||||
|
let val = f.await;
|
||||||
|
signal.with_mut(move |x| {
|
||||||
|
x.set_value(val);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, E: Display> Datum<Result<T, E>> {
|
||||||
|
/// Render the result datum with the given component
|
||||||
|
///
|
||||||
|
/// The error message will be rendered appropriately. If the datum is
|
||||||
|
/// unavailable, nothing will be rendered (loading state is rendered
|
||||||
|
/// differently)
|
||||||
|
pub fn render_with<'a, F>(&self, cx: &'a Scoped<'a, ()>, component: F) -> Element<'a>
|
||||||
|
where
|
||||||
|
F: FnOnce(&T) -> Element<'a>,
|
||||||
|
{
|
||||||
|
match self.current_value()? {
|
||||||
|
Ok(value) => component(value),
|
||||||
|
Err(err) => render! {
|
||||||
|
div {
|
||||||
|
class: "p-4 my-1 text-left text-sm font-mono text-white bg-red-500 rounded",
|
||||||
|
"Error: {err}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
src/app/widget.rs
Normal file
45
src/app/widget.rs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
//! Various widgets
|
||||||
|
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
/// A refresh button with a busy indicator
|
||||||
|
///
|
||||||
|
/// You want to use [crate::state::datum] for this.
|
||||||
|
#[component]
|
||||||
|
pub fn RefreshButton<F>(cx: Scope, busy: bool, handler: F) -> Element
|
||||||
|
where
|
||||||
|
F: Fn(Event<MouseData>),
|
||||||
|
{
|
||||||
|
let button_cls = if *busy {
|
||||||
|
"bg-gray-400 text-white"
|
||||||
|
} else {
|
||||||
|
"bg-blue-700 text-white hover:bg-blue-800"
|
||||||
|
};
|
||||||
|
render! {
|
||||||
|
div { class: "flex-col items-center justify-center space-y-2 mb-4",
|
||||||
|
button {
|
||||||
|
class: "py-1 px-2 shadow-lg border-1 {button_cls} rounded-md",
|
||||||
|
disabled: *busy,
|
||||||
|
onclick: handler,
|
||||||
|
"Refresh "
|
||||||
|
if *busy {
|
||||||
|
render! { "⏳" }
|
||||||
|
} else {
|
||||||
|
render! { "🔄" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if *busy {
|
||||||
|
render! { Loader {} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Loader(cx: Scope) -> Element {
|
||||||
|
render! {
|
||||||
|
div { class: "flex justify-center items-center",
|
||||||
|
div { class: "animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-purple-500" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
src/cli.rs
21
src/cli.rs
@ -1,31 +1,10 @@
|
|||||||
//! Command-line interface
|
//! Command-line interface
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use std::net::SocketAddr;
|
|
||||||
|
|
||||||
use crate::logging;
|
use crate::logging;
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
/// Do not automatically open the application in the local browser
|
|
||||||
///
|
|
||||||
/// Enabled by default if the app is running under `cargo leptos ...`
|
|
||||||
#[arg(short = 'n', long = "no-open", env = "NIX_BROWSER_NO_OPEN")]
|
|
||||||
pub no_open: bool,
|
|
||||||
|
|
||||||
/// The address to serve the application on
|
|
||||||
///
|
|
||||||
/// Format: `IP_ADDRESS:PORT`
|
|
||||||
///
|
|
||||||
/// Uses localhost and random port by default. To use a different port, pass
|
|
||||||
/// `127.0.0.1:8080`
|
|
||||||
#[arg(
|
|
||||||
short = 's',
|
|
||||||
long = "site-addr",
|
|
||||||
default_value = "127.0.0.1:0",
|
|
||||||
env = "LEPTOS_SITE_ADDR"
|
|
||||||
)]
|
|
||||||
pub site_addr: Option<SocketAddr>,
|
|
||||||
|
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
pub verbosity: logging::Verbosity,
|
pub verbosity: logging::Verbosity,
|
||||||
}
|
}
|
||||||
|
27
src/lib.rs
27
src/lib.rs
@ -1,27 +0,0 @@
|
|||||||
#![feature(associated_type_defaults)]
|
|
||||||
//! nix-browser crate; see GitHub [README] for details.
|
|
||||||
//!
|
|
||||||
//! [README]: https://github.com/juspay/nix-browser
|
|
||||||
pub mod app;
|
|
||||||
pub mod logging;
|
|
||||||
pub mod widget;
|
|
||||||
#[cfg(feature = "hydrate")]
|
|
||||||
use wasm_bindgen::prelude::wasm_bindgen;
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
pub mod cli;
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
pub mod server;
|
|
||||||
|
|
||||||
/// Main entry point for the WASM frontend
|
|
||||||
#[cfg(feature = "hydrate")]
|
|
||||||
#[wasm_bindgen]
|
|
||||||
pub fn hydrate() {
|
|
||||||
use crate::app::*;
|
|
||||||
use leptos::*;
|
|
||||||
|
|
||||||
logging::setup_client_logging();
|
|
||||||
tracing::info!("Hydrating app");
|
|
||||||
leptos::mount_to_body(move |cx| {
|
|
||||||
view! { cx, <App/> }
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,52 +1,15 @@
|
|||||||
//! Logging setup for the server and client
|
//! Logging setup for the server and client
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
use tower_http::{
|
|
||||||
classify::{ServerErrorsAsFailures, SharedClassifier},
|
|
||||||
trace::TraceLayer,
|
|
||||||
};
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
use tracing_subscriber::filter::{Directive, LevelFilter};
|
use tracing_subscriber::filter::{Directive, LevelFilter};
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
/// Setup server-side logging using [tracing_subscriber]
|
pub fn setup_logging(verbosity: &Verbosity) {
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
pub fn setup_server_logging(verbosity: &Verbosity) {
|
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
.with_env_filter(verbosity.log_filter())
|
.with_env_filter(verbosity.log_filter())
|
||||||
.compact()
|
.compact()
|
||||||
.init();
|
.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Setup browser console logging using [tracing_subscriber_wasm]
|
|
||||||
#[cfg(feature = "hydrate")]
|
|
||||||
pub fn setup_client_logging() {
|
|
||||||
tracing_subscriber::fmt()
|
|
||||||
.with_writer(
|
|
||||||
// To avoide trace events in the browser from showing their
|
|
||||||
// JS backtrace, which is very annoying, in my opinion
|
|
||||||
tracing_subscriber_wasm::MakeConsoleWriter::default()
|
|
||||||
.map_trace_level_to(tracing::Level::DEBUG),
|
|
||||||
)
|
|
||||||
.with_max_level(tracing::Level::INFO)
|
|
||||||
// For some reason, if we don't do this in the browser, we get
|
|
||||||
// a runtime error.
|
|
||||||
.without_time()
|
|
||||||
.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Setup HTTP request logging
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
pub fn http_trace_layer() -> TraceLayer<SharedClassifier<ServerErrorsAsFailures>> {
|
|
||||||
use tower_http::trace;
|
|
||||||
use tracing::Level;
|
|
||||||
|
|
||||||
TraceLayer::new_for_http()
|
|
||||||
.make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO))
|
|
||||||
.on_response(trace::DefaultOnResponse::new().level(Level::INFO))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(clap::Args, Debug, Clone)]
|
#[derive(clap::Args, Debug, Clone)]
|
||||||
pub struct Verbosity {
|
pub struct Verbosity {
|
||||||
/// Server logging level
|
/// Server logging level
|
||||||
@ -56,7 +19,6 @@ pub struct Verbosity {
|
|||||||
pub verbose: u8,
|
pub verbose: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
impl Verbosity {
|
impl Verbosity {
|
||||||
/// Return the log filter for CLI flag.
|
/// Return the log filter for CLI flag.
|
||||||
fn log_filter(&self) -> EnvFilter {
|
fn log_filter(&self) -> EnvFilter {
|
||||||
@ -74,25 +36,18 @@ impl Verbosity {
|
|||||||
LevelFilter::WARN.into(),
|
LevelFilter::WARN.into(),
|
||||||
"nix_browser=info".parse().unwrap(),
|
"nix_browser=info".parse().unwrap(),
|
||||||
"nix_rs=info".parse().unwrap(),
|
"nix_rs=info".parse().unwrap(),
|
||||||
"leptos_extra=info".parse().unwrap(),
|
|
||||||
],
|
],
|
||||||
// -v: log app DEBUG level, as well as http requests
|
// -v: log app DEBUG level, as well as http requests
|
||||||
1 => vec![
|
1 => vec![
|
||||||
LevelFilter::WARN.into(),
|
LevelFilter::WARN.into(),
|
||||||
"nix_browser=debug".parse().unwrap(),
|
"nix_browser=debug".parse().unwrap(),
|
||||||
"nix_rs=debug".parse().unwrap(),
|
"nix_rs=debug".parse().unwrap(),
|
||||||
"leptos_extra=debug".parse().unwrap(),
|
|
||||||
// 3rd-party libraries
|
|
||||||
"tower_http=info".parse().unwrap(),
|
|
||||||
],
|
],
|
||||||
// -vv: log app TRACE level, as well as http requests
|
// -vv: log app TRACE level, as well as http requests
|
||||||
2 => vec![
|
2 => vec![
|
||||||
LevelFilter::WARN.into(),
|
LevelFilter::WARN.into(),
|
||||||
"nix_browser=trace".parse().unwrap(),
|
"nix_browser=trace".parse().unwrap(),
|
||||||
"nix_rs=trace".parse().unwrap(),
|
"nix_rs=trace".parse().unwrap(),
|
||||||
"leptos_extra=trace".parse().unwrap(),
|
|
||||||
// 3rd-party libraries
|
|
||||||
"tower_http=info".parse().unwrap(),
|
|
||||||
],
|
],
|
||||||
// -vvv: log DEBUG level of app and libraries
|
// -vvv: log DEBUG level of app and libraries
|
||||||
3 => vec![LevelFilter::DEBUG.into()],
|
3 => vec![LevelFilter::DEBUG.into()],
|
||||||
|
26
src/main.rs
26
src/main.rs
@ -1,13 +1,23 @@
|
|||||||
#[cfg(feature = "ssr")]
|
use dioxus_desktop::{LogicalSize, WindowBuilder};
|
||||||
|
|
||||||
|
mod app;
|
||||||
|
mod cli;
|
||||||
|
mod logging;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
human_panic::setup_panic!();
|
let args = crate::cli::Args::parse();
|
||||||
let args = nix_browser::cli::Args::parse();
|
crate::logging::setup_logging(&args.verbosity);
|
||||||
nix_browser::server::main(args).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "ssr"))]
|
dioxus_desktop::launch_cfg(
|
||||||
fn main() {
|
app::App,
|
||||||
// No main entry point for wasm
|
dioxus_desktop::Config::new()
|
||||||
|
.with_custom_head(r#" <link rel="stylesheet" href="tailwind.css"> "#.to_string())
|
||||||
|
.with_window(
|
||||||
|
WindowBuilder::new()
|
||||||
|
.with_title("nix-browser")
|
||||||
|
.with_inner_size(LogicalSize::new(900, 600)),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,87 +0,0 @@
|
|||||||
//! Axum server
|
|
||||||
use std::convert::Infallible;
|
|
||||||
|
|
||||||
use crate::app::App;
|
|
||||||
use axum::response::Response as AxumResponse;
|
|
||||||
use axum::routing::IntoMakeService;
|
|
||||||
use axum::{body::Body, http::Request, response::IntoResponse};
|
|
||||||
use axum::{routing::post, Router};
|
|
||||||
use hyper::server::conn::AddrIncoming;
|
|
||||||
use leptos::*;
|
|
||||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
|
||||||
use std::net::SocketAddr;
|
|
||||||
use tower_http::services::ServeDir;
|
|
||||||
use tracing::instrument;
|
|
||||||
|
|
||||||
use crate::cli;
|
|
||||||
|
|
||||||
/// Axum server main entry point
|
|
||||||
pub async fn main(args: cli::Args) {
|
|
||||||
crate::logging::setup_server_logging(&args.verbosity);
|
|
||||||
let leptos_options = get_leptos_options(&args).await;
|
|
||||||
let server = create_server(leptos_options).await;
|
|
||||||
if !args.no_open {
|
|
||||||
open_http_app(server.local_addr()).await;
|
|
||||||
}
|
|
||||||
server.await.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create an Axum server for the Leptos app
|
|
||||||
#[instrument(name = "server")]
|
|
||||||
#[allow(clippy::async_yields_async)]
|
|
||||||
async fn create_server(
|
|
||||||
leptos_options: leptos_config::LeptosOptions,
|
|
||||||
) -> axum::Server<AddrIncoming, IntoMakeService<axum::Router>> {
|
|
||||||
tracing::debug!("Firing up Leptos app with config: {:?}", leptos_options);
|
|
||||||
leptos_query::suppress_query_load(true); // https://github.com/nicoburniske/leptos_query/issues/6
|
|
||||||
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
|
|
||||||
leptos_query::suppress_query_load(false);
|
|
||||||
let client_dist = ServeDir::new(leptos_options.site_root.clone());
|
|
||||||
let leptos_options_clone = leptos_options.clone(); // A copy to move to the closure below.
|
|
||||||
let not_found_service =
|
|
||||||
tower::service_fn(move |req| not_found_handler(leptos_options_clone.to_owned(), req));
|
|
||||||
let app = Router::new()
|
|
||||||
// server functions API routes
|
|
||||||
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
|
||||||
// application routes
|
|
||||||
.leptos_routes(&leptos_options, routes, |cx| view! { cx, <App/> })
|
|
||||||
// static files are served as fallback (but *before* falling back to
|
|
||||||
// error handler)
|
|
||||||
.fallback_service(client_dist.clone().not_found_service(not_found_service))
|
|
||||||
// enable HTTP request logging
|
|
||||||
.layer(crate::logging::http_trace_layer())
|
|
||||||
.with_state(leptos_options.clone());
|
|
||||||
|
|
||||||
let server = axum::Server::bind(&leptos_options.site_addr).serve(app.into_make_service());
|
|
||||||
tracing::info!("nix-browser web 🌀️ http://{}", server.local_addr());
|
|
||||||
server
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_leptos_options(args: &cli::Args) -> leptos_config::LeptosOptions {
|
|
||||||
let conf_file = get_configuration(None).await.unwrap();
|
|
||||||
leptos_config::LeptosOptions {
|
|
||||||
site_addr: args.site_addr.unwrap_or(conf_file.leptos_options.site_addr),
|
|
||||||
..conf_file.leptos_options
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handler for missing routes
|
|
||||||
///
|
|
||||||
/// On missing routes, just delegate to the leptos app, which has a route
|
|
||||||
/// fallback rendering 404 response.
|
|
||||||
async fn not_found_handler(
|
|
||||||
options: LeptosOptions,
|
|
||||||
req: Request<Body>,
|
|
||||||
) -> Result<AxumResponse, Infallible> {
|
|
||||||
let handler =
|
|
||||||
leptos_axum::render_app_to_stream(options.to_owned(), move |cx| view! { cx, <App/> });
|
|
||||||
Ok(handler(req).await.into_response())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Open a http address in the user's web browser
|
|
||||||
async fn open_http_app(addr: SocketAddr) {
|
|
||||||
let url = format!("http://{}", &addr);
|
|
||||||
if let Err(err) = open::that(url) {
|
|
||||||
tracing::warn!("Unable to open in web browser: {}", err)
|
|
||||||
}
|
|
||||||
}
|
|
176
src/widget.rs
176
src/widget.rs
@ -1,176 +0,0 @@
|
|||||||
//! Various Leptos widgets
|
|
||||||
|
|
||||||
use std::{fmt::Display, hash::Hash, str::FromStr};
|
|
||||||
|
|
||||||
use cfg_if::cfg_if;
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
use http::status::StatusCode;
|
|
||||||
use leptos::*;
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
use leptos_axum::ResponseOptions;
|
|
||||||
use leptos_router::*;
|
|
||||||
|
|
||||||
// A loading spinner
|
|
||||||
#[component]
|
|
||||||
pub fn Spinner(cx: Scope) -> impl IntoView {
|
|
||||||
view! { cx,
|
|
||||||
<div
|
|
||||||
class="animate-spin inline-block w-6 h-6 border-[3px] border-current border-t-transparent text-blue-600 rounded-full"
|
|
||||||
role="status"
|
|
||||||
aria-label="loading"
|
|
||||||
>
|
|
||||||
<span class="sr-only">"Loading..."</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A `<a>` link
|
|
||||||
#[component]
|
|
||||||
pub fn Link(cx: Scope, link: &'static str, text: &'static str) -> impl IntoView {
|
|
||||||
view! { cx,
|
|
||||||
<A href=link class="text-primary-100 hover:no-underline">
|
|
||||||
{text}
|
|
||||||
</A>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A `<a>` link that links to an external site
|
|
||||||
#[component]
|
|
||||||
pub fn LinkExternal(cx: Scope, link: &'static str, text: &'static str) -> impl IntoView {
|
|
||||||
view! { cx,
|
|
||||||
<a
|
|
||||||
href=link
|
|
||||||
class="underline text-primary-500 hover:no-underline"
|
|
||||||
rel="external"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 404 page
|
|
||||||
#[component]
|
|
||||||
pub fn NotFound(cx: Scope) -> impl IntoView {
|
|
||||||
cfg_if! { if #[cfg(feature="ssr")] {
|
|
||||||
if let Some(response) = use_context::<ResponseOptions>(cx) {
|
|
||||||
response.set_status(StatusCode::NOT_FOUND);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
view! { cx,
|
|
||||||
// The HTML for 404 not found
|
|
||||||
<div class="grid w-full min-h-screen bg-center bg-cover bg-base-100 place-items-center">
|
|
||||||
<div class="z-0 flex items-center justify-center col-start-1 row-start-1 text-center">
|
|
||||||
<div class="flex flex-col space-y-3">
|
|
||||||
<h1 class="text-5xl font-bold">"404"</h1>
|
|
||||||
<p class="py-6">
|
|
||||||
<h2 class="text-3xl font-bold text-gray-500">"Page not found"</h2>
|
|
||||||
<p class="my-1">"The page you are looking for does not exist."</p>
|
|
||||||
</p>
|
|
||||||
<Link link="/" text="Go to home page"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Display errors to the user
|
|
||||||
#[component]
|
|
||||||
pub fn Errors(cx: Scope, errors: Errors) -> impl IntoView {
|
|
||||||
tracing::error!("Errors: {:?}", errors);
|
|
||||||
view! { cx,
|
|
||||||
<div class="flex flex-col justify-center overflow-auto">
|
|
||||||
<header class="p-2 text-xl font-bold text-white bg-error-500">"💣 ERROR 💣"</header>
|
|
||||||
<div class="p-2 font-mono text-sm text-left whitespace-pre-wrap bg-black">
|
|
||||||
<ul>
|
|
||||||
{errors
|
|
||||||
.into_iter()
|
|
||||||
.map(|(k, e)| {
|
|
||||||
view! { cx,
|
|
||||||
<li class="mb-4">
|
|
||||||
<header class="px-2 mb-2 font-bold text-gray-100">
|
|
||||||
{format!("{:?}", k)}
|
|
||||||
</header>
|
|
||||||
<div class="px-2 text-gray-400 hover:text-gray-100">
|
|
||||||
{e.to_string()}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect_view(cx)}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Like [Suspense] but also handles errors using [ErrorBoundary]
|
|
||||||
#[component(transparent)]
|
|
||||||
pub fn SuspenseWithErrorHandling(cx: Scope, children: ChildrenFn) -> impl IntoView {
|
|
||||||
let children = store_value(cx, children);
|
|
||||||
view! { cx,
|
|
||||||
<Suspense fallback=move || view! { cx, <Spinner/> }>
|
|
||||||
<ErrorBoundary fallback=|cx, errors| {
|
|
||||||
view! { cx, <Errors errors=errors.get()/> }
|
|
||||||
}>{children.with_value(|c| c(cx))}</ErrorBoundary>
|
|
||||||
</Suspense>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An input element component with suggestions.
|
|
||||||
///
|
|
||||||
/// A label, input element, and datalist are rendered, as well as error div.
|
|
||||||
/// [FromStr::from_str] is used to parse the input value into `K`.
|
|
||||||
///
|
|
||||||
/// Arguments:
|
|
||||||
/// * `id`: The id of the input element
|
|
||||||
/// * `label`: The label string
|
|
||||||
/// * `suggestions`: The initial suggestions to show in the datalist
|
|
||||||
/// * `val`: The [RwSignal] mirror'ing the input element value
|
|
||||||
#[component]
|
|
||||||
pub fn TextInput<K>(
|
|
||||||
cx: Scope,
|
|
||||||
id: &'static str,
|
|
||||||
label: &'static str,
|
|
||||||
/// Initial suggestions to show in the datalist
|
|
||||||
suggestions: Vec<K>,
|
|
||||||
val: RwSignal<K>,
|
|
||||||
) -> impl IntoView
|
|
||||||
where
|
|
||||||
K: ToString + FromStr + Hash + Eq + Clone + Display + 'static,
|
|
||||||
<K as std::str::FromStr>::Err: Display,
|
|
||||||
{
|
|
||||||
let datalist_id = &format!("{}-datalist", id);
|
|
||||||
// Input query to the server fn
|
|
||||||
// Errors in input element (based on [FromStr::from_str])
|
|
||||||
let (input_err, set_input_err) = create_signal(cx, None::<String>);
|
|
||||||
view! { cx,
|
|
||||||
<label for=id>{label}</label>
|
|
||||||
<input
|
|
||||||
list=datalist_id
|
|
||||||
id=id.to_string()
|
|
||||||
type="text"
|
|
||||||
class="w-full p-1 font-mono"
|
|
||||||
on:change=move |ev| {
|
|
||||||
match FromStr::from_str(&event_target_value(&ev)) {
|
|
||||||
Ok(s) => {
|
|
||||||
val.set(s);
|
|
||||||
set_input_err(None)
|
|
||||||
}
|
|
||||||
Err(e) => set_input_err(Some(e.to_string())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
prop:value=move || val.get().to_string()
|
|
||||||
/>
|
|
||||||
<span class="text-red-500">{input_err}</span>
|
|
||||||
// TODO: use local storage, and cache user's inputs
|
|
||||||
<datalist id=datalist_id>
|
|
||||||
{suggestions
|
|
||||||
.iter()
|
|
||||||
.map(|s| view! { cx, <option value=s.to_string()></option> })
|
|
||||||
.collect_view(cx)}
|
|
||||||
|
|
||||||
</datalist>
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,7 +4,7 @@ const defaultTheme = require('tailwindcss/defaultTheme')
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
content: [
|
content: [
|
||||||
"./src/**/*.rs",
|
"./src/**/*.rs",
|
||||||
"./crates/leptos_extra/**/*.rs"
|
"./dist/**/*.html"
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
|
Loading…
Reference in New Issue
Block a user