feat: added search bar into Game Output live logs (#716)

Co-authored-by: rozwader <maciek.rozwadowski2006@gmail.com>
This commit is contained in:
LynithDev 2026-08-19 15:27:13 +02:00 • committed by GitHub
parent ff22c8ac3b
commit 578ec187cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 159 additions and 57 deletions

View file

@ -247,7 +247,7 @@ fn toolbar(title: String, streaming: bool, auto_scroll: State<bool>) -> 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()

View file

@ -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;

View file

@ -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<Option<Confirm>>,
}
impl UploadToMclogs {
pub fn new(has_log: bool, confirm: State<Option<Confirm>>) -> 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::<String>);
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();
}
_ => {}
});
}

View file

@ -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<LogLevel> {
pub fn to_level(self) -> Option<LogLevel> {
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::<Confirm>);
let category = use_state(|| Category::Logs);
let mut selected = use_state(PathBuf::new);
let mut handled_upload = use_state(|| None::<String>);
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<ClusterLogsQuery>, category: Category) -> Vec<LogFileInfo> {
pub fn visible_files(query: &UseQuery<ClusterLogsQuery>, category: Category) -> Vec<LogFileInfo> {
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<String>,
level: State<LevelFilter>,
mut confirm: State<Option<Confirm>>,
confirm: State<Option<Confirm>>,
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<Option<Confirm>>,
action: UseLogAction,

View file

@ -1,4 +1,4 @@
mod logs;
pub mod logs;
mod overview;
mod package_manager;

View file

@ -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::<Confirm>);
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<Vec<Arc<str>>>, search: &str, level: LevelFilter) -> Arc<Vec<Arc<str>>> {
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())