mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-27 04:31:13 +00:00
add clear logs button to logs windows (#7429)
This commit is contained in:
11
desktop/src-tauri/src/main.rs
vendored
11
desktop/src-tauri/src/main.rs
vendored
@@ -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,
|
||||||
|
|||||||
25
desktop/src-tauri/src/tauri_handlers/jupyter.rs
vendored
25
desktop/src-tauri/src/tauri_handlers/jupyter.rs
vendored
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
42
desktop/src-tauri/src/utils/process_monitor.rs
vendored
42
desktop/src-tauri/src/utils/process_monitor.rs
vendored
@@ -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();
|
||||||
|
|||||||
2
desktop/src-tauri/tauri.conf.json
vendored
2
desktop/src-tauri/tauri.conf.json
vendored
@@ -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",
|
||||||
|
|||||||
20
desktop/src/components/BackendLogsPage.tsx
vendored
20
desktop/src/components/BackendLogsPage.tsx
vendored
@@ -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>
|
||||||
|
|||||||
20
desktop/src/components/JupyterLogsPage.tsx
vendored
20
desktop/src/components/JupyterLogsPage.tsx
vendored
@@ -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>
|
||||||
|
|||||||
4
desktop/src/routes/api-keys.tsx
vendored
4
desktop/src/routes/api-keys.tsx
vendored
@@ -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
|
||||||
|
|||||||
15
desktop/src/styles/jupyter-logs.css
vendored
15
desktop/src/styles/jupyter-logs.css
vendored
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user