add clear logs button to logs windows (#7429)

This commit is contained in:
Danglewood
2026-04-18 22:31:45 -07:00
committed by GitHub
parent 207c24a466
commit 203f62e7d2
8 changed files with 164 additions and 57 deletions

View File

@@ -56,8 +56,9 @@ use crate::tauri_handlers::helpers::{
use tauri_plugin_updater::UpdaterExt; use tauri_plugin_updater::UpdaterExt;
use crate::utils::process_monitor::{ use crate::utils::process_monitor::{
GetProcessLogsRequest, LogEntry, LogStorage, RunningProcesses, get_log_storage, GetProcessLogsRequest, LogEntry, LogStorage, RunningProcesses, clear_process_logs,
get_process_logs, init_process_monitoring, register_process, unregister_process, get_log_storage, get_process_logs, init_process_monitoring, register_process,
unregister_process,
}; };
use crate::uninstall::uninstall_application; use crate::uninstall::uninstall_application;
@@ -91,6 +92,11 @@ fn get_process_logs_history(
get_process_logs(&state.0.clone(), request) get_process_logs(&state.0.clone(), request)
} }
#[tauri::command]
fn clear_process_logs_history(state: State<ProcessLogState>, process_id: String) -> bool {
clear_process_logs(&state.0, &process_id)
}
async fn check_and_apply_update(app: AppHandle, always_prompt: bool) { async fn check_and_apply_update(app: AppHandle, always_prompt: bool) {
let show_error = |app: &AppHandle, title: &str, message: String| { let show_error = |app: &AppHandle, title: &str, message: String| {
app.dialog() app.dialog()
@@ -526,6 +532,7 @@ fn main() {
register_process_monitoring, register_process_monitoring,
unregister_process_monitoring, unregister_process_monitoring,
get_process_logs_history, get_process_logs_history,
clear_process_logs_history,
open_jupyter_logs_window, open_jupyter_logs_window,
update_jupyter_status, update_jupyter_status,
open_backend_logs_window, open_backend_logs_window,

View File

@@ -391,14 +391,31 @@ pub async fn stop_jupyter_server_impl<R: tauri::Runtime, E: EnvSystem>(
if let Ok(pid) = pid_str.parse::<u32>() { if let Ok(pid) = pid_str.parse::<u32>() {
log::debug!("🎯 Found PID {pid} using port {port}, terminating..."); log::debug!("🎯 Found PID {pid} using port {port}, terminating...");
// Kill this specific PID let term_result = env_sys
let kill_result = env_sys .new_command("kill")
.args(["-15", &pid.to_string()])
.output();
match term_result {
Ok(_) => {
log::debug!(
"Sent SIGTERM to PID {pid}, waiting for graceful shutdown..."
);
std::thread::sleep(std::time::Duration::from_secs(2));
let check = env_sys
.new_command("kill")
.args(["-0", &pid.to_string()])
.output();
if check.is_ok() && check.unwrap().status.success() {
log::debug!("PID {pid} still running, sending SIGKILL...");
let _ = env_sys
.new_command("kill") .new_command("kill")
.args(["-9", &pid.to_string()]) .args(["-9", &pid.to_string()])
.output(); .output();
}
match kill_result {
Ok(_) => {
log::debug!("✅ Successfully terminated Jupyter PID {pid}"); log::debug!("✅ Successfully terminated Jupyter PID {pid}");
killed_any = true; killed_any = true;
} }

View File

@@ -77,6 +77,16 @@ pub fn unregister_process(logs: &LogStorage, process_id: &str) -> bool {
storage.remove(process_id).is_some() storage.remove(process_id).is_some()
} }
pub fn clear_process_logs(logs: &LogStorage, process_id: &str) -> bool {
let mut storage = logs.lock().unwrap();
if let Some(buffer) = storage.get_mut(process_id) {
buffer.entries.clear();
true
} else {
false
}
}
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct GetProcessLogsRequest { pub struct GetProcessLogsRequest {
pub process_id: String, pub process_id: String,
@@ -569,6 +579,38 @@ mod tests {
assert_eq!(logs[1].content, "Message 5"); assert_eq!(logs[1].content, "Message 5");
} }
#[test]
fn test_clear_process_logs_existing() {
let storage = create_log_storage();
register_process(&storage, "test_process");
{
let mut locked = storage.lock().unwrap();
let buffer = locked.get_mut("test_process").unwrap();
for i in 1..=3 {
buffer.add(LogEntry {
timestamp: i as i64,
content: format!("Message {i}"),
process_id: "test_process".to_string(),
});
}
}
let result = clear_process_logs(&storage, "test_process");
assert!(result);
let locked = storage.lock().unwrap();
let buffer = locked.get("test_process").unwrap();
assert_eq!(buffer.entries.len(), 0);
}
#[test]
fn test_clear_process_logs_nonexistent() {
let storage = create_log_storage();
let result = clear_process_logs(&storage, "nonexistent");
assert!(!result);
}
#[test] #[test]
fn test_running_processes_add_process_mock() { fn test_running_processes_add_process_mock() {
let processes = TestRunningProcesses::new(); let processes = TestRunningProcesses::new();

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "Open Data Platform by OpenBB", "productName": "Open Data Platform by OpenBB",
"version": "1.0.1", "version": "1.0.2",
"identifier": "co.openbb.platform", "identifier": "co.openbb.platform",
"build": { "build": {
"frontendDist": "../dist", "frontendDist": "../dist",

View File

@@ -3,6 +3,7 @@ import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event'; import { listen } from '@tauri-apps/api/event';
import { useSearch } from '@tanstack/react-router'; import { useSearch } from '@tanstack/react-router';
import SearchBar from './SearchBar'; import SearchBar from './SearchBar';
import CustomIcon from './Icon';
import '../styles/jupyter-logs.css'; import '../styles/jupyter-logs.css';
interface LogEntry { interface LogEntry {
@@ -21,6 +22,7 @@ const BackendLogsPage: React.FC = () => {
const [searchVisible, setSearchVisible] = useState(false); const [searchVisible, setSearchVisible] = useState(false);
const [currentMatchIndex, setCurrentMatchIndex] = useState(0); const [currentMatchIndex, setCurrentMatchIndex] = useState(0);
const [caseSensitive, setCaseSensitive] = useState(false); const [caseSensitive, setCaseSensitive] = useState(false);
const [logsCleared, setLogsCleared] = useState(false);
const logContainerRef = useRef<HTMLDivElement>(null); const logContainerRef = useRef<HTMLDivElement>(null);
const searchInputRef = useRef<HTMLInputElement>(null); const searchInputRef = useRef<HTMLInputElement>(null);
@@ -173,6 +175,7 @@ const BackendLogsPage: React.FC = () => {
const unsubscribe = listen<{ processId: string, output: string, timestamp: number }>('process-output', (event) => { const unsubscribe = listen<{ processId: string, output: string, timestamp: number }>('process-output', (event) => {
const { processId: eventProcessId, output, timestamp } = event.payload; const { processId: eventProcessId, output, timestamp } = event.payload;
if (eventProcessId === processId) { if (eventProcessId === processId) {
setLogsCleared(false);
setLogs(prev => ([...prev, { timestamp, content: cleanLogContent(output), process_id: eventProcessId }])); setLogs(prev => ([...prev, { timestamp, content: cleanLogContent(output), process_id: eventProcessId }]));
} }
}); });
@@ -195,10 +198,21 @@ const BackendLogsPage: React.FC = () => {
return ( return (
<div className="h-full w-full flex flex-col bg-theme-secondary relative flex-grow"> <div className="h-full w-full flex flex-col bg-theme-secondary relative flex-grow">
{!searchVisible && ( {!searchVisible && (
<div className="search-help-container opacity-0 hover:opacity-100 transition-opacity hover:cursor-default"> <div className="logs-toolbar-container opacity-0 hover:opacity-100 transition-opacity">
<div className="body-xs-regular text-theme-muted items-center justify-center"> <div className="body-xs-regular text-theme-muted hover:cursor-default">
Press Ctrl+F (Cmd+F) to search Press Ctrl+F (Cmd+F) to search
</div> </div>
<div className="flex items-center gap-1 hover:cursor-pointer"
onClick={async () => {
const processId = `backend-${backendId}`;
await invoke('clear_process_logs_history', { processId });
setLogs([]);
setLogsCleared(true);
}}
>
<CustomIcon id="delete" className="h-3.5 w-3.5 text-theme-muted" />
<span className="body-xs-regular text-theme-muted">Clear logs</span>
</div>
</div> </div>
)} )}
@@ -227,7 +241,7 @@ const BackendLogsPage: React.FC = () => {
</div> </div>
) : error ? ( ) : error ? (
<div className="text-red-500 py-2 px-2">{error}</div> <div className="text-red-500 py-2 px-2">{error}</div>
) : logs.length === 0 ? ( ) : logs.length === 0 && !logsCleared ? (
<div className="text-theme-secondary">No logs available for this backend. Try starting a backend service first.</div> <div className="text-theme-secondary">No logs available for this backend. Try starting a backend service first.</div>
) : ( ) : (
<div> <div>

View File

@@ -3,6 +3,7 @@ import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event'; import { listen } from '@tauri-apps/api/event';
import { useSearch } from '@tanstack/react-router'; import { useSearch } from '@tanstack/react-router';
import SearchBar from './SearchBar'; import SearchBar from './SearchBar';
import CustomIcon from './Icon';
import '../styles/jupyter-logs.css'; import '../styles/jupyter-logs.css';
interface LogEntry { interface LogEntry {
@@ -23,6 +24,7 @@ const JupyterLogsPage: React.FC = () => {
const [searchVisible, setSearchVisible] = useState(false); const [searchVisible, setSearchVisible] = useState(false);
const [currentMatchIndex, setCurrentMatchIndex] = useState(0); const [currentMatchIndex, setCurrentMatchIndex] = useState(0);
const [caseSensitive, setCaseSensitive] = useState(false); const [caseSensitive, setCaseSensitive] = useState(false);
const [logsCleared, setLogsCleared] = useState(false);
const logContainerRef = useRef<HTMLDivElement>(null); const logContainerRef = useRef<HTMLDivElement>(null);
const searchInputRef = useRef<HTMLInputElement>(null); const searchInputRef = useRef<HTMLInputElement>(null);
@@ -245,6 +247,7 @@ const JupyterLogsPage: React.FC = () => {
const { processId: eventProcessId, output, timestamp } = event.payload; const { processId: eventProcessId, output, timestamp } = event.payload;
if (eventProcessId === processId) { if (eventProcessId === processId) {
setLogsCleared(false);
// Add the new log entry // Add the new log entry
setLogs(prev => { setLogs(prev => {
const newLogs = [...prev, { const newLogs = [...prev, {
@@ -287,10 +290,21 @@ const JupyterLogsPage: React.FC = () => {
return ( return (
<div className="h-full w-full flex flex-col bg-theme-secondary relative flex-grow"> <div className="h-full w-full flex flex-col bg-theme-secondary relative flex-grow">
{!searchVisible && ( {!searchVisible && (
<div className="search-help-container opacity-0 hover:opacity-100 transition-opacity hover:cursor-default"> <div className="logs-toolbar-container opacity-0 hover:opacity-100 transition-opacity">
<div className="body-xs-regular text-theme-muted items-center justify-center"> <div className="body-xs-regular text-theme-muted hover:cursor-default">
Press Ctrl+F (Cmd+F) to search Press Ctrl+F (Cmd+F) to search
</div> </div>
<div className="flex items-center gap-1 hover:cursor-pointer"
onClick={async () => {
const processId = `jupyter-${environmentName}`;
await invoke('clear_process_logs_history', { processId });
setLogs([]);
setLogsCleared(true);
}}
>
<CustomIcon id="delete" className="h-3.5 w-3.5 text-theme-muted" />
<span className="body-xs-regular text-theme-muted">Clear logs</span>
</div>
</div> </div>
)} )}
@@ -319,7 +333,7 @@ const JupyterLogsPage: React.FC = () => {
</div> </div>
) : error ? ( ) : error ? (
<div className="text-red-500 py-2 px-2">{error}</div> <div className="text-red-500 py-2 px-2">{error}</div>
) : logs.length === 0 ? ( ) : logs.length === 0 && !logsCleared ? (
<div className="text-theme-secondary">No logs available for this environment. Try starting a Jupyter server first.</div> <div className="text-theme-secondary">No logs available for this environment. Try starting a Jupyter server first.</div>
) : ( ) : (
<div> <div>

View File

@@ -89,7 +89,9 @@ export default function ApiKeysPage() {
}); });
} }
} catch (e) { } catch (e) {
throw new Error(`Invalid JSON file: ${e}`); const err = new Error(`Invalid JSON file: ${e}`);
(err as unknown as Record<string, unknown>).cause = e;
throw err;
} }
} else if (extension === "env") { } else if (extension === "env") {
// Parse .env file // Parse .env file

View File

@@ -27,7 +27,7 @@ body.jupyter-logs-view main > div {
} }
/* Target any potential horizontal scrollbars */ /* Target any potential horizontal scrollbars */
body.jupyter-logs-view div, body.jupyter-logs-view div:not(.logs-toolbar-container):not(.logs-toolbar-container *),
body.jupyter-logs-view pre, body.jupyter-logs-view pre,
body.jupyter-logs-view code { body.jupyter-logs-view code {
overflow-x: hidden !important; /* No horizontal scrolling */ overflow-x: hidden !important; /* No horizontal scrolling */
@@ -51,7 +51,7 @@ header.logs-page-header {
.search-help-container { .search-help-container {
position: fixed; position: fixed;
top: 65px; top: 75px;
left: 0; left: 0;
right: 0; right: 0;
z-index: 50; z-index: 50;
@@ -60,6 +60,17 @@ header.logs-page-header {
align-items: flex-start; align-items: flex-start;
} }
.logs-toolbar-container {
position: fixed;
top: 75px;
left: 0;
right: 0;
z-index: 50;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 18px;
}
/* Search highlight styles */ /* Search highlight styles */
.search-highlight { .search-highlight {