Add an HTTP API endpoint + UI widgets to stream logs (#49)
* Added JSON logs to Desktop * Move logging config into librqbit for reuse * Log printer now available in both Desktop and Web UI * Fix JS type error
This commit is contained in:
parent
9385524a1a
commit
2017c5ec94
21 changed files with 462 additions and 333 deletions
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
|
@ -2,5 +2,8 @@
|
|||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[rust]": {
|
||||
"editor.defaultFormatter": "rust-lang.rust-analyzer"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
13
Cargo.lock
generated
13
Cargo.lock
generated
|
|
@ -2667,6 +2667,16 @@ dependencies = [
|
|||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-serde"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.18"
|
||||
|
|
@ -2677,12 +2687,15 @@ dependencies = [
|
|||
"nu-ansi-term",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
"tracing-serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use std::time::Duration;
|
|||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use anyhow::Context;
|
||||
use tracing::{debug, error, error_span, info, trace, warn};
|
||||
use tracing::{error, error_span, info, trace, warn};
|
||||
|
||||
use crate::peer_store::PeerStore;
|
||||
use crate::routing_table::RoutingTable;
|
||||
|
|
@ -86,8 +86,8 @@ impl PersistentDht {
|
|||
};
|
||||
|
||||
info!(
|
||||
"will store DHT routing table to {:?} periodically",
|
||||
&config_filename
|
||||
filename=?config_filename,
|
||||
"will store DHT routing table periodically",
|
||||
);
|
||||
|
||||
if let Some(parent) = config_filename.parent() {
|
||||
|
|
@ -100,13 +100,14 @@ impl PersistentDht {
|
|||
let reader = BufReader::new(dht_json);
|
||||
match serde_json::from_reader::<_, DhtSerialize<RoutingTable, PeerStore>>(reader) {
|
||||
Ok(r) => {
|
||||
info!("loaded DHT routing table from {:?}", &config_filename);
|
||||
info!(filename=?config_filename, "loaded DHT routing table from");
|
||||
Some(r)
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"cannot deserialize routing table from file {:?}: {:#}",
|
||||
&config_filename, e
|
||||
filename=?config_filename,
|
||||
"cannot deserialize routing table: {:#}",
|
||||
e
|
||||
);
|
||||
None
|
||||
}
|
||||
|
|
@ -152,9 +153,9 @@ impl PersistentDht {
|
|||
tokio::time::sleep(dump_interval).await;
|
||||
|
||||
match dump_dht(&dht, &config_filename, &tempfile_name) {
|
||||
Ok(_) => debug!("dumped DHT to {:?}", &config_filename),
|
||||
Ok(_) => trace!(filename=?config_filename, "dumped DHT"),
|
||||
Err(e) => {
|
||||
error!("error dumping DHT to {:?}: {:#}", &config_filename, e)
|
||||
error!(filename=?config_filename, "error dumping DHT: {:#}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ rand = "0.8"
|
|||
openssl = {version="0.10", optional=true}
|
||||
crypto-hash = {version="0.3", optional=true}
|
||||
sha1 = {version = "0.10", optional=true}
|
||||
tracing-subscriber = {version = "0.3", default-features = false}
|
||||
tracing-subscriber = {version = "0.3", default-features = false, features = ["json"]}
|
||||
|
||||
uuid = {version = "1.2", features = ["v4"]}
|
||||
futures = "0.3"
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ use crate::{
|
|||
torrent_state::{
|
||||
peer::stats::snapshot::{PeerStatsFilter, PeerStatsSnapshot},
|
||||
ManagedTorrentHandle,
|
||||
}, log_subscriber::LineBroadcast,
|
||||
},
|
||||
tracing_subscriber_config_utils::LineBroadcast,
|
||||
};
|
||||
|
||||
pub use crate::torrent_state::stats::{LiveStats, TorrentStats};
|
||||
|
|
@ -39,12 +40,12 @@ impl Api {
|
|||
pub fn new(
|
||||
session: Arc<Session>,
|
||||
rust_log_reload_tx: Option<UnboundedSender<String>>,
|
||||
line_broadcast: Option<LineBroadcast>
|
||||
line_broadcast: Option<LineBroadcast>,
|
||||
) -> Self {
|
||||
Self {
|
||||
session,
|
||||
rust_log_reload_tx,
|
||||
line_broadcast
|
||||
line_broadcast,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -262,12 +262,12 @@ impl HttpApi {
|
|||
};
|
||||
|
||||
let app = app
|
||||
.layer(cors_layer)
|
||||
.layer(cors_layer)
|
||||
.layer(tower_http::trace::TraceLayer::new_for_http())
|
||||
.with_state(state)
|
||||
.into_make_service();
|
||||
|
||||
info!("starting HTTP server on {}", addr);
|
||||
info!(%addr, "starting HTTP server");
|
||||
|
||||
use tokio::net::TcpListener;
|
||||
let listener = TcpListener::bind(&addr)
|
||||
|
|
|
|||
|
|
@ -29,12 +29,12 @@ mod dht_utils;
|
|||
mod file_ops;
|
||||
pub mod http_api;
|
||||
pub mod http_api_client;
|
||||
pub mod log_subscriber;
|
||||
mod peer_connection;
|
||||
mod peer_info_reader;
|
||||
mod session;
|
||||
mod spawn_utils;
|
||||
mod torrent_state;
|
||||
pub mod tracing_subscriber_config_utils;
|
||||
mod tracker_comms;
|
||||
mod type_aliases;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
use std::io::LineWriter;
|
||||
|
||||
use bytes::Bytes;
|
||||
use tracing_subscriber::fmt::MakeWriter;
|
||||
|
||||
pub struct Subscriber {
|
||||
tx: tokio::sync::broadcast::Sender<Bytes>,
|
||||
}
|
||||
|
||||
pub struct Writer {
|
||||
tx: tokio::sync::broadcast::Sender<Bytes>,
|
||||
}
|
||||
|
||||
pub type LineBroadcast = tokio::sync::broadcast::Sender<Bytes>;
|
||||
|
||||
impl Subscriber {
|
||||
pub fn new() -> (Self, LineBroadcast) {
|
||||
let (tx, _) = tokio::sync::broadcast::channel(100);
|
||||
(Self { tx: tx.clone() }, tx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> MakeWriter<'a> for Subscriber {
|
||||
type Writer = LineWriter<Writer>;
|
||||
|
||||
fn make_writer(&self) -> Self::Writer {
|
||||
LineWriter::new(Writer {
|
||||
tx: self.tx.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::io::Write for Writer {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
let len = buf.len();
|
||||
if self.tx.receiver_count() == 0 {
|
||||
return Ok(len);
|
||||
}
|
||||
let arc = buf.to_vec().into();
|
||||
let _ = self.tx.send(arc);
|
||||
Ok(len)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -725,7 +725,7 @@ impl Session {
|
|||
|
||||
std::fs::rename(&tmp_filename, &self.persistence_filename)
|
||||
.context("error renaming persistence file")?;
|
||||
debug!("wrote persistence to {:?}", &self.persistence_filename);
|
||||
trace!(filename=?self.persistence_filename, "wrote persistence");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -779,7 +779,7 @@ impl Session {
|
|||
})
|
||||
.collect();
|
||||
|
||||
debug!("querying DHT for {:?}", info_hash);
|
||||
debug!(?info_hash, "querying DHT");
|
||||
let (info, dht_rx, initial_peers) = match read_metainfo_from_peer_receiver(
|
||||
self.peer_id,
|
||||
info_hash,
|
||||
|
|
@ -794,7 +794,7 @@ impl Session {
|
|||
anyhow::bail!("DHT died, no way to discover torrent metainfo")
|
||||
}
|
||||
};
|
||||
debug!("received result from DHT: {:?}", info);
|
||||
debug!(?info, "received result from DHT");
|
||||
(
|
||||
info_hash,
|
||||
info,
|
||||
|
|
@ -828,7 +828,7 @@ impl Session {
|
|||
|
||||
let dht_rx = match self.dht.as_ref() {
|
||||
Some(dht) if !opts.paused && !opts.list_only => {
|
||||
debug!("reading peers for {:?} from DHT", torrent.info_hash);
|
||||
debug!(info_hash=?torrent.info_hash, "reading peers from DHT");
|
||||
Some(dht.get_peers(torrent.info_hash, announce_port)?)
|
||||
}
|
||||
_ => None,
|
||||
|
|
@ -911,7 +911,7 @@ impl Session {
|
|||
continue;
|
||||
}
|
||||
if !list_only {
|
||||
info!("Will download {:?}", filename);
|
||||
info!(?filename, "will download");
|
||||
}
|
||||
}
|
||||
Ok(Some(only_files))
|
||||
|
|
@ -1043,14 +1043,14 @@ impl Session {
|
|||
match (paused, delete_files) {
|
||||
(Err(e), true) => Err(e).context("torrent deleted, but could not delete files"),
|
||||
(Err(e), false) => {
|
||||
warn!("could not delete torrent files: {:?}", e);
|
||||
warn!(error=?e, "could not delete torrent files");
|
||||
Ok(())
|
||||
}
|
||||
(Ok(Some(paused)), true) => {
|
||||
drop(paused.files);
|
||||
for file in paused.filenames {
|
||||
if let Err(e) = std::fs::remove_file(&file) {
|
||||
warn!("could not delete file {:?}: {:?}", file, e);
|
||||
warn!(?file, error=?e, "could not delete file");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
|
|
|||
142
crates/librqbit/src/tracing_subscriber_config_utils.rs
Normal file
142
crates/librqbit/src/tracing_subscriber_config_utils.rs
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
use std::io::LineWriter;
|
||||
|
||||
use anyhow::Context;
|
||||
use bytes::Bytes;
|
||||
use librqbit_core::spawn_utils::spawn;
|
||||
use tracing::error_span;
|
||||
use tracing_subscriber::fmt::MakeWriter;
|
||||
|
||||
struct Subscriber {
|
||||
tx: tokio::sync::broadcast::Sender<Bytes>,
|
||||
}
|
||||
|
||||
struct Writer {
|
||||
tx: tokio::sync::broadcast::Sender<Bytes>,
|
||||
}
|
||||
|
||||
pub type LineBroadcast = tokio::sync::broadcast::Sender<Bytes>;
|
||||
|
||||
impl Subscriber {
|
||||
pub fn new() -> (Self, LineBroadcast) {
|
||||
let (tx, _) = tokio::sync::broadcast::channel(100);
|
||||
(Self { tx: tx.clone() }, tx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> MakeWriter<'a> for Subscriber {
|
||||
type Writer = LineWriter<Writer>;
|
||||
|
||||
fn make_writer(&self) -> Self::Writer {
|
||||
LineWriter::new(Writer {
|
||||
tx: self.tx.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::io::Write for Writer {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
let len = buf.len();
|
||||
if self.tx.receiver_count() == 0 {
|
||||
return Ok(len);
|
||||
}
|
||||
let arc = buf.to_vec().into();
|
||||
let _ = self.tx.send(arc);
|
||||
Ok(len)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InitLoggingOptions<'a> {
|
||||
pub default_rust_log_value: Option<&'a str>,
|
||||
pub log_file: Option<&'a str>,
|
||||
pub log_file_rust_log: Option<&'a str>,
|
||||
}
|
||||
|
||||
pub struct InitLoggingResult {
|
||||
pub rust_log_reload_tx: tokio::sync::mpsc::UnboundedSender<String>,
|
||||
pub line_broadcast: LineBroadcast,
|
||||
}
|
||||
|
||||
pub fn init_logging(opts: InitLoggingOptions) -> anyhow::Result<InitLoggingResult> {
|
||||
let stderr_filter = EnvFilter::builder()
|
||||
.with_default_directive(
|
||||
opts.default_rust_log_value
|
||||
.unwrap_or("info")
|
||||
.parse()
|
||||
.context("can't parse provided rust_log value")?,
|
||||
)
|
||||
.from_env()
|
||||
.context("invalid RUST_LOG value")?;
|
||||
|
||||
let (stderr_filter, reload_stderr_filter) =
|
||||
tracing_subscriber::reload::Layer::new(stderr_filter);
|
||||
|
||||
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
|
||||
|
||||
let (line_sub, line_broadcast) = Subscriber::new();
|
||||
|
||||
let layered = tracing_subscriber::registry()
|
||||
// Stderr logging layer.
|
||||
.with(fmt::layer().with_filter(stderr_filter))
|
||||
// HTTP API log broadcast layer.
|
||||
.with(
|
||||
fmt::layer()
|
||||
.with_ansi(false)
|
||||
.fmt_fields(tracing_subscriber::fmt::format::JsonFields::new())
|
||||
.event_format(fmt::format().with_ansi(false).json())
|
||||
.with_writer(line_sub)
|
||||
.with_filter(EnvFilter::builder().parse("info,librqbit=debug").unwrap()),
|
||||
);
|
||||
if let Some(log_file) = &opts.log_file {
|
||||
let log_file = log_file.to_string();
|
||||
let log_file = move || {
|
||||
LineWriter::new(
|
||||
std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.write(true)
|
||||
.open(&log_file)
|
||||
.with_context(|| format!("error opening log file {:?}", log_file))
|
||||
.unwrap(),
|
||||
)
|
||||
};
|
||||
layered
|
||||
.with(
|
||||
fmt::layer()
|
||||
.with_ansi(false)
|
||||
.with_writer(log_file)
|
||||
.with_filter(
|
||||
EnvFilter::builder()
|
||||
.parse(opts.log_file_rust_log.unwrap_or("info,librqbit=debug"))
|
||||
.context("can't parse log-file-rust-log")?,
|
||||
),
|
||||
)
|
||||
.try_init()
|
||||
.context("can't init logging")?;
|
||||
} else {
|
||||
layered.try_init().context("can't init logging")?;
|
||||
}
|
||||
|
||||
let (reload_tx, mut reload_rx) = tokio::sync::mpsc::unbounded_channel::<String>();
|
||||
spawn(error_span!("fmt_filter_reloader"), async move {
|
||||
while let Some(rust_log) = reload_rx.recv().await {
|
||||
let stderr_env_filter = match EnvFilter::builder().parse(&rust_log) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
eprintln!("can't parse env filter {:?}: {:#?}", rust_log, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
eprintln!("setting RUST_LOG to {:?}", rust_log);
|
||||
let _ = reload_stderr_filter.reload(stderr_env_filter);
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
Ok(InitLoggingResult {
|
||||
rust_log_reload_tx: reload_tx,
|
||||
line_broadcast,
|
||||
})
|
||||
}
|
||||
20
crates/librqbit/webui/dist/assets/index.js
vendored
20
crates/librqbit/webui/dist/assets/index.js
vendored
File diff suppressed because one or more lines are too long
2
crates/librqbit/webui/dist/manifest.json
vendored
2
crates/librqbit/webui/dist/manifest.json
vendored
|
|
@ -4,7 +4,7 @@
|
|||
"src": "assets/logo.svg"
|
||||
},
|
||||
"index.html": {
|
||||
"file": "assets/index-b673c0d1.js",
|
||||
"file": "assets/index-050cef91.js",
|
||||
"isEntry": true,
|
||||
"src": "index.html"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { ErrorWithLabel } from "../rqbit-web";
|
||||
import { ErrorComponent } from "./ErrorComponent";
|
||||
import { Form } from "react-bootstrap";
|
||||
|
||||
interface LogStreamProps {
|
||||
httpApiBase: string;
|
||||
|
|
@ -10,6 +17,8 @@ interface LogStreamProps {
|
|||
interface Line {
|
||||
id: number;
|
||||
content: string;
|
||||
parsed: JSONLogLine;
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
const mergeBuffers = (a1: Uint8Array, a2: Uint8Array): Uint8Array => {
|
||||
|
|
@ -21,13 +30,13 @@ const mergeBuffers = (a1: Uint8Array, a2: Uint8Array): Uint8Array => {
|
|||
|
||||
const streamLogs = (
|
||||
httpApiBase: string,
|
||||
addLine: (text: string) => void,
|
||||
addLine: React.MutableRefObject<(text: string) => void>,
|
||||
setError: (error: ErrorWithLabel | null) => void
|
||||
): (() => void) => {
|
||||
const controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
|
||||
let canceled = true;
|
||||
let canceled = false;
|
||||
|
||||
const cancel = () => {
|
||||
console.log("cancelling fetch");
|
||||
|
|
@ -90,7 +99,7 @@ const streamLogs = (
|
|||
}
|
||||
let lineBytes = buffer.slice(0, newLineIdx);
|
||||
let line = new TextDecoder().decode(lineBytes);
|
||||
addLine(line);
|
||||
addLine.current(line);
|
||||
buffer = buffer.slice(newLineIdx + 1);
|
||||
}
|
||||
}
|
||||
|
|
@ -100,45 +109,125 @@ const streamLogs = (
|
|||
return cancel;
|
||||
};
|
||||
|
||||
const SplitByLevelRegexp = new RegExp(
|
||||
/(.*?) +(INFO|WARN|TRACE|ERROR|DEBUG) +(.*)/
|
||||
);
|
||||
type Value = string | number | boolean;
|
||||
|
||||
const LogLine = ({ line }: { line: string }) => {
|
||||
line.split;
|
||||
const getClassNameByLevel = (level: string) => {
|
||||
interface Span {
|
||||
name: string;
|
||||
[key: string]: Value;
|
||||
}
|
||||
|
||||
interface JSONLogLine {
|
||||
level: string;
|
||||
timestamp: string;
|
||||
fields: {
|
||||
message: string;
|
||||
[key: string]: Value;
|
||||
};
|
||||
target: string;
|
||||
span: Span;
|
||||
spans: Span[];
|
||||
}
|
||||
|
||||
const EXAMPLE_LOG_JSON: JSONLogLine = {
|
||||
timestamp: "2023-12-08T21:48:13.649165Z",
|
||||
level: "DEBUG",
|
||||
fields: { message: "successfully port forwarded 192.168.0.112:4225" },
|
||||
target: "librqbit_upnp",
|
||||
span: { port: 4225, name: "manage_port" },
|
||||
spans: [
|
||||
{ port: 4225, name: "upnp_forward" },
|
||||
{
|
||||
location: "http://192.168.0.1:49152/IGDdevicedesc_brlan0.xml",
|
||||
name: "upnp_endpoint",
|
||||
},
|
||||
{ device: "ARRIS TG3492LG", name: "device" },
|
||||
{ device: "WANDevice:1", name: "device" },
|
||||
{ device: "WANConnectionDevice:1", name: "device" },
|
||||
{ url: "/upnp/control/WANIPConnection0", name: "service" },
|
||||
{ port: 4225, name: "manage_port" },
|
||||
],
|
||||
};
|
||||
|
||||
const LogLine = ({ line }: { line: Line }) => {
|
||||
const parsed = line.parsed;
|
||||
|
||||
const classNameByLevel = (level: string) => {
|
||||
switch (level) {
|
||||
case "DEBUG":
|
||||
return "text-primary";
|
||||
case "INFO":
|
||||
return "text-success";
|
||||
case "WARN":
|
||||
return "text-warning";
|
||||
case "ERROR":
|
||||
return "text-danger";
|
||||
case "DEBUG":
|
||||
return "text-primary";
|
||||
default:
|
||||
return "text-secondary";
|
||||
return "text-muted";
|
||||
}
|
||||
};
|
||||
|
||||
const getContent = () => {
|
||||
let match = line.match(SplitByLevelRegexp);
|
||||
if (!match) {
|
||||
return line;
|
||||
const spanFields = (span: Span) => {
|
||||
let fields = Object.entries(span).filter(([name, value]) => name != "name");
|
||||
if (fields.length == 0) {
|
||||
return null;
|
||||
}
|
||||
const [beforeLevel, level, afterLevel] = match.slice(1);
|
||||
return (
|
||||
<>
|
||||
{beforeLevel}
|
||||
<span className={`${getClassNameByLevel(level)} m-2`}>{level}</span>
|
||||
{afterLevel}
|
||||
{"{"}
|
||||
{fields
|
||||
.map(([name, value]) => {
|
||||
return (
|
||||
<span key={name}>
|
||||
{name} = {value}
|
||||
</span>
|
||||
);
|
||||
})
|
||||
.reduce((prev, curr) => (
|
||||
<>
|
||||
{prev}, {curr}
|
||||
</>
|
||||
))}
|
||||
{"}"}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<p className="font-monospace m-0" style={{ fontSize: "10px" }}>
|
||||
{getContent()}
|
||||
<p
|
||||
hidden={!line.show}
|
||||
className="font-monospace m-0 text-break"
|
||||
style={{ fontSize: "10px" }}
|
||||
>
|
||||
<span className="m-1">{parsed.timestamp}</span>
|
||||
<span className={`m-1 ${classNameByLevel(parsed.level)}`}>
|
||||
{parsed.level}
|
||||
</span>
|
||||
|
||||
<span className="m-1">
|
||||
{parsed.spans?.map((span, i) => (
|
||||
<span key={i}>
|
||||
<span className="fw-bold">{span.name}</span>
|
||||
{spanFields(span)}:
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
<span className="m-1 text-muted">{parsed.target}</span>
|
||||
<span
|
||||
className={`m-1 ${
|
||||
parsed.fields.message.match(/error|fail/g)
|
||||
? "text-danger"
|
||||
: "text-muted"
|
||||
}`}
|
||||
>
|
||||
{parsed.fields.message}
|
||||
{Object.entries(parsed.fields)
|
||||
.filter(([key, value]) => key != "message")
|
||||
.map(([key, value]) => (
|
||||
<span className="m-1" key={key}>
|
||||
<span className="fst-italic fw-bold">{key}</span>={value}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
|
@ -149,32 +238,72 @@ export const LogStream: React.FC<LogStreamProps> = ({
|
|||
}) => {
|
||||
const [logLines, setLogLines] = useState<Line[]>([]);
|
||||
const [error, setError] = useState<ErrorWithLabel | null>(null);
|
||||
const [filter, setFilter] = useState<string>("");
|
||||
const filterRegex = useRef(new RegExp(""));
|
||||
|
||||
const maxL = maxLines ?? 1000;
|
||||
|
||||
const addLine = (text: string) => {
|
||||
setLogLines((logLines: Line[]) => {
|
||||
const nextLineId = logLines.length == 0 ? 0 : logLines[0].id + 1;
|
||||
const addLine = useCallback(
|
||||
(text: string) => {
|
||||
setLogLines((logLines: Line[]) => {
|
||||
const nextLineId = logLines.length == 0 ? 0 : logLines[0].id + 1;
|
||||
|
||||
let newLogLines = [
|
||||
{
|
||||
id: nextLineId,
|
||||
content: text,
|
||||
},
|
||||
...logLines.slice(0, maxL - 1),
|
||||
];
|
||||
return newLogLines;
|
||||
});
|
||||
let newLogLines = [
|
||||
{
|
||||
id: nextLineId,
|
||||
content: text,
|
||||
parsed: JSON.parse(text) as JSONLogLine,
|
||||
show: !!text.match(filterRegex.current),
|
||||
},
|
||||
...logLines.slice(0, maxL - 1),
|
||||
];
|
||||
return newLogLines;
|
||||
});
|
||||
},
|
||||
[filterRegex.current, maxLines]
|
||||
);
|
||||
|
||||
const addLineRef = useRef(addLine);
|
||||
addLineRef.current = addLine;
|
||||
|
||||
const handleFilterChange = (value: string) => {
|
||||
setFilter(value);
|
||||
try {
|
||||
let regex = new RegExp(value);
|
||||
filterRegex.current = regex;
|
||||
setLogLines((logLines) => {
|
||||
let tmp = [...logLines];
|
||||
for (let line of tmp) {
|
||||
line.show = !!line.content.match(regex);
|
||||
}
|
||||
return tmp;
|
||||
});
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return streamLogs(httpApiBase, addLine, setError);
|
||||
return streamLogs(httpApiBase, addLineRef, setError);
|
||||
}, [httpApiBase]);
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<ErrorComponent error={error} />
|
||||
<div className="mb-3">
|
||||
Showing last {maxL} logs since this window was opened
|
||||
</div>
|
||||
<Form>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Control
|
||||
type="text"
|
||||
value={filter}
|
||||
placeholder="Enter filter (regex)"
|
||||
onChange={(e) => handleFilterChange(e.target.value)}
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
|
||||
{logLines.map((line) => (
|
||||
<LogLine key={line.id} line={line.content} />
|
||||
<LogLine key={line.id} line={line} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ export const APIContext = createContext<RqbitAPI>({
|
|||
delete: () => {
|
||||
throw new Error("Function not implemented.");
|
||||
},
|
||||
getHttpBaseUrl: () => {
|
||||
throw new Error("Function not implemented.");
|
||||
},
|
||||
});
|
||||
export const AppContext = createContext<ContextType>({
|
||||
setCloseableError: (_) => {},
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ import { TorrentId, ErrorDetails as ApiErrorDetails } from "./api-types";
|
|||
import { AppContext, APIContext } from "./context";
|
||||
import { RootContent } from "./components/RootContent";
|
||||
import { customSetInterval } from "./helper/customSetInterval";
|
||||
import { IconButton } from "./components/IconButton";
|
||||
import { BsBodyText } from "react-icons/bs";
|
||||
import { LogStreamModal } from "./components/LogStreamModal";
|
||||
|
||||
export interface ErrorWithLabel {
|
||||
text: string;
|
||||
|
|
@ -14,7 +17,10 @@ export interface ContextType {
|
|||
refreshTorrents: () => void;
|
||||
}
|
||||
|
||||
export const RqbitWebUI = (props: { title: string }) => {
|
||||
export const RqbitWebUI = (props: {
|
||||
title: string;
|
||||
menuButtons?: JSX.Element[];
|
||||
}) => {
|
||||
const [closeableError, setCloseableError] = useState<ErrorWithLabel | null>(
|
||||
null
|
||||
);
|
||||
|
|
@ -22,6 +28,8 @@ export const RqbitWebUI = (props: { title: string }) => {
|
|||
|
||||
const [torrents, setTorrents] = useState<Array<TorrentId> | null>(null);
|
||||
const [torrentsLoading, setTorrentsLoading] = useState(false);
|
||||
let [logsOpened, setLogsOpened] = useState<boolean>(false);
|
||||
|
||||
const API = useContext(APIContext);
|
||||
|
||||
const refreshTorrents = async () => {
|
||||
|
|
@ -66,6 +74,17 @@ export const RqbitWebUI = (props: { title: string }) => {
|
|||
torrentsLoading={torrentsLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Menu buttons */}
|
||||
<div className="position-absolute top-0 start-0 p-1">
|
||||
{props.menuButtons &&
|
||||
props.menuButtons.map((b, i) => <span key={i}>{b}</span>)}
|
||||
<IconButton onClick={() => setLogsOpened(true)}>
|
||||
<BsBodyText />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<LogStreamModal show={logsOpened} onClose={() => setLogsOpened(false)} />
|
||||
</AppContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
use std::{io::LineWriter, net::SocketAddr, path::PathBuf, sync::Arc, time::Duration};
|
||||
use std::{net::SocketAddr, path::PathBuf, sync::Arc, time::Duration};
|
||||
|
||||
use anyhow::Context;
|
||||
use clap::{Parser, ValueEnum};
|
||||
use librqbit::{
|
||||
api::ApiAddTorrentResponse, http_api::{HttpApi, HttpApiOptions}, http_api_client, librqbit_spawn, AddTorrent,
|
||||
AddTorrentOptions, AddTorrentResponse, ListOnlyResponse, ManagedTorrentState,
|
||||
PeerConnectionOptions, Session, SessionOptions, log_subscriber::LineBroadcast, Api,
|
||||
api::ApiAddTorrentResponse,
|
||||
http_api::{HttpApi, HttpApiOptions},
|
||||
http_api_client, librqbit_spawn,
|
||||
tracing_subscriber_config_utils::{init_logging, InitLoggingOptions},
|
||||
AddTorrent, AddTorrentOptions, AddTorrentResponse, Api, ListOnlyResponse, ManagedTorrentState,
|
||||
PeerConnectionOptions, Session, SessionOptions,
|
||||
};
|
||||
use size_format::SizeFormatterBinary as SF;
|
||||
use tracing::{error, error_span, info, trace_span, warn};
|
||||
|
|
@ -178,125 +181,6 @@ enum SubCommand {
|
|||
Download(DownloadOpts),
|
||||
}
|
||||
|
||||
struct InitLoggingResult {
|
||||
rust_log_reload_tx: tokio::sync::mpsc::UnboundedSender<String>,
|
||||
line_broadcast: LineBroadcast,
|
||||
}
|
||||
|
||||
// Init logging and make a channel to send new RUST_LOG values to.
|
||||
fn init_logging(opts: &Opts) -> InitLoggingResult {
|
||||
let default_rust_log = match opts.log_level.as_ref() {
|
||||
Some(level) => match level {
|
||||
LogLevel::Trace => "trace",
|
||||
LogLevel::Debug => "debug",
|
||||
LogLevel::Info => "info",
|
||||
LogLevel::Warn => "warn",
|
||||
LogLevel::Error => "error",
|
||||
},
|
||||
None => "info",
|
||||
};
|
||||
let stderr_filter = match std::env::var("RUST_LOG").ok() {
|
||||
Some(rust_log) => EnvFilter::builder()
|
||||
.parse(rust_log)
|
||||
.expect("can't parse RUST_LOG"),
|
||||
None => EnvFilter::builder()
|
||||
.parse(default_rust_log)
|
||||
.expect("can't parse default_rust_log"),
|
||||
};
|
||||
|
||||
let (stderr_filter, reload_stderr_filter) =
|
||||
tracing_subscriber::reload::Layer::new(stderr_filter);
|
||||
|
||||
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
|
||||
|
||||
let (line_sub, line_broadcast) = librqbit::log_subscriber::Subscriber::new();
|
||||
|
||||
#[cfg(feature = "tokio-console")]
|
||||
{
|
||||
let (console_layer, server) = console_subscriber::Builder::default()
|
||||
.with_default_env()
|
||||
.build();
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(fmt::layer().with_filter(stderr_filter))
|
||||
.with(console_layer)
|
||||
.init();
|
||||
|
||||
spawn(
|
||||
"console_subscriber server",
|
||||
error_span!("console_subscriber server"),
|
||||
async move {
|
||||
server
|
||||
.serve()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("{:#?}", e))
|
||||
.context("error running console subscriber server")
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "tokio-console"))]
|
||||
{
|
||||
let layered = tracing_subscriber::registry()
|
||||
.with(fmt::layer().with_filter(stderr_filter))
|
||||
.with(
|
||||
fmt::layer()
|
||||
.event_format(fmt::format().with_ansi(false).compact())
|
||||
.with_ansi(false)
|
||||
.with_writer(line_sub)
|
||||
.with_filter(EnvFilter::builder().parse("info").unwrap()),
|
||||
);
|
||||
if let Some(log_file) = &opts.log_file {
|
||||
let log_file = log_file.clone();
|
||||
let log_file = move || {
|
||||
LineWriter::new(
|
||||
std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.write(true)
|
||||
.open(&log_file)
|
||||
.with_context(|| format!("error opening log file {:?}", log_file))
|
||||
.unwrap(),
|
||||
)
|
||||
};
|
||||
layered
|
||||
.with(
|
||||
fmt::layer()
|
||||
.with_ansi(false)
|
||||
.with_writer(log_file)
|
||||
.with_filter(EnvFilter::builder().parse(&opts.log_file_rust_log).unwrap()),
|
||||
)
|
||||
.init();
|
||||
} else {
|
||||
layered.init();
|
||||
}
|
||||
}
|
||||
|
||||
let (reload_tx, mut reload_rx) = tokio::sync::mpsc::unbounded_channel::<String>();
|
||||
librqbit_spawn(
|
||||
"fmt_filter_reloader",
|
||||
error_span!("fmt_filter_reloader"),
|
||||
async move {
|
||||
while let Some(rust_log) = reload_rx.recv().await {
|
||||
let stderr_env_filter = match EnvFilter::builder().parse(&rust_log) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
eprintln!("can't parse env filter {:?}: {:#?}", rust_log, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
eprintln!("setting RUST_LOG to {:?}", rust_log);
|
||||
let _ = reload_stderr_filter.reload(stderr_env_filter);
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
);
|
||||
InitLoggingResult {
|
||||
rust_log_reload_tx: reload_tx,
|
||||
line_broadcast,
|
||||
}
|
||||
}
|
||||
|
||||
fn _start_deadlock_detector_thread() {
|
||||
use parking_lot::deadlock;
|
||||
use std::thread;
|
||||
|
|
@ -350,7 +234,17 @@ fn main() -> anyhow::Result<()> {
|
|||
}
|
||||
|
||||
async fn async_main(opts: Opts) -> anyhow::Result<()> {
|
||||
let log_config = init_logging(&opts);
|
||||
let log_config = init_logging(InitLoggingOptions {
|
||||
default_rust_log_value: Some(match opts.log_level.unwrap_or(LogLevel::Info) {
|
||||
LogLevel::Trace => "trace",
|
||||
LogLevel::Debug => "debug",
|
||||
LogLevel::Info => "info",
|
||||
LogLevel::Warn => "warn",
|
||||
LogLevel::Error => "error",
|
||||
}),
|
||||
log_file: opts.log_file.as_deref(),
|
||||
log_file_rust_log: Some(&opts.log_file_rust_log),
|
||||
})?;
|
||||
|
||||
let mut sopts = SessionOptions {
|
||||
disable_dht: opts.disable_dht,
|
||||
|
|
@ -445,10 +339,14 @@ async fn async_main(opts: Opts) -> anyhow::Result<()> {
|
|||
trace_span!("stats_printer"),
|
||||
stats_printer(session.clone()),
|
||||
);
|
||||
let api = Api::new(session, Some(log_config.rust_log_reload_tx), Some(log_config.line_broadcast));
|
||||
let api = Api::new(
|
||||
session,
|
||||
Some(log_config.rust_log_reload_tx),
|
||||
Some(log_config.line_broadcast),
|
||||
);
|
||||
let http_api = HttpApi::new(
|
||||
api,
|
||||
Some(HttpApiOptions{
|
||||
Some(HttpApiOptions {
|
||||
read_only: false,
|
||||
cors_enable_all: false,
|
||||
}),
|
||||
|
|
@ -532,9 +430,17 @@ async fn async_main(opts: Opts) -> anyhow::Result<()> {
|
|||
trace_span!("stats_printer"),
|
||||
stats_printer(session.clone()),
|
||||
);
|
||||
let api = Api::new(session.clone(), Some(log_config.rust_log_reload_tx), Some(log_config.line_broadcast));
|
||||
let api = Api::new(
|
||||
session.clone(),
|
||||
Some(log_config.rust_log_reload_tx),
|
||||
Some(log_config.line_broadcast),
|
||||
);
|
||||
let http_api = HttpApi::new(
|
||||
api, Some(HttpApiOptions { cors_enable_all: false, read_only: true })
|
||||
api,
|
||||
Some(HttpApiOptions {
|
||||
cors_enable_all: false,
|
||||
read_only: true,
|
||||
}),
|
||||
);
|
||||
let http_api_listen_addr = opts.http_api_listen_addr;
|
||||
librqbit_spawn(
|
||||
|
|
|
|||
|
|
@ -105,11 +105,11 @@ async fn forward_port(
|
|||
.await
|
||||
.context("error reading response text")?;
|
||||
|
||||
trace!("AddPortMapping response: {} {}", status, response_text);
|
||||
trace!(status = %status, text=response_text, "AddPortMapping response");
|
||||
if !status.is_success() {
|
||||
bail!("failed port forwarding: {}", status);
|
||||
} else {
|
||||
debug!("successfully port forwarded {}:{}", local_ip, port);
|
||||
debug!(%local_ip, port, "successfully port forwarded");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -155,7 +155,7 @@ impl Device {
|
|||
}
|
||||
|
||||
pub fn span(&self, parent: tracing::Span) -> tracing::Span {
|
||||
error_span!(parent: parent, "device", name = self.name())
|
||||
error_span!(parent: parent, "device", device = self.name())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -355,11 +355,11 @@ impl UpnpPortForwarder {
|
|||
let response = match std::str::from_utf8(&buffer[..len]) {
|
||||
Ok(response) => response,
|
||||
Err(_) => {
|
||||
warn!("received invalid utf-8 from {addr}");
|
||||
warn!(%addr, "received invalid utf-8");
|
||||
continue;
|
||||
},
|
||||
};
|
||||
trace!("received response from {addr}: {response}");
|
||||
trace!(%addr, response, "response");
|
||||
match parse_upnp_discover_response(response, addr) {
|
||||
Ok(r) => {
|
||||
tx.send(r)?;
|
||||
|
|
|
|||
13
desktop/src-tauri/Cargo.lock
generated
13
desktop/src-tauri/Cargo.lock
generated
|
|
@ -4126,6 +4126,16 @@ dependencies = [
|
|||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-serde"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.18"
|
||||
|
|
@ -4136,12 +4146,15 @@ dependencies = [
|
|||
"nu-ansi-term",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
"tracing-serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ anyhow = "1.0.75"
|
|||
base64 = "0.21.5"
|
||||
http = "1.0.0"
|
||||
directories = "5.0.1"
|
||||
tracing-subscriber = {version = "0.3.18", features = ["env-filter"] }
|
||||
tracing-subscriber = {version = "0.3.18", features = ["env-filter", "json"] }
|
||||
tracing = "0.1"
|
||||
serde_with = "3.4.0"
|
||||
parking_lot = "0.12.1"
|
||||
|
|
|
|||
|
|
@ -19,8 +19,7 @@ use librqbit::{
|
|||
TorrentStats,
|
||||
},
|
||||
dht::PersistentDhtConfig,
|
||||
librqbit_spawn,
|
||||
log_subscriber::LineBroadcast,
|
||||
tracing_subscriber_config_utils::{init_logging, InitLoggingOptions, InitLoggingResult},
|
||||
AddTorrent, AddTorrentOptions, Api, ApiError, PeerConnectionOptions, Session, SessionOptions,
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
|
|
@ -35,12 +34,10 @@ struct StateShared {
|
|||
api: Option<Api>,
|
||||
}
|
||||
|
||||
type RustLogReloadTx = tokio::sync::mpsc::UnboundedSender<String>;
|
||||
|
||||
struct State {
|
||||
config_filename: String,
|
||||
shared: Arc<RwLock<Option<StateShared>>>,
|
||||
init_logging: InitLogging,
|
||||
init_logging: InitLoggingResult,
|
||||
}
|
||||
|
||||
fn read_config(path: &str) -> anyhow::Result<RqbitDesktopConfig> {
|
||||
|
|
@ -65,7 +62,7 @@ fn write_config(path: &str, config: &RqbitDesktopConfig) -> anyhow::Result<()> {
|
|||
}
|
||||
|
||||
async fn api_from_config(
|
||||
init_logging: &InitLogging,
|
||||
init_logging: &InitLoggingResult,
|
||||
config: &RqbitDesktopConfig,
|
||||
) -> anyhow::Result<Api> {
|
||||
let session = Session::new_with_opts(
|
||||
|
|
@ -98,7 +95,7 @@ async fn api_from_config(
|
|||
|
||||
let api = Api::new(
|
||||
session.clone(),
|
||||
Some(init_logging.reload_stdout_tx.clone()),
|
||||
Some(init_logging.rust_log_reload_tx.clone()),
|
||||
Some(init_logging.line_broadcast.clone()),
|
||||
);
|
||||
|
||||
|
|
@ -118,7 +115,7 @@ async fn api_from_config(
|
|||
}
|
||||
|
||||
impl State {
|
||||
async fn new(init_logging: InitLogging) -> Self {
|
||||
async fn new(init_logging: InitLoggingResult) -> Self {
|
||||
let config_filename = directories::ProjectDirs::from("com", "rqbit", "desktop")
|
||||
.expect("directories::ProjectDirs::from")
|
||||
.config_dir()
|
||||
|
|
@ -302,64 +299,16 @@ fn get_version() -> &'static str {
|
|||
env!("CARGO_PKG_VERSION")
|
||||
}
|
||||
|
||||
struct InitLogging {
|
||||
reload_stdout_tx: RustLogReloadTx,
|
||||
line_broadcast: LineBroadcast,
|
||||
}
|
||||
|
||||
fn init_logging() -> InitLogging {
|
||||
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
|
||||
let (stderr_filter, reload_stderr_filter) = tracing_subscriber::reload::Layer::new(
|
||||
EnvFilter::builder()
|
||||
.with_default_directive("info".parse().unwrap())
|
||||
.from_env()
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let (line_sub, line_broadcast) = librqbit::log_subscriber::Subscriber::new();
|
||||
|
||||
let layered = tracing_subscriber::registry()
|
||||
.with(fmt::layer().with_filter(stderr_filter))
|
||||
.with(
|
||||
fmt::layer()
|
||||
.with_ansi(false)
|
||||
.fmt_fields(tracing_subscriber::fmt::format::DefaultFields::new().delimited(","))
|
||||
.event_format(fmt::format().with_ansi(false))
|
||||
.with_writer(line_sub)
|
||||
.with_filter(EnvFilter::builder().parse("info,librqbit=debug").unwrap()),
|
||||
);
|
||||
layered.init();
|
||||
|
||||
let (reload_tx, mut reload_rx) = tokio::sync::mpsc::unbounded_channel::<String>();
|
||||
librqbit_spawn(
|
||||
"fmt_filter_reloader",
|
||||
error_span!("fmt_filter_reloader"),
|
||||
async move {
|
||||
while let Some(rust_log) = reload_rx.recv().await {
|
||||
let stderr_env_filter = match EnvFilter::builder().parse(&rust_log) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
eprintln!("can't parse env filter {:?}: {:#?}", rust_log, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
eprintln!("setting RUST_LOG to {:?}", rust_log);
|
||||
let _ = reload_stderr_filter.reload(stderr_env_filter);
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
);
|
||||
InitLogging {
|
||||
reload_stdout_tx: reload_tx,
|
||||
line_broadcast,
|
||||
}
|
||||
}
|
||||
|
||||
async fn start() {
|
||||
tauri::async_runtime::set(tokio::runtime::Handle::current());
|
||||
let rust_log_reload_tx = init_logging();
|
||||
let init_logging_result = init_logging(InitLoggingOptions {
|
||||
default_rust_log_value: Some("info"),
|
||||
log_file: None,
|
||||
log_file_rust_log: None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let state = State::new(rust_log_reload_tx).await;
|
||||
let state = State::new(init_logging_result).await;
|
||||
|
||||
tauri::Builder::default()
|
||||
.manage(state)
|
||||
|
|
|
|||
|
|
@ -18,27 +18,25 @@ export const RqbitDesktop: React.FC<{
|
|||
currentState.config ?? defaultConfig
|
||||
);
|
||||
let [configurationOpened, setConfigurationOpened] = useState<boolean>(false);
|
||||
let [logsOpened, setLogsOpened] = useState<boolean>(false);
|
||||
|
||||
const configButton = (
|
||||
<IconButton
|
||||
className="p-3 text-primary"
|
||||
onClick={() => {
|
||||
setConfigurationOpened(true);
|
||||
}}
|
||||
>
|
||||
<BsSliders2 />
|
||||
</IconButton>
|
||||
);
|
||||
|
||||
return (
|
||||
<APIContext.Provider value={makeAPI(config)}>
|
||||
{configured && (
|
||||
<RqbitWebUI title={`Rqbit Desktop v${version}`}></RqbitWebUI>
|
||||
)}
|
||||
{configured && (
|
||||
<div className="position-absolute top-0 start-0">
|
||||
<IconButton
|
||||
className="p-3 text-primary"
|
||||
onClick={() => {
|
||||
setConfigurationOpened(true);
|
||||
}}
|
||||
>
|
||||
<BsSliders2 />
|
||||
</IconButton>
|
||||
<IconButton onClick={() => setLogsOpened(true)}>
|
||||
<BsBodyText />
|
||||
</IconButton>
|
||||
</div>
|
||||
<RqbitWebUI
|
||||
title={`Rqbit Desktop v${version}`}
|
||||
menuButtons={[configButton]}
|
||||
></RqbitWebUI>
|
||||
)}
|
||||
<ConfigModal
|
||||
show={!configured || configurationOpened}
|
||||
|
|
@ -56,7 +54,6 @@ export const RqbitDesktop: React.FC<{
|
|||
initialConfig={config}
|
||||
defaultConfig={defaultConfig}
|
||||
/>
|
||||
<LogStreamModal show={logsOpened} onClose={() => setLogsOpened(false)} />
|
||||
</APIContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue