chore: stream app logs to log window (#5019)
* chore: stream app logs to log window * chore: remove unused states
This commit is contained in:
parent
2ae7417e10
commit
28c7e0d105
@ -1,15 +1,10 @@
|
|||||||
use rmcp::model::{CallToolRequestParam, CallToolResult, Tool};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::{Map, Value};
|
|
||||||
use std::{fs, path::PathBuf};
|
use std::{fs, path::PathBuf};
|
||||||
use tauri::{AppHandle, Manager, Runtime, State};
|
use tauri::{AppHandle, Manager, Runtime, State};
|
||||||
|
|
||||||
use super::{server, setup, state::AppState};
|
use super::{server, setup, state::AppState};
|
||||||
|
|
||||||
const CONFIGURATION_FILE_NAME: &str = "settings.json";
|
const CONFIGURATION_FILE_NAME: &str = "settings.json";
|
||||||
const DEFAULT_MCP_CONFIG: &str = r#"{
|
|
||||||
"mcpServers": {}
|
|
||||||
}"#;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct AppConfiguration {
|
pub struct AppConfiguration {
|
||||||
@ -296,100 +291,13 @@ pub async fn stop_server() -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieves all available tools from all MCP servers
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `state` - Application state containing MCP server connections
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// * `Result<Vec<Tool>, String>` - A vector of all tools if successful, or an error message if failed
|
|
||||||
///
|
|
||||||
/// This function:
|
|
||||||
/// 1. Locks the MCP servers mutex to access server connections
|
|
||||||
/// 2. Iterates through all connected servers
|
|
||||||
/// 3. Gets the list of tools from each server
|
|
||||||
/// 4. Combines all tools into a single vector
|
|
||||||
/// 5. Returns the combined list of all available tools
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_tools(state: State<'_, AppState>) -> Result<Vec<Tool>, String> {
|
pub async fn read_logs(app: AppHandle) -> Result<String, String> {
|
||||||
let servers = state.mcp_servers.lock().await;
|
let log_path = get_jan_data_folder_path(app).join("logs").join("app.log");
|
||||||
let mut all_tools: Vec<Tool> = Vec::new();
|
if log_path.exists() {
|
||||||
|
let content = fs::read_to_string(log_path).map_err(|e| e.to_string())?;
|
||||||
for (_, service) in servers.iter() {
|
Ok(content)
|
||||||
// List tools
|
} else {
|
||||||
let tools = service.list_all_tools().await.map_err(|e| e.to_string())?;
|
Err(format!("Log file not found"))
|
||||||
|
|
||||||
for tool in tools {
|
|
||||||
all_tools.push(tool);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(all_tools)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calls a tool on an MCP server by name with optional arguments
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `state` - Application state containing MCP server connections
|
|
||||||
/// * `tool_name` - Name of the tool to call
|
|
||||||
/// * `arguments` - Optional map of argument names to values
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// * `Result<CallToolResult, String>` - Result of the tool call if successful, or error message if failed
|
|
||||||
///
|
|
||||||
/// This function:
|
|
||||||
/// 1. Locks the MCP servers mutex to access server connections
|
|
||||||
/// 2. Searches through all servers for one containing the named tool
|
|
||||||
/// 3. When found, calls the tool on that server with the provided arguments
|
|
||||||
/// 4. Returns error if no server has the requested tool
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn call_tool(
|
|
||||||
state: State<'_, AppState>,
|
|
||||||
tool_name: String,
|
|
||||||
arguments: Option<Map<String, Value>>,
|
|
||||||
) -> Result<CallToolResult, String> {
|
|
||||||
let servers = state.mcp_servers.lock().await;
|
|
||||||
|
|
||||||
// Iterate through servers and find the first one that contains the tool
|
|
||||||
for (_, service) in servers.iter() {
|
|
||||||
if let Ok(tools) = service.list_all_tools().await {
|
|
||||||
if tools.iter().any(|t| t.name == tool_name) {
|
|
||||||
return service
|
|
||||||
.call_tool(CallToolRequestParam {
|
|
||||||
name: tool_name.into(),
|
|
||||||
arguments,
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(|e| e.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(format!("Tool {} not found", tool_name))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn get_mcp_configs(app: AppHandle) -> Result<String, String> {
|
|
||||||
let mut path = get_jan_data_folder_path(app);
|
|
||||||
path.push("mcp_config.json");
|
|
||||||
log::info!("read mcp configs, path: {:?}", path);
|
|
||||||
|
|
||||||
// Create default empty config if file doesn't exist
|
|
||||||
if !path.exists() {
|
|
||||||
log::info!("mcp_config.json not found, creating default empty config");
|
|
||||||
fs::write(&path, DEFAULT_MCP_CONFIG)
|
|
||||||
.map_err(|e| format!("Failed to create default MCP config: {}", e))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let contents = fs::read_to_string(path).map_err(|e| e.to_string())?;
|
|
||||||
return Ok(contents);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn save_mcp_configs(app: AppHandle, configs: String) -> Result<(), String> {
|
|
||||||
let mut path = get_jan_data_folder_path(app);
|
|
||||||
path.push("mcp_config.json");
|
|
||||||
log::info!("save mcp configs, path: {:?}", path);
|
|
||||||
|
|
||||||
fs::write(path, configs).map_err(|e| e.to_string())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,19 @@
|
|||||||
use std::{collections::HashMap, env, sync::Arc};
|
use std::{collections::HashMap, env, sync::Arc};
|
||||||
|
|
||||||
|
use rmcp::model::{CallToolRequestParam, CallToolResult, Tool};
|
||||||
use rmcp::{service::RunningService, transport::TokioChildProcess, RoleClient, ServiceExt};
|
use rmcp::{service::RunningService, transport::TokioChildProcess, RoleClient, ServiceExt};
|
||||||
use serde_json::Value;
|
use serde_json::{Map, Value};
|
||||||
use tauri::{AppHandle, Emitter, Runtime, State};
|
use tauri::{AppHandle, Emitter, Runtime, State};
|
||||||
use tokio::{process::Command, sync::Mutex};
|
use tokio::{process::Command, sync::Mutex};
|
||||||
|
use std::{fs};
|
||||||
|
|
||||||
use super::{cmd::get_jan_data_folder_path, state::AppState};
|
use super::{cmd::get_jan_data_folder_path, state::AppState};
|
||||||
|
|
||||||
|
const DEFAULT_MCP_CONFIG: &str = r#"{
|
||||||
|
"mcpServers": {}
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
|
||||||
/// Runs MCP commands by reading configuration from a JSON file and initializing servers
|
/// Runs MCP commands by reading configuration from a JSON file and initializing servers
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
@ -175,6 +182,104 @@ pub async fn get_connected_servers(
|
|||||||
Ok(servers_map.keys().cloned().collect())
|
Ok(servers_map.keys().cloned().collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieves all available tools from all MCP servers
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `state` - Application state containing MCP server connections
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Result<Vec<Tool>, String>` - A vector of all tools if successful, or an error message if failed
|
||||||
|
///
|
||||||
|
/// This function:
|
||||||
|
/// 1. Locks the MCP servers mutex to access server connections
|
||||||
|
/// 2. Iterates through all connected servers
|
||||||
|
/// 3. Gets the list of tools from each server
|
||||||
|
/// 4. Combines all tools into a single vector
|
||||||
|
/// 5. Returns the combined list of all available tools
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_tools(state: State<'_, AppState>) -> Result<Vec<Tool>, String> {
|
||||||
|
let servers = state.mcp_servers.lock().await;
|
||||||
|
let mut all_tools: Vec<Tool> = Vec::new();
|
||||||
|
|
||||||
|
for (_, service) in servers.iter() {
|
||||||
|
// List tools
|
||||||
|
let tools = service.list_all_tools().await.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
for tool in tools {
|
||||||
|
all_tools.push(tool);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(all_tools)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calls a tool on an MCP server by name with optional arguments
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `state` - Application state containing MCP server connections
|
||||||
|
/// * `tool_name` - Name of the tool to call
|
||||||
|
/// * `arguments` - Optional map of argument names to values
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Result<CallToolResult, String>` - Result of the tool call if successful, or error message if failed
|
||||||
|
///
|
||||||
|
/// This function:
|
||||||
|
/// 1. Locks the MCP servers mutex to access server connections
|
||||||
|
/// 2. Searches through all servers for one containing the named tool
|
||||||
|
/// 3. When found, calls the tool on that server with the provided arguments
|
||||||
|
/// 4. Returns error if no server has the requested tool
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn call_tool(
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
tool_name: String,
|
||||||
|
arguments: Option<Map<String, Value>>,
|
||||||
|
) -> Result<CallToolResult, String> {
|
||||||
|
let servers = state.mcp_servers.lock().await;
|
||||||
|
|
||||||
|
// Iterate through servers and find the first one that contains the tool
|
||||||
|
for (_, service) in servers.iter() {
|
||||||
|
if let Ok(tools) = service.list_all_tools().await {
|
||||||
|
if tools.iter().any(|t| t.name == tool_name) {
|
||||||
|
return service
|
||||||
|
.call_tool(CallToolRequestParam {
|
||||||
|
name: tool_name.into(),
|
||||||
|
arguments,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(format!("Tool {} not found", tool_name))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_mcp_configs(app: AppHandle) -> Result<String, String> {
|
||||||
|
let mut path = get_jan_data_folder_path(app);
|
||||||
|
path.push("mcp_config.json");
|
||||||
|
log::info!("read mcp configs, path: {:?}", path);
|
||||||
|
|
||||||
|
// Create default empty config if file doesn't exist
|
||||||
|
if !path.exists() {
|
||||||
|
log::info!("mcp_config.json not found, creating default empty config");
|
||||||
|
fs::write(&path, DEFAULT_MCP_CONFIG)
|
||||||
|
.map_err(|e| format!("Failed to create default MCP config: {}", e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let contents = fs::read_to_string(path).map_err(|e| e.to_string())?;
|
||||||
|
return Ok(contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn save_mcp_configs(app: AppHandle, configs: String) -> Result<(), String> {
|
||||||
|
let mut path = get_jan_data_folder_path(app);
|
||||||
|
path.push("mcp_config.json");
|
||||||
|
log::info!("save mcp configs, path: {:?}", path);
|
||||||
|
|
||||||
|
fs::write(path, configs).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@ -44,13 +44,14 @@ pub fn run() {
|
|||||||
core::cmd::app_token,
|
core::cmd::app_token,
|
||||||
core::cmd::start_server,
|
core::cmd::start_server,
|
||||||
core::cmd::stop_server,
|
core::cmd::stop_server,
|
||||||
core::cmd::save_mcp_configs,
|
core::cmd::read_logs,
|
||||||
core::cmd::get_mcp_configs,
|
|
||||||
// MCP commands
|
// MCP commands
|
||||||
core::cmd::get_tools,
|
core::mcp::get_tools,
|
||||||
core::cmd::call_tool,
|
core::mcp::call_tool,
|
||||||
core::mcp::restart_mcp_servers,
|
core::mcp::restart_mcp_servers,
|
||||||
core::mcp::get_connected_servers,
|
core::mcp::get_connected_servers,
|
||||||
|
core::mcp::save_mcp_configs,
|
||||||
|
core::mcp::get_mcp_configs,
|
||||||
// Threads
|
// Threads
|
||||||
core::threads::list_threads,
|
core::threads::list_threads,
|
||||||
core::threads::create_thread,
|
core::threads::create_thread,
|
||||||
@ -76,14 +77,14 @@ pub fn run() {
|
|||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
app.handle().plugin(
|
app.handle().plugin(
|
||||||
tauri_plugin_log::Builder::default()
|
tauri_plugin_log::Builder::default()
|
||||||
.targets([if cfg!(debug_assertions) {
|
.targets([
|
||||||
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout)
|
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout),
|
||||||
} else {
|
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Webview),
|
||||||
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Folder {
|
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Folder {
|
||||||
path: get_jan_data_folder_path(app.handle().clone()).join("logs"),
|
path: get_jan_data_folder_path(app.handle().clone()).join("logs"),
|
||||||
file_name: Some("app".to_string()),
|
file_name: Some("app".to_string()),
|
||||||
})
|
}),
|
||||||
}])
|
])
|
||||||
.build(),
|
.build(),
|
||||||
)?;
|
)?;
|
||||||
// Install extensions
|
// Install extensions
|
||||||
|
|||||||
@ -6,6 +6,8 @@ type AppState = {
|
|||||||
streamingContent?: ThreadMessage
|
streamingContent?: ThreadMessage
|
||||||
loadingModel?: boolean
|
loadingModel?: boolean
|
||||||
tools: MCPTool[]
|
tools: MCPTool[]
|
||||||
|
serverStatus: 'running' | 'stopped' | 'pending'
|
||||||
|
setServerStatus: (value: 'running' | 'stopped' | 'pending') => void
|
||||||
updateStreamingContent: (content: ThreadMessage | undefined) => void
|
updateStreamingContent: (content: ThreadMessage | undefined) => void
|
||||||
updateLoadingModel: (loading: boolean) => void
|
updateLoadingModel: (loading: boolean) => void
|
||||||
updateTools: (tools: MCPTool[]) => void
|
updateTools: (tools: MCPTool[]) => void
|
||||||
@ -15,6 +17,7 @@ export const useAppState = create<AppState>()((set) => ({
|
|||||||
streamingContent: undefined,
|
streamingContent: undefined,
|
||||||
loadingModel: false,
|
loadingModel: false,
|
||||||
tools: [],
|
tools: [],
|
||||||
|
serverStatus: 'stopped',
|
||||||
updateStreamingContent: (content) => {
|
updateStreamingContent: (content) => {
|
||||||
set({ streamingContent: content })
|
set({ streamingContent: content })
|
||||||
},
|
},
|
||||||
@ -24,4 +27,5 @@ export const useAppState = create<AppState>()((set) => ({
|
|||||||
updateTools: (tools) => {
|
updateTools: (tools) => {
|
||||||
set({ tools })
|
set({ tools })
|
||||||
},
|
},
|
||||||
|
setServerStatus: (value) => set({ serverStatus: value }),
|
||||||
}))
|
}))
|
||||||
|
|||||||
@ -21,9 +21,6 @@ type LocalApiServerState = {
|
|||||||
// Verbose server logs
|
// Verbose server logs
|
||||||
verboseLogs: boolean
|
verboseLogs: boolean
|
||||||
setVerboseLogs: (value: boolean) => void
|
setVerboseLogs: (value: boolean) => void
|
||||||
// Server status
|
|
||||||
serverStatus: 'running' | 'stopped' | 'pending'
|
|
||||||
setServerStatus: (value: 'running' | 'stopped' | 'pending') => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useLocalApiServer = create<LocalApiServerState>()(
|
export const useLocalApiServer = create<LocalApiServerState>()(
|
||||||
@ -41,8 +38,6 @@ export const useLocalApiServer = create<LocalApiServerState>()(
|
|||||||
setCorsEnabled: (value) => set({ corsEnabled: value }),
|
setCorsEnabled: (value) => set({ corsEnabled: value }),
|
||||||
verboseLogs: true,
|
verboseLogs: true,
|
||||||
setVerboseLogs: (value) => set({ verboseLogs: value }),
|
setVerboseLogs: (value) => set({ verboseLogs: value }),
|
||||||
serverStatus: 'stopped',
|
|
||||||
setServerStatus: (value) => set({ serverStatus: value }),
|
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: localStoregeKey.settingLocalApiServer,
|
name: localStoregeKey.settingLocalApiServer,
|
||||||
|
|||||||
@ -26,6 +26,7 @@ export const AppRoutes = [
|
|||||||
'getMcpConfigs',
|
'getMcpConfigs',
|
||||||
'restartMcpServers',
|
'restartMcpServers',
|
||||||
'getConnectedServers',
|
'getConnectedServers',
|
||||||
|
'readLogs',
|
||||||
]
|
]
|
||||||
// Define API routes based on different route types
|
// Define API routes based on different route types
|
||||||
export const Routes = [...CoreRoutes, ...APIRoutes, ...AppRoutes].map((r) => ({
|
export const Routes = [...CoreRoutes, ...APIRoutes, ...AppRoutes].map((r) => ({
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { ExtensionManager } from '@/lib/extension'
|
import { ExtensionManager } from '@/lib/extension'
|
||||||
import { APIs } from '@/lib/service'
|
import { APIs } from '@/lib/service'
|
||||||
import { EventEmitter } from '@/services/eventsService'
|
import { EventEmitter } from '@/services/events'
|
||||||
import { EngineManager, ModelManager } from '@janhq/core'
|
import { EngineManager, ModelManager } from '@janhq/core'
|
||||||
import { PropsWithChildren, useCallback, useEffect, useState } from 'react'
|
import { PropsWithChildren, useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import { createFileRoute } from '@tanstack/react-router'
|
|||||||
import { route } from '@/constants/routes'
|
import { route } from '@/constants/routes'
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import { parseLogLine, readLogs } from '@/services/app'
|
||||||
|
import { listen } from '@tauri-apps/api/event'
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export const Route = createFileRoute(route.localApiServerlogs as any)({
|
export const Route = createFileRoute(route.localApiServerlogs as any)({
|
||||||
@ -12,72 +14,36 @@ export const Route = createFileRoute(route.localApiServerlogs as any)({
|
|||||||
interface LogEntry {
|
interface LogEntry {
|
||||||
timestamp: string
|
timestamp: string
|
||||||
level: 'info' | 'warn' | 'error' | 'debug'
|
level: 'info' | 'warn' | 'error' | 'debug'
|
||||||
|
target: string
|
||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate dummy log data
|
const SERVER_LOG_TARGET = 'app_lib::core::server'
|
||||||
const generateDummyLogs = (): LogEntry[] => {
|
const LOG_EVENT_NAME = 'log://log'
|
||||||
const logs: LogEntry[] = []
|
|
||||||
const levels: ('info' | 'warn' | 'error' | 'debug')[] = [
|
|
||||||
'info',
|
|
||||||
'warn',
|
|
||||||
'error',
|
|
||||||
'debug',
|
|
||||||
]
|
|
||||||
const messages = [
|
|
||||||
'Server started on port 3000',
|
|
||||||
'Received request: GET /api/v1/models',
|
|
||||||
'Processing request...',
|
|
||||||
'Request completed in 120ms',
|
|
||||||
'Connection established with client',
|
|
||||||
'Authentication successful for user',
|
|
||||||
'Failed to connect to database',
|
|
||||||
'API rate limit exceeded',
|
|
||||||
'Memory usage: 256MB',
|
|
||||||
'CPU usage: 45%',
|
|
||||||
'Websocket connection closed',
|
|
||||||
'Cache miss for key: model_list',
|
|
||||||
'Updating configuration...',
|
|
||||||
'Configuration updated successfully',
|
|
||||||
'Initializing model...',
|
|
||||||
]
|
|
||||||
|
|
||||||
// Generate 50 log entries
|
|
||||||
const now = new Date()
|
|
||||||
for (let i = 0; i < 50; i++) {
|
|
||||||
const timestamp = new Date(now.getTime() - (50 - i) * 30000) // 30 seconds apart
|
|
||||||
logs.push({
|
|
||||||
timestamp: timestamp.toISOString(),
|
|
||||||
level: levels[Math.floor(Math.random() * levels.length)],
|
|
||||||
message: messages[Math.floor(Math.random() * messages.length)],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return logs
|
|
||||||
}
|
|
||||||
|
|
||||||
function LogsViewer() {
|
function LogsViewer() {
|
||||||
const [logs, setLogs] = useState<LogEntry[]>([])
|
const [logs, setLogs] = useState<LogEntry[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Load dummy logs when component mounts
|
readLogs().then((logData) => {
|
||||||
setLogs(generateDummyLogs())
|
const logs = logData
|
||||||
|
.filter((log) => log?.target === SERVER_LOG_TARGET)
|
||||||
// Simulate new logs coming in every 5 seconds
|
.filter(Boolean) as LogEntry[]
|
||||||
const interval = setInterval(() => {
|
setLogs(logs)
|
||||||
setLogs((currentLogs) => {
|
})
|
||||||
const newLog: LogEntry = {
|
let unsubscribe = () => {}
|
||||||
timestamp: new Date().toISOString(),
|
listen(LOG_EVENT_NAME, (event) => {
|
||||||
level: ['info', 'warn', 'error', 'debug'][
|
const { message } = event.payload as { message: string }
|
||||||
Math.floor(Math.random() * 4)
|
const log: LogEntry | undefined = parseLogLine(message)
|
||||||
] as 'info' | 'warn' | 'error' | 'debug',
|
if (log?.target === SERVER_LOG_TARGET) {
|
||||||
message: `New activity at ${new Date().toLocaleTimeString()}`,
|
setLogs((prevLogs) => [...prevLogs, log])
|
||||||
}
|
}
|
||||||
return [...currentLogs, newLog]
|
}).then((unsub) => {
|
||||||
})
|
unsubscribe = unsub
|
||||||
}, 5000)
|
})
|
||||||
|
return () => {
|
||||||
return () => clearInterval(interval)
|
unsubscribe()
|
||||||
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Function to get appropriate color for log level
|
// Function to get appropriate color for log level
|
||||||
|
|||||||
@ -8,6 +8,17 @@ import { Card, CardItem } from '@/containers/Card'
|
|||||||
import LanguageSwitcher from '@/containers/LanguageSwitcher'
|
import LanguageSwitcher from '@/containers/LanguageSwitcher'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useGeneralSetting } from '@/hooks/useGeneralSetting'
|
import { useGeneralSetting } from '@/hooks/useGeneralSetting'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
import { factoryReset } from '@/services/app'
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export const Route = createFileRoute(route.settings.general as any)({
|
export const Route = createFileRoute(route.settings.general as any)({
|
||||||
@ -18,6 +29,11 @@ function General() {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { spellCheckChatInput, setSpellCheckChatInput } = useGeneralSetting()
|
const { spellCheckChatInput, setSpellCheckChatInput } = useGeneralSetting()
|
||||||
|
|
||||||
|
const resetApp = async () => {
|
||||||
|
// TODO: Loading indicator
|
||||||
|
await factoryReset()
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<HeaderPage>
|
<HeaderPage>
|
||||||
@ -95,9 +111,42 @@ function General() {
|
|||||||
ns: 'settings',
|
ns: 'settings',
|
||||||
})}
|
})}
|
||||||
actions={
|
actions={
|
||||||
<Button variant="destructive" size="sm">
|
<Dialog>
|
||||||
{t('common.reset')}
|
<DialogTrigger asChild>
|
||||||
</Button>
|
<Button variant="destructive" size="sm">
|
||||||
|
{t('common.reset')}
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Factory Reset</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Are you sure you want to reset the app to factory
|
||||||
|
settings? This action is irreversible and recommended
|
||||||
|
only if the application is corrupted.
|
||||||
|
</DialogDescription>
|
||||||
|
<DialogFooter className="mt-2 flex items-center">
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
size="sm"
|
||||||
|
className="hover:no-underline"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => resetApp()}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogHeader>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { PortInput } from '@/containers/PortInput'
|
|||||||
import { ApiPrefixInput } from '@/containers/ApiPrefixInput'
|
import { ApiPrefixInput } from '@/containers/ApiPrefixInput'
|
||||||
import { useLocalApiServer } from '@/hooks/useLocalApiServer'
|
import { useLocalApiServer } from '@/hooks/useLocalApiServer'
|
||||||
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||||
|
import { useAppState } from '@/hooks/useAppState'
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export const Route = createFileRoute(route.settings.local_api_server as any)({
|
export const Route = createFileRoute(route.settings.local_api_server as any)({
|
||||||
@ -27,10 +28,10 @@ function LocalAPIServer() {
|
|||||||
serverHost,
|
serverHost,
|
||||||
serverPort,
|
serverPort,
|
||||||
apiPrefix,
|
apiPrefix,
|
||||||
serverStatus,
|
|
||||||
setServerStatus,
|
|
||||||
} = useLocalApiServer()
|
} = useLocalApiServer()
|
||||||
|
|
||||||
|
const { serverStatus, setServerStatus } = useAppState()
|
||||||
|
|
||||||
const toggleAPIServer = async () => {
|
const toggleAPIServer = async () => {
|
||||||
setServerStatus('pending')
|
setServerStatus('pending')
|
||||||
if (serverStatus === 'stopped') {
|
if (serverStatus === 'stopped') {
|
||||||
@ -115,8 +116,7 @@ function LocalAPIServer() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={toggleAPIServer}>
|
<Button onClick={toggleAPIServer}>
|
||||||
{`${serverStatus === 'running' ? 'Stop' : 'Start'}`}{' '}
|
{`${serverStatus === 'running' ? 'Stop' : 'Start'}`} Server
|
||||||
Server
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
52
web-app/src/services/app.ts
Normal file
52
web-app/src/services/app.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { AppConfiguration, fs } from '@janhq/core'
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description This function is used to reset the app to its factory settings.
|
||||||
|
* It will remove all the data from the app, including the data folder and local storage.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
export const factoryReset = async () => {
|
||||||
|
const appConfiguration: AppConfiguration | undefined =
|
||||||
|
await window.core?.api?.getAppConfigurations()
|
||||||
|
|
||||||
|
const janDataFolderPath = appConfiguration?.data_folder
|
||||||
|
if (janDataFolderPath) await fs.rm(janDataFolderPath)
|
||||||
|
window.localStorage.clear()
|
||||||
|
await window.core?.api?.installExtensions()
|
||||||
|
await window.core?.api?.relaunch()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description This function is used to read the logs from the app.
|
||||||
|
* It will return the logs as a string.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const readLogs = async () => {
|
||||||
|
const logData: string = (await invoke('read_logs')) ?? ''
|
||||||
|
return logData.split('\n').map(parseLogLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description This function is used to parse a log line.
|
||||||
|
* It will return the log line as an object.
|
||||||
|
* @param line
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const parseLogLine = (line: string) => {
|
||||||
|
const regex = /^\[(.*?)\]\[(.*?)\]\[(.*?)\]\[(.*?)\]\s(.*)$/
|
||||||
|
const match = line.match(regex)
|
||||||
|
|
||||||
|
if (!match) return undefined // Skip invalid lines
|
||||||
|
|
||||||
|
const [, date, time, target, levelRaw, message] = match
|
||||||
|
|
||||||
|
const level = levelRaw.toLowerCase() as 'info' | 'warn' | 'error' | 'debug'
|
||||||
|
|
||||||
|
return {
|
||||||
|
timestamp: `${date} ${time}`,
|
||||||
|
level,
|
||||||
|
target,
|
||||||
|
message,
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user