pinakes-ui: restyle tasks and statistics components with icons
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Ib7e4888602163f828f8aaa9bce2bc5e66a6a6964
This commit is contained in:
parent
3595f89fec
commit
445281ea5a
3 changed files with 367 additions and 261 deletions
|
|
@ -1,4 +1,8 @@
|
|||
use dioxus::prelude::*;
|
||||
use dioxus_free_icons::Icon;
|
||||
use dioxus_free_icons::icons::fa_solid_icons::{
|
||||
FaChartBar, FaCircle, FaClock, FaDatabase, FaFolder, FaLink, FaTags,
|
||||
};
|
||||
|
||||
use super::utils::format_size;
|
||||
use crate::client::LibraryStatisticsResponse;
|
||||
|
|
@ -10,184 +14,228 @@ pub fn Statistics(
|
|||
on_refresh: EventHandler<()>,
|
||||
) -> Element {
|
||||
rsx! {
|
||||
div { class: "card mb-16",
|
||||
div { class: "card-header",
|
||||
h3 { class: "card-title", "Library Statistics" }
|
||||
button {
|
||||
class: "btn btn-sm btn-secondary",
|
||||
onclick: move |_| on_refresh.call(()),
|
||||
"\u{21bb} Refresh"
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref err) = error {
|
||||
div { class: "alert alert-error mb-8",
|
||||
span { "{err}" }
|
||||
div { class: "statistics-page",
|
||||
div { class: "card",
|
||||
div { class: "card-header",
|
||||
h3 { class: "card-title", "Library Statistics" }
|
||||
button {
|
||||
class: "btn btn-sm btn-secondary ml-8",
|
||||
class: "btn btn-sm btn-secondary",
|
||||
onclick: move |_| on_refresh.call(()),
|
||||
"Retry"
|
||||
"\u{21bb} Refresh"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match stats.as_ref() {
|
||||
Some(s) => {
|
||||
let total_size = format_size(s.total_size_bytes);
|
||||
let avg_size = format_size(s.avg_file_size_bytes);
|
||||
rsx! {
|
||||
// Overview
|
||||
div { class: "stats-grid",
|
||||
div { class: "stat-card",
|
||||
div { class: "stat-value", "{s.total_media}" }
|
||||
div { class: "stat-label", "Total Media" }
|
||||
}
|
||||
div { class: "stat-card",
|
||||
div { class: "stat-value", "{total_size}" }
|
||||
div { class: "stat-label", "Total Size" }
|
||||
}
|
||||
div { class: "stat-card",
|
||||
div { class: "stat-value", "{avg_size}" }
|
||||
div { class: "stat-label", "Avg File Size" }
|
||||
}
|
||||
div { class: "stat-card",
|
||||
div { class: "stat-value", "{s.total_tags}" }
|
||||
div { class: "stat-label", "Tags" }
|
||||
}
|
||||
div { class: "stat-card",
|
||||
div { class: "stat-value", "{s.total_collections}" }
|
||||
div { class: "stat-label", "Collections" }
|
||||
}
|
||||
div { class: "stat-card",
|
||||
div { class: "stat-value", "{s.total_duplicates}" }
|
||||
div { class: "stat-label", "Duplicate Hashes" }
|
||||
}
|
||||
if let Some(ref err) = error {
|
||||
div { class: "alert alert-error mb-8",
|
||||
span { "{err}" }
|
||||
button {
|
||||
class: "btn btn-sm btn-secondary ml-8",
|
||||
onclick: move |_| on_refresh.call(()),
|
||||
"Retry"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Media by Type
|
||||
match stats.as_ref() {
|
||||
Some(s) => {
|
||||
let total_size = format_size(s.total_size_bytes);
|
||||
let avg_size = format_size(s.avg_file_size_bytes);
|
||||
rsx! {
|
||||
div { class: "stats-overview",
|
||||
div { class: "stat-card stat-primary",
|
||||
div { class: "stat-icon",
|
||||
Icon { icon: FaFolder, width: 20, height: 20 }
|
||||
}
|
||||
div { class: "stat-content",
|
||||
div { class: "stat-value", "{s.total_media}" }
|
||||
div { class: "stat-label", "Total Media" }
|
||||
}
|
||||
}
|
||||
div { class: "stat-card stat-success",
|
||||
div { class: "stat-icon",
|
||||
Icon { icon: FaDatabase, width: 20, height: 20 }
|
||||
}
|
||||
div { class: "stat-content",
|
||||
div { class: "stat-value", "{total_size}" }
|
||||
div { class: "stat-label", "Total Size" }
|
||||
}
|
||||
}
|
||||
div { class: "stat-card stat-info",
|
||||
div { class: "stat-icon",
|
||||
Icon { icon: FaChartBar, width: 20, height: 20 }
|
||||
}
|
||||
div { class: "stat-content",
|
||||
div { class: "stat-value", "{avg_size}" }
|
||||
div { class: "stat-label", "Average Size" }
|
||||
}
|
||||
}
|
||||
div { class: "stat-card stat-warning",
|
||||
div { class: "stat-icon",
|
||||
Icon { icon: FaTags, width: 20, height: 20 }
|
||||
}
|
||||
div { class: "stat-content",
|
||||
div { class: "stat-value", "{s.total_tags}" }
|
||||
div { class: "stat-label", "Tags" }
|
||||
}
|
||||
}
|
||||
div { class: "stat-card stat-purple",
|
||||
div { class: "stat-icon",
|
||||
Icon { icon: FaCircle, width: 20, height: 20 }
|
||||
}
|
||||
div { class: "stat-content",
|
||||
div { class: "stat-value", "{s.total_collections}" }
|
||||
div { class: "stat-label", "Collections" }
|
||||
}
|
||||
}
|
||||
div { class: "stat-card stat-danger",
|
||||
div { class: "stat-icon",
|
||||
Icon { icon: FaLink, width: 20, height: 20 }
|
||||
}
|
||||
div { class: "stat-content",
|
||||
div { class: "stat-value", "{s.total_duplicates}" }
|
||||
div { class: "stat-label", "Duplicates" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Storage by Type
|
||||
if !s.media_by_type.is_empty() {
|
||||
{
|
||||
let max_count = s.media_by_type.iter().map(|i| i.count).max().unwrap_or(1) as f64;
|
||||
|
||||
// Top Tags
|
||||
|
||||
// Top Collections
|
||||
|
||||
// Date Range
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if !s.media_by_type.is_empty() {
|
||||
div { class: "card mt-16",
|
||||
h4 { class: "card-title", "Media by Type" }
|
||||
table { class: "table",
|
||||
thead {
|
||||
tr {
|
||||
th { "Type" }
|
||||
th { "Count" }
|
||||
rsx! {
|
||||
div { class: "stats-section",
|
||||
h4 { class: "section-title",
|
||||
Icon { icon: FaChartBar, width: 16, height: 16, style: "margin-right: 8px; vertical-align: middle;" }
|
||||
"Media by Type"
|
||||
}
|
||||
div { class: "chart-bars",
|
||||
for item in s.media_by_type.iter() {
|
||||
{
|
||||
let percentage = (item.count as f64 / max_count) * 100.0;
|
||||
let name = item.name.clone();
|
||||
let count = item.count;
|
||||
rsx! {
|
||||
div { key: "{name}", class: "bar-item",
|
||||
div { class: "bar-label", "{name}" }
|
||||
div { class: "bar-track",
|
||||
div {
|
||||
class: "bar-fill bar-primary",
|
||||
style: "width: {percentage}%",
|
||||
}
|
||||
}
|
||||
div { class: "bar-value", "{count}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
for item in s.media_by_type.iter() {
|
||||
tr {
|
||||
td { "{item.name}" }
|
||||
td { "{item.count}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !s.storage_by_type.is_empty() {
|
||||
{
|
||||
let max_size = s.storage_by_type.iter().map(|i| i.count).max().unwrap_or(1) as f64;
|
||||
|
||||
rsx! {
|
||||
div { class: "stats-section",
|
||||
h4 { class: "section-title",
|
||||
Icon { icon: FaDatabase, width: 16, height: 16, style: "margin-right: 8px; vertical-align: middle;" }
|
||||
"Storage by Type"
|
||||
}
|
||||
div { class: "chart-bars",
|
||||
for item in s.storage_by_type.iter() {
|
||||
{
|
||||
let percentage = (item.count as f64 / max_size) * 100.0;
|
||||
let name = item.name.clone();
|
||||
let size_str = format_size(item.count);
|
||||
rsx! {
|
||||
div { key: "{name}", class: "bar-item",
|
||||
div { class: "bar-label", "{name}" }
|
||||
div { class: "bar-track",
|
||||
div {
|
||||
class: "bar-fill bar-success",
|
||||
style: "width: {percentage}%",
|
||||
}
|
||||
}
|
||||
div { class: "bar-value", "{size_str}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !s.storage_by_type.is_empty() {
|
||||
div { class: "card mt-16",
|
||||
h4 { class: "card-title", "Storage by Type" }
|
||||
table { class: "table",
|
||||
thead {
|
||||
tr {
|
||||
th { "Type" }
|
||||
th { "Size" }
|
||||
}
|
||||
if !s.top_tags.is_empty() {
|
||||
div { class: "stats-section",
|
||||
h4 { class: "section-title",
|
||||
Icon { icon: FaTags, width: 16, height: 16, style: "margin-right: 8px; vertical-align: middle;" }
|
||||
"Top Tags"
|
||||
}
|
||||
tbody {
|
||||
for item in s.storage_by_type.iter() {
|
||||
tr {
|
||||
td { "{item.name}" }
|
||||
td { "{format_size(item.count)}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !s.top_tags.is_empty() {
|
||||
div { class: "card mt-16",
|
||||
h4 { class: "card-title", "Top Tags" }
|
||||
table { class: "table",
|
||||
thead {
|
||||
tr {
|
||||
th { "Tag" }
|
||||
th { "Count" }
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
div { class: "tag-list",
|
||||
for item in s.top_tags.iter() {
|
||||
tr {
|
||||
td { "{item.name}" }
|
||||
td { "{item.count}" }
|
||||
div { class: "tag-item",
|
||||
span { class: "tag-badge", "{item.name}" }
|
||||
span { class: "tag-count", "{item.count}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !s.top_collections.is_empty() {
|
||||
div { class: "card mt-16",
|
||||
h4 { class: "card-title", "Top Collections" }
|
||||
table { class: "table",
|
||||
thead {
|
||||
tr {
|
||||
th { "Collection" }
|
||||
th { "Members" }
|
||||
}
|
||||
if !s.top_collections.is_empty() {
|
||||
div { class: "stats-section",
|
||||
h4 { class: "section-title",
|
||||
Icon { icon: FaCircle, width: 16, height: 16, style: "margin-right: 8px; vertical-align: middle;" }
|
||||
"Top Collections"
|
||||
}
|
||||
tbody {
|
||||
div { class: "collection-list",
|
||||
for item in s.top_collections.iter() {
|
||||
tr {
|
||||
td { "{item.name}" }
|
||||
td { "{item.count}" }
|
||||
div { class: "collection-item",
|
||||
Icon { icon: FaFolder, width: 16, height: 16, class: "collection-icon" }
|
||||
span { class: "collection-name", "{item.name}" }
|
||||
span { class: "collection-count", "{item.count}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div { class: "card mt-16",
|
||||
h4 { class: "card-title", "Date Range" }
|
||||
div { class: "stats-grid",
|
||||
div { class: "stat-card",
|
||||
div { class: "stat-value", "{s.oldest_item.as_deref().unwrap_or(\"N/A\")}" }
|
||||
div { class: "stat-label", "Oldest Item" }
|
||||
div { class: "stats-section",
|
||||
h4 { class: "section-title",
|
||||
Icon { icon: FaClock, width: 16, height: 16, style: "margin-right: 8px; vertical-align: middle;" }
|
||||
"Date Range"
|
||||
}
|
||||
div { class: "stat-card",
|
||||
div { class: "stat-value", "{s.newest_item.as_deref().unwrap_or(\"N/A\")}" }
|
||||
div { class: "stat-label", "Newest Item" }
|
||||
div { class: "date-range",
|
||||
div { class: "date-item",
|
||||
Icon { icon: FaClock, width: 16, height: 16 }
|
||||
div { class: "date-content",
|
||||
div { class: "date-label", "Oldest Item" }
|
||||
div { class: "date-value", "{s.oldest_item.as_deref().unwrap_or(\"N/A\")}" }
|
||||
}
|
||||
}
|
||||
div { class: "date-item",
|
||||
Icon { icon: FaClock, width: 16, height: 16 }
|
||||
div { class: "date-content",
|
||||
div { class: "date-label", "Newest Item" }
|
||||
div { class: "date-value", "{s.newest_item.as_deref().unwrap_or(\"N/A\")}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => rsx! {
|
||||
div { class: "empty-state",
|
||||
div { class: "spinner" }
|
||||
p { "Loading statistics..." }
|
||||
}
|
||||
},
|
||||
}
|
||||
None => rsx! {
|
||||
div { class: "empty-state",
|
||||
p { "Loading statistics..." }
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
use dioxus::prelude::*;
|
||||
use dioxus_free_icons::Icon;
|
||||
use dioxus_free_icons::icons::fa_solid_icons::{
|
||||
FaArrowsRotate, FaCalendar, FaCircleCheck, FaClock, FaPause, FaPlay,
|
||||
};
|
||||
|
||||
use crate::client::ScheduledTaskResponse;
|
||||
|
||||
|
|
@ -11,80 +15,134 @@ pub fn Tasks(
|
|||
on_run_now: EventHandler<String>,
|
||||
) -> Element {
|
||||
rsx! {
|
||||
div { class: "card mb-16",
|
||||
div { class: "card-header",
|
||||
h3 { class: "card-title", "Scheduled Tasks" }
|
||||
button {
|
||||
class: "btn btn-sm btn-secondary",
|
||||
onclick: move |_| on_refresh.call(()),
|
||||
"\u{21bb} Refresh"
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref err) = error {
|
||||
div { class: "alert alert-error mb-8",
|
||||
span { "{err}" }
|
||||
div { class: "tasks-container",
|
||||
div { class: "card mb-16",
|
||||
div { class: "card-header",
|
||||
h3 { class: "card-title", "Scheduled Tasks" }
|
||||
button {
|
||||
class: "btn btn-sm btn-secondary ml-8",
|
||||
class: "btn btn-sm btn-secondary",
|
||||
onclick: move |_| on_refresh.call(()),
|
||||
"Retry"
|
||||
Icon { icon: FaArrowsRotate, width: 14, height: 14 }
|
||||
" Refresh"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if tasks.is_empty() {
|
||||
div { class: "empty-state",
|
||||
p { "No scheduled tasks configured." }
|
||||
}
|
||||
} else {
|
||||
table { class: "table",
|
||||
thead {
|
||||
tr {
|
||||
th { "Enabled" }
|
||||
th { "Name" }
|
||||
th { "Schedule" }
|
||||
th { "Last Run" }
|
||||
th { "Next Run" }
|
||||
th { "Status" }
|
||||
th { "Actions" }
|
||||
if let Some(ref err) = error {
|
||||
div { class: "alert alert-error mb-8",
|
||||
span { "{err}" }
|
||||
button {
|
||||
class: "btn btn-sm btn-secondary ml-8",
|
||||
onclick: move |_| on_refresh.call(()),
|
||||
"Retry"
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
}
|
||||
|
||||
if tasks.is_empty() {
|
||||
div { class: "empty-state",
|
||||
div { class: "empty-icon",
|
||||
Icon { icon: FaCalendar, width: 48, height: 48 }
|
||||
}
|
||||
p { "No scheduled tasks configured." }
|
||||
p { class: "text-muted", "Tasks will appear here once configured on the server." }
|
||||
}
|
||||
} else {
|
||||
div { class: "tasks-grid",
|
||||
for task in tasks.iter() {
|
||||
{
|
||||
let task_id_toggle = task.id.clone();
|
||||
let task_id_run = task.id.clone();
|
||||
let last_run = task.last_run.clone().unwrap_or_else(|| "-".to_string());
|
||||
let next_run = task.next_run.clone().unwrap_or_else(|| "-".to_string());
|
||||
let last_status = task.last_status.clone().unwrap_or_else(|| "-".to_string());
|
||||
let last_run = task.last_run.clone().unwrap_or_else(|| "Never".to_string());
|
||||
let next_run = task.next_run.clone().unwrap_or_else(|| "Not scheduled".to_string());
|
||||
let last_status = task.last_status.clone().unwrap_or_else(|| "No runs yet".to_string());
|
||||
let is_enabled = task.enabled;
|
||||
let task_name = task.name.clone();
|
||||
let schedule = task.schedule.clone();
|
||||
|
||||
rsx! {
|
||||
tr {
|
||||
td {
|
||||
if task.enabled {
|
||||
span { class: "badge badge-success", "\u{2713}" }
|
||||
} else {
|
||||
span { class: "badge badge-muted", "\u{2715}" }
|
||||
div {
|
||||
class: if is_enabled { "task-card task-card-enabled" } else { "task-card task-card-disabled" },
|
||||
|
||||
// Header with status and actions
|
||||
div { class: "task-card-header",
|
||||
div { class: "task-header-left",
|
||||
div { class: "task-name", "{task_name}" }
|
||||
div { class: "task-schedule",
|
||||
span { class: "schedule-icon",
|
||||
Icon { icon: FaClock, width: 14, height: 14 }
|
||||
}
|
||||
"{schedule}"
|
||||
}
|
||||
}
|
||||
div { class: "task-status-badge",
|
||||
if is_enabled {
|
||||
span { class: "status-badge status-enabled",
|
||||
span { class: "status-dot" }
|
||||
"Active"
|
||||
}
|
||||
} else {
|
||||
span { class: "status-badge status-disabled",
|
||||
span { class: "status-dot" }
|
||||
"Disabled"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
td { "{task.name}" }
|
||||
td { "{task.schedule}" }
|
||||
td { "{last_run}" }
|
||||
td { "{next_run}" }
|
||||
td { "{last_status}" }
|
||||
td {
|
||||
|
||||
// Task info grid
|
||||
div { class: "task-info-grid",
|
||||
div { class: "task-info-item",
|
||||
div { class: "task-info-icon",
|
||||
Icon { icon: FaClock, width: 16, height: 16 }
|
||||
}
|
||||
div { class: "task-info-content",
|
||||
div { class: "task-info-label", "Last Run" }
|
||||
div { class: "task-info-value", "{last_run}" }
|
||||
}
|
||||
}
|
||||
div { class: "task-info-item",
|
||||
div { class: "task-info-icon",
|
||||
Icon { icon: FaClock, width: 16, height: 16 }
|
||||
}
|
||||
div { class: "task-info-content",
|
||||
div { class: "task-info-label", "Next Run" }
|
||||
div { class: "task-info-value", "{next_run}" }
|
||||
}
|
||||
}
|
||||
div { class: "task-info-item",
|
||||
div { class: "task-info-icon",
|
||||
Icon { icon: FaCircleCheck, width: 16, height: 16 }
|
||||
}
|
||||
div { class: "task-info-content",
|
||||
div { class: "task-info-label", "Last Status" }
|
||||
div { class: "task-info-value", "{last_status}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Actions
|
||||
div { class: "task-card-actions",
|
||||
button {
|
||||
class: "btn btn-sm btn-secondary mr-8",
|
||||
class: if is_enabled { "btn btn-sm btn-secondary" } else { "btn btn-sm btn-primary" },
|
||||
onclick: move |_| on_toggle.call(task_id_toggle.clone()),
|
||||
if task.enabled {
|
||||
"Disable"
|
||||
if is_enabled {
|
||||
span {
|
||||
Icon { icon: FaPause, width: 14, height: 14 }
|
||||
" Disable"
|
||||
}
|
||||
} else {
|
||||
"Enable"
|
||||
span {
|
||||
Icon { icon: FaPlay, width: 14, height: 14 }
|
||||
" Enable"
|
||||
}
|
||||
}
|
||||
}
|
||||
button {
|
||||
class: "btn btn-sm btn-primary",
|
||||
onclick: move |_| on_run_now.call(task_id_run.clone()),
|
||||
"Run Now"
|
||||
disabled: !is_enabled,
|
||||
Icon { icon: FaPlay, width: 14, height: 14 }
|
||||
" Run Now"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue