From 578ec187cd17d1a614f869889e0c3fc5f612ecac Mon Sep 17 00:00:00 2001 From: LynithDev <61880709+LynithDev@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:27:13 +0200 Subject: [PATCH] feat: added search bar into Game Output live logs (#716) Co-authored-by: rozwader --- .../src/components/log_viewer.rs | 2 +- packages/oneclient_app/src/components/mod.rs | 1 + .../src/components/upload_mclogs.rs | 70 ++++++++++++++++++ .../src/view/app/cluster/logs.rs | 67 ++++------------- .../oneclient_app/src/view/app/cluster/mod.rs | 2 +- .../src/view/app/cluster/process_logs.rs | 74 ++++++++++++++++++- 6 files changed, 159 insertions(+), 57 deletions(-) create mode 100644 packages/oneclient_app/src/components/upload_mclogs.rs diff --git a/packages/oneclient_app/src/components/log_viewer.rs b/packages/oneclient_app/src/components/log_viewer.rs index 306991fe..49b18d55 100644 --- a/packages/oneclient_app/src/components/log_viewer.rs +++ b/packages/oneclient_app/src/components/log_viewer.rs @@ -247,7 +247,7 @@ fn toolbar(title: String, streaming: bool, auto_scroll: State) -> impl Int .width(Size::fill()) .cross_align(Alignment::Center) .content(Content::Flex) - .padding(Gaps::new(12., 20., 12., 20.)) + .padding(Gaps::new(12., 20., 0., 20.)) .spacing(12.) .child( label() diff --git a/packages/oneclient_app/src/components/mod.rs b/packages/oneclient_app/src/components/mod.rs index af74c911..d9d29b74 100644 --- a/packages/oneclient_app/src/components/mod.rs +++ b/packages/oneclient_app/src/components/mod.rs @@ -36,6 +36,7 @@ mod toasts; mod toggle; mod update_prompt; mod version_card; +pub mod upload_mclogs; pub use account_switcher::AccountSwitcher; pub use active_cluster_panel::ActiveClusterPanel; diff --git a/packages/oneclient_app/src/components/upload_mclogs.rs b/packages/oneclient_app/src/components/upload_mclogs.rs new file mode 100644 index 00000000..81ead70a --- /dev/null +++ b/packages/oneclient_app/src/components/upload_mclogs.rs @@ -0,0 +1,70 @@ +use freya::prelude::*; +use freya::query::MutationStateData; +use freya::text_edit::Clipboard; + +use crate::components::{Button, Icon, IconType}; +use crate::hooks::{UseUploadLog, use_dispatch}; +use crate::view::app::cluster::logs::Confirm; + +#[derive(PartialEq)] +pub struct UploadToMclogs { + has_log: bool, + confirm: State>, +} + +impl UploadToMclogs { + pub fn new(has_log: bool, confirm: State>) -> Self { + Self { + has_log, + confirm + } + } +} + +impl Component for UploadToMclogs { + fn render(&self) -> impl IntoElement { + let mut confirm = self.confirm; + + Button::new() + .secondary() + .enabled(self.has_log) + .on_press(move |_| confirm.set(Some(Confirm::Upload))) + .child(Icon::new(IconType::LinkExternal01).size(15.)) + .text("Upload to mclo.gs") + } +} + +pub fn use_mclogs_feedback(upload: UseUploadLog) { + let dispatch = use_dispatch(); + let mut handled = use_state(|| None::); + + use_side_effect(move || match &*upload.read().state() { + MutationStateData::Settled { + res: Ok(result), .. + } => { + if handled.peek().as_deref() == Some(result.url.as_str()) { + return; + } + + handled.set(Some(result.url.clone())); + let _ = Clipboard::set(result.url.clone()); + + dispatch + .notify("Uploaded to mclo.gs") + .body(format!("{} (copied to clipboard)", result.url)) + .info() + .icon(IconType::LinkExternal01) + .send(); + } + MutationStateData::Settled { res: Err(err), .. } => { + let msg = err.to_string(); + if handled.peek().as_deref() == Some(msg.as_str()) { + return; + } + + handled.set(Some(msg.clone())); + dispatch.notify("Upload failed").body(msg).error().send(); + } + _ => {} + }); +} diff --git a/packages/oneclient_app/src/view/app/cluster/logs.rs b/packages/oneclient_app/src/view/app/cluster/logs.rs index 9e4fb1cf..332bf38a 100644 --- a/packages/oneclient_app/src/view/app/cluster/logs.rs +++ b/packages/oneclient_app/src/view/app/cluster/logs.rs @@ -2,19 +2,19 @@ use std::path::PathBuf; use std::sync::Arc; use freya::prelude::*; -use freya::query::{MutationStateData, UseQuery}; -use freya::text_edit::Clipboard; +use freya::query::UseQuery; use oneclient_common::search::SearchQuery; use oneclient_core::{LogFileInfo, LogKind, LogLevel}; +use crate::components::upload_mclogs::{UploadToMclogs, use_mclogs_feedback}; use crate::components::{ Button, Icon, IconType, LogViewer, OverlayPopup, ScrollArea, Segment, SegmentedControl, TextInput, open_folder_button, }; use crate::hooks::{ ClusterLogsQuery, LogAction, UploadLogKeys, UseLogAction, UseUploadLog, - invalidate_logs_queries, try_cluster_logs, try_log_content, use_cluster_logs, use_dispatch, - use_log_action, use_log_content, use_upload_log, + invalidate_logs_queries, try_cluster_logs, try_log_content, use_cluster_logs, use_log_action, + use_log_content, use_upload_log, }; use crate::layout::cluster_content; use crate::theme::colors; @@ -29,7 +29,7 @@ const PICKER_ROW_SPACING: f32 = 2.; const PICKER_LIST_MAX_H: f32 = 300.; #[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum LevelFilter { +pub enum LevelFilter { All, Error, Warn, @@ -38,7 +38,7 @@ enum LevelFilter { } impl LevelFilter { - fn to_level(self) -> Option { + pub fn to_level(self) -> Option { match self { Self::All => None, Self::Error => Some(LogLevel::Error), @@ -48,7 +48,7 @@ impl LevelFilter { } } - fn label(self) -> &'static str { + pub fn label(self) -> &'static str { match self { Self::All => "All", Self::Error => "Errors", @@ -60,14 +60,14 @@ impl LevelFilter { } #[derive(Clone, Copy, PartialEq)] -enum Confirm { +pub enum Confirm { Clear, Upload, } /// A crash report buried among thirty rotated `.log.gz` files is one nobody finds #[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum Category { +pub enum Category { Logs, CrashReports, } @@ -120,13 +120,13 @@ impl Component for ClusterLogs { let logs_query = use_cluster_logs(cluster_id); let upload = use_upload_log(); let action = use_log_action(); - let dispatch = use_dispatch(); let search = use_state(String::new); let level = use_state(|| LevelFilter::All); let confirm = use_state(|| None::); let category = use_state(|| Category::Logs); let mut selected = use_state(PathBuf::new); - let mut handled_upload = use_state(|| None::); + + use_mclogs_feedback(upload); // Reads `category` too so switching the picker re-picks a file instead of keeping a stale selection use_side_effect(move || { @@ -154,36 +154,6 @@ impl Component for ClusterLogs { } }); - use_side_effect(move || match &*upload.read().state() { - MutationStateData::Settled { - res: Ok(result), .. - } => { - if handled_upload.peek().as_deref() == Some(result.url.as_str()) { - return; - } - - handled_upload.set(Some(result.url.clone())); - let _ = Clipboard::set(result.url.clone()); - - dispatch - .notify("Uploaded to mclo.gs") - .body(format!("{} (copied to clipboard)", result.url)) - .info() - .icon(IconType::LinkExternal01) - .send(); - } - MutationStateData::Settled { res: Err(err), .. } => { - let msg = err.to_string(); - if handled_upload.peek().as_deref() == Some(msg.as_str()) { - return; - } - - handled_upload.set(Some(msg.clone())); - dispatch.notify("Upload failed").body(msg).error().send(); - } - _ => {} - }); - let selected_path = selected.read().clone(); let search_query = { let value = search.read().clone(); @@ -243,7 +213,7 @@ impl Component for ClusterLogs { } } -fn visible_files(query: &UseQuery, category: Category) -> Vec { +pub fn visible_files(query: &UseQuery, category: Category) -> Vec { try_cluster_logs(query) .unwrap_or_default() .into_iter() @@ -321,10 +291,10 @@ fn top_bar( .into_element() } -fn viewer_header( +pub fn viewer_header( search: State, level: State, - mut confirm: State>, + confirm: State>, has_log: bool, ) -> impl IntoElement { let search_input = { @@ -367,12 +337,7 @@ fn viewer_header( .into_element(), ) .child( - Button::new() - .secondary() - .enabled(has_log) - .on_press(move |_| confirm.set(Some(Confirm::Upload))) - .child(Icon::new(IconType::LinkExternal01).size(15.)) - .text("Upload to mclo.gs"), + UploadToMclogs::new(has_log, confirm) ); rect() @@ -624,7 +589,7 @@ impl Component for PickerRow { } } -fn confirm_overlay( +pub fn confirm_overlay( kind: Confirm, mut confirm: State>, action: UseLogAction, diff --git a/packages/oneclient_app/src/view/app/cluster/mod.rs b/packages/oneclient_app/src/view/app/cluster/mod.rs index 4f845e2f..94ae1ec8 100644 --- a/packages/oneclient_app/src/view/app/cluster/mod.rs +++ b/packages/oneclient_app/src/view/app/cluster/mod.rs @@ -1,4 +1,4 @@ -mod logs; +pub mod logs; mod overview; mod package_manager; diff --git a/packages/oneclient_app/src/view/app/cluster/process_logs.rs b/packages/oneclient_app/src/view/app/cluster/process_logs.rs index feec885a..a6325538 100644 --- a/packages/oneclient_app/src/view/app/cluster/process_logs.rs +++ b/packages/oneclient_app/src/view/app/cluster/process_logs.rs @@ -1,12 +1,20 @@ +use std::sync::Arc; + use freya::prelude::*; +use oneclient_cluster::logs::parse_level; +use oneclient_core::{LogKind, LogLevel}; use crate::components::LogViewer; -use crate::hooks::use_game_snapshot; +use crate::components::upload_mclogs::use_mclogs_feedback; +use crate::hooks::{ + try_cluster_logs, use_cluster, use_cluster_logs, use_game_snapshot, use_log_action, + use_upload_log, +}; use crate::layout::cluster_content; use crate::theme::colors; +use crate::view::app::cluster::logs::{Confirm, LevelFilter, confirm_overlay, viewer_header}; use super::cluster_not_found; -use crate::hooks::use_cluster; #[derive(PartialEq)] pub struct ProcessLogs { @@ -15,6 +23,16 @@ pub struct ProcessLogs { impl Component for ProcessLogs { fn render(&self) -> impl IntoElement { + let logs_query = use_cluster_logs(self.cluster_id); + let upload = use_upload_log(); + let action = use_log_action(); + + let search = use_state(String::new); + let level = use_state(|| LevelFilter::All); + let confirm = use_state(|| None::); + + use_mclogs_feedback(upload); + let game = use_game_snapshot(); let Some(_cluster) = use_cluster(self.cluster_id) else { return cluster_not_found(); @@ -23,17 +41,65 @@ impl Component for ProcessLogs { let active = game.stage(self.cluster_id).is_some(); let body = if active { - LogViewer::new("Game output", game.logs_for(self.cluster_id)) + let lines = game.logs_for(self.cluster_id); + let has_log = !lines.is_empty(); + let visible = filter_lines(&lines, &search.read(), *level.read()); + + LogViewer::new("Game output", visible) .streaming(true) + .header(viewer_header(search, level, confirm, has_log)) .into_element() } else { not_running() }; - cluster_content().child(body).into_element() + let output_path = try_cluster_logs(&logs_query) + .unwrap_or_default() + .into_iter() + .find(|file| matches!(file.kind, LogKind::Game { .. })) + .map(|file| file.path); + + cluster_content() + .child(body) + .maybe_child( + (*confirm.read()) + .zip(output_path) + .map(|(kind, path)| confirm_overlay(kind, confirm, action, upload, path)), + ) + .into_element() } } +fn filter_lines(lines: &Arc>>, search: &str, level: LevelFilter) -> Arc>> { + let needle = (!search.trim().is_empty()).then(|| search.to_lowercase()); + let wanted = level.to_level(); + + if needle.is_none() && wanted.is_none() { + return lines.clone(); + } + + let mut carried = LogLevel::Unknown; + let mut out = Vec::new(); + + for line in lines.iter() { + carried = parse_level(line).unwrap_or(carried); + + if wanted.is_some_and(|wanted| wanted != carried) { + continue; + } + if needle + .as_ref() + .is_some_and(|needle| !line.to_lowercase().contains(needle)) + { + continue; + } + + out.push(line.clone()); + } + + Arc::new(out) +} + fn not_running() -> Element { rect() .width(Size::fill())