diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3bab89e --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Generated by Cargo +# will have compiled files and executables +/target +.DS_Store + +# These are backup files generated by rustfmt +**/*.rs.bk + +*.lock + +assets/tailwind.css +assets/*.csv diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..07f62eb --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "vulkan-cts-analyzer" +version = "0.1.0" +authors = ["Kbz-8 "] +edition = "2021" + +[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 } + +[features] +default = ["web"] +web = ["dioxus/web"] +desktop = ["dioxus/desktop"] +mobile = ["dioxus/mobile"] diff --git a/Dioxus.toml b/Dioxus.toml new file mode 100644 index 0000000..0c8081b --- /dev/null +++ b/Dioxus.toml @@ -0,0 +1,11 @@ +[application] + +[web.app] +title = "Vulkan CTS Report" + +[web.resource] +style = [] +script = [] + +[web.resource.dev] +script = [] diff --git a/README.md b/README.md new file mode 100644 index 0000000..12681a5 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# Development + +Your new bare-bones project includes minimal organization with a single `main.rs` file and a few assets. + +``` +project/ +├─ assets/ # Any assets that are used by the app should be placed here +├─ src/ +│ ├─ main.rs # main.rs is the entry point to your application and currently contains all components for the app +├─ Cargo.toml # The Cargo.toml file defines the dependencies and feature flags for your project +``` + +### Automatic Tailwind (Dioxus 0.7+) + +As of Dioxus 0.7, there no longer is a need to manually install tailwind. Simply `dx serve` and you're good to go! + +Automatic tailwind is supported by checking for a file called `tailwind.css` in your app's manifest directory (next to Cargo.toml). To customize the file, use the dioxus.toml: + +```toml +[application] +tailwind_input = "my.css" +tailwind_output = "assets/out.css" # also customize the location of the out file! +``` + +### Tailwind Manual Install + +To use tailwind plugins or manually customize tailwind, you can can install the Tailwind CLI and use it directly. + +### Tailwind +1. Install npm: https://docs.npmjs.com/downloading-and-installing-node-js-and-npm +2. Install the Tailwind CSS CLI: https://tailwindcss.com/docs/installation/tailwind-cli +3. Run the following command in the root of the project to start the Tailwind CSS compiler: + +```bash +npx @tailwindcss/cli -i ./input.css -o ./assets/tailwind.css --watch +``` + +### Serving Your App + +Run the following command in the root of your project to start developing with the default platform: + +```bash +dx serve +``` + +To run for a different platform, use the `--platform platform` flag. E.g. +```bash +dx serve --platform desktop +``` + diff --git a/assets/dx-components-theme.css b/assets/dx-components-theme.css new file mode 100644 index 0000000..6d51a26 --- /dev/null +++ b/assets/dx-components-theme.css @@ -0,0 +1,83 @@ +/* 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/assets/favicon.ico b/assets/favicon.ico new file mode 100644 index 0000000..cc95deb Binary files /dev/null and b/assets/favicon.ico differ diff --git a/src/components/mod.rs b/src/components/mod.rs new file mode 100644 index 0000000..2c73525 --- /dev/null +++ b/src/components/mod.rs @@ -0,0 +1,2 @@ +// AUTOGENERTED Components module +pub mod skeleton; diff --git a/src/components/skeleton/component.rs b/src/components/skeleton/component.rs new file mode 100644 index 0000000..d811889 --- /dev/null +++ b/src/components/skeleton/component.rs @@ -0,0 +1,9 @@ +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 new file mode 100644 index 0000000..9a8ae55 --- /dev/null +++ b/src/components/skeleton/mod.rs @@ -0,0 +1,2 @@ +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 new file mode 100644 index 0000000..95be22e --- /dev/null +++ b/src/components/skeleton/style.css @@ -0,0 +1,16 @@ +.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/landing.rs b/src/landing.rs new file mode 100644 index 0000000..4ebaa1e --- /dev/null +++ b/src/landing.rs @@ -0,0 +1,232 @@ +use dioxus::prelude::*; + +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, +} + +fn percentage(count: usize, total: usize) -> f32 { + (count as f32 * 100.0) / total as f32 +} + +#[component] +pub fn Landing() -> Element { + 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, + _ => {}, + } + 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", + 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: "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), + } + } + div { class: "mx-auto size-[200px]", + StatsPieChart { stats: global_stats } + } + div { class: "mt-12 w-full bg-gray-900 overflow-hidden border-1 border-slate-700 rounded-lg", + table { class: "w-full border-collapse border-spacing-0", + tr { + class: "border-b-1 border-slate-700", + style: "background: radial-gradient(circle at top, rgba(56, 189, 248, 0.1), rgba(15, 23, 42, 1));", + 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", + } + } + for test in result[0..100].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]}" + } + } + } + } + } + } + } +} + +#[component] +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", + div { + class: "rounded-full size-4", + style: format!("background-color: {color};"), + } + h3 { class: "text-sm text-gray-300", + "{name}" + } + } + h2 { + class: "text-2xl font-bold", + style: format!("color: {color};"), + "{count}", + } + p { class: "text-xs text-gray-400", + "{stat:.1}% of total" + } + } + } +} + +struct Segment { + percentage: f32, + start: f32, + end: f32, + color: String, +} + +#[component] +fn StatsPieChart(stats: GlobalStats) -> Element { + let total = stats.count as f32; + + if total == 0.0 { + return rsx!{}; + } + + 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 + ); + + 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 { + segments.push(Segment { + percentage: pct * 100.0, + start: cumulative, + end: cumulative + pct, + color: color.to_string(), + }); + cumulative += pct; + } + } + + let radius: f32 = 80.0; + 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 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 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! { + svg { + class: "max-w-[200px] h-auto", + view_box: "0 0 200 200", + {paths} + } + } +} diff --git a/src/loader.rs b/src/loader.rs new file mode 100644 index 0000000..83d34bd --- /dev/null +++ b/src/loader.rs @@ -0,0 +1,64 @@ +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 new file mode 100644 index 0000000..2db63bf --- /dev/null +++ b/src/main.rs @@ -0,0 +1,38 @@ +use dioxus::prelude::*; + +use csv::{ReaderBuilder, StringRecord}; + +mod landing; +mod navbar; +mod routes; +mod loader; + +use crate::routes::Route; + +const FAVICON: Asset = asset!("/assets/favicon.ico"); +const TAILWIND_CSS: Asset = asset!("/assets/tailwind.css"); + +fn main() { + dioxus::launch(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 records = reader.into_records().collect::, csv::Error>>()?; + + let result = use_context_provider(|| records); + + rsx! { + document::Link { rel: "icon", href: FAVICON } + document::Link { rel: "stylesheet", href: TAILWIND_CSS } + div { + class: "text-white min-h-screen", + style: "background: radial-gradient(circle at top, #1e293b 0, #020617 45%, #000 100%);", + Router:: {} + } + } +} diff --git a/src/navbar.rs b/src/navbar.rs new file mode 100644 index 0000000..69c560b --- /dev/null +++ b/src/navbar.rs @@ -0,0 +1,54 @@ +use dioxus::prelude::*; + +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", + Link { class: "flex flex-row h-16 text-4xl md:text-5xl select-none cursor-pointer", + to: Route::Landing {}, + VulkanVSvg {} + p { class: "hidden md:block mt-auto font-bold -ml-3.5 text-[#9d1b1f]", + "ulkan" + } + p { class: "mt-auto ml-2 font-bold text-gray-300", + "CTS Report" + } + } + } + + 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" + } + } +} + +#[component] +fn VulkanVSvg() -> Element { + rsx! { + svg { + view_box: "0 0 192 192", + g { + transform: "translate(0.0, 192.0) scale(0.1, -0.1)", + fill: "#9d1b1f", + stroke: "none", + path { + d: "M320 1703 c1 -10 18 -70 38 -133 35 -106 40 -115 63 -114 41 2 311 33 315 36 1 2 -8 35 -21 73 -14 39 -29 89 -35 113 l-11 42 -174 0 c-160 0 -175 -1 -175 -17z", + } + path { + d: "M1336 1678 c-38 -104 -77 -240 -73 -251 3 -7 31 -21 63 -31 33 -10 97 -34 143 -52 46 -19 88 -33 93 -32 8 3 138 380 138 401 0 4 -78 7 -174 7 l-174 0 -16 -42z", + } + path { + d: "M550 1369 c-235 -26 -412 -107 -509 -232 l-41 -54 0 -91 c0 -89 1 -92 39 -149 50 -75 117 -142 206 -205 124 -87 339 -197 350 -178 3 6 -35 50 -84 98 -160 157 -189 276 -96 402 35 47 116 105 183 130 285 108 789 43 1208 -155 107 -50 114 -52 113 -32 0 14 -26 44 -74 86 -218 191 -465 307 -765 361 -134 24 -401 34 -530 19z", + } + path { + d: "M739 1025 c-75 -15 -166 -51 -172 -68 -4 -13 221 -717 242 -755 12 -22 14 -23 192 -20 98 1 184 5 189 8 9 6 81 213 205 589 31 95 53 177 48 182 -10 10 -294 67 -308 62 -10 -3 -83 -220 -106 -312 -9 -38 -12 -42 -25 -29 -8 8 -39 91 -68 184 l-53 169 -39 2 c-21 1 -69 -4 -105 -12z", + } + } + } + } +} diff --git a/src/routes.rs b/src/routes.rs new file mode 100644 index 0000000..02943b2 --- /dev/null +++ b/src/routes.rs @@ -0,0 +1,26 @@ +use dioxus::prelude::*; + +use crate::{ + landing::Landing, + navbar::Navbar, +}; + +#[derive(Debug, Clone, Routable, PartialEq)] +#[rustfmt::skip] +pub enum Route { + #[layout(Navbar)] + #[route("/")] + Landing {}, + + #[route("/:..route")] + PageNotFound { + route: Vec, + }, +} + +#[component] +fn PageNotFound(route: Vec) -> Element { + rsx! { + "test" + } +} diff --git a/tailwind.css b/tailwind.css new file mode 100644 index 0000000..604ab07 --- /dev/null +++ b/tailwind.css @@ -0,0 +1,20 @@ +@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; + } + } +}