From 861a3ba88020ef41f82c9be24a51a35866398be6 Mon Sep 17 00:00:00 2001 From: Kbz-8 Date: Thu, 11 Dec 2025 22:25:46 +0100 Subject: [PATCH] yes --- Cargo.toml | 4 +- assets/dx-components-theme.css | 83 ------ src/components/mod.rs | 3 +- src/components/select/component.rs | 116 ++++++++ src/components/select/mod.rs | 2 + src/components/select/style.css | 150 ++++++++++ src/components/skeleton/component.rs | 9 - src/components/skeleton/mod.rs | 2 - src/components/skeleton/style.css | 16 -- src/components/toast/component.rs | 15 + src/components/toast/mod.rs | 2 + src/components/toast/style.css | 180 ++++++++++++ src/landing.rs | 401 ++++++++++++++++++--------- src/loader.rs | 64 ----- src/main.rs | 100 ++++++- src/navbar.rs | 15 +- src/routes.rs | 5 +- tailwind.css | 22 +- 18 files changed, 855 insertions(+), 334 deletions(-) delete mode 100644 assets/dx-components-theme.css create mode 100644 src/components/select/component.rs create mode 100644 src/components/select/mod.rs create mode 100644 src/components/select/style.css delete mode 100644 src/components/skeleton/component.rs delete mode 100644 src/components/skeleton/mod.rs delete mode 100644 src/components/skeleton/style.css create mode 100644 src/components/toast/component.rs create mode 100644 src/components/toast/mod.rs create mode 100644 src/components/toast/style.css delete mode 100644 src/loader.rs diff --git a/Cargo.toml b/Cargo.toml index 07f62eb..e53d526 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,12 +2,14 @@ name = "vulkan-cts-analyzer" version = "0.1.0" authors = ["Kbz-8 "] -edition = "2021" +edition = "2024" [dependencies] csv = "1.4.0" dioxus = { version = "0.7.2", features = ["router"] } dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false } +reqwest = "0.12.25" +strum = { version = "0.27.2", default-features = false, features = ["derive"] } [features] default = ["web"] diff --git a/assets/dx-components-theme.css b/assets/dx-components-theme.css deleted file mode 100644 index 6d51a26..0000000 --- a/assets/dx-components-theme.css +++ /dev/null @@ -1,83 +0,0 @@ -/* This file contains the global styles for the styled dioxus components. You only - * need to import this file once in your project root. - */ -@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"); - -body { - padding: 0; - margin: 0; - background-color: var(--primary-color); - color: var(--secondary-color-4); - font-family: Inter, sans-serif; - font-optical-sizing: auto; - font-style: normal; - font-weight: 400; -} - -@media (prefers-color-scheme: dark) { - :root { - --dark: initial; - --light: ; - } -} - -@media (prefers-color-scheme: light) { - :root { - --dark: ; - --light: initial; - } -} - -:root { - /* Primary colors */ - --primary-color: var(--dark, #000) var(--light, #fff); - --primary-color-1: var(--dark, #0e0e0e) var(--light, #fbfbfb); - --primary-color-2: var(--dark, #0a0a0a) var(--light, #fff); - --primary-color-3: var(--dark, #141313) var(--light, #f8f8f8); - --primary-color-4: var(--dark, #1a1a1a) var(--light, #f8f8f8); - --primary-color-5: var(--dark, #262626) var(--light, #f5f5f5); - --primary-color-6: var(--dark, #232323) var(--light, #e5e5e5); - --primary-color-7: var(--dark, #3e3e3e) var(--light, #b0b0b0); - - /* Secondary colors */ - --secondary-color: var(--dark, #fff) var(--light, #000); - --secondary-color-1: var(--dark, #fafafa) var(--light, #000); - --secondary-color-2: var(--dark, #e6e6e6) var(--light, #0d0d0d); - --secondary-color-3: var(--dark, #dcdcdc) var(--light, #2b2b2b); - --secondary-color-4: var(--dark, #d4d4d4) var(--light, #111); - --secondary-color-5: var(--dark, #a1a1a1) var(--light, #848484); - --secondary-color-6: var(--dark, #5d5d5d) var(--light, #d0d0d0); - - /* Highlight colors */ - --focused-border-color: var(--dark, #2b7fff) var(--light, #2b7fff); - --primary-success-color: var(--dark, #02271c) var(--light, #ecfdf5); - --secondary-success-color: var(--dark, #b6fae3) var(--light, #10b981); - --primary-warning-color: var(--dark, #342203) var(--light, #fffbeb); - --secondary-warning-color: var(--dark, #feeac7) var(--light, #f59e0b); - --primary-error-color: var(--dark, #a22e2e) var(--light, #dc2626); - --secondary-error-color: var(--dark, #9b1c1c) var(--light, #ef4444); - --contrast-error-color: var(--dark, var(--secondary-color-3)) - var(--light, var(--primary-color)); - --primary-info-color: var(--dark, var(--primary-color-5)) - var(--light, var(--primary-color)); - --secondary-info-color: var(--dark, var(--primary-color-7)) - var(--light, var(--secondary-color-3)); -} - -/* Modern browsers with `scrollbar-*` support */ -@supports (scrollbar-width: auto) { - :not(:hover) { - scrollbar-color: rgb(0 0 0 / 0%) rgb(0 0 0 / 0%); - } - - :hover { - scrollbar-color: var(--secondary-color-2) rgb(0 0 0 / 0%); - } -} - -/* Legacy browsers with `::-webkit-scrollbar-*` support */ -@supports selector(::-webkit-scrollbar) { - :root::-webkit-scrollbar-track { - background: transparent; - } -} diff --git a/src/components/mod.rs b/src/components/mod.rs index 2c73525..4a7361d 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,2 +1,3 @@ // AUTOGENERTED Components module -pub mod skeleton; +pub mod select; +pub mod toast; diff --git a/src/components/select/component.rs b/src/components/select/component.rs new file mode 100644 index 0000000..65d58e7 --- /dev/null +++ b/src/components/select/component.rs @@ -0,0 +1,116 @@ +use dioxus::prelude::*; +use dioxus_primitives::select::{ + self, SelectGroupLabelProps, SelectGroupProps, SelectListProps, SelectOptionProps, SelectProps, + SelectTriggerProps, SelectValueProps, +}; + +#[component] +pub fn Select(props: SelectProps) -> Element { + rsx! { + document::Link { rel: "stylesheet", href: asset!("./style.css") } + select::Select { + class: "select", + value: props.value, + default_value: props.default_value, + on_value_change: props.on_value_change, + disabled: props.disabled, + name: props.name, + placeholder: props.placeholder, + roving_loop: props.roving_loop, + typeahead_timeout: props.typeahead_timeout, + attributes: props.attributes, + {props.children} + } + } +} + +#[component] +pub fn SelectTrigger(props: SelectTriggerProps) -> Element { + rsx! { + select::SelectTrigger { attributes: props.attributes, + {props.children} + svg { + class: "select-expand-icon", + view_box: "0 0 24 24", + xmlns: "http://www.w3.org/2000/svg", + polyline { points: "6 9 12 15 18 9" } + } + } + } +} + +#[component] +pub fn SelectValue(props: SelectValueProps) -> Element { + rsx! { + select::SelectValue { attributes: props.attributes } + } +} + +#[component] +pub fn SelectList(props: SelectListProps) -> Element { + rsx! { + select::SelectList { + class: "select-list", + id: props.id, + attributes: props.attributes, + {props.children} + } + } +} + +#[component] +pub fn SelectGroup(props: SelectGroupProps) -> Element { + rsx! { + select::SelectGroup { + class: "select-group", + disabled: props.disabled, + id: props.id, + attributes: props.attributes, + {props.children} + } + } +} + +#[component] +pub fn SelectGroupLabel(props: SelectGroupLabelProps) -> Element { + rsx! { + select::SelectGroupLabel { + class: "select-group-label", + id: props.id, + attributes: props.attributes, + {props.children} + } + } +} + +#[component] +pub fn SelectOption(props: SelectOptionProps) -> Element { + rsx! { + select::SelectOption:: { + class: "select-option", + value: props.value, + text_value: props.text_value, + disabled: props.disabled, + id: props.id, + index: props.index, + aria_label: props.aria_label, + aria_roledescription: props.aria_roledescription, + attributes: props.attributes, + {props.children} + } + } +} + +#[component] +pub fn SelectItemIndicator() -> Element { + rsx! { + select::SelectItemIndicator { + svg { + class: "select-check-icon", + view_box: "0 0 24 24", + xmlns: "http://www.w3.org/2000/svg", + path { d: "M5 13l4 4L19 7" } + } + } + } +} diff --git a/src/components/select/mod.rs b/src/components/select/mod.rs new file mode 100644 index 0000000..2590c01 --- /dev/null +++ b/src/components/select/mod.rs @@ -0,0 +1,2 @@ +mod component; +pub use component::*; diff --git a/src/components/select/style.css b/src/components/select/style.css new file mode 100644 index 0000000..32d98e7 --- /dev/null +++ b/src/components/select/style.css @@ -0,0 +1,150 @@ +.select { + position: relative; +} + +.select-trigger { + position: relative; + display: flex; + box-sizing: border-box; + flex-direction: row; + align-items: center; + justify-content: space-between; + padding: 0.25rem; + padding: 8px 12px; + border: none; + border-radius: 0.5rem; + border-radius: calc(0.5rem); + background: none; + background: var(--primary-color-3); + box-shadow: inset 0 0 0 1px var(--primary-color-7); + color: var(--secondary-color-4); + cursor: pointer; + gap: 0.25rem; + transition: background-color 100ms ease-out; +} + +.select-trigger span[data-placeholder="true"] { + color: var(--secondary-color-5); +} + +.select[data-state="open"] .select-trigger { + pointer-events: none; +} + +.select-expand-icon { + width: 20px; + height: 20px; + fill: none; + stroke: var(--secondary-color-5); + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2; +} + +.select-check-icon { + width: 1rem; + height: 1rem; + fill: none; + stroke: var(--secondary-color-5); + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2; +} + +.select[data-disabled="true"] .select-trigger { + color: var(--secondary-color-5); + cursor: not-allowed; +} + +.select-trigger:hover:not([data-disabled="true"]), +.select-trigger:focus-visible { + background: var(--light, var(--primary-color-4)) + var(--dark, var(--primary-color-5)); + color: var(--secondary-color-1); + outline: none; +} + +.select-list { + position: absolute; + z-index: 1000; + top: 100%; + left: 0; + min-width: 100%; + box-sizing: border-box; + padding: 0.25rem; + border-radius: 0.5rem; + margin-top: 0.25rem; + background: var(--primary-color-5); + box-shadow: inset 0 0 0 1px var(--primary-color-7); + opacity: 0; + pointer-events: none; + transform-origin: top; + will-change: transform, opacity; +} + +.select-list[data-state="closed"] { + animation: select-list-animate-out 150ms ease-in forwards; + pointer-events: none; +} + +@keyframes select-list-animate-out { + 0% { + opacity: 1; + transform: scale(1) translateY(0); + } + + 100% { + opacity: 0; + transform: scale(0.95) translateY(-2px); + } +} + +.select-list[data-state="open"] { + animation: select-list-animate-in 150ms ease-out forwards; + pointer-events: auto; +} + +@keyframes select-list-animate-in { + 0% { + opacity: 0; + transform: scale(0.95) translateY(-2px); + } + + 100% { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +.select-option { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + border-radius: calc(0.5rem - 0.25rem); + cursor: pointer; + font-size: 14px; +} + +.select-option[data-disabled="true"] { + color: var(--secondary-color-5); + cursor: not-allowed; +} + +.select-option:hover:not([data-disabled="true"]), +.select-option:focus-visible { + background: var(--primary-color-7); + color: var(--secondary-color-1); + outline: none; +} + +.select-group-label { + padding: 4px 12px; + color: var(--secondary-color-5); + font-size: 0.75rem; +} + +[data-disabled="true"] { + cursor: not-allowed; + opacity: 0.5; +} diff --git a/src/components/skeleton/component.rs b/src/components/skeleton/component.rs deleted file mode 100644 index d811889..0000000 --- a/src/components/skeleton/component.rs +++ /dev/null @@ -1,9 +0,0 @@ -use dioxus::prelude::*; - -#[component] -pub fn Skeleton(#[props(extends=GlobalAttributes)] attributes: Vec) -> Element { - rsx! { - document::Link { rel: "stylesheet", href: asset!("./style.css") } - div { class: "skeleton", ..attributes } - } -} diff --git a/src/components/skeleton/mod.rs b/src/components/skeleton/mod.rs deleted file mode 100644 index 9a8ae55..0000000 --- a/src/components/skeleton/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod component; -pub use component::*; \ No newline at end of file diff --git a/src/components/skeleton/style.css b/src/components/skeleton/style.css deleted file mode 100644 index 95be22e..0000000 --- a/src/components/skeleton/style.css +++ /dev/null @@ -1,16 +0,0 @@ -.skeleton { - border-radius: 0.375rem; - animation: skeleton-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; - background-color: var(--primary-color-5); -} - -@keyframes skeleton-pulse { - 0%, - 100% { - opacity: 1; - } - - 61.8% { - opacity: 0.5; - } -} diff --git a/src/components/toast/component.rs b/src/components/toast/component.rs new file mode 100644 index 0000000..4b6878f --- /dev/null +++ b/src/components/toast/component.rs @@ -0,0 +1,15 @@ +use dioxus::prelude::*; +use dioxus_primitives::toast::{self, ToastProviderProps}; + +#[component] +pub fn ToastProvider(props: ToastProviderProps) -> Element { + rsx! { + document::Link { rel: "stylesheet", href: asset!("./style.css") } + toast::ToastProvider { + default_duration: props.default_duration, + max_toasts: props.max_toasts, + render_toast: props.render_toast, + {props.children} + } + } +} diff --git a/src/components/toast/mod.rs b/src/components/toast/mod.rs new file mode 100644 index 0000000..2590c01 --- /dev/null +++ b/src/components/toast/mod.rs @@ -0,0 +1,2 @@ +mod component; +pub use component::*; diff --git a/src/components/toast/style.css b/src/components/toast/style.css new file mode 100644 index 0000000..6731a46 --- /dev/null +++ b/src/components/toast/style.css @@ -0,0 +1,180 @@ +.toast-container { + position: fixed; + z-index: 9999; + right: 20px; + bottom: 20px; + max-width: 350px; +} + +.toast-list { + display: flex; + flex-direction: column-reverse; + padding: 0; + margin: 0; + gap: 0.75rem; +} + +.toast-item { + display: flex; +} + +.toast { + z-index: calc(var(--toast-count) - var(--toast-index)); + display: flex; + overflow: hidden; + width: 18rem; + height: 4rem; + box-sizing: border-box; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border: 1px solid var(--primary-color-7); + border-radius: 0.5rem; + margin-top: -4rem; + box-shadow: 0 4px 12px rgb(0 0 0 / 15%); + filter: brightness(calc(0.5 + 0.5 * (1 - ((var(--toast-index) + 1) / 4)))); + opacity: calc(1 - var(--toast-hidden)); + transform: scale( + calc(100% - var(--toast-index) * 5%), + calc(100% - var(--toast-index) * 2%) + ); + transition: transform 0.2s ease, margin-top 0.2s ease, opacity 0.2s ease; + + --toast-hidden: calc(min(max(0, var(--toast-index) - 2), 1)); +} + +.toast-container:not(:hover, :focus-within) + .toast[data-toast-even]:not([data-top]) { + animation: slide-up-even 0.2s ease-out; +} + +.toast-container:not(:hover, :focus-within) + .toast[data-toast-odd]:not([data-top]) { + animation: slide-up-odd 0.2s ease-out; +} + +@keyframes slide-up-even { + from { + transform: translateY(0.5rem) + scale( + calc(100% - var(--toast-index) * 5%), + calc(100% - var(--toast-index) * 2%) + ); + } + + to { + transform: translateY(0) + scale( + calc(100% - var(--toast-index) * 5%), + calc(100% - var(--toast-index) * 2%) + ); + } +} + +@keyframes slide-up-odd { + from { + transform: translateY(0.5rem) + scale( + calc(100% - var(--toast-index) * 5%), + calc(100% - var(--toast-index) * 2%) + ); + } + + to { + transform: translateY(0) + scale( + calc(100% - var(--toast-index) * 5%), + calc(100% - var(--toast-index) * 2%) + ); + } +} + +.toast[data-top] { + animation: slide-in 0.2s ease-out; +} + +.toast-container:hover .toast[data-top], +.toast-container:focus-within .toast[data-top] { + animation: slide-in 0 ease-out; +} + +@keyframes slide-in { + from { + opacity: 0; + transform: translateY(100%) + scale( + calc(110% - var(--toast-index) * 5%), + calc(110% - var(--toast-index) * 2%) + ); + } + + to { + opacity: 1; + transform: translateY(0) + scale( + calc(100% - var(--toast-index) * 5%), + calc(100% - var(--toast-index) * 2%) + ); + } +} + +.toast-container:hover .toast, +.toast-container:focus-within .toast { + margin-top: var(--toast-padding); + filter: brightness(1); + opacity: 1; + transform: scale(calc(100%)); +} + +.toast[data-type="success"] { + background-color: var(--primary-success-color); + color: var(--secondary-success-color); +} + +.toast[data-type="error"] { + background-color: var(--primary-error-color); + color: var(--contrast-error-color); +} + +.toast[data-type="warning"] { + background-color: var(--primary-warning-color); + color: var(--secondary-warning-color); +} + +.toast[data-type="info"] { + background-color: var(--primary-info-color); + color: var(--secondary-info-color); +} + +.toast-content { + flex: 1; + margin-right: 8px; + transition: filter 0.2s ease; +} + +.toast-title { + margin-bottom: 4px; + color: var(--secondary-color-4); + font-weight: 600; +} + +.toast-description { + color: var(--secondary-color-3); + font-size: 0.875rem; +} + +.toast-close { + align-self: flex-start; + padding: 0; + border: none; + margin: 0; + background: none; + color: var(--secondary-color-3); + cursor: pointer; + font-size: 18px; + line-height: 1; +} + +.toast-close:hover { + color: var(--secondary-color-1); +} diff --git a/src/landing.rs b/src/landing.rs index 4ebaa1e..e16f032 100644 --- a/src/landing.rs +++ b/src/landing.rs @@ -1,86 +1,211 @@ use dioxus::prelude::*; +use std::{collections::HashMap, thread::current}; +use std::str::FromStr; + use csv::StringRecord; use std::f32::consts::PI; -#[derive(Default, Clone, Copy, PartialEq, Eq)] -struct GlobalStats { - count: usize, - passed: usize, - failed: usize, - skip: usize, - flake: usize, - crash: usize, +use strum::{EnumCount, IntoEnumIterator}; + +use crate::components::select::*; + +const PAGE_SIZE: usize = 100_usize; + +#[derive( + Debug, + Eq, + Hash, + Clone, + Copy, + PartialEq, + strum::EnumCount, + strum::EnumIter, + strum::Display, + strum::EnumString, +)] +enum TestStatus { + Pass, + Fail, + Skip, + Flake, + Crash, } -fn percentage(count: usize, total: usize) -> f32 { - (count as f32 * 100.0) / total as f32 +impl TestStatus { + const fn emoji(&self) -> &'static str { + match self { + TestStatus::Pass => "✅", + TestStatus::Fail => "❌", + TestStatus::Skip => "❎", + TestStatus::Flake => "⚠️", + TestStatus::Crash => "💥", + } + } + + const fn color(&self) -> &'static str { + match self { + TestStatus::Pass => "#22c55e", + TestStatus::Fail => "#ff6467", + TestStatus::Skip => "#ffdf20", + TestStatus::Flake => "#38bdf8", + TestStatus::Crash => "#e7000b", + } + } +} + +fn percentage(count: usize, total: f32) -> f32 { + (count as f32 * 100.0) / total } #[component] pub fn Landing() -> Element { - let result = use_context::>(); + let result = use_context::>>(); - let global_stats = result.iter().fold(GlobalStats::default(), |acc, record| { - let mut new_acc = acc; - match &record[1] { - "Pass" => new_acc.passed += 1, - "Skip" => new_acc.skip += 1, - "Fail" => new_acc.failed += 1, - "Flake" => new_acc.flake += 1, - "Crash" => new_acc.crash += 1, - _ => {}, + let global_stats = use_memo(move || result + .read() + .iter() + .fold(HashMap::from_iter(TestStatus::iter().map(|s| (s, 0))), |mut acc, record| { + if let Ok(status) = TestStatus::from_str(&record[1]) { + *acc.entry(status).or_insert(0) += 1; + } + acc + }) + ); + + let total = use_memo(move || global_stats.read().iter().fold(0.0_f32, |acc, (_, v)| acc + *v as f32)); + + let stats_cards = TestStatus::iter().map(|s| { + rsx! { + StatCard { + name: s.to_string(), + color: s.color().to_string(), + count: global_stats.read()[&s], + stat: percentage(global_stats.read()[&s], total()), + } + } + }); + + let mut filter: Signal> = use_signal(|| None); + let mut filtered_count = use_signal(|| 0_usize); + + let mut current_page = use_memo(move || 0_usize); + let mut page_count = use_memo(move || filtered_count().max(PAGE_SIZE - 1) / PAGE_SIZE); + + let page = use_memo(move || { + if let Some(f) = filter() { + current_page.set(0); + let mut count = 0_usize; + filtered_count.set(0_usize); + let shift = (current_page() * PAGE_SIZE); + result + .read() + .iter() + .filter(|r| { + if let Ok(status) = TestStatus::from_str(&r[1]) && status == f { + filtered_count += 1; + if *filtered_count.peek() < shift { + return false; + } + count += 1; + if count >= PAGE_SIZE { + false + } else { + true + } + } else { + false + } + }) + .cloned() + .collect() + } else { + filtered_count.set(total() as usize); + let start = current_page() * PAGE_SIZE; + let end = result.read().len().min((current_page() * PAGE_SIZE) + PAGE_SIZE); + result.read()[start..end].to_vec() + } + }); + + let statuses = TestStatus::iter().enumerate().map(|(i, s)| { + rsx! { + SelectOption::> { + index: i, + value: s, + text_value: "{s}", + {format!("{} {s}", s.emoji())} + SelectItemIndicator {} + } } - new_acc.count = new_acc.passed + new_acc.failed + new_acc.skip + new_acc.flake + new_acc.crash; - new_acc }); rsx! { div { - class: "flex flex-col space-y-4 rounded-3xl p-4 w-full h-fit shadow-xl shadow-slate-950", + class: "flex flex-col space-y-4 rounded-3xl p-4 pt-8 w-full h-fit shadow-xl shadow-slate-950", style: "background: linear-gradient(145deg, #020617 0, #02081f 60%, #020617 100%);", - div { class: "border-1 border-slate-400 bg-slate-400/15 text-slate-400 w-fit rounded-3xl p-1 flex flex-row space-x-1 items-center", - div { class: "bg-[#22c55e] rounded-full size-3" } - p { class: "text-xs", - "Count: {global_stats.count} tests" + div { class: "flex flex-row space-x-4", + div { class: "border-1 border-[#38bdf8] bg-[#38bdf8]/15 text-slate-400 w-fit rounded-3xl py-1 px-2 flex flex-row space-x-1 items-center", + div { class: "bg-[#38bdf8] rounded-full size-3" } + p { class: "text-xs", + "Total: {total} tests" + } + } + div { class: "border-1 border-[#22c55e] bg-[#22c55e]/15 text-slate-400 w-fit rounded-3xl py-1 px-2 flex flex-row space-x-1 items-center", + div { class: "bg-[#22c55e] rounded-full size-3" } + p { class: "text-xs", + "Filtered: {filtered_count} tests" + } } } div { class: "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4", - StatCard { - name: "PASSED".to_string(), - color: "#22c55e".to_string(), - count: global_stats.passed, - stat: percentage(global_stats.passed, global_stats.count), - } - StatCard { - name: "FAILED".to_string(), - color: "#ff6467".to_string(), - count: global_stats.failed, - stat: percentage(global_stats.failed, global_stats.count), - } - StatCard { - name: "SKIPPED".to_string(), - color: "#ffdf20".to_string(), - count: global_stats.skip, - stat: percentage(global_stats.skip, global_stats.count), - } - StatCard { - name: "FLAKE".to_string(), - color: "#38bdf8".to_string(), - count: global_stats.flake, - stat: percentage(global_stats.flake, global_stats.count), - } - StatCard { - name: "CRASH".to_string(), - color: "#e7000b".to_string(), - count: global_stats.crash, - stat: percentage(global_stats.crash, global_stats.count), - } + {stats_cards} } div { class: "mx-auto size-[200px]", - StatsPieChart { stats: global_stats } + StatsPieChart { stats: global_stats, total } } - div { class: "mt-12 w-full bg-gray-900 overflow-hidden border-1 border-slate-700 rounded-lg", + div { class: "mt-12 w-full flex flex-row justify-between text-gray-400 text-sm", + p { "Page {current_page() + 1} of {page_count() + 1}" } + div { class: "flex flex-row space-x-2", + button { class: "pagination-button", + disabled: current_page() == 0, + onclick: move |_| current_page.set(0_usize), + "First" + } + button { class: "pagination-button", + disabled: current_page() == 0, + onclick: move |_| current_page -= 1, + "Prev" + } + + if current_page() > 2 { + p { "..." } + } + + for i in ((current_page() as i32 - 2).max(0) as usize)..=(current_page() + 2).min(page_count()) { + button { class: "pagination-button", + "data-active": i == current_page(), + onclick: move |_| current_page.set(i), + "{i + 1}" + } + } + + if current_page() < (page_count() as i32 - 2).max(0) as usize { + p { "..." } + } + + button { class: "pagination-button", + disabled: current_page() >= page_count(), + onclick: move |_| current_page += 1, + "Next" + } + button { class: "pagination-button", + disabled: current_page() >= page_count(), + onclick: move |_| current_page.set(page_count()), + "Last" + } + } + } + div { class: "w-full bg-gray-900 overflow-hidden border-1 border-slate-700 rounded-lg text-gray-400", table { class: "w-full border-collapse border-spacing-0", tr { class: "border-b-1 border-slate-700", @@ -88,17 +213,43 @@ pub fn Landing() -> Element { th { class: "text-left uppercase bold whitespace-nowrap py-2 px-3", "Test name", } - th { class: "text-left uppercase bold whitespace-nowrap py-2 px-3", - "Status", + th { class: "uppercase bold whitespace-nowrap py-2 px-3", + Select::> { + placeholder: "STATUS", + on_value_change: move |value: Option>| filter.set(value.unwrap_or(None)), + SelectTrigger { + class: "select-trigger mx-auto w-fit !bg-transparent !shadow-none !text-gray-400 cursor-pointer uppercase", + aria_label: "Select Trigger", + SelectValue { class: "!bg-transparent !shadow-none !text-gray-400" } + } + SelectList { + aria_label: "Select status", + SelectGroup { + {statuses} + SelectOption::> { + index: TestStatus::COUNT, + value: None, + text_value: "Status", + "🔄 None" + SelectItemIndicator {} + } + } + } + } } } - for test in result[0..100].iter() { + for test in page.iter() { tr { class: "text-sm hover:bg-[#38bef7]/5", td { class: "py-2 px-3", "{&test[0]}" } td { class: "py-2 px-3", - "{&test[1]}" + StatusBadge { + status: match TestStatus::from_str(&test[1]) { + Ok(s) => Some(s), + _ => None, + }, + } } } } @@ -109,12 +260,7 @@ pub fn Landing() -> Element { } #[component] -fn StatCard( - name: String, - color: String, - count: usize, - stat: f32, -) -> Element { +fn StatCard(name: String, color: String, count: usize, stat: f32) -> Element { rsx! { div { class: "rounded-2xl p-4 border-1 border-slate-800 shadow-xl shadow-[#02081f] w-full h-fit bg-[#090f21] flex flex-col space-y-2", div { class: "flex flex-row space-x-2 flex items-center", @@ -138,53 +284,56 @@ fn StatCard( } } -struct Segment { - percentage: f32, - start: f32, - end: f32, - color: String, +#[component] +fn StatusBadge(status: Option) -> Element { + let color = match status { + Some(s) => s.color(), + None => "#FFF", + }; + let name = match status { + Some(s) => s.to_string(), + None => "Unrecognized".to_string(), + }; + + rsx! { + div { + class: "mx-auto border-1 py-1 px-3 rounded-3xl text-xs w-fit select-none", + style: format!(r#" + background-color: {color}0F; + color: {color}; + border-color: {color}; + "#), + "{name}" + } + } } #[component] -fn StatsPieChart(stats: GlobalStats) -> Element { - let total = stats.count as f32; - - if total == 0.0 { - return rsx!{}; +fn StatsPieChart(stats: ReadSignal>, total: ReadSignal) -> Element { + struct Segment { + percentage: f32, + start: f32, + end: f32, + color: String, } - let passed = stats.passed as f32 / total; - let failed = stats.failed as f32 / total; - let skip = stats.skip as f32 / total; - let flake = stats.flake as f32 / total; - let crash = stats.crash as f32 / total; - - let colors = ( - "#22c55e", // passed - "#ff6467", // failed - "#ffdf20", // skipped - "#38bdf8", // flake - "#e7000b", // crash - ); + if total() == 0.0 { + return rsx! {}; + } let mut segments: Vec = Vec::new(); let mut cumulative = 0.0_f32; - for (pct, color) in [ - (passed, colors.0), - (failed, colors.1), - (skip, colors.2), - (flake, colors.3), - (crash, colors.4), - ] { - if pct > 0.0 { + for (key, val) in stats.read().iter() { + let stat = *val as f32 / total(); + if stat > 0.0 { segments.push(Segment { - percentage: pct * 100.0, + percentage: stat * 100.0, start: cumulative, - end: cumulative + pct, - color: color.to_string(), + end: cumulative + stat, + color: key.color().to_string(), }); - cumulative += pct; + cumulative += stat; } } @@ -192,35 +341,31 @@ fn StatsPieChart(stats: GlobalStats) -> Element { let cx: f32 = 100.0; let cy: f32 = 100.0; - let paths = segments - .iter() - .enumerate() - .map(|(idx, seg)| { - let start_angle = seg.start * 2.0 * PI; - let end_angle = seg.end * 2.0 * PI; + let paths = segments.iter().enumerate().map(|(idx, seg)| { + let start_angle = seg.start * 2.0 * PI; + let end_angle = seg.end * 2.0 * PI; - let x1 = cx + radius * start_angle.cos(); - let y1 = cy + radius * start_angle.sin(); - let x2 = cx + radius * end_angle.cos(); - let y2 = cy + radius * end_angle.sin(); + let x1 = cx + radius * start_angle.cos(); + let y1 = cy + radius * start_angle.sin(); + let x2 = cx + radius * end_angle.cos(); + let y2 = cy + radius * end_angle.sin(); - let large_arc_flag = if (end_angle - start_angle) > PI { 1 } else { 0 }; + let large_arc_flag = if (end_angle - start_angle) > PI { 1 } else { 0 }; - let d = format!( - "M {cx} {cy} L {x1} {y1} A {radius} {radius} 0 {large_arc_flag} 1 {x2} {y2} Z" - ); + let d = + format!("M {cx} {cy} L {x1} {y1} A {radius} {radius} 0 {large_arc_flag} 1 {x2} {y2} Z"); - rsx! { - path { - key: "{idx}", - d: "{d}", - fill: "{seg.color}", - opacity: "0.9", - stroke: "rgba(255, 255, 255, 0.1)", - "stroke-width": "1", - } + rsx! { + path { + key: "{idx}", + d: "{d}", + fill: "{seg.color}", + opacity: "0.9", + stroke: "rgba(255, 255, 255, 0.1)", + "stroke-width": "1", } - }); + } + }); rsx! { svg { diff --git a/src/loader.rs b/src/loader.rs deleted file mode 100644 index 83d34bd..0000000 --- a/src/loader.rs +++ /dev/null @@ -1,64 +0,0 @@ -use dioxus::prelude::*; - -/// Default placeholder for loading state -#[component] -pub fn LoadingPlaceholder(message: String) -> Element { - rsx! { - div { class: "rounded-radius h-[50vh] w-1/2 bg-foreground/20 animate-pulse text-foreground flex justify-center items-center place-self-center mt-14 mx-auto", - h3 { class: "h3", {message} } - } - } -} - -#[derive(Default, Clone)] -struct SuspenseContextPlaceholder { - element: Option, -} - -/// Trait to create a suspense with a default loading placeholder -pub trait Loader { - fn load( - &self, - message: impl ToString, - ) -> Result> /* wtf Dioxus ??? */>, RenderError>; - fn load_with( - &self, - element: Element, - ) -> Result>>, RenderError>; -} - -impl Loader for Resource { - fn load( - &self, - message: impl ToString, - ) -> Result>>, RenderError> { - let mut context = use_context::>(); - context.write().element = Some(rsx! { - LoadingPlaceholder { message: message.to_string() } - }); - self.suspend() - } - - fn load_with( - &self, - element: Element, - ) -> Result>>, RenderError> { - let mut context = use_context::>(); - context.write().element = Some(element); - self.suspend() - } -} - -#[component] -pub fn Suspense(children: Element) -> Element { - let context = use_context_provider(|| Signal::new(SuspenseContextPlaceholder::default())); - - rsx! { - SuspenseBoundary { - fallback: move |_| { - context.read().clone().element.unwrap_or_else(|| rsx! { "Loading..." }) - }, - {children} - } - } -} diff --git a/src/main.rs b/src/main.rs index 2db63bf..d97ffbc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,33 +2,110 @@ use dioxus::prelude::*; use csv::{ReaderBuilder, StringRecord}; +mod components; mod landing; mod navbar; mod routes; -mod loader; use crate::routes::Route; +use dioxus_primitives::toast::{ToastOptions, ToastProvider, use_toast}; +use std::time::Duration; + const FAVICON: Asset = asset!("/assets/favicon.ico"); const TAILWIND_CSS: Asset = asset!("/assets/tailwind.css"); +const RESULT: Asset = asset!( + "/assets/results.csv", + AssetOptions::builder().with_hash_suffix(false) +); fn main() { - dioxus::launch(App); + dioxus::launch(AppWrapper); +} + +#[component] +fn AppWrapper() -> Element { + rsx! { + ToastProvider { App{} } + } } #[component] fn App() -> Element { - // TODO: get results from request - let result_data = include_str!("../assets/results.csv"); - let mut reader = ReaderBuilder::new().from_reader(result_data.as_bytes()); + let toast = use_toast(); - let records = reader.into_records().collect::, csv::Error>>()?; + let resource = use_resource(move || get_results()); + let mut result = use_context_provider(|| Signal::new(Vec::::new())); - let result = use_context_provider(|| records); + use_effect(move || match &*resource.read() { + Some(Ok(results)) => { + let mut reader = ReaderBuilder::new().from_reader(results.as_bytes()); + let records = match reader + .into_records() + .collect::, csv::Error>>() + { + Ok(res) => res, + Err(e) => { + error!("Failed to parse results: {e}"); + toast.error( + "Error".to_string(), + ToastOptions::new() + .description(format!("Failed to parse CTS results data: {e}")) + .duration(Duration::from_secs(2)) + .permanent(false), + ); + Vec::new() + } + }; + result.set(records); + } + Some(Err(e)) => { + error!("Failed to fetch results: {e}"); + toast.error( + "Error".to_string(), + ToastOptions::new() + .description(format!("Failed to fetch CTS results data: {e}")) + .duration(Duration::from_secs(2)) + .permanent(false), + ); + } + None => {} + }); rsx! { document::Link { rel: "icon", href: FAVICON } document::Link { rel: "stylesheet", href: TAILWIND_CSS } + document::Style { " + :root {{ + --primary-color: #000; + --primary-color-1: #020618; + --primary-color-2: #0a0a0a; + --primary-color-3: #020618; + --primary-color-4: #1a1a1a; + --primary-color-5: #02081e; + --primary-color-6: #232323; + --primary-color-7: #1e2939; + + --secondary-color: #fff; + --secondary-color-1: #fafafa; + --secondary-color-2: #e6e6e6; + --secondary-color-3: #dcdcdc; + --secondary-color-4: #d4d4d4; + --secondary-color-5: #ddd; + --secondary-color-6: #5d5d5d; + + --focused-border-color: #2b7fff; + --primary-success-color: #02271c; + --secondary-success-color: #b6fae3; + --primary-warning-color: #342203; + --secondary-warning-color: #feeac7; + --primary-error-color: #a22e2e; + --secondary-error-color: #9b1c1c; + --contrast-error-color: var(--secondary-color-3)); + --primary-info-color: var(--primary-color-5)); + --secondary-info-color: var(--primary-color-7)); + }} + "} div { class: "text-white min-h-screen", style: "background: radial-gradient(circle at top, #1e293b 0, #020617 45%, #000 100%);", @@ -36,3 +113,12 @@ fn App() -> Element { } } } + +async fn get_results() -> Result { + Ok( + reqwest::get(format!("{}/assets/results.csv", std::env!("URL"))) + .await? + .text() + .await?, + ) +} diff --git a/src/navbar.rs b/src/navbar.rs index 69c560b..f873262 100644 --- a/src/navbar.rs +++ b/src/navbar.rs @@ -5,7 +5,7 @@ use crate::routes::Route; #[component] pub fn Navbar() -> Element { rsx! { - div { class: "w-screen mb-12 py-2 px-6 flex flex-row justify-between", + div { class: "mx-auto container mb-12 py-2 px-6 sm:px-0 flex flex-row justify-between", Link { class: "flex flex-row h-16 text-4xl md:text-5xl select-none cursor-pointer", to: Route::Landing {}, VulkanVSvg {} @@ -21,8 +21,17 @@ pub fn Navbar() -> Element { main { class: "mx-auto container mb-24", Outlet:: {} } - footer { class: "w-screen flex flex-row justify-between px-6 text-sm text-gray-400", - "Made by kbz_8 with Dioxus" + footer { class: "w-screen h-11 flex flex-row justify-start space-x-1 px-6 text-sm text-gray-400", + p { "Made by"} + a { class: "hover:underline text-white", + href: "https://portfolio.kbz8.me/", + "kbz_8" + } + p { "with" } + a { class: "hover:underline text-white", + href: "https://dioxuslabs.com/", + "Dioxus" + } } } } diff --git a/src/routes.rs b/src/routes.rs index 02943b2..a89dd69 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -1,9 +1,6 @@ use dioxus::prelude::*; -use crate::{ - landing::Landing, - navbar::Navbar, -}; +use crate::{landing::Landing, navbar::Navbar}; #[derive(Debug, Clone, Routable, PartialEq)] #[rustfmt::skip] diff --git a/tailwind.css b/tailwind.css index 604ab07..a044469 100644 --- a/tailwind.css +++ b/tailwind.css @@ -1,20 +1,10 @@ @import "tailwindcss"; -@layer "base" { - .skeleton { - border-radius: 0.375rem; - animation: skeleton-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; - background-color: #262626; - } - - @keyframes skeleton-pulse { - 0%, - 100% { - opacity: 1; - } - - 61.8% { - opacity: 0.5; - } +@layer base { + .pagination-button { + @apply py-1 px-3 border-1 border-gray-500 bg-gray-500/15 text-gray-500 rounded-3xl cursor-pointer; + @apply hover:border-[#38bdf8] hover:bg-[#38bdf8]/15 hover:text-[#38bdf8]; + @apply data-[active=true]:border-[#38bdf8] data-[active=true]:bg-[#38bdf8]/15 data-[active=true]:text-[#38bdf8] data-[active=true]:cursor-default; + @apply disabled:border-dashed disabled:border-gray-700 disabled:bg-gray-800/15 disabled:text-gray-500 disabled:cursor-default; } }