fix(database): make src/api/database compile under WASM (web-only) build
The /admin/system feature was never verified against the WASM frontend target (dx serve builds src/main.rs with --features web and no server). The entire src/api/database/ tree unconditionally pulled in server-only crates (axum, sqlparser, dashmap, tokio, tokio_postgres, futures, regex) and server-gated helpers (get_current_admin_user, parse_session_token, get_user_by_token), so the frontend build failed with 43 errors. Root cause + fixes (mirror the established settings.rs pattern): - Gate server-only imports behind #[cfg(feature = server)] in status/system_status/sql_console/schema/tasks/backup. The #[server] macro strips call sites but NOT use statements, so unresolved imports remained. - Ungate in main.rs: SystemSnapshot is a shared serde type referenced by ServerStatus.host on both targets; only the sampler task/SNAPSHOT static are internally server-gated. - Gate (pure Axum handler, zero WASM consumers) and the download_backup Axum handler in backup.rs. - Move restore_backup's validation preamble (regex/backup_path/fs) inside the #[cfg(feature = server)] block; gate std::path/chrono::Utc imports. - Add [[bin]] required-features=[server] for generate_highlight_css so the syntect-dependent build tool is skipped in web-only builds. Genuine WASM bugs surfaced once the server bodies stopped masking them: - system.rs: Closure import path (dioxus::prelude::wasm_bindgen -> wasm_bindgen, matching write.rs); setTimeout expects i32 not u32; editor_handle() -> editor_handle.read() (Signal is not Fn in 0.7). - codemirror_bridge.rs: set_schema extern took &SqlSchema but SqlSchema is a serde type with no IntoWasmAbi. Changed both set_schema signatures to &JsValue; call site now serializes via serde-wasm-bindgen::to_value. - system.rs: signals .set() inside spawn/use_future/onclick closures need bindings (incl. the *_f rebinding copies); added cfg_attr allow(unused_mut) on the 4 tab components, matching write.rs convention. Verified: cargo check (default/server) clean; cargo check --features web --target wasm32-unknown-unknown clean (0 errors); dx check clean; 411 cargo tests pass.
This commit is contained in:
parent
37d7ec49eb
commit
ecf3c73715
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -5496,6 +5496,7 @@ dependencies = [
|
|||||||
"rand 0.8.6",
|
"rand 0.8.6",
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde-wasm-bindgen",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serial_test",
|
"serial_test",
|
||||||
"sha2 0.10.9",
|
"sha2 0.10.9",
|
||||||
|
|||||||
@ -3,6 +3,13 @@ name = "yggdrasil"
|
|||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
# 构建时工具二进制:用 syntect(server-only 依赖)生成 highlight.css。
|
||||||
|
# 仅在启用 server feature 时编译——WASM 前端构建(--no-default-features --features web)
|
||||||
|
# 不会拉入 syntect,此二进制若也参与编译会因找不到 syntect 报错。
|
||||||
|
[[bin]]
|
||||||
|
name = "generate_highlight_css"
|
||||||
|
required-features = ["server"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dioxus = { version = "0.7.9", features = ["fullstack", "router"] }
|
dioxus = { version = "0.7.9", features = ["fullstack", "router"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
@ -47,6 +54,8 @@ web-sys = { version = "0.3", features = ["Document", "Window", "Storage", "Eleme
|
|||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
wasm-bindgen-futures = "0.4"
|
wasm-bindgen-futures = "0.4"
|
||||||
js-sys = "0.3"
|
js-sys = "0.3"
|
||||||
|
# 把 serde 类型(如 SqlSchema)序列化为 JsValue,供 wasm-bindgen extern 传递。
|
||||||
|
serde-wasm-bindgen = "0.6"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "1.52", features = ["rt-multi-thread", "macros", "fs", "time", "sync"] }
|
tokio = { version = "1.52", features = ["rt-multi-thread", "macros", "fs", "time", "sync"] }
|
||||||
|
|||||||
@ -7,14 +7,21 @@
|
|||||||
//! 恢复:仅接受本系统生成的备份(签名校验)+ 二次确认 + 路径穿越防护。
|
//! 恢复:仅接受本系统生成的备份(签名校验)+ 二次确认 + 路径穿越防护。
|
||||||
//! 长耗时操作走后台任务 + 进度轮询(见 [`super::tasks`])。
|
//! 长耗时操作走后台任务 + 进度轮询(见 [`super::tasks`])。
|
||||||
|
|
||||||
|
// Component/PathBuf/chrono::Utc 仅 server 构建的备份逻辑用到。
|
||||||
|
#[cfg(feature = "server")]
|
||||||
use std::path::{Component, PathBuf};
|
use std::path::{Component, PathBuf};
|
||||||
|
|
||||||
|
#[cfg(feature = "server")]
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
// admin 鉴权 + AppError + tasks 进度表仅在 server 构建里被 server function 体引用。
|
||||||
|
#[cfg(feature = "server")]
|
||||||
use crate::api::auth::get_current_admin_user;
|
use crate::api::auth::get_current_admin_user;
|
||||||
|
#[cfg(feature = "server")]
|
||||||
use crate::api::database::tasks::{self, TaskKind, TaskStatus};
|
use crate::api::database::tasks::{self, TaskKind, TaskStatus};
|
||||||
|
#[cfg(feature = "server")]
|
||||||
use crate::api::error::AppError;
|
use crate::api::error::AppError;
|
||||||
|
|
||||||
/// 备份目录(项目根,与 uploads/ 平级,gitignored)。
|
/// 备份目录(项目根,与 uploads/ 平级,gitignored)。
|
||||||
@ -316,6 +323,10 @@ async fn run_sql_fallback_backup(task_id: &str, timestamp: &str) {
|
|||||||
pub async fn restore_backup(filename: String, confirm: bool) -> Result<String, ServerFnError> {
|
pub async fn restore_backup(filename: String, confirm: bool) -> Result<String, ServerFnError> {
|
||||||
let _user = get_current_admin_user().await?;
|
let _user = get_current_admin_user().await?;
|
||||||
|
|
||||||
|
// 全部校验都在 server cfg 块内:confirm/regex/backup_path/std::fs 都是 server-only。
|
||||||
|
// WASM 侧的 server-function 客户端桩只返回 Ok(String::new())。
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
{
|
||||||
if !confirm {
|
if !confirm {
|
||||||
return Err(AppError::BadRequest("需确认恢复(会覆盖现有数据)".to_string()).into());
|
return Err(AppError::BadRequest("需确认恢复(会覆盖现有数据)".to_string()).into());
|
||||||
}
|
}
|
||||||
@ -338,8 +349,6 @@ pub async fn restore_backup(filename: String, confirm: bool) -> Result<String, S
|
|||||||
return Err(AppError::BadRequest("非本系统生成的备份文件,拒绝恢复".to_string()).into());
|
return Err(AppError::BadRequest("非本系统生成的备份文件,拒绝恢复".to_string()).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
|
||||||
{
|
|
||||||
let task_id = uuid::Uuid::new_v4().to_string();
|
let task_id = uuid::Uuid::new_v4().to_string();
|
||||||
tasks::insert(task_id.clone(), TaskKind::Restore);
|
tasks::insert(task_id.clone(), TaskKind::Restore);
|
||||||
let tid = task_id.clone();
|
let tid = task_id.clone();
|
||||||
@ -351,6 +360,8 @@ pub async fn restore_backup(filename: String, confirm: bool) -> Result<String, S
|
|||||||
}
|
}
|
||||||
#[cfg(not(feature = "server"))]
|
#[cfg(not(feature = "server"))]
|
||||||
{
|
{
|
||||||
|
// WASM 客户端桩:忽略参数,返回空 task_id。
|
||||||
|
let _ = (filename, confirm);
|
||||||
Ok(String::new())
|
Ok(String::new())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -525,6 +536,8 @@ fn backup_path(filename: &str) -> PathBuf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Axum 处理器:下载备份文件(admin 鉴权 + 路径白名单)。
|
/// Axum 处理器:下载备份文件(admin 鉴权 + 路径白名单)。
|
||||||
|
/// 仅 server 构建:纯 Axum 路由(在 main.rs 注册),无 WASM 消费者。
|
||||||
|
#[cfg(feature = "server")]
|
||||||
pub async fn download_backup(
|
pub async fn download_backup(
|
||||||
axum::extract::Path(filename): axum::extract::Path<String>,
|
axum::extract::Path(filename): axum::extract::Path<String>,
|
||||||
headers: axum::http::HeaderMap,
|
headers: axum::http::HeaderMap,
|
||||||
|
|||||||
@ -14,7 +14,8 @@ pub mod system_status;
|
|||||||
pub mod sql_console;
|
pub mod sql_console;
|
||||||
/// SQL 补全用 schema 拉取。
|
/// SQL 补全用 schema 拉取。
|
||||||
pub mod schema;
|
pub mod schema;
|
||||||
/// 数据导出 Axum 流式处理器。
|
/// 数据导出 Axum 流式处理器(仅 server:纯 Axum 路由,无 WASM 消费者)。
|
||||||
|
#[cfg(feature = "server")]
|
||||||
pub mod export;
|
pub mod export;
|
||||||
/// 备份/恢复(双模式 + 任务进度)。
|
/// 备份/恢复(双模式 + 任务进度)。
|
||||||
pub mod backup;
|
pub mod backup;
|
||||||
|
|||||||
@ -4,10 +4,15 @@
|
|||||||
|
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
// admin 鉴权 + DB 查询仅在 server 构建里被 server function 体引用。
|
||||||
|
#[cfg(feature = "server")]
|
||||||
use crate::api::auth::get_current_admin_user;
|
use crate::api::auth::get_current_admin_user;
|
||||||
|
#[cfg(feature = "server")]
|
||||||
use crate::api::error::AppError;
|
use crate::api::error::AppError;
|
||||||
use crate::codemirror_bridge::SqlSchema;
|
#[cfg(feature = "server")]
|
||||||
use crate::db::pool::get_conn;
|
use crate::db::pool::get_conn;
|
||||||
|
// SqlSchema/SqlTable 是两端共享的纯数据类型(定义在 codemirror_bridge)。
|
||||||
|
use crate::codemirror_bridge::SqlSchema;
|
||||||
|
|
||||||
/// 拉取数据库 schema(表名 + 列名),供 CodeMirror SQL 补全。
|
/// 拉取数据库 schema(表名 + 列名),供 CodeMirror SQL 补全。
|
||||||
#[server(GetDbSchema, "/api")]
|
#[server(GetDbSchema, "/api")]
|
||||||
|
|||||||
@ -14,8 +14,12 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
// admin 鉴权 + DB 查询仅在 server 构建里被 server function 体引用。
|
||||||
|
#[cfg(feature = "server")]
|
||||||
use crate::api::auth::get_current_admin_user;
|
use crate::api::auth::get_current_admin_user;
|
||||||
|
#[cfg(feature = "server")]
|
||||||
use crate::api::error::AppError;
|
use crate::api::error::AppError;
|
||||||
|
#[cfg(feature = "server")]
|
||||||
use crate::db::pool::get_conn;
|
use crate::db::pool::get_conn;
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, Clone, Copy)]
|
#[derive(Deserialize, Serialize, Debug, Clone, Copy)]
|
||||||
|
|||||||
@ -9,8 +9,13 @@ use chrono::{DateTime, Utc};
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
// 仅 server 构建用到:admin 鉴权 + DB 查询。WASM 侧的 server-function 客户端桩
|
||||||
|
// 不解析这些符号,必须 gate 以避免在非 server 构建里找不到 server-only 符号。
|
||||||
|
#[cfg(feature = "server")]
|
||||||
use crate::api::auth::get_current_admin_user;
|
use crate::api::auth::get_current_admin_user;
|
||||||
|
#[cfg(feature = "server")]
|
||||||
use crate::api::error::AppError;
|
use crate::api::error::AppError;
|
||||||
|
#[cfg(feature = "server")]
|
||||||
use crate::db::pool::get_conn;
|
use crate::db::pool::get_conn;
|
||||||
|
|
||||||
/// 数据库状态聚合数据。
|
/// 数据库状态聚合数据。
|
||||||
|
|||||||
@ -8,7 +8,10 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
// admin 鉴权 + AppError 仅在 server 构建里被 server function 体引用。
|
||||||
|
#[cfg(feature = "server")]
|
||||||
use crate::api::auth::get_current_admin_user;
|
use crate::api::auth::get_current_admin_user;
|
||||||
|
#[cfg(feature = "server")]
|
||||||
use crate::api::error::AppError;
|
use crate::api::error::AppError;
|
||||||
|
|
||||||
/// 单个缓存的统计快照(前端展示用)。
|
/// 单个缓存的统计快照(前端展示用)。
|
||||||
|
|||||||
@ -6,14 +6,18 @@
|
|||||||
//! [`update`] 更新进度,前端轮询 [`get_task_progress`]。
|
//! [`update`] 更新进度,前端轮询 [`get_task_progress`]。
|
||||||
//! 已完成超过 1 小时的任务惰性清理,避免内存累积。
|
//! 已完成超过 1 小时的任务惰性清理,避免内存累积。
|
||||||
|
|
||||||
use std::sync::LazyLock;
|
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use dashmap::DashMap;
|
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
// admin 鉴权仅 server 构建用到。
|
||||||
|
#[cfg(feature = "server")]
|
||||||
use crate::api::auth::get_current_admin_user;
|
use crate::api::auth::get_current_admin_user;
|
||||||
|
// DashMap / LazyLock 仅 server 构建持有任务进度表;WASM 端只序列化 TaskProgress。
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
use dashmap::DashMap;
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
|
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
|
||||||
pub enum TaskKind {
|
pub enum TaskKind {
|
||||||
|
|||||||
@ -32,7 +32,6 @@ pub struct SqlTable {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
pub mod wasm {
|
pub mod wasm {
|
||||||
use super::SqlSchema;
|
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
|
|
||||||
@ -91,8 +90,10 @@ pub mod wasm {
|
|||||||
pub fn set_theme(this: &EditorInstance, theme: &str);
|
pub fn set_theme(this: &EditorInstance, theme: &str);
|
||||||
|
|
||||||
/// 更新 SQL 补全 schema(Compartment.reconfigure)。
|
/// 更新 SQL 补全 schema(Compartment.reconfigure)。
|
||||||
|
/// 参数为 serde_wasm_bindgen::to_value 序列化后的 JsValue
|
||||||
|
///(SqlSchema 是 serde 类型,非 wasm-bindgen 类型,故不能直接传 &SqlSchema)。
|
||||||
#[wasm_bindgen(method, js_name = setSchema)]
|
#[wasm_bindgen(method, js_name = setSchema)]
|
||||||
pub fn set_schema(this: &EditorInstance, schema: &SqlSchema);
|
pub fn set_schema(this: &EditorInstance, schema: &wasm_bindgen::JsValue);
|
||||||
|
|
||||||
/// 让编辑器获取焦点。
|
/// 让编辑器获取焦点。
|
||||||
#[wasm_bindgen(method)]
|
#[wasm_bindgen(method)]
|
||||||
@ -126,9 +127,9 @@ pub mod wasm {
|
|||||||
#[wasm_bindgen(method, setter, js_name = vim)]
|
#[wasm_bindgen(method, setter, js_name = vim)]
|
||||||
pub fn set_vim(this: &EditorOptions, v: bool);
|
pub fn set_vim(this: &EditorOptions, v: bool);
|
||||||
|
|
||||||
/// SQL 补全 schema(表/列数据)。
|
/// SQL 补全 schema(表/列数据)。v 为 serde_wasm_bindgen::to_value 序列化结果。
|
||||||
#[wasm_bindgen(method, setter, js_name = schema)]
|
#[wasm_bindgen(method, setter, js_name = schema)]
|
||||||
pub fn set_schema(this: &EditorOptions, v: &SqlSchema);
|
pub fn set_schema(this: &EditorOptions, v: &wasm_bindgen::JsValue);
|
||||||
|
|
||||||
/// 初始文档内容。
|
/// 初始文档内容。
|
||||||
#[wasm_bindgen(method, setter, js_name = value)]
|
#[wasm_bindgen(method, setter, js_name = value)]
|
||||||
|
|||||||
@ -23,8 +23,9 @@ mod hooks;
|
|||||||
mod models;
|
mod models;
|
||||||
mod pages;
|
mod pages;
|
||||||
mod router;
|
mod router;
|
||||||
// sysinfo_sampler 仅在 server feature 启用时编译:主机指标后台采样 + RwLock 快照。
|
// sysinfo_sampler:主机指标快照。
|
||||||
#[cfg(feature = "server")]
|
// SystemSnapshot 结构体两端都编译(被 system_status 的 ServerStatus 字段引用);
|
||||||
|
// 真正的采样任务 / RwLock / read_snapshot 实现在本模块内部自行 #[cfg(feature = "server")] gate。
|
||||||
mod sysinfo_sampler;
|
mod sysinfo_sampler;
|
||||||
// ssr_cache 仅在 server feature 启用时编译;保存 SSR 世代号失效状态。
|
// ssr_cache 仅在 server feature 启用时编译;保存 SSR 世代号失效状态。
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
|
|||||||
@ -97,7 +97,7 @@ async fn wasm_sleep(ms: u32) {
|
|||||||
.expect("no window")
|
.expect("no window")
|
||||||
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||||
&resolve.unchecked_into(),
|
&resolve.unchecked_into(),
|
||||||
ms,
|
ms.try_into().unwrap(),
|
||||||
)
|
)
|
||||||
.expect("set_timeout failed");
|
.expect("set_timeout failed");
|
||||||
});
|
});
|
||||||
@ -107,6 +107,9 @@ async fn wasm_sleep(ms: u32) {
|
|||||||
/// 数据库状态 tab:概览卡片 + 表清单 + 索引 Top + 活跃连接。
|
/// 数据库状态 tab:概览卡片 + 表清单 + 索引 Top + 活跃连接。
|
||||||
/// 手动刷新按钮 + 自动刷新开关(1s/2s/5s/30s/手动,默认手动)。
|
/// 手动刷新按钮 + 自动刷新开关(1s/2s/5s/30s/手动,默认手动)。
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
|
// status/error/loading 在 spawn/onclick 闭包里 .set(),仅 WASM 前端真正用到;
|
||||||
|
// server 构建里这些 set 调用都在被剥离的 #[cfg(wasm32)] 块内,故 allow unused_mut。
|
||||||
|
#[cfg_attr(not(target_arch = "wasm32"), allow(unused_mut, unused_variables))]
|
||||||
fn DbStatusTab() -> Element {
|
fn DbStatusTab() -> Element {
|
||||||
use crate::api::database::status::DbStatus;
|
use crate::api::database::status::DbStatus;
|
||||||
// get_db_status 只在 WASM 前端调用,server 构建时该 server function 的客户端桩不需要导入。
|
// get_db_status 只在 WASM 前端调用,server 构建时该 server function 的客户端桩不需要导入。
|
||||||
@ -115,9 +118,9 @@ fn DbStatusTab() -> Element {
|
|||||||
use crate::components::ui::{ADMIN_CARD_CLASS, ADMIN_TABLE_CLASS};
|
use crate::components::ui::{ADMIN_CARD_CLASS, ADMIN_TABLE_CLASS};
|
||||||
|
|
||||||
// Signal 是 Copy,可在多个 spawn/effect 中捕获同一副本;set 走内部可变(&self)。
|
// Signal 是 Copy,可在多个 spawn/effect 中捕获同一副本;set 走内部可变(&self)。
|
||||||
let status = use_signal(|| Option::<DbStatus>::None);
|
let mut status = use_signal(|| Option::<DbStatus>::None);
|
||||||
let mut loading = use_signal(|| true);
|
let mut loading = use_signal(|| true);
|
||||||
let error = use_signal(|| Option::<String>::None);
|
let mut error = use_signal(|| Option::<String>::None);
|
||||||
// 自动刷新间隔(秒);None = 手动。DB 查询有成本,最低 1s。
|
// 自动刷新间隔(秒);None = 手动。DB 查询有成本,最低 1s。
|
||||||
let mut refresh_interval: Signal<Option<u32>> = use_signal(|| None);
|
let mut refresh_interval: Signal<Option<u32>> = use_signal(|| None);
|
||||||
|
|
||||||
@ -155,9 +158,9 @@ fn DbStatusTab() -> Element {
|
|||||||
use_future(move || {
|
use_future(move || {
|
||||||
// 在依赖闭包里读 interval,使 use_future 在它变化时重新执行。
|
// 在依赖闭包里读 interval,使 use_future 在它变化时重新执行。
|
||||||
let interval_secs = refresh_interval();
|
let interval_secs = refresh_interval();
|
||||||
let status_f = status;
|
let mut status_f = status;
|
||||||
let loading_f = loading;
|
let mut loading_f = loading;
|
||||||
let error_f = error;
|
let mut error_f = error;
|
||||||
async move {
|
async move {
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
{
|
{
|
||||||
@ -397,15 +400,16 @@ fn format_uptime(secs: u64) -> String {
|
|||||||
/// 手动刷新 + 自动刷新开关(500ms/1s/2s/5s/手动,默认手动)。
|
/// 手动刷新 + 自动刷新开关(500ms/1s/2s/5s/手动,默认手动)。
|
||||||
/// 主机层数据由后台 500ms 采样,前端轮询只读快照零成本,故可高频。
|
/// 主机层数据由后台 500ms 采样,前端轮询只读快照零成本,故可高频。
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
|
#[cfg_attr(not(target_arch = "wasm32"), allow(unused_mut, unused_variables))]
|
||||||
fn ServerStatusTab() -> Element {
|
fn ServerStatusTab() -> Element {
|
||||||
use crate::api::database::system_status::ServerStatus;
|
use crate::api::database::system_status::ServerStatus;
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
use crate::api::database::system_status::get_server_status;
|
use crate::api::database::system_status::get_server_status;
|
||||||
use crate::components::ui::{ADMIN_CARD_CLASS, ADMIN_TABLE_CLASS};
|
use crate::components::ui::{ADMIN_CARD_CLASS, ADMIN_TABLE_CLASS};
|
||||||
|
|
||||||
let status = use_signal(|| Option::<ServerStatus>::None);
|
let mut status = use_signal(|| Option::<ServerStatus>::None);
|
||||||
let mut loading = use_signal(|| true);
|
let mut loading = use_signal(|| true);
|
||||||
let error = use_signal(|| Option::<String>::None);
|
let mut error = use_signal(|| Option::<String>::None);
|
||||||
// 自动刷新间隔(毫秒);None = 手动。主机层后台采样,前端可高频轮询。
|
// 自动刷新间隔(毫秒);None = 手动。主机层后台采样,前端可高频轮询。
|
||||||
let mut refresh_ms: Signal<Option<u32>> = use_signal(|| None);
|
let mut refresh_ms: Signal<Option<u32>> = use_signal(|| None);
|
||||||
|
|
||||||
@ -437,9 +441,9 @@ fn ServerStatusTab() -> Element {
|
|||||||
// 自动刷新:refresh_ms 变化时重建 future。
|
// 自动刷新:refresh_ms 变化时重建 future。
|
||||||
use_future(move || {
|
use_future(move || {
|
||||||
let interval_ms = refresh_ms();
|
let interval_ms = refresh_ms();
|
||||||
let status_f = status;
|
let mut status_f = status;
|
||||||
let loading_f = loading;
|
let mut loading_f = loading;
|
||||||
let error_f = error;
|
let mut error_f = error;
|
||||||
async move {
|
async move {
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
{
|
{
|
||||||
@ -639,6 +643,7 @@ fn ServerStatusTab() -> Element {
|
|||||||
///
|
///
|
||||||
/// 护栏 4(前端二次确认):提交写操作前弹窗确认。
|
/// 护栏 4(前端二次确认):提交写操作前弹窗确认。
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
|
#[cfg_attr(not(target_arch = "wasm32"), allow(unused_mut, unused_variables))]
|
||||||
fn SqlConsoleTab() -> Element {
|
fn SqlConsoleTab() -> Element {
|
||||||
use crate::api::database::sql_console::SqlResult;
|
use crate::api::database::sql_console::SqlResult;
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
@ -654,7 +659,7 @@ fn SqlConsoleTab() -> Element {
|
|||||||
|
|
||||||
let theme = use_theme();
|
let theme = use_theme();
|
||||||
let sql_text = use_signal(String::new);
|
let sql_text = use_signal(String::new);
|
||||||
let result = use_signal(|| Option::<SqlResult>::None);
|
let mut result = use_signal(|| Option::<SqlResult>::None);
|
||||||
let mut error = use_signal(|| Option::<String>::None);
|
let mut error = use_signal(|| Option::<String>::None);
|
||||||
let mut running = use_signal(|| false);
|
let mut running = use_signal(|| false);
|
||||||
// 选项 toggles
|
// 选项 toggles
|
||||||
@ -672,9 +677,9 @@ fn SqlConsoleTab() -> Element {
|
|||||||
// 初始化 CodeMirror + 拉取 schema 注入补全。仅 WASM。
|
// 初始化 CodeMirror + 拉取 schema 注入补全。仅 WASM。
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
{
|
{
|
||||||
use dioxus::prelude::wasm_bindgen::closure::Closure;
|
use wasm_bindgen::closure::Closure;
|
||||||
use_effect(move || {
|
use_effect(move || {
|
||||||
if editor_handle().is_some() {
|
if editor_handle.read().is_some() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let mut text = sql_text;
|
let mut text = sql_text;
|
||||||
@ -702,8 +707,12 @@ fn SqlConsoleTab() -> Element {
|
|||||||
// 异步拉取 schema 注入补全
|
// 异步拉取 schema 注入补全
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
if let Ok(schema) = get_db_schema().await {
|
if let Ok(schema) = get_db_schema().await {
|
||||||
|
// SqlSchema 是 serde 类型:先用 serde_wasm_bindgen 转 JsValue,
|
||||||
|
// 再传给 CodeMirror(extern set_schema 只接受 &JsValue)。
|
||||||
|
if let Ok(js) = serde_wasm_bindgen::to_value(&schema) {
|
||||||
if let Some(h) = editor_handle.read().as_ref() {
|
if let Some(h) = editor_handle.read().as_ref() {
|
||||||
h.instance().set_schema(&schema);
|
h.instance().set_schema(&js);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -1040,6 +1049,7 @@ fn urlencode(s: &str) -> String {
|
|||||||
|
|
||||||
/// 备份恢复 tab:备份按钮 + 进度轮询 + 备份列表(下载/恢复/删除)。
|
/// 备份恢复 tab:备份按钮 + 进度轮询 + 备份列表(下载/恢复/删除)。
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
|
#[cfg_attr(not(target_arch = "wasm32"), allow(unused_mut, unused_variables))]
|
||||||
fn BackupTab() -> Element {
|
fn BackupTab() -> Element {
|
||||||
use crate::api::database::backup::BackupInfo;
|
use crate::api::database::backup::BackupInfo;
|
||||||
use crate::api::database::tasks::TaskProgress;
|
use crate::api::database::tasks::TaskProgress;
|
||||||
@ -1049,23 +1059,25 @@ fn BackupTab() -> Element {
|
|||||||
use crate::api::database::tasks::{get_task_progress, TaskStatus};
|
use crate::api::database::tasks::{get_task_progress, TaskStatus};
|
||||||
use crate::components::ui::{ADMIN_CARD_CLASS, ADMIN_TABLE_CLASS};
|
use crate::components::ui::{ADMIN_CARD_CLASS, ADMIN_TABLE_CLASS};
|
||||||
|
|
||||||
|
// backups/active_task_id 仅在闭包内的重绑定副本上 .set()(如 backups_f),
|
||||||
|
// 外层绑定本身不改值,故无需 mut。
|
||||||
let backups = use_signal(Vec::<BackupInfo>::new);
|
let backups = use_signal(Vec::<BackupInfo>::new);
|
||||||
let mut loading = use_signal(|| false);
|
let mut loading = use_signal(|| false);
|
||||||
let error = use_signal(|| Option::<String>::None);
|
let mut error = use_signal(|| Option::<String>::None);
|
||||||
// 当前进行中的任务(备份/恢复)id + 进度
|
// 当前进行中的任务(备份/恢复)id + 进度
|
||||||
let active_task_id: Signal<Option<String>> = use_signal(|| None);
|
let active_task_id: Signal<Option<String>> = use_signal(|| None);
|
||||||
let active_progress = use_signal(|| Option::<TaskProgress>::None);
|
let mut active_progress = use_signal(|| Option::<TaskProgress>::None);
|
||||||
// 待恢复的文件名(确认对话框用)
|
// 待恢复的文件名(确认对话框用)
|
||||||
let mut pending_restore: Signal<Option<String>> = use_signal(|| None);
|
let mut pending_restore: Signal<Option<String>> = use_signal(|| None);
|
||||||
let busy = use_signal(|| false);
|
let mut busy = use_signal(|| false);
|
||||||
|
|
||||||
// 刷新备份列表
|
// 刷新备份列表
|
||||||
let mut refresh_list = move || {
|
let mut refresh_list = move || {
|
||||||
loading.set(true);
|
loading.set(true);
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
{
|
{
|
||||||
let backups = backups;
|
let mut backups = backups;
|
||||||
let error = error;
|
let mut error = error;
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
match list_backups().await {
|
match list_backups().await {
|
||||||
Ok(list) => {
|
Ok(list) => {
|
||||||
@ -1089,17 +1101,19 @@ fn BackupTab() -> Element {
|
|||||||
|
|
||||||
// 恢复确认:pending_restore 被设置时弹出原生 confirm,确认后发起 restore_backup
|
// 恢复确认:pending_restore 被设置时弹出原生 confirm,确认后发起 restore_backup
|
||||||
// 并开始进度轮询。用 use_future 响应 pending_restore 变化。
|
// 并开始进度轮询。用 use_future 响应 pending_restore 变化。
|
||||||
|
// _pending_for_confirm 被 clone 进 async move(FnMut 可重入),用 Option::clone 兜底。
|
||||||
let _pending_for_confirm = pending_restore.read().clone();
|
let _pending_for_confirm = pending_restore.read().clone();
|
||||||
use_future(move || {
|
use_future(move || {
|
||||||
let pending_restore = pending_restore;
|
let mut pending_restore = pending_restore;
|
||||||
let busy = busy;
|
let mut busy = busy;
|
||||||
let active_progress = active_progress;
|
let mut active_progress = active_progress;
|
||||||
let active_task_id = active_task_id;
|
let mut active_task_id = active_task_id;
|
||||||
let error = error;
|
let mut error = error;
|
||||||
|
let pending_for_confirm = _pending_for_confirm.clone();
|
||||||
async move {
|
async move {
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
{
|
{
|
||||||
let fname = match _pending_for_confirm {
|
let fname = match pending_for_confirm {
|
||||||
Some(f) => f,
|
Some(f) => f,
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
@ -1128,22 +1142,24 @@ fn BackupTab() -> Element {
|
|||||||
}
|
}
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
{
|
{
|
||||||
let _ = (pending_restore, busy, active_progress, active_task_id, error);
|
let _ = (pending_restore, busy, active_progress, active_task_id, error, pending_for_confirm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 任务进度轮询:active_task_id 存在时每 1.5s 拉取进度,Done/Failed 后停止 + 刷新列表
|
// 任务进度轮询:active_task_id 存在时每 1.5s 拉取进度,Done/Failed 后停止 + 刷新列表
|
||||||
|
// _task_id_for_poll 被 clone 进 async move(FnMut 可重入)。
|
||||||
let _task_id_for_poll = active_task_id.read().clone();
|
let _task_id_for_poll = active_task_id.read().clone();
|
||||||
use_future(move || {
|
use_future(move || {
|
||||||
let active_task_id = active_task_id;
|
let mut active_task_id = active_task_id;
|
||||||
let active_progress = active_progress;
|
let mut active_progress = active_progress;
|
||||||
let backups_f = backups;
|
let mut backups_f = backups;
|
||||||
let busy_f = busy;
|
let mut busy_f = busy;
|
||||||
|
let task_id_for_poll = _task_id_for_poll.clone();
|
||||||
async move {
|
async move {
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
{
|
{
|
||||||
let tid = match _task_id_for_poll {
|
let tid = match task_id_for_poll {
|
||||||
Some(t) => t,
|
Some(t) => t,
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
@ -1174,7 +1190,7 @@ fn BackupTab() -> Element {
|
|||||||
}
|
}
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
{
|
{
|
||||||
let _ = (active_task_id, active_progress, backups_f, busy_f);
|
let _ = (active_task_id, active_progress, backups_f, busy_f, task_id_for_poll);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -1209,7 +1225,7 @@ fn BackupTab() -> Element {
|
|||||||
{
|
{
|
||||||
busy.set(true);
|
busy.set(true);
|
||||||
active_progress.set(None);
|
active_progress.set(None);
|
||||||
let active_task_id = active_task_id;
|
let mut active_task_id = active_task_id;
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
match create_backup().await {
|
match create_backup().await {
|
||||||
Ok(id) => active_task_id.set(Some(id)),
|
Ok(id) => active_task_id.set(Some(id)),
|
||||||
@ -1276,8 +1292,8 @@ fn BackupTab() -> Element {
|
|||||||
size_str: size_str.clone(),
|
size_str: size_str.clone(),
|
||||||
dl_url: dl_url.clone(),
|
dl_url: dl_url.clone(),
|
||||||
busy: is_busy,
|
busy: is_busy,
|
||||||
on_restore: move |f| pending_restore.set(Some(f)),
|
on_restore: move |f: String| pending_restore.set(Some(f)),
|
||||||
on_delete: move |_fname_del| {
|
on_delete: move |_fname_del: String| {
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
{
|
{
|
||||||
let fname_del = _fname_del;
|
let fname_del = _fname_del;
|
||||||
@ -1288,7 +1304,7 @@ fn BackupTab() -> Element {
|
|||||||
})
|
})
|
||||||
== Some(true);
|
== Some(true);
|
||||||
if confirmed {
|
if confirmed {
|
||||||
let backups = backups;
|
let mut backups = backups;
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
let _ = delete_backup(fname_del.clone()).await;
|
let _ = delete_backup(fname_del.clone()).await;
|
||||||
if let Ok(list) = list_backups().await {
|
if let Ok(list) = list_backups().await {
|
||||||
|
|||||||
@ -5,7 +5,10 @@
|
|||||||
//!
|
//!
|
||||||
//! 采样间隔由环境变量 `SYSINFO_SAMPLE_SECS` 配置(默认 0.5 秒,支持小数如 0.1)。
|
//! 采样间隔由环境变量 `SYSINFO_SAMPLE_SECS` 配置(默认 0.5 秒,支持小数如 0.1)。
|
||||||
|
|
||||||
|
// LazyLock / Duration 仅 server 构建的采样任务用到;WASM 端只序列化 SystemSnapshot。
|
||||||
|
#[cfg(feature = "server")]
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
#[cfg(feature = "server")]
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user