feat: added search bar into Game Output live logs (#716)
Co-authored-by: rozwader <maciek.rozwadowski2006@gmail.com>
This commit is contained in:
parent
ff22c8ac3b
commit
578ec187cd
6 changed files with 159 additions and 57 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
70
packages/oneclient_app/src/components/upload_mclogs.rs
Normal file
70
packages/oneclient_app/src/components/upload_mclogs.rs
Normal 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();
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
mod logs;
|
||||
pub mod logs;
|
||||
mod overview;
|
||||
mod package_manager;
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue