mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
refactor(admin): tidy the admin routes and templates (#2734)
This commit is contained in:
1 parent
5afbf67a70
commit
33737e0f79
48 files changed
+697
-679
No files matched your search
@@ -41,17 +41,14 @@ pub fn archives_tab(
|
||||
}
|
||||
}
|
||||
|
||||
fn status_text(archive: &Archive) -> String {
|
||||
fn status_text(archive: &Archive) -> &str {
|
||||
if archive.failed_at.is_some() {
|
||||
return "Failed".to_owned();
|
||||
return "Failed";
|
||||
}
|
||||
if archive.completed_at.is_some() {
|
||||
return "Completed".to_owned();
|
||||
return "Completed";
|
||||
}
|
||||
archive
|
||||
.progress_step
|
||||
.clone()
|
||||
.unwrap_or_else(|| "In Progress".to_owned())
|
||||
archive.progress_step.as_deref().unwrap_or("In Progress")
|
||||
}
|
||||
|
||||
fn archives_table(base: &str, archives: &[Archive]) -> Markup {
|
||||
|
||||
@@ -66,9 +66,15 @@ pub fn audit_logs_for_target(
|
||||
) -> Markup {
|
||||
let has_previous = current_page > 0;
|
||||
let offset = (current_page as u64) * (PAGE_SIZE as u64);
|
||||
let has_next = offset + (entries.len() as u64) < total;
|
||||
let total_pages = ((total as f64) / (PAGE_SIZE as f64)).ceil().max(1.0) as u64;
|
||||
let all_logs_href = format!("{base_path}/audit-logs?target_id={target_id}");
|
||||
let next_page = current_page
|
||||
.checked_add(1)
|
||||
.filter(|_| offset + (entries.len() as u64) < total);
|
||||
let total_pages = total.div_ceil(u64::from(PAGE_SIZE)).max(1);
|
||||
let page_number = u64::from(current_page) + 1;
|
||||
let all_logs_href = format!(
|
||||
"{base_path}/audit-logs?target_id={}",
|
||||
urlencoding::encode(target_id)
|
||||
);
|
||||
|
||||
html! {
|
||||
div class="rounded-lg bg-white transition-all border border-neutral-200 p-6" {
|
||||
@@ -108,7 +114,7 @@ pub fn audit_logs_for_target(
|
||||
}))
|
||||
}))
|
||||
}
|
||||
@if has_previous || has_next {
|
||||
@if has_previous || next_page.is_some() {
|
||||
div class="flex items-center justify-center gap-2" {
|
||||
@if has_previous {
|
||||
a href={(tab_href_base) "&audit_logs_page=" (current_page - 1)}
|
||||
@@ -119,10 +125,10 @@ pub fn audit_logs_for_target(
|
||||
}
|
||||
}
|
||||
span class="text-sm text-neutral-500" {
|
||||
"Page " (current_page + 1) " of " (total_pages)
|
||||
"Page " (page_number) " of " (total_pages)
|
||||
}
|
||||
@if has_next {
|
||||
a href={(tab_href_base) "&audit_logs_page=" (current_page + 1)}
|
||||
@if let Some(page) = next_page {
|
||||
a href={(tab_href_base) "&audit_logs_page=" (page)}
|
||||
class="inline-flex items-center justify-center gap-2 font-medium \
|
||||
rounded-lg bg-neutral-50 text-neutral-700 border \
|
||||
border-neutral-300 px-3 py-1.5 text-sm" {
|
||||
|
||||
@@ -81,20 +81,15 @@ pub fn guild_asset_url(
|
||||
}
|
||||
|
||||
pub fn initials(name: &str) -> String {
|
||||
let parts = name
|
||||
.split_whitespace()
|
||||
.filter(|part| !part.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
match parts.as_slice() {
|
||||
[] => "?".to_owned(),
|
||||
[part] => part.chars().next().unwrap_or('?').to_uppercase().collect(),
|
||||
[first, .., last] => {
|
||||
let mut value = String::new();
|
||||
value.extend(first.chars().next().unwrap_or('?').to_uppercase());
|
||||
value.extend(last.chars().next().unwrap_or('?').to_uppercase());
|
||||
value
|
||||
}
|
||||
}
|
||||
let mut parts = name.split_whitespace();
|
||||
let Some(first) = parts.next() else {
|
||||
return "?".to_owned();
|
||||
};
|
||||
std::iter::once(first)
|
||||
.chain(parts.next_back())
|
||||
.flat_map(|part| part.chars().take(1))
|
||||
.flat_map(char::to_uppercase)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn media_asset_url(
|
||||
|
||||
@@ -45,8 +45,8 @@ fn ncmec_badge(att: &Attachment) -> Markup {
|
||||
}
|
||||
|
||||
fn render_image_attachments(msg: &Message, include_delete: bool) -> Markup {
|
||||
let images: Vec<&Attachment> = msg.attachments.iter().filter(|a| is_image(a)).collect();
|
||||
if images.is_empty() {
|
||||
let mut images = msg.attachments.iter().filter(|a| is_image(a)).peekable();
|
||||
if images.peek().is_none() {
|
||||
return html! {};
|
||||
}
|
||||
let spacer = if !msg.content.is_empty() {
|
||||
@@ -56,7 +56,7 @@ fn render_image_attachments(msg: &Message, include_delete: bool) -> Markup {
|
||||
};
|
||||
html! {
|
||||
div class=(spacer) {
|
||||
@for att in &images {
|
||||
@for att in images {
|
||||
div class="max-w-xl overflow-hidden rounded-xl border border-neutral-200 bg-neutral-50" {
|
||||
a href=(att.url) target="_blank" rel="noopener noreferrer"
|
||||
class="block overflow-hidden bg-neutral-100" {
|
||||
@@ -116,8 +116,8 @@ fn render_image_attachments(msg: &Message, include_delete: bool) -> Markup {
|
||||
}
|
||||
|
||||
fn render_other_attachments(msg: &Message, has_content_or_images: bool) -> Markup {
|
||||
let others: Vec<&Attachment> = msg.attachments.iter().filter(|a| !is_image(a)).collect();
|
||||
if others.is_empty() {
|
||||
let mut others = msg.attachments.iter().filter(|a| !is_image(a)).peekable();
|
||||
if others.peek().is_none() {
|
||||
return html! {};
|
||||
}
|
||||
let spacer = if has_content_or_images {
|
||||
@@ -127,7 +127,7 @@ fn render_other_attachments(msg: &Message, has_content_or_images: bool) -> Marku
|
||||
};
|
||||
html! {
|
||||
div class=(spacer) {
|
||||
@for att in &others {
|
||||
@for att in others {
|
||||
div class="flex flex-wrap items-center gap-2 text-xs" {
|
||||
(paperclip_icon("text-neutral-400"))
|
||||
a href=(att.url) target="_blank" rel="noopener noreferrer"
|
||||
|
||||
@@ -17,7 +17,6 @@ pub mod message_data;
|
||||
pub mod message_list;
|
||||
pub mod nsfw_indicators;
|
||||
pub mod page_container;
|
||||
pub mod pagination;
|
||||
pub mod resource_link;
|
||||
pub mod section_card;
|
||||
pub mod stack;
|
||||
|
||||
@@ -19,7 +19,7 @@ pub fn adult_content_badge(is_adult: bool, label: Option<&str>) -> Markup {
|
||||
}
|
||||
|
||||
fn truncate(text: &str, max: usize) -> String {
|
||||
if text.len() <= max {
|
||||
if text.chars().nth(max).is_none() {
|
||||
text.to_owned()
|
||||
} else {
|
||||
let boundary = max.saturating_sub(1);
|
||||
@@ -29,10 +29,7 @@ fn truncate(text: &str, max: usize) -> String {
|
||||
}
|
||||
|
||||
pub fn content_warning_badge(level: Option<i32>, text: Option<&str>, inline_text: bool) -> Markup {
|
||||
let Some(lvl) = level else {
|
||||
return html! {};
|
||||
};
|
||||
if lvl != CONTENT_WARNING_LEVEL {
|
||||
if level != Some(CONTENT_WARNING_LEVEL) {
|
||||
return html! {};
|
||||
}
|
||||
let default_text = "This contains sensitive content.";
|
||||
@@ -82,13 +79,8 @@ fn resolve_nsfw_source(
|
||||
|
||||
fn source_label(source: NsfwSource, explicit: Option<bool>) -> &'static str {
|
||||
match source {
|
||||
NsfwSource::Channel => {
|
||||
if explicit == Some(true) {
|
||||
"(override on)"
|
||||
} else {
|
||||
"(override off)"
|
||||
}
|
||||
}
|
||||
NsfwSource::Channel if explicit == Some(true) => "(override on)",
|
||||
NsfwSource::Channel => "(override off)",
|
||||
NsfwSource::Category => "(from category)",
|
||||
NsfwSource::Community => "(from community)",
|
||||
NsfwSource::None => "",
|
||||
@@ -110,11 +102,7 @@ pub fn channel_nsfw_state_badge(
|
||||
}
|
||||
let has_context =
|
||||
nsfw_override.is_some() || category_override.is_some() || guild_nsfw.is_some();
|
||||
let (source, explicit) = if has_context {
|
||||
resolve_nsfw_source(nsfw_override, category_override, guild_nsfw)
|
||||
} else {
|
||||
(NsfwSource::None, None)
|
||||
};
|
||||
let (source, explicit) = resolve_nsfw_source(nsfw_override, category_override, guild_nsfw);
|
||||
html! {
|
||||
span class="inline-flex flex-wrap items-center gap-1" {
|
||||
@if is_nsfw {
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use maud::{Markup, PreEscaped, html};
|
||||
|
||||
pub fn pagination(base_url: &str, current_page: u32, has_more: bool, extra_params: &str) -> Markup {
|
||||
let sep = if base_url.contains('?') { "&" } else { "?" };
|
||||
let has_previous = current_page > 1;
|
||||
html! {
|
||||
div class="mt-4 flex items-center justify-between" {
|
||||
@if has_previous {
|
||||
p class="text-sm font-normal text-neutral-500" {
|
||||
a href={
|
||||
(base_url) (sep) "page=" (current_page - 1) (extra_params)
|
||||
} class="text-neutral-900 underline decoration-neutral-300 hover:text-neutral-600 hover:decoration-neutral-500 hover:text-neutral-900" {
|
||||
(PreEscaped("← Previous"))
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
span {}
|
||||
}
|
||||
@if has_more {
|
||||
p class="text-sm font-normal text-neutral-500" {
|
||||
a href={
|
||||
(base_url) (sep) "page=" (current_page + 1) (extra_params)
|
||||
} class="text-neutral-900 underline decoration-neutral-300 hover:text-neutral-600 hover:decoration-neutral-500 hover:text-neutral-900" {
|
||||
(PreEscaped("Next →"))
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
span {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,6 @@ const RESOURCE_LINK_CLASS: &str = "text-neutral-900 underline decoration-neutral
|
||||
hover:text-neutral-600 hover:decoration-neutral-500 text-sm";
|
||||
const NAV_LINK_CLASS: &str = "label rounded-lg border border-neutral-300 bg-white \
|
||||
px-3 py-2 text-neutral-700 transition-colors hover:bg-neutral-50";
|
||||
const TEXT_LINK_CLASS: &str = "text-neutral-900 underline decoration-neutral-300 \
|
||||
hover:text-neutral-600 hover:decoration-neutral-500";
|
||||
|
||||
impl ResourceType {
|
||||
fn path_segment(self) -> &'static str {
|
||||
@@ -91,18 +89,3 @@ pub fn nav_link(href: &str, content: Markup) -> Markup {
|
||||
a href=(href) class=(NAV_LINK_CLASS) { (content) }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn text_link(href: &str, content: Markup, external: bool, _mono: bool) -> Markup {
|
||||
if external {
|
||||
html! {
|
||||
a href=(href) class=(TEXT_LINK_CLASS)
|
||||
target="_blank" rel="noopener noreferrer" {
|
||||
(content)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
html! {
|
||||
a href=(href) class=(TEXT_LINK_CLASS) { (content) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,27 +72,22 @@ fn filters_section(base: &str, params: &AuditLogsParams<'_>) -> Markup {
|
||||
}
|
||||
|
||||
fn build_pagination_url(base: &str, page: u32, params: &AuditLogsParams<'_>) -> String {
|
||||
let mut url = format!("{base}/audit-logs?page={page}");
|
||||
if !params.query.is_empty() {
|
||||
url.push_str(&format!("&q={}", params.query));
|
||||
}
|
||||
if !params.admin_user_id.is_empty() {
|
||||
url.push_str(&format!("&admin_user_id={}", params.admin_user_id));
|
||||
}
|
||||
if !params.target_id.is_empty() {
|
||||
url.push_str(&format!("&target_id={}", params.target_id));
|
||||
}
|
||||
if !params.target_type.is_empty() {
|
||||
url.push_str(&format!("&target_type={}", params.target_type));
|
||||
}
|
||||
if !params.sort_by.is_empty() {
|
||||
url.push_str(&format!("&sort_by={}", params.sort_by));
|
||||
}
|
||||
if !params.sort_order.is_empty() {
|
||||
url.push_str(&format!("&sort_order={}", params.sort_order));
|
||||
}
|
||||
url.push_str(&format!("&limit={}", params.limit));
|
||||
url
|
||||
let mut query = url::form_urlencoded::Serializer::new(String::new());
|
||||
query.append_pair("page", &page.to_string());
|
||||
query.extend_pairs(
|
||||
[
|
||||
("q", params.query),
|
||||
("admin_user_id", params.admin_user_id),
|
||||
("target_id", params.target_id),
|
||||
("target_type", params.target_type),
|
||||
("sort_by", params.sort_by),
|
||||
("sort_order", params.sort_order),
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|(_, value)| !value.is_empty()),
|
||||
);
|
||||
query.append_pair("limit", ¶ms.limit.to_string());
|
||||
format!("{base}/audit-logs?{}", query.finish())
|
||||
}
|
||||
|
||||
pub fn audit_logs_page(
|
||||
@@ -107,10 +102,15 @@ pub fn audit_logs_page(
|
||||
let total = data.total;
|
||||
let entries = &data.logs;
|
||||
let total_pages = if params.limit > 0 {
|
||||
((total as f64) / (params.limit as f64)).ceil().max(1.0) as u64
|
||||
total.div_ceil(u64::from(params.limit)).max(1)
|
||||
} else {
|
||||
1
|
||||
};
|
||||
let page_number = u64::from(params.current_page) + 1;
|
||||
let next_page = params
|
||||
.current_page
|
||||
.checked_add(1)
|
||||
.filter(|page| u64::from(*page) < total_pages);
|
||||
let showing = format!("Showing {} of {} entries", entries.len(), total);
|
||||
html! {
|
||||
(page_header_with_actions("Audit Logs", None, html! {
|
||||
@@ -134,10 +134,10 @@ pub fn audit_logs_page(
|
||||
}
|
||||
} @else { span {} }
|
||||
span class="text-sm text-neutral-500" {
|
||||
"Page " (params.current_page + 1) " of " (total_pages)
|
||||
"Page " (page_number) " of " (total_pages)
|
||||
}
|
||||
@if (params.current_page + 1) < total_pages as u32 {
|
||||
a href=(build_pagination_url(base, params.current_page + 1, params))
|
||||
@if let Some(page) = next_page {
|
||||
a href=(build_pagination_url(base, page, params))
|
||||
class="text-sm text-neutral-900 underline" {
|
||||
(PreEscaped("Next →"))
|
||||
}
|
||||
|
||||
@@ -14,12 +14,7 @@ use crate::{
|
||||
use maud::{Markup, html};
|
||||
|
||||
pub fn format_action(action: &str) -> String {
|
||||
let replaced = action.replace('_', " ");
|
||||
let mut chars = replaced.chars();
|
||||
match chars.next() {
|
||||
None => String::new(),
|
||||
Some(c) => c.to_uppercase().to_string() + chars.as_str(),
|
||||
}
|
||||
capitalise(&action.replace('_', " "))
|
||||
}
|
||||
|
||||
pub fn action_badge_variant(action: &str) -> BadgeVariant {
|
||||
@@ -92,10 +87,8 @@ pub fn admin_user_cell(base: &str, entry: &AuditLogEntry) -> Markup {
|
||||
}
|
||||
}
|
||||
|
||||
fn target_guild_label(guild: Option<&AuditLogGuildSummary>) -> String {
|
||||
guild
|
||||
.map(|guild| guild.name.clone())
|
||||
.unwrap_or_else(|| "Guild".to_owned())
|
||||
fn target_guild_label(guild: Option<&AuditLogGuildSummary>) -> &str {
|
||||
guild.map(|guild| guild.name.as_str()).unwrap_or("Guild")
|
||||
}
|
||||
|
||||
pub fn target_cell(base: &str, entry: &AuditLogEntry) -> Markup {
|
||||
|
||||
@@ -16,7 +16,7 @@ use crate::{
|
||||
};
|
||||
use maud::{Markup, html};
|
||||
|
||||
const MAX_GIFT_CODES: u32 = 100;
|
||||
pub const MAX_GIFT_CODES: u32 = 100;
|
||||
const DEFAULT_GIFT_COUNT: u32 = 10;
|
||||
|
||||
pub fn gift_codes_page(
|
||||
@@ -44,12 +44,12 @@ pub fn gift_codes_page(
|
||||
(csrf_input(csrf_token))
|
||||
div class="flex flex-col gap-4" {
|
||||
(form_field_group(
|
||||
"Number of codes", "gift-count-slider", false, None,
|
||||
"Number of codes", "gift-count-slider", true, None,
|
||||
Some(&format!("Range: 1-{MAX_GIFT_CODES}")),
|
||||
html! {
|
||||
input type="number" id="gift-count-slider" name="count"
|
||||
value=(DEFAULT_GIFT_COUNT) min="1"
|
||||
max=(MAX_GIFT_CODES)
|
||||
max=(MAX_GIFT_CODES) required
|
||||
class=(FORM_INPUT_CLASS);
|
||||
},
|
||||
))
|
||||
|
||||
@@ -25,7 +25,9 @@ pub fn members_tab(
|
||||
let total = response.total;
|
||||
let total_pages = if limit > 0 { total.div_ceil(limit) } else { 1 };
|
||||
let has_previous = page > 0;
|
||||
let has_next = (page as u64) < total_pages.saturating_sub(1);
|
||||
let next_page = page
|
||||
.checked_add(1)
|
||||
.filter(|page| u64::from(*page) < total_pages);
|
||||
|
||||
html! {
|
||||
div class="space-y-4" {
|
||||
@@ -34,12 +36,16 @@ pub fn members_tab(
|
||||
"Guild Members (" (total) ")"
|
||||
}
|
||||
p class="text-sm text-neutral-500" {
|
||||
@let start = response.offset + 1;
|
||||
@let end = std::cmp::min(
|
||||
response.offset + response.members.len() as u64,
|
||||
total,
|
||||
);
|
||||
"Showing " (start) "-" (end) " of " (total)
|
||||
@if response.members.is_empty() {
|
||||
"Showing 0 of " (total)
|
||||
} @else {
|
||||
@let start = u128::from(response.offset) + 1;
|
||||
@let end = std::cmp::min(
|
||||
u128::from(response.offset) + response.members.len() as u128,
|
||||
u128::from(total),
|
||||
);
|
||||
"Showing " (start) "-" (end) " of " (total)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,10 +76,10 @@ pub fn members_tab(
|
||||
}
|
||||
}
|
||||
p class="text-sm text-neutral-500" {
|
||||
"Page " (page + 1) " of " (total_pages)
|
||||
"Page " (u64::from(page) + 1) " of " (total_pages)
|
||||
}
|
||||
@if has_next {
|
||||
a href={(base) "/guilds/" (guild.id) "?tab=members&page=" (page + 1)}
|
||||
@if let Some(next_page) = next_page {
|
||||
a href={(base) "/guilds/" (guild.id) "?tab=members&page=" (next_page)}
|
||||
class="inline-flex items-center rounded-md bg-brand-primary px-3 \
|
||||
py-2 text-sm font-medium text-white hover:bg-brand-primary-dark" {
|
||||
"Next \u{2192}"
|
||||
|
||||
@@ -40,13 +40,13 @@ fn channel_type_label(channel_type: i32) -> &'static str {
|
||||
pub fn overview_tab(config: &AdminConfig, guild: &GuildDetailInfo, csrf_token: &str) -> Markup {
|
||||
let base = &config.base_path;
|
||||
|
||||
let mut sorted_channels = guild.channels.clone();
|
||||
let mut sorted_channels: Vec<_> = guild.channels.iter().collect();
|
||||
sorted_channels.sort_by_key(|c| c.position);
|
||||
|
||||
let channels_by_id: std::collections::HashMap<&str, &crate::api::types::GuildChannelSummary> =
|
||||
guild.channels.iter().map(|c| (c.id.as_str(), c)).collect();
|
||||
|
||||
let mut sorted_roles = guild.roles.clone();
|
||||
let mut sorted_roles: Vec<_> = guild.roles.iter().collect();
|
||||
sorted_roles.sort_by_key(|role| std::cmp::Reverse(role.position));
|
||||
|
||||
let icon_url = guild_icon_url(config, &guild.id, guild.icon.as_deref(), 256, true);
|
||||
@@ -102,16 +102,10 @@ pub fn overview_tab(config: &AdminConfig, guild: &GuildDetailInfo, csrf_token: &
|
||||
p class="text-sm font-semibold text-neutral-500" {
|
||||
"Custom warning text"
|
||||
}
|
||||
@if let Some(ref text) = guild.content_warning_text {
|
||||
@if !text.trim().is_empty() {
|
||||
blockquote class="border-amber-300 border-l-2 bg-amber-50 \
|
||||
px-3 py-2 text-neutral-800 text-sm italic" {
|
||||
(text)
|
||||
}
|
||||
} @else {
|
||||
p class="text-sm text-neutral-500" {
|
||||
"\u{2014} (default fallback shown to users)"
|
||||
}
|
||||
@if let Some(text) = guild.content_warning_text.as_deref().filter(|text| !text.trim().is_empty()) {
|
||||
blockquote class="border-amber-300 border-l-2 bg-amber-50 \
|
||||
px-3 py-2 text-neutral-800 text-sm italic" {
|
||||
(text)
|
||||
}
|
||||
} @else {
|
||||
p class="text-sm text-neutral-500" {
|
||||
|
||||
@@ -17,8 +17,9 @@ pub fn reports_tab(
|
||||
let base = &config.base_path;
|
||||
let page_size: u64 = 25;
|
||||
let has_previous = page > 0;
|
||||
let has_next = (page as u64) * page_size + reports.len() as u64 > 0
|
||||
&& (page as u64 + 1) * page_size < total;
|
||||
let next_page = page.checked_add(1).filter(|next_page| {
|
||||
(page > 0 || !reports.is_empty()) && u64::from(*next_page) * page_size < total
|
||||
});
|
||||
html! {
|
||||
div class="space-y-4" {
|
||||
div class="flex items-center justify-between" {
|
||||
@@ -48,7 +49,7 @@ pub fn reports_tab(
|
||||
))
|
||||
}
|
||||
|
||||
@if has_previous || has_next {
|
||||
@if has_previous || next_page.is_some() {
|
||||
div class="flex justify-center gap-2" {
|
||||
@if has_previous {
|
||||
a href={(base) "/guilds/" (guild.id) "?tab=reports&reports_page=" (page - 1)}
|
||||
@@ -58,8 +59,8 @@ pub fn reports_tab(
|
||||
"\u{2190} Newer"
|
||||
}
|
||||
}
|
||||
@if has_next {
|
||||
a href={(base) "/guilds/" (guild.id) "?tab=reports&reports_page=" (page + 1)}
|
||||
@if let Some(next_page) = next_page {
|
||||
a href={(base) "/guilds/" (guild.id) "?tab=reports&reports_page=" (next_page)}
|
||||
class="inline-flex items-center rounded-md border border-neutral-300 \
|
||||
bg-white px-3 py-2 text-sm font-medium text-neutral-700 \
|
||||
hover:bg-neutral-50" {
|
||||
|
||||
@@ -64,7 +64,6 @@ impl GuildsListParams {
|
||||
pub struct GuildsListResults<'a> {
|
||||
pub guilds: Option<&'a [GuildInfo]>,
|
||||
pub total: Option<u64>,
|
||||
pub has_more: bool,
|
||||
}
|
||||
|
||||
pub fn guilds_list_page(
|
||||
@@ -73,14 +72,12 @@ pub fn guilds_list_page(
|
||||
params: &GuildsListParams,
|
||||
results: Option<&[GuildInfo]>,
|
||||
total: Option<u64>,
|
||||
has_more: bool,
|
||||
is_htmx: bool,
|
||||
) -> Markup {
|
||||
let base = &config.base_path;
|
||||
let result_state = GuildsListResults {
|
||||
guilds: results,
|
||||
total,
|
||||
has_more,
|
||||
};
|
||||
let has_results = results.is_some_and(|guilds| !guilds.is_empty());
|
||||
let header = html! {
|
||||
@@ -164,7 +161,10 @@ fn render_results(
|
||||
fn pagination(config: &AdminConfig, params: &GuildsListParams, total: u64) -> Markup {
|
||||
let total_pages = total.div_ceil(u64::from(params.limit)).max(1);
|
||||
let has_previous = params.page > 0;
|
||||
let has_next = u64::from(params.page) < total_pages.saturating_sub(1);
|
||||
let next_page = params
|
||||
.page
|
||||
.checked_add(1)
|
||||
.filter(|page| u64::from(*page) < total_pages);
|
||||
html! {
|
||||
div class="mt-6 flex items-center justify-center gap-3" {
|
||||
@if has_previous {
|
||||
@@ -181,10 +181,10 @@ fn pagination(config: &AdminConfig, params: &GuildsListParams, total: u64) -> Ma
|
||||
}
|
||||
}
|
||||
span class="text-neutral-600 text-sm" {
|
||||
"Page " (params.page + 1) " of " (total_pages)
|
||||
"Page " (u64::from(params.page) + 1) " of " (total_pages)
|
||||
}
|
||||
@if has_next {
|
||||
a href=(pagination_url(&config.base_path, params, params.page + 1))
|
||||
@if let Some(next_page) = next_page {
|
||||
a href=(pagination_url(&config.base_path, params, next_page))
|
||||
class="rounded-lg bg-neutral-900 px-6 py-2 font-medium text-sm \
|
||||
text-white no-underline transition-colors hover:bg-neutral-800" {
|
||||
"Next \u{2192}"
|
||||
@@ -200,8 +200,7 @@ fn pagination(config: &AdminConfig, params: &GuildsListParams, total: u64) -> Ma
|
||||
}
|
||||
|
||||
fn pagination_url(base: &str, params: &GuildsListParams, page: u32) -> String {
|
||||
let mut parts = Vec::new();
|
||||
parts.push(format!("page={page}"));
|
||||
let mut parts = vec![format!("page={page}"), format!("limit={}", params.limit)];
|
||||
if !params.q.is_empty() {
|
||||
parts.push(format!("q={}", urlencoding::encode(¶ms.q)));
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ use crate::{
|
||||
templates::{
|
||||
components::{
|
||||
auto_refresh::auto_refresh,
|
||||
badge::{BadgeVariant, badge},
|
||||
data_field::{data_field_mono, data_field_text, data_grid},
|
||||
form::{csrf_input, danger_button, form_field_group},
|
||||
page_container::{page_header_with_actions, page_header_with_back},
|
||||
@@ -18,17 +17,7 @@ use crate::{
|
||||
};
|
||||
use maud::{Markup, html};
|
||||
|
||||
fn status_badge(status: &str) -> Markup {
|
||||
let variant = match status {
|
||||
"queued" => BadgeVariant::Default,
|
||||
"running" => BadgeVariant::Info,
|
||||
"succeeded" => BadgeVariant::Success,
|
||||
"failed" | "deadletter" => BadgeVariant::Danger,
|
||||
"cancelled" => BadgeVariant::Warning,
|
||||
_ => BadgeVariant::Default,
|
||||
};
|
||||
badge(status, variant)
|
||||
}
|
||||
use super::jobs_list_helpers::status_badge;
|
||||
|
||||
fn val_str<'a>(job: &'a serde_json::Value, key: &str) -> &'a str {
|
||||
job.get(key).and_then(|v| v.as_str()).unwrap_or("")
|
||||
@@ -71,7 +60,7 @@ fn progress_bar(job: &serde_json::Value) -> Markup {
|
||||
}
|
||||
(cur, Some(tot)) => {
|
||||
let c = cur.unwrap_or(0);
|
||||
let pct = (c * 100 / tot).min(100);
|
||||
let pct = (u128::from(c) * 100 / u128::from(tot)).min(100);
|
||||
html! {
|
||||
div role="progressbar" aria-valuenow=(pct) aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
|
||||
@@ -33,25 +33,26 @@ pub(crate) fn format_progress(job: &serde_json::Value) -> String {
|
||||
(Some(cur), None | Some(0)) => format!("{cur}"),
|
||||
(cur, Some(tot)) => {
|
||||
let c = cur.unwrap_or(0);
|
||||
let pct = c.saturating_mul(100).checked_div(tot).unwrap_or(0);
|
||||
let pct = (u128::from(c) * 100)
|
||||
.checked_div(u128::from(tot))
|
||||
.unwrap_or(0);
|
||||
format!("{c} / {tot} ({pct}%)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn job_row(base: &str, job: &serde_json::Value) -> Markup {
|
||||
let job_id = job
|
||||
.get("job_id")
|
||||
.and_then(|v| v.as_str().or_else(|| v.as_u64().map(|_| "")))
|
||||
.unwrap_or("");
|
||||
let job_id_display = job
|
||||
fn job_id_text(value: &serde_json::Value) -> String {
|
||||
value
|
||||
.get("job_id")
|
||||
.map(|v| match v {
|
||||
serde_json::Value::String(s) => s.clone(),
|
||||
serde_json::Value::Number(n) => n.to_string(),
|
||||
_ => v.to_string(),
|
||||
})
|
||||
.unwrap_or_default();
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(crate) fn job_row(base: &str, job: &serde_json::Value) -> Markup {
|
||||
let job_id = job_id_text(job);
|
||||
let task_type = job
|
||||
.get("task_type")
|
||||
.and_then(|v| v.as_str())
|
||||
@@ -79,23 +80,17 @@ pub(crate) fn job_row(base: &str, job: &serde_json::Value) -> Markup {
|
||||
.unwrap_or_else(|| "cron".to_owned());
|
||||
let progress = format_progress(job);
|
||||
|
||||
let link_id = if job_id.is_empty() {
|
||||
&job_id_display
|
||||
} else {
|
||||
job_id
|
||||
};
|
||||
|
||||
table_row(html! {
|
||||
(table_cell(true, html! {
|
||||
span class="whitespace-nowrap text-sm" { (created_at) }
|
||||
}))
|
||||
(table_cell(false, html! {
|
||||
a href={(base) "/jobs/" (link_id)}
|
||||
a href={(base) "/jobs/" (job_id)}
|
||||
hx-target="#main-content"
|
||||
hx-swap="innerHTML"
|
||||
hx-push-url="true"
|
||||
class="text-blue-600 text-sm hover:underline" {
|
||||
(job_id_display)
|
||||
(job_id)
|
||||
}
|
||||
}))
|
||||
(table_cell(false, html! {
|
||||
@@ -103,7 +98,7 @@ pub(crate) fn job_row(base: &str, job: &serde_json::Value) -> Markup {
|
||||
}))
|
||||
(table_cell(false, html! { (status_badge(status)) }))
|
||||
(table_cell(false, html! {
|
||||
span class="text-sm" data-job-progress=(link_id) { (progress) }
|
||||
span class="text-sm" data-job-progress=(job_id) { (progress) }
|
||||
}))
|
||||
(table_cell(true, html! {
|
||||
span class="text-sm" { (attempts) "/" (max_attempts) }
|
||||
@@ -112,7 +107,7 @@ pub(crate) fn job_row(base: &str, job: &serde_json::Value) -> Markup {
|
||||
span class="text-sm" { (requester) }
|
||||
}))
|
||||
(table_cell(false, html! {
|
||||
a href={(base) "/jobs/" (link_id)}
|
||||
a href={(base) "/jobs/" (job_id)}
|
||||
hx-target="#main-content"
|
||||
hx-swap="innerHTML"
|
||||
hx-push-url="true"
|
||||
@@ -159,14 +154,7 @@ pub(crate) fn next_page_link(
|
||||
.get("created_at")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
let job_id = cursor
|
||||
.get("job_id")
|
||||
.map(|v| match v {
|
||||
serde_json::Value::String(s) => s.clone(),
|
||||
serde_json::Value::Number(n) => n.to_string(),
|
||||
_ => v.to_string(),
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let job_id = job_id_text(cursor);
|
||||
|
||||
let mut params = vec![
|
||||
format!("cursor_bucket_day={bucket_day}"),
|
||||
|
||||
@@ -466,7 +466,10 @@ fn reports_pagination(
|
||||
limit: u32,
|
||||
total: u64,
|
||||
) -> Markup {
|
||||
let total_pages = ((total + u64::from(limit).saturating_sub(1)) / u64::from(limit)).max(1);
|
||||
let total_pages = total.div_ceil(u64::from(limit)).max(1);
|
||||
let next_page = page
|
||||
.checked_add(1)
|
||||
.filter(|page| u64::from(*page) < total_pages);
|
||||
html! {
|
||||
div class="mt-4 flex items-center justify-between" {
|
||||
@if page > 0 {
|
||||
@@ -478,10 +481,10 @@ fn reports_pagination(
|
||||
span {}
|
||||
}
|
||||
span class="text-neutral-500 text-sm" {
|
||||
"Page " (page + 1) " of " (total_pages)
|
||||
"Page " (u64::from(page) + 1) " of " (total_pages)
|
||||
}
|
||||
@if u64::from(page + 1) < total_pages {
|
||||
a href=(reports_url(config, filters, page + 1, limit))
|
||||
@if let Some(next_page) = next_page {
|
||||
a href=(reports_url(config, filters, next_page, limit))
|
||||
class="text-neutral-900 underline decoration-neutral-300 hover:text-neutral-600 hover:decoration-neutral-500" {
|
||||
(PreEscaped("Next →"))
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ fn status_section(base: &str, rs: &RefreshStatus) -> Markup {
|
||||
|
||||
@if is_in_progress {
|
||||
@if let (Some(idx), Some(tot)) = (rs.indexed, rs.total) {
|
||||
@let pct = idx.saturating_mul(100).checked_div(tot).unwrap_or(0);
|
||||
@let pct = (u128::from(idx) * 100).checked_div(u128::from(tot)).unwrap_or(0);
|
||||
div class="space-y-2" {
|
||||
div class="flex items-center justify-between" {
|
||||
p class="text-sm text-neutral-700" {
|
||||
|
||||
@@ -146,7 +146,7 @@ fn deletion_card(base: &str, user: &AdminUser, csrf_token: &str) -> Markup {
|
||||
(csrf_input(csrf_token))
|
||||
div class="space-y-3" {
|
||||
(form_label("Days until deletion"))
|
||||
input type="number" name="days" value="60"
|
||||
input type="number" name="days_until_deletion" value="60"
|
||||
min="60" max="365"
|
||||
class="block w-full rounded-md border border-neutral-300 \
|
||||
px-3 py-2 text-sm shadow-sm \
|
||||
@@ -322,9 +322,8 @@ fn message_shred_status_content(status: &serde_json::Value) -> Markup {
|
||||
let total = value_u64(status, "total").unwrap_or(0);
|
||||
let processed = value_u64(status, "processed").unwrap_or(0);
|
||||
let skipped = value_u64(status, "skipped").unwrap_or(0);
|
||||
let percentage = processed
|
||||
.saturating_mul(100)
|
||||
.checked_div(total)
|
||||
let percentage = (u128::from(processed) * 100)
|
||||
.checked_div(u128::from(total))
|
||||
.unwrap_or(0)
|
||||
.min(100);
|
||||
html! {
|
||||
|
||||
@@ -78,9 +78,11 @@ fn report_section(
|
||||
}
|
||||
};
|
||||
|
||||
let offset = current_page as u64 * limit as u64;
|
||||
let offset = u64::from(current_page) * u64::from(limit);
|
||||
let has_previous = current_page > 0;
|
||||
let has_next = offset + (reports.len() as u64) < total;
|
||||
let next_page = current_page
|
||||
.checked_add(1)
|
||||
.filter(|_| (reports.len() as u64) < total.saturating_sub(offset));
|
||||
|
||||
let (sent_page, received_page) = if kind == "sent" {
|
||||
(current_page, other_page)
|
||||
@@ -115,7 +117,7 @@ fn report_section(
|
||||
))
|
||||
}
|
||||
|
||||
@if has_previous || has_next {
|
||||
@if has_previous || next_page.is_some() {
|
||||
div class="flex justify-center gap-2" {
|
||||
@if has_previous {
|
||||
a href={(base) "/users/" (user_id) "?tab=reports&reports_limit=" (limit) "&reports_sent_page=" (if kind == "sent" { current_page - 1 } else { sent_page }) "&reports_received_page=" (if kind == "received" { current_page - 1 } else { received_page })}
|
||||
@@ -125,8 +127,8 @@ fn report_section(
|
||||
"Previous"
|
||||
}
|
||||
}
|
||||
@if has_next {
|
||||
a href={(base) "/users/" (user_id) "?tab=reports&reports_limit=" (limit) "&reports_sent_page=" (if kind == "sent" { current_page + 1 } else { sent_page }) "&reports_received_page=" (if kind == "received" { current_page + 1 } else { received_page })}
|
||||
@if let Some(next_page) = next_page {
|
||||
a href={(base) "/users/" (user_id) "?tab=reports&reports_limit=" (limit) "&reports_sent_page=" (if kind == "sent" { next_page } else { sent_page }) "&reports_received_page=" (if kind == "received" { next_page } else { received_page })}
|
||||
class="inline-flex items-center rounded-md border \
|
||||
border-neutral-300 bg-white px-3 py-2 text-sm \
|
||||
font-medium text-neutral-700 hover:bg-neutral-50" {
|
||||
|
||||
@@ -393,6 +393,7 @@ fn render_users_table(config: &AdminConfig, users: &[AdminUser], can_view_email:
|
||||
}
|
||||
|
||||
fn pagination_controls(base: &str, params: &UserListParams, has_more: bool) -> Markup {
|
||||
let next_page = params.page.checked_add(1).filter(|_| has_more);
|
||||
html! {
|
||||
div class="mt-4 flex items-center justify-between" {
|
||||
@if params.page > 0 {
|
||||
@@ -403,8 +404,8 @@ fn pagination_controls(base: &str, params: &UserListParams, has_more: bool) -> M
|
||||
} @else {
|
||||
span {}
|
||||
}
|
||||
@if has_more {
|
||||
a href=(users_url(base, params, params.page + 1))
|
||||
@if let Some(next_page) = next_page {
|
||||
a href=(users_url(base, params, next_page))
|
||||
class="text-neutral-900 underline decoration-neutral-300 hover:text-neutral-600 hover:decoration-neutral-500" {
|
||||
"Next >"
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user