This commit is contained in:
2025-12-13 14:13:04 +01:00
parent 861a3ba880
commit 8df34776e5
13 changed files with 912 additions and 363 deletions

View File

@@ -7,6 +7,7 @@ edition = "2024"
[dependencies]
csv = "1.4.0"
dioxus = { version = "0.7.2", features = ["router"] }
dioxus-sdk-time = "0.7.0"
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"] }

View File

@@ -23,7 +23,7 @@
display: flex;
overflow: hidden;
width: 18rem;
height: 4rem;
height: 5rem;
box-sizing: border-box;
align-items: center;
justify-content: space-between;
@@ -178,3 +178,171 @@
.toast-close:hover {
color: var(--secondary-color-1);
}
.skeleton {
border-radius: 0.375rem;
animation: skeleton-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
background-color: var(--primary-color-7);
}
@keyframes skeleton-pulse {
0%,
100% {
opacity: 1;
}
61.8% {
opacity: 0.5;
}
}
.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;
}

View File

@@ -1,3 +1,4 @@
// AUTOGENERTED Components module
pub mod select;
pub mod toast;
pub mod skeleton;

View File

@@ -7,7 +7,6 @@ use dioxus_primitives::select::{
#[component]
pub fn Select<T: Clone + PartialEq + 'static>(props: SelectProps<T>) -> Element {
rsx! {
document::Link { rel: "stylesheet", href: asset!("./style.css") }
select::Select {
class: "select",
value: props.value,

View File

@@ -1,150 +0,0 @@
.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;
}

8
src/components/skeleton/component.rs git.filemode.normal_file
View File

@@ -0,0 +1,8 @@
use dioxus::prelude::*;
#[component]
pub fn Skeleton(#[props(extends=GlobalAttributes)] attributes: Vec<Attribute>) -> Element {
rsx! {
div { class: "skeleton", ..attributes }
}
}

2
src/components/skeleton/mod.rs git.filemode.normal_file
View File

@@ -0,0 +1,2 @@
mod component;
pub use component::*;

View File

@@ -4,7 +4,6 @@ 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,

View File

@@ -1,17 +1,40 @@
use crate::components::{select::*, skeleton::*};
use crate::loader::{Loader, Suspense};
use csv::{ReaderBuilder, StringRecord};
use dioxus::prelude::*;
use std::{collections::HashMap, thread::current};
use std::str::FromStr;
use csv::StringRecord;
use dioxus_primitives::toast::{ToastOptions, use_toast};
use dioxus_sdk_time::*;
use std::f32::consts::PI;
use std::str::FromStr;
use std::fmt;
use std::time::Duration;
use std::{collections::HashMap, thread::current};
use strum::{EnumCount, IntoEnumIterator};
use crate::components::select::*;
const PAGE_SIZE: usize = 100_usize;
// Wrapper for displaying a duration in h:m:s (integer seconds, rounded down)
struct HMSDuration(Duration);
impl fmt::Display for HMSDuration {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut ms = self.0.as_millis();
let hours = ms / 3_600_000;
ms %= 3_600_000;
let mins = ms / 60_000;
ms %= 60_000;
let secs = ms / 1000;
ms %= 1000;
if hours > 0 {
write!(f, "{}:{:02}:{:02}.{}", hours, mins, secs, ms)
} else if mins > 0 {
write!(f, "0:{}:{:02}.{}", mins, secs, ms)
} else {
write!(f, "0:0:{}.{}", secs, ms)
}
}
}
#[derive(
Debug,
Eq,
@@ -58,9 +81,162 @@ fn percentage(count: usize, total: f32) -> f32 {
(count as f32 * 100.0) / total
}
#[component]
fn LandingPlaceholder() -> Element {
let stats_cards = TestStatus::iter().map(|s| {
rsx! {
StatCardPlaceholder {
name: s.to_string(),
color: s.color().to_string(),
count: 0,
stat: 0.0_f32,
}
}
});
let statuses = TestStatus::iter().enumerate().map(|(i, s)| {
rsx! {
SelectOption::<Option<TestStatus>> { index: i, value: s, text_value: "{s}",
{format!("{} {s}", s.emoji())}
SelectItemIndicator {}
}
}
});
rsx! {
div {
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: "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: 0 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: 0 tests" }
}
}
div { class: "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4",
{stats_cards}
}
div { class: "mx-auto size-[200px]",
Skeleton { class: "skeleton size-[200px] !rounded-full" }
}
div { class: "mt-12 w-full flex flex-col md:flex-row justify-between text-gray-400 text-sm gap-4 items-center",
Skeleton { class: "hidden lg:block skeleton w-full h-9" }
p { class: "my-auto w-fit text-nowrap", "Page 1 of 1" }
div { class: "hidden lg:block",
PaginationPlaceholder { small: false }
}
div { class: "block lg:hidden",
PaginationPlaceholder { small: true }
}
}
Skeleton { class: "block lg:hidden skeleton w-full h-9" }
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",
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",
"Duration (H:M:S.MS)"
}
th { class: "uppercase bold whitespace-nowrap py-2 px-3",
Select::<Option<TestStatus>> { placeholder: "STATUS",
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::<Option<TestStatus>> {
index: TestStatus::COUNT,
value: None,
text_value: "Status",
"🔄 None"
SelectItemIndicator {}
}
}
}
}
}
}
for _ in 0..12 {
tr { class: "text-sm hover:bg-[#38bef7]/5",
td { class: "py-2 px-3 w-full",
Skeleton { class: "skeleton w-[60%] h-6" }
}
td { class: "py-2",
Skeleton { class: "skeleton w-24 h-6 mx-auto" }
}
td { class: "py-2",
Skeleton { class: "skeleton w-16 h-9 !rounded-3xl mx-auto" }
}
}
}
}
}
}
}
}
#[component]
pub fn Landing() -> Element {
let result = use_context::<Signal<Vec<StringRecord>>>();
let toast = use_toast();
let resource = use_resource(move || {
toast.info(
"Loading...".to_string(),
ToastOptions::new()
.description("Loading CTS results")
.duration(Duration::from_secs(12))
);
get_results()
}).load_with(rsx!{
LandingPlaceholder {}
})?;
let mut result = use_signal(Vec::<StringRecord>::new);
use_effect(move || match &*resource.read() {
Ok(results) => {
let mut reader = ReaderBuilder::new().from_reader(results.as_bytes());
let records = match reader
.into_records()
.collect::<Result<Vec<StringRecord>, csv::Error>>()
{
Ok(res) => res,
Err(e) => {
error!("Failed to parse results: {e}");
toast.error(
"Error".to_string(),
ToastOptions::new()
.description("Failed to parse CTS results")
);
Vec::new()
}
};
result.set(records);
toast.success(
"Success".to_string(),
ToastOptions::new()
.description("Successfully loaded CTS results")
);
},
Err(e) => {
error!("Failed to fetch results: {e}");
toast.error(
"Error".to_string(),
ToastOptions::new()
.description("Failed to fetch CTS results")
);
}
});
let global_stats = use_memo(move || result
.read()
@@ -86,59 +262,113 @@ pub fn Landing() -> Element {
}
});
let mut filter: Signal<Option<TestStatus>> = 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 mut search_input: Signal<Option<String>> = use_signal(|| None);
let mut search_name: Signal<Option<String>> = use_signal(|| None);
let mut filter: Signal<Option<TestStatus>> = use_signal(|| None);
let filtered_count = use_memo(move || {
let f = filter();
let search = search_name();
let rows = result.read();
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()
current_page.set(0_usize);
let mut total = 0_usize;
for r in rows.iter() {
let name = &r[0];
if let Some(ref s) = search {
if !name.contains(s) {
continue;
}
}
if let Some(wanted) = f {
let Ok(status) = TestStatus::from_str(&r[1]) else { continue };
if status != wanted {
continue;
}
}
total += 1;
}
total
});
let mut page_count = use_memo(move || filtered_count().max(PAGE_SIZE - 1) / PAGE_SIZE);
let page = use_memo(move || {
let _ = *filtered_count.read();
let f = filter();
let search = search_name();
let shift = current_page() * PAGE_SIZE;
let rows = result.read();
let mut idx = 0_usize;
let mut out = Vec::with_capacity(PAGE_SIZE);
for r in rows.iter() {
let name = &r[0];
if let Some(ref s) = search {
if !name.contains(s) {
continue;
}
}
if let Some(wanted) = f {
let Ok(status) = TestStatus::from_str(&r[1]) else { continue };
if status != wanted {
continue;
}
}
if idx >= shift && idx < shift + PAGE_SIZE {
out.push(r.clone());
if out.len() == PAGE_SIZE {
break;
}
}
idx += 1;
}
out
});
let statuses = TestStatus::iter().enumerate().map(|(i, s)| {
rsx! {
SelectOption::<Option<TestStatus>> {
index: i,
value: s,
text_value: "{s}",
SelectOption::<Option<TestStatus>> { index: i, value: s, text_value: "{s}",
{format!("{} {s}", s.emoji())}
SelectItemIndicator {}
}
}
});
let mut search_timeout: Signal<Option<TimeoutHandle>> = use_signal(|| None);
let timeout = use_timeout(Duration::from_secs(1), move |()| {
search_timeout.set(None);
search_name.set(search_input());
});
let onsearch_input = move |event: FormEvent| {
if event.value().is_empty() {
search_input.set(None);
} else {
search_input.set(Some(event.value()));
}
};
let onsearch_keyup = move |_| {
if let Some(handle) = *search_timeout.read() {
handle.cancel();
}
let handle = timeout.action(());
search_timeout.set(Some(handle));
};
rsx! {
div {
class: "flex flex-col space-y-4 rounded-3xl p-4 pt-8 w-full h-fit shadow-xl shadow-slate-950",
@@ -146,15 +376,11 @@ pub fn Landing() -> Element {
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"
}
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"
}
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",
@@ -163,55 +389,43 @@ pub fn Landing() -> Element {
div { class: "mx-auto size-[200px]",
StatsPieChart { stats: global_stats, total }
}
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: "mt-12 w-full flex flex-col md:flex-row justify-between text-gray-400 text-sm gap-4 items-center",
input {
class: "hidden lg:block w-full border-1 border-gray-700 px-3 py-1 rounded-lg text-sm",
style: "background: radial-gradient(circle at top, rgba(56, 189, 248, 0.1), rgba(15, 23, 42, 1));",
r#type: "search",
placeholder: "Search tests...",
oninput: onsearch_input,
onkeyup: onsearch_keyup
}
p { class: "my-auto w-fit text-nowrap",
"Page {current_page() + 1} of {page_count() + 1}"
}
div { class: "hidden lg:block",
Pagination { current_page, page_count, small: false }
}
div { class: "block lg:hidden",
Pagination { current_page, page_count, small: true }
}
}
div { class: "w-full bg-gray-900 overflow-hidden border-1 border-slate-700 rounded-lg text-gray-400",
input {
class: "block lg:hidden w-full border-1 border-gray-700 px-3 py-1 rounded-lg text-sm",
style: "background: radial-gradient(circle at top, rgba(56, 189, 248, 0.1), rgba(15, 23, 42, 1));",
r#type: "search",
placeholder: "Search tests...",
oninput: onsearch_input,
onkeyup: onsearch_keyup
}
div { class: "w-full bg-gray-900 overflow-auto 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",
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",
"Test name"
}
th { class: "text-left uppercase bold whitespace-nowrap py-2 px-3",
"Duration (H:M:S.MS)"
}
th { class: "uppercase bold whitespace-nowrap py-2 px-3",
Select::<Option<TestStatus>> {
@@ -222,8 +436,7 @@ pub fn Landing() -> Element {
aria_label: "Select Trigger",
SelectValue { class: "!bg-transparent !shadow-none !text-gray-400" }
}
SelectList {
aria_label: "Select status",
SelectList { aria_label: "Select status",
SelectGroup {
{statuses}
SelectOption::<Option<TestStatus>> {
@@ -240,8 +453,15 @@ pub fn Landing() -> Element {
}
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[0]}"
p { class: "mx-auto w-fit",
if let Ok(duration) = test[2].parse::<f32>() {
"{HMSDuration(Duration::from_secs_f32(duration))}"
} else {
"Invalid data"
}
}
}
td { class: "py-2 px-3",
StatusBadge {
@@ -259,6 +479,23 @@ pub fn Landing() -> Element {
}
}
#[component]
fn StatCardPlaceholder(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}" }
}
Skeleton { class: "skeleton w-32 h-7" }
Skeleton { class: "skeleton w-24 h-4" }
}
}
}
#[component]
fn StatCard(name: String, color: String, count: usize, stat: f32) -> Element {
rsx! {
@@ -268,18 +505,14 @@ fn StatCard(name: String, color: String, count: usize, stat: f32) -> Element {
class: "rounded-full size-4",
style: format!("background-color: {color};"),
}
h3 { class: "text-sm text-gray-300",
"{name}"
}
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"
"{count}"
}
p { class: "text-xs text-gray-400", "{stat:.1}% of total" }
}
}
}
@@ -298,11 +531,13 @@ fn StatusBadge(status: Option<TestStatus>) -> Element {
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};
"#),
style: format!(
r#"
background-color: {color}0F;
color: {color};
border-color: {color};
"#,
),
"{name}"
}
}
@@ -368,10 +603,138 @@ fn StatsPieChart(stats: ReadSignal<HashMap<TestStatus, usize>>, total: ReadSigna
});
rsx! {
svg {
class: "max-w-[200px] h-auto",
view_box: "0 0 200 200",
{paths}
svg { class: "max-w-[200px] h-auto", view_box: "0 0 200 200", {paths} }
}
}
#[component]
fn Pagination(
current_page: Memo<usize>,
page_count: ReadSignal<usize>,
small: bool,
) -> Element {
let range = if small { 1_usize } else { 2_usize };
rsx! {
div {
class: "flex flex-row data-[is-small=true]:flow-col gap-2 max-w-screen overflow-x-auto",
"data-is-small": small,
div { class: "flex flex-row gap-2",
button {
class: "pagination-button",
disabled: current_page() == 0,
onclick: move |_| current_page.set(0_usize),
if small {
"<<"
} else {
"First"
}
}
if !small {
button {
class: "pagination-button",
disabled: current_page() == 0,
onclick: move |_| current_page -= 1,
"Prev"
}
}
}
div { class: "flex flex-row gap-2",
if current_page() > range {
p { "..." }
}
for i in ((current_page() as i32 - range as i32).max(0)
as usize)..=(current_page() + range).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 - range as i32).max(0) as usize {
p { "..." }
}
}
div { class: "flex flex-row gap-2",
if !small {
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()),
if small {
">>"
} else {
"Last"
}
}
}
}
}
}
#[component]
fn PaginationPlaceholder(small: bool) -> Element {
let range = if small { 1_usize } else { 2_usize };
rsx! {
div {
class: "flex flex-row data-[is-small=true]:flow-col gap-2 max-w-screen overflow-x-auto",
"data-is-small": small,
div { class: "flex flex-row gap-2",
button { class: "pagination-button", disabled: true,
if small {
"<<"
} else {
"First"
}
}
if !small {
button { class: "pagination-button", disabled: true, "Prev" }
}
}
div { class: "flex flex-row gap-2",
for i in 1..(if small { 3 } else { 4 }) {
button { class: "pagination-button", disabled: true, "{i}" }
}
p { "..." }
}
div { class: "flex flex-row gap-2",
if !small {
button { class: "pagination-button", disabled: true, "Next" }
}
button { class: "pagination-button", disabled: true,
if small {
">>"
} else {
"Last"
}
}
}
}
}
}
async fn get_results() -> Result<String> {
Ok(
reqwest::get(format!("{}/assets/results.csv", std::env!("URL")))
.await?
.text()
.await?,
)
}

61
src/loader.rs git.filemode.normal_file
View File

@@ -0,0 +1,61 @@
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<Element>,
}
/// Trait to create a suspense with a default loading placeholder
pub trait Loader<T: 'static> {
fn load(
&self,
message: impl ToString,
) -> Result<MappedSignal<T, Signal<Option<T>> /* wtf Dioxus ??? */>, RenderError>;
fn load_with(
&self,
element: Element,
) -> Result<MappedSignal<T, Signal<Option<T>>>, RenderError>;
}
impl<T> Loader<T> for Resource<T> {
fn load(
&self,
message: impl ToString,
) -> Result<MappedSignal<T, Signal<Option<T>>>, RenderError> {
let mut context = use_context::<Signal<SuspenseContextPlaceholder>>();
context.write().element = Some(rsx! {
LoadingPlaceholder { message: message.to_string() }
});
self.suspend()
}
fn load_with(
&self,
element: Element,
) -> Result<MappedSignal<T, Signal<Option<T>>>, RenderError> {
let mut context = use_context::<Signal<SuspenseContextPlaceholder>>();
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}
}
}
}

View File

@@ -1,81 +1,38 @@
use dioxus::prelude::*;
#![allow(non_snake_case)]
use csv::{ReaderBuilder, StringRecord};
use dioxus::prelude::*;
use std::time::Duration;
mod components;
mod landing;
mod loader;
mod navbar;
mod routes;
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 COMPONENTS_CSS: Asset = asset!("/assets/dx-components-theme.css");
const RESULT: Asset = asset!(
"/assets/results.csv",
AssetOptions::builder().with_hash_suffix(false)
);
fn main() {
dioxus::launch(AppWrapper);
}
#[component]
fn AppWrapper() -> Element {
rsx! {
ToastProvider { App{} }
}
dioxus::launch(App);
}
#[component]
fn App() -> Element {
let toast = use_toast();
let resource = use_resource(move || get_results());
let mut result = use_context_provider(|| Signal::new(Vec::<StringRecord>::new()));
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::<Result<Vec<StringRecord>, 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 { "
document::Link { rel: "stylesheet", href: COMPONENTS_CSS }
document::Style {
"
:root {{
--primary-color: #000;
--primary-color-1: #020618;
@@ -95,30 +52,28 @@ fn App() -> Element {
--secondary-color-6: #5d5d5d;
--focused-border-color: #2b7fff;
--primary-success-color: #02271c;
--primary-success-color: #1A7D35;
--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));
--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%);",
Router::<Route> {}
body {{
background-color: #000;
}}
"
}
ToastProvider { default_duration: Duration::from_secs(4), max_toasts: 1_usize,
div {
class: "text-white min-h-screen",
style: "background: radial-gradient(circle at top, #1e293b 0, #020617 45%, #000 100%);",
Router::<Route> {}
}
}
}
}
async fn get_results() -> Result<String> {
Ok(
reqwest::get(format!("{}/assets/results.csv", std::env!("URL")))
.await?
.text()
.await?,
)
}

View File

@@ -1,34 +1,35 @@
use dioxus::prelude::*;
use crate::routes::Route;
use crate::{loader::Suspense, routes::Route};
#[component]
pub fn Navbar() -> Element {
rsx! {
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",
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"
}
p { class: "mt-auto ml-2 font-bold text-gray-300", "CTS Report" }
}
}
main { class: "mx-auto container mb-24",
Outlet::<Route> {}
Suspense { Outlet::<Route> {} }
}
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",
p { "Made by" }
a {
class: "hover:underline text-white",
href: "https://portfolio.kbz8.me/",
"kbz_8"
}
p { "with" }
a { class: "hover:underline text-white",
a {
class: "hover:underline text-white",
href: "https://dioxuslabs.com/",
"Dioxus"
}
@@ -39,24 +40,15 @@ pub fn Navbar() -> Element {
#[component]
fn VulkanVSvg() -> Element {
rsx! {
svg {
view_box: "0 0 192 192",
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",
}
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" }
}
}
}

View File

@@ -17,7 +17,157 @@ pub enum Route {
#[component]
fn PageNotFound(route: Vec<String>) -> Element {
let nav = use_navigator();
rsx! {
"test"
div { class: "w-full flex justify-center my-32",
div { class: "flex bg-slate-800 flex-col md:flex-row justify-center items-center gap-2 border-1 border-gray-700 rounded-xl p-4 lg:p-8 w-fit shadow-xl shadow-slate-950",
PageNotFoundSVG { size_class: "size-64 lg:size-[300px] xl:size-96" }
div { class: "flex flex-col gap-8 items-center",
h1 { class: "h1 text-6xl text-gray-400", "Oops!" }
p { class: "p text-gray-500 text-2xl whitespace-pre-line text-center",
"We couldn't find the page
you were looking for"
}
Link {
class: "mx-auto cursor-pointer hover:underline",
to: Route::Landing {},
"Go back home"
}
}
}
}
}
}
#[component]
fn PageNotFoundSVG(size_class: String) -> Element {
rsx! {
svg {
class: size_class,
"version": "1.1",
"xmlns:svg": "http://www.w3.org/2000/svg",
"xml:space": "preserve",
"viewBox": "0 0 64 64",
"fill": "#000000",
"xmlns": "http://www.w3.org/2000/svg",
id: "svg5",
g { "stroke-width": "0", id: "SVGRepo_bgCarrier" }
g {
"stroke-linecap": "round",
"stroke-linejoin": "round",
id: "SVGRepo_tracerCarrier",
}
g { id: "SVGRepo_iconCarrier",
defs { id: "defs2" }
g { "transform": "translate(-384,-96)", id: "layer1",
path {
"d": "m 393.99999,105 h 49 v 6 h -49 z",
style: "fill:#333333;fill-opacity:1;fill-rule:evenodd;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1",
id: "path27804",
}
path {
"d": "m 393.99999,111 h 49 v 40 h -49 z",
style: "fill:#acbec2;fill-opacity:1;fill-rule:evenodd;stroke-width:2.00001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1",
id: "path27806",
}
path {
"d": "m 393.99999,111 v 40 h 29.76954 a 28.484051,41.392605 35.599482 0 0 18.625,-40 z",
style: "fill:#e8edee;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.00002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1",
id: "path27808",
}
path {
"d": "m 395.99999,104 c -1.64501,0 -3,1.355 -3,3 v 40 c 0,0.55229 0.44772,1 1,1 0.55229,0 1,-0.44771 1,-1 v -40 c 0,-0.56413 0.43587,-1 1,-1 h 45 c 0.56414,0 1,0.43587 1,1 v 3 h -42 c -0.55228,0 -1,0.44772 -1,1 0,0.55229 0.44772,1 1,1 h 42 v 37 c 0,0.56413 -0.43586,1 -1,1 h -49 c -0.55228,0 -1,0.44772 -1,1 0,0.55229 0.44772,1 1,1 h 49 c 1.64501,0 3,-1.35499 3,-3 0,-14 0,-28 0,-42 0,-1.645 -1.35499,-3 -3,-3 z",
style: "color:#000000;fill:#000000;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1",
id: "path27810",
}
path {
style: "color:#000000;fill:#ed7161;fill-opacity:1;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1;-inkscape-stroke:none",
"d": "m 438.99999,107 c -0.55228,0 -1,0.44772 -1,1 0,0.55229 0.44772,1 1,1 0.55229,0 1,-0.44771 1,-1 0,-0.55228 -0.44771,-1 -1,-1 z",
id: "path27812",
}
path {
"d": "m 434.99999,107 c -0.55228,0 -1,0.44772 -1,1 0,0.55229 0.44772,1 1,1 0.55229,0 1,-0.44771 1,-1 0,-0.55228 -0.44771,-1 -1,-1 z",
style: "color:#000000;fill:#ecba16;fill-opacity:1;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1;-inkscape-stroke:none",
id: "path27814",
}
path {
"d": "m 430.99999,107 c -0.55228,0 -1,0.44772 -1,1 0,0.55229 0.44772,1 1,1 0.55229,0 1,-0.44771 1,-1 0,-0.55228 -0.44771,-1 -1,-1 z",
style: "color:#000000;fill:#42b05c;fill-opacity:1;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1;-inkscape-stroke:none",
id: "path27816",
}
path {
style: "color:#000000;fill:#000000;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1;-inkscape-stroke:none",
"d": "m 388.99999,150 a 1,1 0 0 0 -1,1 1,1 0 0 0 1,1 1,1 0 0 0 1,-1 1,1 0 0 0 -1,-1 z",
id: "path27818",
}
path {
"d": "m 396.99999,110 c -0.55228,0 -1,0.44772 -1,1 0,0.55229 0.44772,1 1,1 0.55229,0 1,-0.44771 1,-1 0,-0.55228 -0.44771,-1 -1,-1 z",
style: "color:#000000;fill:#000000;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1;-inkscape-stroke:none",
id: "path27820",
}
rect {
"y": "120",
"rx": "2",
width: "29",
style: "fill:#256ada;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1",
height: "22",
"ry": "2",
"x": "404",
id: "rect4427",
}
path {
"d": "m 406,120 c -1.108,0 -2,0.892 -2,2 v 18 c 0,1.108 0.892,2 2,2 h 19.58398 A 19.317461,16.374676 0 0 0 430.2207,131.36719 19.317461,16.374676 0 0 0 424.80273,120 Z",
style: "fill:#6b9ae6;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1",
id: "path27648",
}
rect {
height: "6",
style: "fill:#50a824;fill-opacity:1;fill-rule:evenodd;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1",
width: "29",
"y": "120",
"x": "404",
id: "rect8552",
}
path {
"d": "m 404,120 v 6 h 24.58984 a 14,8.5 0 0 0 0.10938,-1 14,8.5 0 0 0 -2.67969,-5 z",
style: "fill:#83db57;fill-opacity:1;fill-rule:evenodd;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1",
id: "path8626",
}
g { "transform": "translate(0,-4)", id: "path4429",
path {
"d": "m 404,130 h 29",
style: "color:#000000;fill:#918383;fill-rule:evenodd;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1;-inkscape-stroke:none",
id: "path7162",
}
path {
style: "color:#000000;fill:#000000;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1;-inkscape-stroke:none",
"d": "m 406,123 c -1.6447,0 -3,1.3553 -3,3 0,1.97201 0,3.94401 0,5.91602 0,0.55228 0.44772,1 1,1 0.55228,0 1,-0.44772 1,-1 V 131 h 27 v 6 c 0,0.55228 0.44772,1 1,1 0.55228,0 1,-0.44772 1,-1 0,-3.66667 0,-7.33333 0,-11 0,-1.6447 -1.3553,-3 -3,-3 z m 0,2 h 25 c 0.5713,0 1,0.4287 1,1 v 3 h -27 v -3 c 0,-0.5713 0.4287,-1 1,-1 z m -2,10 c -0.55228,0 -1,0.44772 -1,1 v 8 c 0,1.6447 1.3553,3 3,3 h 25 c 1.6447,0 3,-1.3553 3,-3 v -3 c 0,-0.55228 -0.44772,-1 -1,-1 -0.55228,0 -1,0.44772 -1,1 v 3 c 0,0.5713 -0.4287,1 -1,1 h -25 c -0.5713,0 -1,-0.4287 -1,-1 v -8 c 0,-0.55228 -0.44772,-1 -1,-1 z",
id: "path7164",
}
}
path {
"d": "m 409.93555,129.00195 c -0.45187,0.0293 -0.82765,0.35863 -0.91602,0.80274 l -1,5 C 407.89645,135.42313 408.36944,135.99975 409,136 h 3 v 2 c 0,0.55228 0.44772,1 1,1 0.55228,0 1,-0.44772 1,-1 0,-1.66667 0,-3.33333 0,-5 0,-0.55228 -0.44772,-1 -1,-1 -0.55228,0 -1,0.44772 -1,1 v 1 h -1.78125 l 0.76172,-3.80469 c 0.10771,-0.54147 -0.24375,-1.06778 -0.78516,-1.17578 -0.0854,-0.0172 -0.17278,-0.0231 -0.25976,-0.0176 z",
style: "color:#000000;fill:#000000;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1;-inkscape-stroke:none",
id: "path8873",
}
path {
style: "fill:#ffc343;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1",
"d": "m 418.99999,130 c 1.10801,0 2.00002,0.89201 2.00002,2.00002 v 2.99996 c 0,1.10801 -0.89201,2.00002 -2.00002,2.00002 -1.10801,0 -2.00002,-0.89201 -2.00002,-2.00002 v -2.99996 c 0,-1.10801 0.89201,-2.00002 2.00002,-2.00002 z",
id: "rect5745",
}
path {
"d": "m 419,129 c -1.64471,0 -3,1.35529 -3,3 v 3 c 0,1.64471 1.35529,3 3,3 1.64471,0 3,-1.35529 3,-3 v -3 a 1,1 0 0 0 -1,-1 1,1 0 0 0 -1,1 v 3 c 0,0.57131 -0.42869,1 -1,1 -0.57131,0 -1,-0.42869 -1,-1 v -3 c 0,-0.57131 0.42869,-1 1,-1 a 1,1 0 0 0 1,-1 1,1 0 0 0 -1,-1 z",
style: "color:#000000;fill:#000000;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1;-inkscape-stroke:none",
id: "path7169",
}
path {
style: "color:#000000;fill:#000000;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.1;-inkscape-stroke:none",
"d": "m 425.93555,129.00195 c -0.45187,0.0293 -0.82765,0.35863 -0.91602,0.80274 l -1,5 C 423.89645,135.42313 424.36944,135.99975 425,136 h 3 v 2 c 0,0.55228 0.44772,1 1,1 0.55228,0 1,-0.44772 1,-1 0,-1.66667 0,-3.33333 0,-5 0,-0.55228 -0.44772,-1 -1,-1 -0.55228,0 -1,0.44772 -1,1 v 1 h -1.78125 l 0.76172,-3.80469 c 0.10771,-0.54147 -0.24375,-1.06778 -0.78516,-1.17578 -0.0854,-0.0172 -0.17278,-0.0231 -0.25976,-0.0176 z",
id: "path69785",
}
}
}
}
}
}