mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-05-06 22:12:12 +08:00
[BugFix] ODP Desktop: Fix Dependabot Alerts, Fix Environment Update, Improve Error Message (#7345)
* improve error message on initial environment setup before continue anyway, add openbb-cookiecutter to the extra extensions lists, seperate conda env update into updating conda packages and pip packages, update glob for dependabot * conda update -> conda install for updating the individual packages * cargo fmt * cargo clippy
This commit is contained in:
1040
desktop/Cargo.lock
generated
vendored
1040
desktop/Cargo.lock
generated
vendored
File diff suppressed because it is too large
Load Diff
71
desktop/package-lock.json
generated
vendored
71
desktop/package-lock.json
generated
vendored
@@ -16,17 +16,17 @@
|
||||
"@tanstack/router-core": "^1.114.33",
|
||||
"@tanstack/router-devtools": "^1.131.27",
|
||||
"@tauri-apps/plugin-app": "^2.0.0-alpha.1",
|
||||
"@tauri-apps/plugin-dialog": "^2.3.3",
|
||||
"@tauri-apps/plugin-dialog": "^2.6.0",
|
||||
"@tauri-apps/plugin-fs": "^2.4.2",
|
||||
"@tauri-apps/plugin-http": "^2.5.2",
|
||||
"@tauri-apps/plugin-log": "^2.6.0",
|
||||
"@tauri-apps/plugin-log": "^2.8.0",
|
||||
"@tauri-apps/plugin-opener": "^2.5.0",
|
||||
"@tauri-apps/plugin-process": "^2.3.0",
|
||||
"@tauri-apps/plugin-updater": "^2.9.0",
|
||||
"@tauri-apps/plugin-updater": "^2.9.6",
|
||||
"clsx": "^2.1.1",
|
||||
"csstype": "^3.1.3",
|
||||
"date-fns": "^4.1.0",
|
||||
"glob": ">=10.5.0",
|
||||
"glob": ">=13.0.1",
|
||||
"postcss": "^8.5.6",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
@@ -42,7 +42,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@tanstack/router-vite-plugin": "^1.131.27",
|
||||
"@tauri-apps/api": "^2.8.0",
|
||||
"@tauri-apps/api": "^2.9.6",
|
||||
"@tauri-apps/cli": "^2.9.6",
|
||||
"@tauri-apps/plugin-shell": "^2.3.1",
|
||||
"@tauri-apps/plugin-window": "^2.0.0-alpha.1",
|
||||
@@ -55,6 +55,7 @@
|
||||
"@typescript-eslint/parser": "^8.37.0",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"baseline-browser-mapping": "^2.9.19",
|
||||
"eslint": "^9.33.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"jsdom": "^26.1.0",
|
||||
@@ -1536,9 +1537,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/brace-expansion": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
|
||||
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz",
|
||||
"integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@isaacs/balanced-match": "^4.0.1"
|
||||
@@ -3462,9 +3463,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/api": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.8.0.tgz",
|
||||
"integrity": "sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw==",
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz",
|
||||
"integrity": "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -3713,9 +3714,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-dialog": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.4.0.tgz",
|
||||
"integrity": "sha512-OvXkrEBfWwtd8tzVCEXIvRfNEX87qs2jv6SqmVPiHcJjBhSF/GUvjqUNIDmKByb5N8nvDqVUM7+g1sXwdC/S9w==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.6.0.tgz",
|
||||
"integrity": "sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.8.0"
|
||||
@@ -3740,9 +3741,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-log": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.7.0.tgz",
|
||||
"integrity": "sha512-81XQ2f93x4vmIB5OY0XlYAxy60cHdYLs0Ki8Qp38tNATRiuBit+Orh3frpY3qfYQnqEvYVyRub7YRJWlmW2RRA==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.8.0.tgz",
|
||||
"integrity": "sha512-a+7rOq3MJwpTOLLKbL8d0qGZ85hgHw5pNOWusA9o3cf7cEgtYHiGY/+O8fj8MvywQIGqFv0da2bYQDlrqLE7rw==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.8.0"
|
||||
@@ -3777,12 +3778,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-updater": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-updater/-/plugin-updater-2.9.0.tgz",
|
||||
"integrity": "sha512-j++sgY8XpeDvzImTrzWA08OqqGqgkNyxczLD7FjNJJx/uXxMZFz5nDcfkyoI/rCjYuj2101Tci/r/HFmOmoxCg==",
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-updater/-/plugin-updater-2.10.0.tgz",
|
||||
"integrity": "sha512-ljN8jPlnT0aSn8ecYhuBib84alxfMx6Hc8vJSKMJyzGbTPFZAC44T2I1QNFZssgWKrAlofvJqCC6Rr472JWfkQ==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.6.0"
|
||||
"@tauri-apps/api": "^2.10.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-window": {
|
||||
@@ -4897,9 +4898,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.8.12",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.12.tgz",
|
||||
"integrity": "sha512-vAPMQdnyKCBtkmQA6FMCBvU9qFIppS3nzyXnEM+Lo2IAhG4Mpjv9cCxMudhgV3YdNNJv6TNqXy97dfRVL2LmaQ==",
|
||||
"version": "2.9.19",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
|
||||
"integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -5562,9 +5563,9 @@
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz",
|
||||
"integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==",
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz",
|
||||
"integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
@@ -6582,12 +6583,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz",
|
||||
"integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==",
|
||||
"version": "13.0.1",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.1.tgz",
|
||||
"integrity": "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"minimatch": "^10.1.1",
|
||||
"minimatch": "^10.1.2",
|
||||
"minipass": "^7.1.2",
|
||||
"path-scurry": "^2.0.0"
|
||||
},
|
||||
@@ -6611,12 +6612,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/minimatch": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
|
||||
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
|
||||
"version": "10.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz",
|
||||
"integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/brace-expansion": "^5.0.0"
|
||||
"@isaacs/brace-expansion": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
|
||||
15
desktop/package.json
vendored
15
desktop/package.json
vendored
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "openbb-platform",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"type": "module",
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@@ -21,17 +21,17 @@
|
||||
"@tanstack/router-core": "^1.114.33",
|
||||
"@tanstack/router-devtools": "^1.131.27",
|
||||
"@tauri-apps/plugin-app": "^2.0.0-alpha.1",
|
||||
"@tauri-apps/plugin-dialog": "^2.3.3",
|
||||
"@tauri-apps/plugin-dialog": "^2.6.0",
|
||||
"@tauri-apps/plugin-fs": "^2.4.2",
|
||||
"@tauri-apps/plugin-http": "^2.5.2",
|
||||
"@tauri-apps/plugin-log": "^2.6.0",
|
||||
"@tauri-apps/plugin-log": "^2.8.0",
|
||||
"@tauri-apps/plugin-opener": "^2.5.0",
|
||||
"@tauri-apps/plugin-process": "^2.3.0",
|
||||
"@tauri-apps/plugin-updater": "^2.9.0",
|
||||
"@tauri-apps/plugin-updater": "^2.9.6",
|
||||
"clsx": "^2.1.1",
|
||||
"csstype": "^3.1.3",
|
||||
"date-fns": "^4.1.0",
|
||||
"glob": ">=10.5.0",
|
||||
"glob": ">=13.0.1",
|
||||
"postcss": "^8.5.6",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
@@ -47,10 +47,10 @@
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@tanstack/router-vite-plugin": "^1.131.27",
|
||||
"@tauri-apps/api": "^2.8.0",
|
||||
"@tauri-apps/api": "^2.9.6",
|
||||
"@tauri-apps/cli": "^2.9.6",
|
||||
"@tauri-apps/plugin-shell": "^2.3.1",
|
||||
"@tauri-apps/plugin-window": "^2.0.0-alpha.1",
|
||||
"@tauri-apps/cli": "^2.9.6",
|
||||
"@testing-library/jest-dom": "^6.7.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@types/node": "^20.19.11",
|
||||
@@ -60,6 +60,7 @@
|
||||
"@typescript-eslint/parser": "^8.37.0",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"baseline-browser-mapping": "^2.9.19",
|
||||
"eslint": "^9.33.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"jsdom": "^26.1.0",
|
||||
|
||||
6
desktop/src-tauri/Cargo.toml
vendored
6
desktop/src-tauri/Cargo.toml
vendored
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "openbb-platform"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
description = "Open Data Platform by OpenBB. A desktop application for managing virtual environments, application backend servers."
|
||||
authors = ["OpenBB, Inc."]
|
||||
license = "AGPL-3.0"
|
||||
@@ -20,9 +20,9 @@ serde_yaml = "^0.9"
|
||||
chrono = "^0.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
log = "0.4"
|
||||
tauri = { version = "2.8.2", features = ["tray-icon", "devtools"] }
|
||||
tauri = { version = "2.9.6", features = ["tray-icon", "devtools"] }
|
||||
tauri-cli = "^2.9.6"
|
||||
tauri-plugin-log = "2.6.0"
|
||||
tauri-plugin-log = "2.8.0"
|
||||
regex = "^1.11.1"
|
||||
reqwest = { version = "^0.12.23", features = ["json"] }
|
||||
once_cell = "^1.21.3"
|
||||
|
||||
7
desktop/src-tauri/src/main.rs
vendored
7
desktop/src-tauri/src/main.rs
vendored
@@ -19,8 +19,8 @@ use tauri_plugin_dialog::DialogExt;
|
||||
extern crate winapi;
|
||||
|
||||
use crate::tauri_handlers::startup::{
|
||||
abort_installation, get_installation_status, install_conda, install_to_directory,
|
||||
setup_python_environment,
|
||||
abort_installation, create_default_backend_services, get_installation_status, install_conda,
|
||||
install_to_directory, setup_python_environment,
|
||||
};
|
||||
|
||||
use crate::tauri_handlers::environments::{
|
||||
@@ -538,7 +538,8 @@ fn main() {
|
||||
uninstall_application,
|
||||
quit_application,
|
||||
generate_self_signed_cert,
|
||||
update_openbb_settings
|
||||
update_openbb_settings,
|
||||
create_default_backend_services
|
||||
])
|
||||
.setup(|app_handle| {
|
||||
let install_state = check_installation_on_startup();
|
||||
|
||||
262
desktop/src-tauri/src/tauri_handlers/environments.rs
vendored
262
desktop/src-tauri/src/tauri_handlers/environments.rs
vendored
@@ -2303,16 +2303,16 @@ pub async fn update_extension_impl<F: FileSystem, E: EnvSystem>(
|
||||
conda_dir.join("bin").join("conda")
|
||||
};
|
||||
let conda_args = if environment == "base" {
|
||||
vec!["update", &package, "-y"]
|
||||
vec!["install", &package, "-y"]
|
||||
} else {
|
||||
vec!["update", "-n", &environment, &package, "-y"]
|
||||
vec!["install", "-n", &environment, &package, "-y"]
|
||||
};
|
||||
|
||||
let mut conda_command = env_sys.new_conda_command(&conda_exe, &conda_dir);
|
||||
let conda_output = conda_command
|
||||
.args(&conda_args)
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to update extension with conda: {e}"))?;
|
||||
.map_err(|e| format!("Failed to install extension with conda: {e}"))?;
|
||||
if conda_output.status.success() {
|
||||
log::debug!("Successfully updated extension '{package}' with conda");
|
||||
Ok(true)
|
||||
@@ -2777,7 +2777,7 @@ pub async fn update_environment_impl<F: FileSystem, E: EnvSystem>(
|
||||
) -> Result<bool, String> {
|
||||
use std::path::Path;
|
||||
|
||||
log::debug!("Updating packages in environment: {environment}");
|
||||
log::info!("Updating packages in environment: {environment}");
|
||||
|
||||
// Path to conda
|
||||
let conda_dir = Path::new(&directory).join("conda");
|
||||
@@ -2790,87 +2790,160 @@ pub async fn update_environment_impl<F: FileSystem, E: EnvSystem>(
|
||||
return Err(format!("Environment YAML file not found for {environment}"));
|
||||
}
|
||||
|
||||
// Read and parse YAML to check for 'openbb'
|
||||
// Read and parse YAML to extract packages
|
||||
let yaml_content = fs
|
||||
.read_to_string(&yaml_path)
|
||||
.map_err(|e| format!("Failed to read environment YAML: {e}"))?;
|
||||
let yaml_value: serde_yaml::Value = serde_yaml::from_str(&yaml_content)
|
||||
.map_err(|e| format!("Failed to parse environment YAML: {e}"))?;
|
||||
|
||||
let mut has_openbb = false;
|
||||
// Extract conda and pip packages from YAML
|
||||
let mut conda_packages: Vec<String> = Vec::new();
|
||||
let mut pip_packages: Vec<String> = Vec::new();
|
||||
|
||||
if let Some(deps) = yaml_value.get("dependencies").and_then(|d| d.as_sequence()) {
|
||||
for dep in deps {
|
||||
// Check if it's a pip mapping
|
||||
if let Some(pip_map) = dep.as_mapping()
|
||||
&& let Some(pip_deps) = pip_map
|
||||
.get(serde_yaml::Value::String("pip".to_string()))
|
||||
.and_then(|p| p.as_sequence())
|
||||
{
|
||||
for pip_dep in pip_deps {
|
||||
if let Some(pip_dep_str) = pip_dep.as_str()
|
||||
&& pip_dep_str == "openbb"
|
||||
{
|
||||
has_openbb = true;
|
||||
break;
|
||||
if let Some(pip_dep_str) = pip_dep.as_str() {
|
||||
pip_packages.push(pip_dep_str.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
if has_openbb {
|
||||
break;
|
||||
} else if let Some(conda_dep) = dep.as_str() {
|
||||
// It's a conda package (string entry)
|
||||
// Extract just the package name (remove version specifiers like =3.11)
|
||||
let pkg_name = conda_dep
|
||||
.split(['=', '>', '<', '!'])
|
||||
.next()
|
||||
.unwrap_or(conda_dep);
|
||||
// Skip infrastructure packages - we don't want to upgrade these
|
||||
if !matches!(pkg_name, "python" | "pip" | "nodejs" | "setuptools") {
|
||||
conda_packages.push(pkg_name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!("Using environment definition from: {}", yaml_path.display()); // Get conda executable path
|
||||
log::info!(
|
||||
"Found {} conda packages and {} pip packages to update",
|
||||
conda_packages.len(),
|
||||
pip_packages.len()
|
||||
);
|
||||
|
||||
if conda_packages.is_empty() && pip_packages.is_empty() {
|
||||
log::info!("No packages found in environment YAML, nothing to update");
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Get conda executable
|
||||
let conda_exe = if env_sys.consts_os() == "windows" {
|
||||
conda_dir.join("Scripts").join("conda.exe")
|
||||
} else {
|
||||
conda_dir.join("bin").join("conda")
|
||||
};
|
||||
|
||||
// Update conda itself first using direct conda command
|
||||
log::debug!("Updating conda itself");
|
||||
let mut conda_command = env_sys.new_conda_command(&conda_exe, &conda_dir);
|
||||
let conda_output = conda_command
|
||||
.args(["update", "-n", "base", "-c", "conda-forge", "conda", "-y"])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to update conda: {e}"))?;
|
||||
|
||||
if !conda_output.status.success() {
|
||||
log::debug!(
|
||||
"Warning: Conda self-update may have failed, but continuing with environment update"
|
||||
// Update conda packages if any (excluding python, pip)
|
||||
if !conda_packages.is_empty() {
|
||||
log::info!(
|
||||
"Updating {} conda packages: {:?}",
|
||||
conda_packages.len(),
|
||||
conda_packages
|
||||
);
|
||||
} // Use direct conda command instead of scripts that require activation
|
||||
log::debug!("Updating environment using conda's dependency solver");
|
||||
let mut update_command = env_sys.new_conda_command(&conda_exe, &conda_dir);
|
||||
|
||||
let update_output = update_command
|
||||
.args([
|
||||
"env",
|
||||
"update",
|
||||
"-n",
|
||||
&environment,
|
||||
"-f",
|
||||
&yaml_path.to_string_lossy(),
|
||||
"--prune",
|
||||
])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to update environment: {e}"))?;
|
||||
let mut conda_args = vec!["install", "-n", &environment, "-y"];
|
||||
let pkg_refs: Vec<&str> = conda_packages.iter().map(|s| s.as_str()).collect();
|
||||
conda_args.extend(pkg_refs);
|
||||
|
||||
// Handle output
|
||||
let stdout = String::from_utf8_lossy(&update_output.stdout).to_string();
|
||||
let stderr = String::from_utf8_lossy(&update_output.stderr).to_string();
|
||||
log::info!("Running: {} {}", conda_exe.display(), conda_args.join(" "));
|
||||
|
||||
log::debug!("Update output: {stdout}");
|
||||
// Use spawn with timeout to prevent hanging forever
|
||||
let mut conda_command = env_sys.new_conda_command(&conda_exe, &conda_dir);
|
||||
let mut child = conda_command
|
||||
.args(&conda_args)
|
||||
.stdin(std::process::Stdio::null())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to spawn conda install: {e}"))?;
|
||||
|
||||
if !update_output.status.success() {
|
||||
return Err(format!("Failed to update environment: {stderr}"));
|
||||
// Run the wait in a blocking thread to not block the async runtime
|
||||
let result = tokio::task::spawn_blocking(move || {
|
||||
let timeout = std::time::Duration::from_secs(300);
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
loop {
|
||||
match child.try_wait() {
|
||||
Ok(Some(status)) => {
|
||||
// Process finished
|
||||
let stdout = child
|
||||
.stdout
|
||||
.take()
|
||||
.map(|mut s| {
|
||||
let mut buf = String::new();
|
||||
std::io::Read::read_to_string(&mut s, &mut buf).ok();
|
||||
buf
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let stderr = child
|
||||
.stderr
|
||||
.take()
|
||||
.map(|mut s| {
|
||||
let mut buf = String::new();
|
||||
std::io::Read::read_to_string(&mut s, &mut buf).ok();
|
||||
buf
|
||||
})
|
||||
.unwrap_or_default();
|
||||
return (Some(status), stdout, stderr);
|
||||
}
|
||||
Ok(None) => {
|
||||
// Still running
|
||||
if start.elapsed() > timeout {
|
||||
log::warn!("Conda update timed out after 5 minutes, killing process");
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
return (None, String::new(), "Timed out".to_string());
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
Err(e) => {
|
||||
return (None, String::new(), format!("Error: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap_or((None, String::new(), "Task panicked".to_string()));
|
||||
|
||||
let (status, stdout, stderr) = result;
|
||||
log::info!("conda stdout: {}", stdout);
|
||||
if !stderr.is_empty() {
|
||||
log::info!("conda stderr: {}", stderr);
|
||||
}
|
||||
if let Some(s) = status
|
||||
&& !s.success()
|
||||
{
|
||||
log::warn!(
|
||||
"Conda update had issues: {}",
|
||||
if stderr.is_empty() { &stdout } else { &stderr }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// If 'openbb' is in the environment, reinstall it with --no-deps
|
||||
if has_openbb {
|
||||
log::debug!("'openbb' package found, running pip install --no-deps");
|
||||
// Update pip packages
|
||||
if !pip_packages.is_empty() {
|
||||
log::info!(
|
||||
"Updating {} pip packages: {:?}",
|
||||
pip_packages.len(),
|
||||
pip_packages
|
||||
);
|
||||
|
||||
let env_python_path = if env_sys.consts_os() == "windows" {
|
||||
// Get python executable path for this environment
|
||||
let env_python = if env_sys.consts_os() == "windows" {
|
||||
conda_dir.join("envs").join(&environment).join("python.exe")
|
||||
} else {
|
||||
conda_dir
|
||||
@@ -2880,23 +2953,86 @@ pub async fn update_environment_impl<F: FileSystem, E: EnvSystem>(
|
||||
.join("python")
|
||||
};
|
||||
|
||||
let mut pip_command = env_sys.new_conda_command(&env_python_path, &conda_dir);
|
||||
let pip_output = pip_command
|
||||
.args(["-m", "pip", "install", "openbb", "--no-deps"])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to run pip install for openbb: {e}"))?;
|
||||
|
||||
if !pip_output.status.success() {
|
||||
let pip_stderr = String::from_utf8_lossy(&pip_output.stderr);
|
||||
if !fs.exists(&env_python) {
|
||||
return Err(format!(
|
||||
"Failed to update the openbb meta package, but all other extensions successfully updated -> {}",
|
||||
pip_stderr
|
||||
"Python executable not found for environment {}: {}",
|
||||
environment,
|
||||
env_python.display()
|
||||
));
|
||||
}
|
||||
|
||||
let mut pip_command = env_sys.new_conda_command(&env_python, &conda_dir);
|
||||
let mut args = vec!["-m", "pip", "install", "--upgrade"];
|
||||
let package_refs: Vec<&str> = pip_packages.iter().map(|s| s.as_str()).collect();
|
||||
args.extend(package_refs);
|
||||
|
||||
log::info!("Running: {} {}", env_python.display(), args.join(" "));
|
||||
|
||||
let output = pip_command
|
||||
.args(&args)
|
||||
.stdin(std::process::Stdio::null())
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to run pip upgrade: {e}"))?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
log::info!("pip stdout: {}", stdout);
|
||||
if !stderr.is_empty() {
|
||||
log::info!("pip stderr: {}", stderr);
|
||||
}
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(format!(
|
||||
"Failed to update pip packages: {}",
|
||||
if stderr.is_empty() {
|
||||
stdout.to_string()
|
||||
} else {
|
||||
stderr.to_string()
|
||||
}
|
||||
));
|
||||
}
|
||||
log::debug!("Successfully re-installed openbb with --no-deps");
|
||||
}
|
||||
|
||||
log::debug!("Successfully updated environment: {environment}");
|
||||
// Rebuild OpenBB if it's in the environment
|
||||
if pip_packages
|
||||
.iter()
|
||||
.any(|p| p == "openbb" || p.starts_with("openbb-"))
|
||||
{
|
||||
log::info!("OpenBB packages detected, running openbb-build");
|
||||
|
||||
let openbb_build = if env_sys.consts_os() == "windows" {
|
||||
conda_dir
|
||||
.join("envs")
|
||||
.join(&environment)
|
||||
.join("Scripts")
|
||||
.join("openbb-build.exe")
|
||||
} else {
|
||||
conda_dir
|
||||
.join("envs")
|
||||
.join(&environment)
|
||||
.join("bin")
|
||||
.join("openbb-build")
|
||||
};
|
||||
|
||||
if fs.exists(&openbb_build) {
|
||||
let mut build_command = env_sys.new_conda_command(&openbb_build, &conda_dir);
|
||||
let build_output = build_command
|
||||
.stdin(std::process::Stdio::null())
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to run openbb-build: {e}"))?;
|
||||
|
||||
if !build_output.status.success() {
|
||||
let build_stderr = String::from_utf8_lossy(&build_output.stderr);
|
||||
log::warn!("openbb-build had issues: {}", build_stderr);
|
||||
// Don't fail the whole update if openbb-build has issues
|
||||
} else {
|
||||
log::info!("openbb-build completed successfully");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("Successfully updated environment: {environment}");
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
|
||||
83
desktop/src-tauri/src/tauri_handlers/startup.rs
vendored
83
desktop/src-tauri/src/tauri_handlers/startup.rs
vendored
@@ -1,6 +1,6 @@
|
||||
use crate::tauri_handlers::backends::create_backend_service_impl;
|
||||
use crate::tauri_handlers::helpers::{
|
||||
EnvSystem, FileExtTrait, FileSystem, RealEnvSystem, RealFileExtTrait, RealFileSystem,
|
||||
EnvSystem, FileExtTrait, FileSystem, RealEnvSystem, RealFileSystem,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
use reqwest;
|
||||
@@ -1237,19 +1237,17 @@ pub async fn setup_python_environment(
|
||||
window,
|
||||
&RealFileSystem,
|
||||
&RealEnvSystem,
|
||||
&RealFileExtTrait,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
// Split the large function into a separate implementation
|
||||
async fn setup_python_environment_impl<F: FileSystem, E: EnvSystem, FE: FileExtTrait>(
|
||||
async fn setup_python_environment_impl<F: FileSystem, E: EnvSystem>(
|
||||
directory: String,
|
||||
python_version: String,
|
||||
window: Window,
|
||||
fs: &F,
|
||||
env_sys: &E,
|
||||
file_ext: &FE,
|
||||
) -> Result<bool, String> {
|
||||
use std::path::Path;
|
||||
|
||||
@@ -1291,8 +1289,6 @@ async fn setup_python_environment_impl<F: FileSystem, E: EnvSystem, FE: FileExtT
|
||||
let yaml_path = generate_environment_yaml(&python_version, fs, env_sys).await?;
|
||||
create_environment_from_yaml(&conda_exe, &yaml_path, &report_progress, env_sys).await?;
|
||||
|
||||
install_openbb_package(&conda_path, &report_progress, env_sys).await?;
|
||||
|
||||
// Update OpenBB settings
|
||||
if let Err(e) = crate::tauri_handlers::helpers::update_openbb_settings_impl(
|
||||
&conda_path,
|
||||
@@ -1307,9 +1303,6 @@ async fn setup_python_environment_impl<F: FileSystem, E: EnvSystem, FE: FileExtT
|
||||
|
||||
report_progress("complete", 1.0, "Installation complete");
|
||||
|
||||
// Create default backend service
|
||||
create_default_backend_service(fs, env_sys, file_ext).await;
|
||||
|
||||
window
|
||||
.emit("installation-directory", &directory)
|
||||
.unwrap_or_else(|e| {
|
||||
@@ -1433,71 +1426,19 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn install_openbb_package<F, E: EnvSystem>(
|
||||
conda_path: &Path,
|
||||
report_progress: &F,
|
||||
env_sys: &E,
|
||||
) -> Result<(), String>
|
||||
where
|
||||
F: Fn(&str, f32, &str),
|
||||
{
|
||||
report_progress("config", 0.85, "Installing OpenBB package");
|
||||
|
||||
let env_name = "openbb";
|
||||
let env_path = conda_path.join("envs").join(env_name);
|
||||
|
||||
if !env_path.exists() {
|
||||
return Err(format!(
|
||||
"Environment '{}' does not exist at path: {}",
|
||||
env_name,
|
||||
env_path.display()
|
||||
));
|
||||
}
|
||||
|
||||
let conda_exe = if env_sys.consts_os() == "windows" {
|
||||
conda_path.join("Scripts").join("conda.exe")
|
||||
} else {
|
||||
conda_path.join("bin").join("conda")
|
||||
};
|
||||
|
||||
let mut pip_command = env_sys.new_conda_command(&conda_exe, conda_path);
|
||||
let pip_output = pip_command
|
||||
.args([
|
||||
"run",
|
||||
"-n",
|
||||
env_name,
|
||||
"pip",
|
||||
"install",
|
||||
"openbb",
|
||||
"--no-deps",
|
||||
])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to install openbb package: {e}"))?;
|
||||
|
||||
if !pip_output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&pip_output.stderr);
|
||||
return Err(format!("Failed to install openbb package: {stderr}"));
|
||||
}
|
||||
|
||||
let mut build_command = env_sys.new_conda_command(&conda_exe, conda_path);
|
||||
let build_output = build_command
|
||||
.args(["run", "-n", env_name, "openbb-build"])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to run openbb-build: {e}"))?;
|
||||
|
||||
if !build_output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&build_output.stderr);
|
||||
log::debug!("Warning: openbb-build failed, continuing anyway: {stderr}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
/// Create default backend services (OpenBB API and MCP)
|
||||
/// This should only be called after a successful full installation
|
||||
#[tauri::command]
|
||||
pub async fn create_default_backend_services() -> Result<(), String> {
|
||||
use crate::tauri_handlers::helpers::{RealEnvSystem, RealFileExtTrait, RealFileSystem};
|
||||
create_default_backend_services_impl(&RealFileSystem, &RealEnvSystem, &RealFileExtTrait).await
|
||||
}
|
||||
|
||||
async fn create_default_backend_service<F: FileSystem, E: EnvSystem, FE: FileExtTrait>(
|
||||
async fn create_default_backend_services_impl<F: FileSystem, E: EnvSystem, FE: FileExtTrait>(
|
||||
fs: &F,
|
||||
env_sys: &E,
|
||||
file_ext: &FE,
|
||||
) {
|
||||
) -> Result<(), String> {
|
||||
let backend = crate::tauri_handlers::backends::BackendService {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
name: "OpenBB API".to_string(),
|
||||
@@ -1535,6 +1476,8 @@ async fn create_default_backend_service<F: FileSystem, E: EnvSystem, FE: FileExt
|
||||
url: None,
|
||||
};
|
||||
let _ = create_backend_service_impl(mcp_backend, fs, env_sys, file_ext);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn generate_environment_yaml<F: FileSystem, E: EnvSystem>(
|
||||
@@ -1932,7 +1875,7 @@ mod tests {
|
||||
|
||||
fs.expect_create_dir_all().returning(|_| Ok(()));
|
||||
|
||||
create_default_backend_service(&fs, &env_sys, &file_ext).await;
|
||||
let _ = create_default_backend_services_impl(&fs, &env_sys, &file_ext).await;
|
||||
|
||||
// Clean up the temp file
|
||||
let _ = std::fs::remove_file(&temp_path);
|
||||
|
||||
4
desktop/src-tauri/tauri.conf.json
vendored
4
desktop/src-tauri/tauri.conf.json
vendored
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Open Data Platform by OpenBB",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"identifier": "co.openbb.platform",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
@@ -51,7 +51,7 @@
|
||||
],
|
||||
"category": "DeveloperTool",
|
||||
"publisher": "OpenBB, Inc.",
|
||||
"copyright": "Copyright © 2025 OpenBB, Inc.",
|
||||
"copyright": "Copyright © 2026 OpenBB, Inc.",
|
||||
"license": "AGPLv3",
|
||||
"licenseFile": "./LICENSE"
|
||||
},
|
||||
|
||||
7
desktop/src/components/InstallComponents.tsx
vendored
7
desktop/src/components/InstallComponents.tsx
vendored
@@ -156,6 +156,13 @@ export const ExtensionSelector = ({
|
||||
category: "other-openbb",
|
||||
credentials: [],
|
||||
},
|
||||
{
|
||||
id: "openbb-cookiecutter",
|
||||
name: "OpenBB Cookiecutter",
|
||||
description: "Template for creating new OpenBB extension projects.",
|
||||
category: "other-openbb",
|
||||
credentials: [],
|
||||
},
|
||||
];
|
||||
|
||||
// Update the getFilteredExtensions function to use the new hasMatchingExtensions
|
||||
|
||||
10
desktop/src/main.tsx
vendored
10
desktop/src/main.tsx
vendored
@@ -3,6 +3,16 @@ import './styles.css';
|
||||
import { RouterProvider, createRouter } from '@tanstack/react-router';
|
||||
import { StrictMode } from 'react';
|
||||
|
||||
// Suppress known forwardRef warning from Radix UI in @openbb/ui-pro
|
||||
// This is a harmless warning from older Radix UI versions
|
||||
const originalError = console.error;
|
||||
console.error = (...args) => {
|
||||
if (typeof args[0] === 'string' && args[0].includes('forwardRef render functions accept exactly two parameters')) {
|
||||
return;
|
||||
}
|
||||
originalError.apply(console, args);
|
||||
};
|
||||
|
||||
// Import the generated route tree
|
||||
import { routeTree } from './routeTree.gen'
|
||||
|
||||
|
||||
76
desktop/src/routes/installation-progress.tsx
vendored
76
desktop/src/routes/installation-progress.tsx
vendored
@@ -156,6 +156,13 @@ const ExtensionSelector = ({
|
||||
category: "other-openbb",
|
||||
credentials: [],
|
||||
},
|
||||
{
|
||||
id: "openbb-cookiecutter",
|
||||
name: "OpenBB Cookiecutter",
|
||||
description: "Template for creating new OpenBB extension projects.",
|
||||
category: "other-openbb",
|
||||
credentials: [],
|
||||
}
|
||||
];
|
||||
|
||||
// Add a custom package
|
||||
@@ -1185,7 +1192,7 @@ export default function InstallationProgress() {
|
||||
setIsComplete(true);
|
||||
};
|
||||
|
||||
// Handle completion - continue to app
|
||||
// Handle completion - continue to app (only for successful installations)
|
||||
const handleContinue = async () => {
|
||||
setIsContinuing(true);
|
||||
// Instead of using navigate, use window.location to force a full page reload
|
||||
@@ -1198,13 +1205,39 @@ export default function InstallationProgress() {
|
||||
|
||||
try {
|
||||
await invoke("update_openbb_settings", {
|
||||
condaDir: directory,
|
||||
environment: "openbb",
|
||||
});
|
||||
condaDir: directory,
|
||||
environment: "openbb",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to update OpenBB settings:", error);
|
||||
// Proceed to app even if this fails
|
||||
}
|
||||
|
||||
// Create default backend services only on successful installation
|
||||
try {
|
||||
await invoke("create_default_backend_services");
|
||||
} catch (error) {
|
||||
console.error("Failed to create default backend services:", error);
|
||||
// Proceed to app even if this fails
|
||||
}
|
||||
|
||||
window.localStorage.setItem("environments-first-load-done", "true");
|
||||
window.location.href = `/environments${queryString ? `?${queryString}` : ""}`;
|
||||
};
|
||||
|
||||
// Handle "Continue Anyway" when installation has failed
|
||||
// This skips settings updates since the environment may be incomplete
|
||||
const handleContinueAnyway = () => {
|
||||
setIsContinuing(true);
|
||||
const searchParams = new URLSearchParams();
|
||||
if (directory) searchParams.append("directory", directory);
|
||||
if (userDataDir) searchParams.append("userDataDir", userDataDir);
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
// Don't update settings or create backend configs for failed installations
|
||||
// Just navigate to environments so user can see what's available
|
||||
console.warn("Continuing after failed installation - settings not updated");
|
||||
window.localStorage.setItem("environments-first-load-done", "true");
|
||||
window.location.href = `/environments${queryString ? `?${queryString}` : ""}`;
|
||||
};
|
||||
@@ -1458,7 +1491,34 @@ export default function InstallationProgress() {
|
||||
<div className="mt-4 mb-4 p-4 bg-red-900/30 text-red-300 rounded-md">
|
||||
<p className="body-md-bold">Installation failed</p>
|
||||
<p className="mt-2 body-sm-regular overflow-auto max-h-40">{error}</p>
|
||||
<p className="mt-3 body-xs-regular text-theme-secondary">
|
||||
For common installation issues (permissions, missing compilers, etc.), see the{" "}
|
||||
<a
|
||||
href="https://docs.openbb.co/odp/desktop/troubleshooting"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-400 hover:text-blue-300 underline"
|
||||
>
|
||||
troubleshooting guide
|
||||
</a>.
|
||||
</p>
|
||||
<div className="mt-4 p-3 bg-yellow-900/20 border border-yellow-600/30 rounded-md">
|
||||
<p className="body-xs-bold text-yellow-300">What happens if you continue?</p>
|
||||
<ul className="mt-2 body-xs-regular text-yellow-200/80 list-disc list-inside space-y-1">
|
||||
<li>The environment may be incomplete or non-functional</li>
|
||||
<li>Default backend services (OpenBB API, MCP) will not be configured</li>
|
||||
<li>You may need to manually set up the environment later</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="mt-4 flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleContinueAnyway}
|
||||
className="button-secondary px-2 py-1"
|
||||
size="sm"
|
||||
>
|
||||
Continue Anyway
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleTryAgain}
|
||||
@@ -1467,14 +1527,6 @@ export default function InstallationProgress() {
|
||||
>
|
||||
Try Again
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleContinue}
|
||||
className="button-secondary px-2 py-1"
|
||||
size="sm"
|
||||
>
|
||||
Continue Anyway
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
7
desktop/src/tests/routes/backends.test.tsx
vendored
7
desktop/src/tests/routes/backends.test.tsx
vendored
@@ -303,6 +303,9 @@ describe('BackendsPage', () => {
|
||||
});
|
||||
|
||||
test('handles delete error gracefully', async () => {
|
||||
// Suppress expected console.error output for this error handling test
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
const mockBackend = {
|
||||
id: 'test-backend',
|
||||
name: 'Test Backend',
|
||||
@@ -331,6 +334,10 @@ describe('BackendsPage', () => {
|
||||
await act(async () => { fireEvent.click(within(modal as HTMLElement).getByRole('button', { name: /Delete/i })); });
|
||||
|
||||
await waitFor(() => expect(screen.getByText(/Failed to delete backend/i)).toBeInTheDocument());
|
||||
|
||||
// Verify console.error was called and restore it
|
||||
expect(consoleErrorSpy).toHaveBeenCalled();
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('views backend logs', async () => {
|
||||
|
||||
9
desktop/src/tests/setup.ts
vendored
9
desktop/src/tests/setup.ts
vendored
@@ -1,4 +1,13 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import React from 'react';
|
||||
|
||||
// Mock @openbb/ui-pro to avoid forwardRef warnings in tests
|
||||
vi.mock('@openbb/ui-pro', () => ({
|
||||
Tooltip: ({ children, content }: { children: React.ReactNode; content: string }) =>
|
||||
React.createElement('div', { 'data-tooltip-content': content }, children),
|
||||
Button: ({ children, ...props }: { children: React.ReactNode; [key: string]: unknown }) =>
|
||||
React.createElement('button', props, children),
|
||||
}));
|
||||
|
||||
// Mock ResizeObserver
|
||||
const ResizeObserverMock = vi.fn(() => ({
|
||||
|
||||
Reference in New Issue
Block a user