From 06857c2c7da660ca143978e141a79163f1366f95 Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar <3998+srid@users.noreply.github.com> Date: Thu, 26 Oct 2023 14:58:33 -0400 Subject: [PATCH] Simplify UI (#93) --- crates/nix_rs/src/env.rs | 1 + crates/nix_rs/src/flake/url.rs | 6 +- src/app/flake.rs | 16 +-- src/app/health.rs | 12 +- src/app/info.rs | 12 +- src/app/mod.rs | 196 +++++++++++++++------------------ src/app/state.rs | 25 ++++- src/app/state/datum.rs | 2 +- src/app/widget.rs | 53 +++++---- src/main.rs | 4 +- 10 files changed, 164 insertions(+), 163 deletions(-) diff --git a/crates/nix_rs/src/env.rs b/crates/nix_rs/src/env.rs index 1e8835ba..8aa5b971 100644 --- a/crates/nix_rs/src/env.rs +++ b/crates/nix_rs/src/env.rs @@ -118,6 +118,7 @@ impl MacOSArch { } } +// The [Display] instance affects how [OS] is displayed to the app user impl Display for OS { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/crates/nix_rs/src/flake/url.rs b/crates/nix_rs/src/flake/url.rs index d01967d2..f773030e 100644 --- a/crates/nix_rs/src/flake/url.rs +++ b/crates/nix_rs/src/flake/url.rs @@ -24,8 +24,12 @@ impl FlakeUrl { vec![ FlakeUrl::default(), "github:srid/emanote".into(), + "github:srid/nixos-config".into(), "github:juspay/nix-browser".into(), - "github:nixos/nixpkgs".into(), + "github:juspay/nix-dev-home".into(), + // Commented out until we figure out rendering performance and/or + // search filtering/limit. + // "github:nixos/nixpkgs".into(), ] } diff --git a/src/app/flake.rs b/src/app/flake.rs index 0599f1ef..0731f367 100644 --- a/src/app/flake.rs +++ b/src/app/flake.rs @@ -12,11 +12,8 @@ use nix_rs::flake::{ }; use crate::{ - app::widget::{FolderDialogButton, RefreshButton}, - app::{ - state::{self, AppState}, - Route, - }, + app::widget::FolderDialogButton, + app::{state::AppState, widget::Loader, Route}, }; #[component] @@ -25,7 +22,7 @@ pub fn Flake(cx: Scope) -> Element { let flake = state.flake.read(); let busy = (*flake).is_loading_or_refreshing(); render! { - h1 { class: "text-5xl font-bold", "Flake dashboard" } + h1 { class: "text-5xl font-bold", "Flake browser" } div { class: "p-2 my-1 flex w-full", input { class: "flex-1 w-full p-1 mb-4 font-mono", @@ -49,11 +46,8 @@ pub fn Flake(cx: Scope) -> Element { } } } - RefreshButton { - busy: busy, - handler: move |_| { - state.act(state::Action::RefreshFlake); - } + if flake.is_loading_or_refreshing() { + render! { Loader {} } } flake.render_with(cx, |v| render! { FlakeView { flake: v.clone() } }) } diff --git a/src/app/health.rs b/src/app/health.rs index 21a423fe..17b6ed76 100644 --- a/src/app/health.rs +++ b/src/app/health.rs @@ -3,10 +3,7 @@ use dioxus::prelude::*; use nix_health::traits::{Check, CheckResult}; -use crate::{ - app::state::AppState, - app::{state::Action, widget::RefreshButton}, -}; +use crate::{app::state::AppState, app::widget::Loader}; /// Nix health checks pub fn Health(cx: Scope) -> Element { @@ -15,11 +12,8 @@ pub fn Health(cx: Scope) -> Element { let title = "Nix Health"; render! { h1 { class: "text-5xl font-bold", title } - RefreshButton { - busy: (*health_checks).is_loading_or_refreshing(), - handler: move |_event| { - state.act(Action::GetNixInfo); - } + if health_checks.is_loading_or_refreshing() { + render! { Loader {} } } health_checks.render_with(cx, |checks| render! { div { class: "flex flex-col items-stretch justify-start space-y-8 text-left", diff --git a/src/app/info.rs b/src/app/info.rs index c8d18708..d053a669 100644 --- a/src/app/info.rs +++ b/src/app/info.rs @@ -5,10 +5,7 @@ use std::fmt::Display; use dioxus::prelude::*; use nix_rs::{config::NixConfig, env::NixEnv, info::NixInfo, version::NixVersion}; -use crate::{ - app::state::AppState, - app::{state::Action, widget::RefreshButton}, -}; +use crate::{app::state::AppState, app::widget::Loader}; /// Nix information #[component] @@ -18,11 +15,8 @@ pub fn Info(cx: Scope) -> Element { let nix_info = state.nix_info.read(); render! { h1 { class: "text-5xl font-bold", title } - RefreshButton { - busy: (*nix_info).is_loading_or_refreshing(), - handler: move |_event| { - state.act(Action::GetNixInfo); - } + if nix_info.is_loading_or_refreshing() { + render! { Loader {} } } div { class: "flex items-center justify-center", nix_info.render_with(cx, |v| render! { NixInfoView { info: v.clone() } }) diff --git a/src/app/mod.rs b/src/app/mod.rs index b721015e..89c40515 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -11,13 +11,14 @@ mod widget; use dioxus::prelude::*; use dioxus_router::prelude::*; +use nix_rs::flake::url::FlakeUrl; use crate::app::{ flake::{Flake, FlakeRaw}, health::Health, info::Info, state::AppState, - widget::{Loader, Scrollable}, + widget::{Loader, RefreshButton}, }; #[derive(Routable, PartialEq, Debug, Clone)] @@ -26,8 +27,6 @@ enum Route { #[layout(Wrapper)] #[route("/")] Dashboard {}, - #[route("/about")] - About {}, #[route("/flake")] Flake {}, #[route("/flake/raw")] @@ -42,136 +41,123 @@ enum Route { pub fn App(cx: Scope) -> Element { AppState::provide_state(cx); render! { - body { - div { class: "flex flex-col text-center justify-between w-full overflow-hidden h-screen bg-base-200", - Router:: {} - } - } + body { class: "bg-base-100 overflow-hidden", Router:: {} } } } fn Wrapper(cx: Scope) -> Element { render! { - Nav {} - Scrollable { - div { class: "m-2 py-2", Outlet:: {} } + div { class: "flex flex-col text-center justify-between w-full h-screen", + TopBar {} + div { class: "m-2 py-2 overflow-auto", Outlet:: {} } + Footer {} } - Footer {} } } #[component] -fn Footer(cx: Scope) -> Element { +fn TopBar(cx: Scope) -> Element { + let state = AppState::use_state(cx); + let health_checks = state.health_checks.read(); + let nix_info = state.nix_info.read(); render! { - footer { class: "flex flex-row justify-center w-full p-2 bg-primary-100", - a { href: "https://github.com/juspay/nix-browser", img { src: "images/128x128.png", class: "h-6" } } + div { class: "flex justify-between items-center w-full p-2 bg-primary-100 shadow", + div { class: "flex space-x-2", + Link { to: Route::Dashboard {}, "🏠" } + } + div { class: "flex space-x-2", + ViewRefreshButton {} + Link { to: Route::Health {}, + span { title: "Nix Health Status", + match (*health_checks).current_value() { + Some(Ok(checks)) => render! { + if checks.iter().all(|check| check.result.green()) { + "✅" + } else { + "❌" + } + }, + Some(Err(err)) => render! { "{err}" }, + None => render! { Loader {} }, + } + } + } + Link { to: Route::Info {}, + span { + "Nix " + match (*nix_info).current_value() { + Some(Ok(info)) => render! { + "{info.nix_version} on {info.nix_env.os}" + }, + Some(Err(err)) => render! { "{err}" }, + None => render! { Loader {} }, + } + } + } + } } } } -// Home page -fn Dashboard(cx: Scope) -> Element { - tracing::debug!("Rendering Dashboard page"); +/// Intended to refresh the data behind the current route. +#[component] +fn ViewRefreshButton(cx: Scope) -> Element { let state = AppState::use_state(cx); - let health_checks = state.health_checks.read(); - // A Card component - #[component] - fn Card<'a>(cx: Scope, href: Route, children: Element<'a>) -> Element<'a> { - render! { - Link { - to: "{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 } - } - } - } + let (busy, action) = match use_route(cx).unwrap() { + Route::Flake {} => Some(( + state.flake.read().is_loading_or_refreshing(), + state::Action::RefreshFlake, + )), + Route::Health {} => Some(( + state.health_checks.read().is_loading_or_refreshing(), + state::Action::GetNixInfo, + )), + Route::Info {} => Some(( + state.nix_info.read().is_loading_or_refreshing(), + state::Action::GetNixInfo, + )), + _ => None, + }?; render! { - div { - id: "cards", - class: "flex flex-row justify-center items-center flex-wrap", - Card { href: Route::Health {}, - "Health " - match (*health_checks).current_value() { - Some(Ok(checks)) => render! { - if checks.iter().all(|check| check.result.green()) { - "✅" - } else { - "❌" - } - }, - Some(Err(err)) => render! { "{err}" }, - None => render! { Loader {} }, - } + RefreshButton { + busy: busy, + handler: move |_| { + state.act(action); } - Card { href: Route::Info {}, "Info ℹī¸" } - Card { href: Route::Flake {}, "Flake ❄ī¸ī¸" } } } } -/// Navigation bar -/// -/// TODO Switch to breadcrumbs, as it simplifes the design overall. -fn Nav(cx: Scope) -> Element { - // Common class for all tabs - let class = "flex-grow block py-1.5 mx-1 text-center rounded-t-md"; - - // Active tab styling: Highlighted background and pronounced text color - let active_class = "bg-primary-200 font-bold text-black"; - - // Inactive tab styling: Muted background and text color - let inactive_class = "bg-gray-200 text-gray-600"; - +#[component] +fn Footer(cx: Scope) -> Element { render! { - nav { class: "flex flex-row w-full bg-gray-100 border-b border-gray-300 pt-2", - - Link { - to: Route::Dashboard {}, - class: "{class} {inactive_class}", - active_class: active_class, - "Dashboard" - } - Link { - to: Route::Flake {}, - class: "{class} {inactive_class}", - active_class: active_class, - "Flake" - } - Link { - to: Route::Health {}, - class: "{class} {inactive_class}", - active_class: active_class, - "Nix Health" - } - Link { - to: Route::Info {}, - class: "{class} {inactive_class}", - active_class: active_class, - "Nix Info" - } - Link { - to: Route::About {}, - class: "{class} {inactive_class}", - active_class: active_class, - "About" - } - div { class: "flex-grow font-bold text-end px-3 py-1", "nix-browser" } + footer { class: "flex flex-row justify-center w-full bg-primary-100 p-2", + a { href: "https://github.com/juspay/nix-browser", img { src: "images/128x128.png", class: "h-4" } } } } } -/// About page -fn About(cx: Scope) -> Element { +// Home page +fn Dashboard(cx: Scope) -> Element { + tracing::debug!("Rendering Dashboard page"); + // TODO: Store and show user's recent flake visits + let suggestions = FlakeUrl::suggestions(); render! { - h1 { class: "text-5xl font-bold", "About" } - p { - "nix-browser is still work in progress. Track its development " - a { - href: "https://github.com/juspay/nix-browser", - class: "underline text-primary-500 hover:no-underline", - rel: "external", - target: "_blank", - "on Github" + div { class: "pl-4", + h2 { class: "text-2xl", "We have hand-picked some flakes for you to try out:" } + div { class: "flex flex-col", + for flake in suggestions { + a { + onclick: move |_| { + let state = AppState::use_state(cx); + let nav = use_navigator(cx); + state.flake_url.set(flake.clone()); + nav.replace(Route::Flake {}); + }, + class: "cursor-pointer text-primary-600 underline hover:no-underline", + "{flake.clone()}" + } + } } } } diff --git a/src/app/state.rs b/src/app/state.rs index 4d6e89b5..7ce14666 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -5,12 +5,13 @@ mod datum; use std::fmt::Display; use dioxus::prelude::{use_context, use_context_provider, use_future, Scope}; -use dioxus_signals::{use_signal, Signal}; +use dioxus_signals::{use_signal, CopyValue, Signal}; use nix_health::NixHealth; use nix_rs::{ command::NixCmdError, flake::{url::FlakeUrl, Flake}, }; +use tokio::task::AbortHandle; use self::datum::Datum; @@ -20,13 +21,14 @@ use self::datum::Datum; /// loading and subsequent refreshing. /// /// Use [Action] to mutate this state, in addition to [Signal::set]. -#[derive(Default, Clone, Copy, Debug)] +#[derive(Default, Clone, Copy, Debug, PartialEq)] pub struct AppState { pub nix_info: Signal>>, pub health_checks: Signal, SystemError>>>, pub flake_url: Signal, pub flake: Signal>>, + pub flake_task_abort: CopyValue>, pub action: Signal<(usize, Action)>, } @@ -104,13 +106,24 @@ impl AppState { let idx = *refresh_action.read(); use_future(cx, (&flake_url, &idx), |(flake_url, idx)| async move { tracing::info!("Updating flake [{}] {} ...", flake_url, idx); + // Abort previously running task, otherwise Datum refresh will panic + // TODO: Refactor this by changing the [Datum] type to be a + // struct (not enum) containing the + // `CopyValue>>` + self.flake_task_abort.with_mut(|abort_handle| { + if let Some(abort_handle) = abort_handle.take() { + abort_handle.abort(); + } + }); Datum::refresh_with(self.flake, async move { - tokio::spawn(async move { + let join_handle = tokio::spawn(async move { Flake::from_nix(&nix_rs::command::NixCmd::default(), flake_url.clone()) .await - }) - .await - .unwrap() + }); + *self.flake_task_abort.write() = Some(join_handle.abort_handle()); + let v = join_handle.await.unwrap(); + *self.flake_task_abort.write() = None; + v }) .await; }); diff --git a/src/app/state/datum.rs b/src/app/state/datum.rs index 9165a3bb..016f0392 100644 --- a/src/app/state/datum.rs +++ b/src/app/state/datum.rs @@ -4,7 +4,7 @@ use dioxus::prelude::*; use dioxus_signals::Signal; /// Represent loading/refreshing state of UI data -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Default, Clone, Copy, PartialEq)] pub enum Datum { #[default] Loading, diff --git a/src/app/widget.rs b/src/app/widget.rs index 3b5b5e5a..c4df4532 100644 --- a/src/app/widget.rs +++ b/src/app/widget.rs @@ -12,27 +12,16 @@ pub fn RefreshButton(cx: Scope, busy: bool, handler: F) -> Element where F: Fn(Event), { - 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! { "🔄" } + button { + disabled: *busy, + onclick: move |evt| { + if !*busy { + handler(evt) } - } - if *busy { - render! { Loader {} } - } + }, + title: "Refresh current data being viewed", + render! { LoaderIcon {loading: *busy} } } } } @@ -100,6 +89,32 @@ pub fn Loader(cx: Scope) -> Element { } } +#[component] +pub fn LoaderIcon(cx: Scope, loading: bool) -> Element { + let cls = if *loading { + "animate-spin text-base-800" + } else { + "text-primary-700 hover:text-primary-500" + }; + render! { + div { class: cls, + svg { + class: "h-6 w-6 scale-x-[-1]", + xmlns: "http://www.w3.org/2000/svg", + view_box: "0 0 24 24", + fill: "none", + stroke: "currentColor", + path { + d: "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15", + stroke_linecap: "round", + stroke_linejoin: "round", + stroke_width: "2" + } + } + } + } +} + /// A div that can get scrollbar for long content /// /// Since our body container is `overflow-hidden`, we need to wrap content that diff --git a/src/main.rs b/src/main.rs index 3a69a950..445dc43e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,8 +17,8 @@ async fn main() { .with_custom_head(r#" "#.to_string()) .with_window( WindowBuilder::new() - .with_title("nix-browser") - .with_inner_size(LogicalSize::new(900, 600)), + .with_title("Nix Browser") + .with_inner_size(LogicalSize::new(800, 700)), ), ) }