mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-04-28 23:06:41 +02:00
feat: in-house proxies
This commit is contained in:
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./dist/dev/types/routes.d.ts";
|
||||
import "./.next/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -12,6 +12,8 @@ use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::browser::ProxySettings;
|
||||
use crate::proxy_manager::PROXY_MANAGER;
|
||||
use crate::settings_manager::SettingsManager;
|
||||
use crate::sync;
|
||||
|
||||
@@ -71,6 +73,20 @@ struct SyncTokenResponse {
|
||||
sync_token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
struct CloudProxyConfigResponse {
|
||||
host: String,
|
||||
port: u16,
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
protocol: String,
|
||||
#[serde(rename = "bandwidthLimitMb")]
|
||||
bandwidth_limit_mb: i64,
|
||||
#[serde(rename = "bandwidthUsedMb")]
|
||||
bandwidth_used_mb: i64,
|
||||
}
|
||||
|
||||
pub struct CloudAuthManager {
|
||||
client: Client,
|
||||
state: Mutex<Option<CloudAuthState>>,
|
||||
@@ -400,6 +416,7 @@ impl CloudAuthManager {
|
||||
let status = response.status();
|
||||
if status == reqwest::StatusCode::UNAUTHORIZED {
|
||||
// Refresh token expired — clear everything
|
||||
PROXY_MANAGER.remove_cloud_proxy();
|
||||
self.clear_auth().await;
|
||||
let _ = crate::events::emit_empty("cloud-auth-expired");
|
||||
return Err("Session expired. Please log in again.".to_string());
|
||||
@@ -519,6 +536,9 @@ impl CloudAuthManager {
|
||||
.await;
|
||||
}
|
||||
|
||||
// Remove cloud proxy on logout
|
||||
PROXY_MANAGER.remove_cloud_proxy();
|
||||
|
||||
self.clear_auth().await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -568,6 +588,81 @@ impl CloudAuthManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch proxy configuration from the cloud backend
|
||||
async fn fetch_proxy_config(&self) -> Result<Option<CloudProxyConfigResponse>, String> {
|
||||
// Check cached user state for proxy bandwidth
|
||||
{
|
||||
let state = self.state.lock().await;
|
||||
match &*state {
|
||||
Some(auth) if auth.user.proxy_bandwidth_limit_mb > 0 => {}
|
||||
_ => return Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
match self
|
||||
.api_call_with_retry(|access_token| {
|
||||
let url = format!("{CLOUD_API_URL}/api/proxy/config");
|
||||
let client = self.client.clone();
|
||||
async move {
|
||||
let response = client
|
||||
.get(&url)
|
||||
.header("Authorization", format!("Bearer {access_token}"))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to fetch proxy config: {e}"))?;
|
||||
|
||||
let status = response.status();
|
||||
if status == reqwest::StatusCode::FORBIDDEN {
|
||||
return Err("__403__".to_string());
|
||||
}
|
||||
|
||||
if !response.status().is_success() {
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
return Err(format!("Proxy config fetch failed ({status}): {body}"));
|
||||
}
|
||||
|
||||
response
|
||||
.json::<CloudProxyConfigResponse>()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to parse proxy config: {e}"))
|
||||
}
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(config) => Ok(Some(config)),
|
||||
Err(e) if e.contains("__403__") => Ok(None),
|
||||
Err(e) => {
|
||||
log::warn!("Failed to fetch cloud proxy config: {e}");
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sync the cloud-managed proxy: fetch config and upsert or remove
|
||||
pub async fn sync_cloud_proxy(&self) {
|
||||
match self.fetch_proxy_config().await {
|
||||
Ok(Some(config)) => {
|
||||
let settings = ProxySettings {
|
||||
proxy_type: config.protocol,
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
username: config.username,
|
||||
password: config.password,
|
||||
};
|
||||
match PROXY_MANAGER.upsert_cloud_proxy(settings) {
|
||||
Ok(_) => log::debug!("Cloud proxy synced successfully"),
|
||||
Err(e) => log::warn!("Failed to upsert cloud proxy: {e}"),
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
PROXY_MANAGER.remove_cloud_proxy();
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to sync cloud proxy: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Background loop that refreshes the sync token periodically
|
||||
pub async fn start_sync_token_refresh_loop(app_handle: tauri::AppHandle) {
|
||||
loop {
|
||||
@@ -601,6 +696,9 @@ impl CloudAuthManager {
|
||||
log::debug!("Failed to refresh cloud profile: {e}");
|
||||
}
|
||||
|
||||
// Sync cloud proxy credentials
|
||||
CLOUD_AUTH.sync_cloud_proxy().await;
|
||||
|
||||
let _ = &app_handle; // keep app_handle alive
|
||||
}
|
||||
}
|
||||
@@ -628,6 +726,9 @@ pub async fn cloud_verify_otp(
|
||||
}
|
||||
}
|
||||
|
||||
// Sync cloud proxy after login
|
||||
CLOUD_AUTH.sync_cloud_proxy().await;
|
||||
|
||||
let _ = &app_handle;
|
||||
Ok(state)
|
||||
}
|
||||
@@ -654,6 +755,30 @@ pub async fn cloud_has_active_subscription() -> Result<bool, String> {
|
||||
Ok(CLOUD_AUTH.has_active_paid_subscription().await)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct CloudProxyUsage {
|
||||
pub used_mb: i64,
|
||||
pub limit_mb: i64,
|
||||
pub remaining_mb: i64,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn cloud_get_proxy_usage() -> Result<Option<CloudProxyUsage>, String> {
|
||||
let state = CLOUD_AUTH.state.lock().await;
|
||||
match &*state {
|
||||
Some(auth) if auth.user.proxy_bandwidth_limit_mb > 0 => {
|
||||
let used = auth.user.proxy_bandwidth_used_mb;
|
||||
let limit = auth.user.proxy_bandwidth_limit_mb;
|
||||
Ok(Some(CloudProxyUsage {
|
||||
used_mb: used,
|
||||
limit_mb: limit,
|
||||
remaining_mb: (limit - used).max(0),
|
||||
}))
|
||||
}
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn restart_sync_service(app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||
// Stop existing scheduler
|
||||
|
||||
@@ -1100,6 +1100,8 @@ pub fn run() {
|
||||
if let Err(e) = cloud_auth::CLOUD_AUTH.get_or_refresh_sync_token().await {
|
||||
log::warn!("Failed to refresh cloud sync token on startup: {e}");
|
||||
}
|
||||
// Sync cloud proxy credentials on startup
|
||||
cloud_auth::CLOUD_AUTH.sync_cloud_proxy().await;
|
||||
}
|
||||
cloud_auth::CloudAuthManager::start_sync_token_refresh_loop(app_handle_cloud).await;
|
||||
});
|
||||
@@ -1218,6 +1220,7 @@ pub fn run() {
|
||||
cloud_auth::cloud_get_user,
|
||||
cloud_auth::cloud_refresh_profile,
|
||||
cloud_auth::cloud_logout,
|
||||
cloud_auth::cloud_get_proxy_usage,
|
||||
cloud_auth::restart_sync_service
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
|
||||
@@ -91,6 +91,8 @@ pub struct ProxyCheckResult {
|
||||
pub is_valid: bool,
|
||||
}
|
||||
|
||||
pub const CLOUD_PROXY_ID: &str = "cloud-included-proxy";
|
||||
|
||||
// Stored proxy configuration with name and ID for reuse
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StoredProxy {
|
||||
@@ -101,6 +103,8 @@ pub struct StoredProxy {
|
||||
pub sync_enabled: bool,
|
||||
#[serde(default)]
|
||||
pub last_sync: Option<u64>,
|
||||
#[serde(default)]
|
||||
pub is_cloud_managed: bool,
|
||||
}
|
||||
|
||||
impl StoredProxy {
|
||||
@@ -111,6 +115,7 @@ impl StoredProxy {
|
||||
proxy_settings,
|
||||
sync_enabled: false,
|
||||
last_sync: None,
|
||||
is_cloud_managed: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,6 +400,61 @@ impl ProxyManager {
|
||||
Ok(stored_proxy)
|
||||
}
|
||||
|
||||
// Upsert the cloud-managed proxy (create or update)
|
||||
pub fn upsert_cloud_proxy(&self, proxy_settings: ProxySettings) -> Result<StoredProxy, String> {
|
||||
let mut stored_proxies = self.stored_proxies.lock().unwrap();
|
||||
|
||||
if let Some(existing) = stored_proxies.get_mut(CLOUD_PROXY_ID) {
|
||||
existing.proxy_settings = proxy_settings;
|
||||
let updated = existing.clone();
|
||||
drop(stored_proxies);
|
||||
|
||||
if let Err(e) = self.save_proxy(&updated) {
|
||||
log::warn!("Failed to save cloud proxy: {e}");
|
||||
}
|
||||
if let Err(e) = events::emit_empty("proxies-changed") {
|
||||
log::error!("Failed to emit proxies-changed event: {e}");
|
||||
}
|
||||
Ok(updated)
|
||||
} else {
|
||||
let cloud_proxy = StoredProxy {
|
||||
id: CLOUD_PROXY_ID.to_string(),
|
||||
name: "Included Proxy".to_string(),
|
||||
proxy_settings,
|
||||
sync_enabled: false,
|
||||
last_sync: None,
|
||||
is_cloud_managed: true,
|
||||
};
|
||||
stored_proxies.insert(CLOUD_PROXY_ID.to_string(), cloud_proxy.clone());
|
||||
drop(stored_proxies);
|
||||
|
||||
if let Err(e) = self.save_proxy(&cloud_proxy) {
|
||||
log::warn!("Failed to save cloud proxy: {e}");
|
||||
}
|
||||
if let Err(e) = events::emit_empty("proxies-changed") {
|
||||
log::error!("Failed to emit proxies-changed event: {e}");
|
||||
}
|
||||
Ok(cloud_proxy)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the cloud-managed proxy
|
||||
pub fn remove_cloud_proxy(&self) {
|
||||
let removed = {
|
||||
let mut stored_proxies = self.stored_proxies.lock().unwrap();
|
||||
stored_proxies.remove(CLOUD_PROXY_ID).is_some()
|
||||
};
|
||||
|
||||
if removed {
|
||||
if let Err(e) = self.delete_proxy_file(CLOUD_PROXY_ID) {
|
||||
log::warn!("Failed to delete cloud proxy file: {e}");
|
||||
}
|
||||
if let Err(e) = events::emit_empty("proxies-changed") {
|
||||
log::error!("Failed to emit proxies-changed event: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get all stored proxies
|
||||
pub fn get_stored_proxies(&self) -> Vec<StoredProxy> {
|
||||
let stored_proxies = self.stored_proxies.lock().unwrap();
|
||||
@@ -423,6 +483,14 @@ impl ProxyManager {
|
||||
return Err(format!("Proxy with ID '{proxy_id}' not found"));
|
||||
}
|
||||
|
||||
// Block editing cloud-managed proxies
|
||||
if stored_proxies
|
||||
.get(proxy_id)
|
||||
.is_some_and(|p| p.is_cloud_managed)
|
||||
{
|
||||
return Err("Cannot edit a cloud-managed proxy".to_string());
|
||||
}
|
||||
|
||||
// Check if new name conflicts with existing proxies
|
||||
if let Some(ref new_name) = name {
|
||||
if stored_proxies
|
||||
@@ -471,6 +539,15 @@ impl ProxyManager {
|
||||
// Remember if sync was enabled before deleting
|
||||
let was_sync_enabled = {
|
||||
let stored_proxies = self.stored_proxies.lock().unwrap();
|
||||
|
||||
// Block deleting cloud-managed proxies
|
||||
if stored_proxies
|
||||
.get(proxy_id)
|
||||
.is_some_and(|p| p.is_cloud_managed)
|
||||
{
|
||||
return Err("Cannot delete a cloud-managed proxy".to_string());
|
||||
}
|
||||
|
||||
stored_proxies
|
||||
.get(proxy_id)
|
||||
.map(|p| p.sync_enabled)
|
||||
@@ -601,6 +678,7 @@ impl ProxyManager {
|
||||
let stored_proxies = self.stored_proxies.lock().unwrap();
|
||||
let proxies: Vec<ExportedProxy> = stored_proxies
|
||||
.values()
|
||||
.filter(|p| !p.is_cloud_managed)
|
||||
.map(|p| ExportedProxy {
|
||||
name: p.name.clone(),
|
||||
proxy_type: p.proxy_settings.proxy_type.clone(),
|
||||
@@ -626,6 +704,7 @@ impl ProxyManager {
|
||||
let stored_proxies = self.stored_proxies.lock().unwrap();
|
||||
stored_proxies
|
||||
.values()
|
||||
.filter(|p| !p.is_cloud_managed)
|
||||
.map(|p| Self::build_proxy_url(&p.proxy_settings))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
|
||||
@@ -1228,6 +1228,11 @@ pub async fn set_proxy_sync_enabled(
|
||||
.find(|p| p.id == proxy_id)
|
||||
.ok_or_else(|| format!("Proxy with ID '{proxy_id}' not found"))?;
|
||||
|
||||
// Block modifying sync for cloud-managed proxies
|
||||
if proxy.is_cloud_managed {
|
||||
return Err("Cannot modify sync for a cloud-managed proxy".to_string());
|
||||
}
|
||||
|
||||
// If disabling, check if proxy is used by any synced profile
|
||||
if !enabled && is_proxy_used_by_synced_profile(&proxy_id) {
|
||||
return Err("Sync cannot be disabled while this proxy is used by synced profiles".to_string());
|
||||
|
||||
@@ -144,6 +144,7 @@ export function ProxyAssignmentDialog({
|
||||
{storedProxies.map((proxy) => (
|
||||
<SelectItem key={proxy.id} value={proxy.id}>
|
||||
{proxy.name}
|
||||
{proxy.is_cloud_managed ? " (Included)" : ""}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
||||
@@ -100,7 +100,37 @@ export function ProxyManagementDialog({
|
||||
{},
|
||||
);
|
||||
|
||||
const { storedProxies, proxyUsage, isLoading } = useProxyEvents();
|
||||
const { storedProxies: rawProxies, proxyUsage, isLoading } = useProxyEvents();
|
||||
const [cloudProxyUsage, setCloudProxyUsage] = useState<{
|
||||
used_mb: number;
|
||||
limit_mb: number;
|
||||
} | null>(null);
|
||||
|
||||
// Sort cloud-managed proxies first
|
||||
const storedProxies = [...rawProxies].sort((a, b) => {
|
||||
if (a.is_cloud_managed && !b.is_cloud_managed) return -1;
|
||||
if (!a.is_cloud_managed && b.is_cloud_managed) return 1;
|
||||
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
||||
});
|
||||
|
||||
// Fetch cloud proxy usage
|
||||
useEffect(() => {
|
||||
const fetchUsage = async () => {
|
||||
try {
|
||||
const usage = await invoke<{
|
||||
used_mb: number;
|
||||
limit_mb: number;
|
||||
remaining_mb: number;
|
||||
} | null>("cloud_get_proxy_usage");
|
||||
setCloudProxyUsage(usage);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
if (isOpen) {
|
||||
void fetchUsage();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// Listen for proxy sync status events
|
||||
useEffect(() => {
|
||||
@@ -281,6 +311,7 @@ export function ProxyManagementDialog({
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{storedProxies.map((proxy) => {
|
||||
const isCloud = proxy.is_cloud_managed === true;
|
||||
const syncDot = getSyncStatusDot(
|
||||
proxy,
|
||||
proxySyncStatus[proxy.id],
|
||||
@@ -288,20 +319,32 @@ export function ProxyManagementDialog({
|
||||
return (
|
||||
<TableRow key={proxy.id}>
|
||||
<TableCell className="font-medium">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full shrink-0 ${syncDot.color} ${
|
||||
syncDot.animate ? "animate-pulse" : ""
|
||||
}`}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{syncDot.tooltip}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{proxy.name}
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<div className="flex items-center gap-2">
|
||||
{!isCloud && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full shrink-0 ${syncDot.color} ${
|
||||
syncDot.animate
|
||||
? "animate-pulse"
|
||||
: ""
|
||||
}`}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{syncDot.tooltip}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{proxy.name}
|
||||
</div>
|
||||
{isCloud && cloudProxyUsage && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{cloudProxyUsage.used_mb} /{" "}
|
||||
{cloudProxyUsage.limit_mb} MB used
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
@@ -310,36 +353,40 @@ export function ProxyManagementDialog({
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Checkbox
|
||||
checked={proxy.sync_enabled}
|
||||
onCheckedChange={() =>
|
||||
handleToggleSync(proxy)
|
||||
}
|
||||
disabled={
|
||||
isTogglingSync[proxy.id] ||
|
||||
proxyInUse[proxy.id]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{proxyInUse[proxy.id] ? (
|
||||
<p>
|
||||
Sync cannot be disabled while this proxy
|
||||
is used by synced profiles
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
{proxy.sync_enabled
|
||||
? "Disable sync"
|
||||
: "Enable sync"}
|
||||
</p>
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{isCloud ? (
|
||||
<Badge variant="outline">Cloud</Badge>
|
||||
) : (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Checkbox
|
||||
checked={proxy.sync_enabled}
|
||||
onCheckedChange={() =>
|
||||
handleToggleSync(proxy)
|
||||
}
|
||||
disabled={
|
||||
isTogglingSync[proxy.id] ||
|
||||
proxyInUse[proxy.id]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{proxyInUse[proxy.id] ? (
|
||||
<p>
|
||||
Sync cannot be disabled while this proxy
|
||||
is used by synced profiles
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
{proxy.sync_enabled
|
||||
? "Disable sync"
|
||||
: "Enable sync"}
|
||||
</p>
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-1">
|
||||
@@ -362,47 +409,55 @@ export function ProxyManagementDialog({
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEditProxy(proxy)}
|
||||
>
|
||||
<LuPencil className="w-4 h-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Edit proxy</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteProxy(proxy)}
|
||||
disabled={
|
||||
(proxyUsage[proxy.id] ?? 0) > 0
|
||||
}
|
||||
>
|
||||
<LuTrash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{(proxyUsage[proxy.id] ?? 0) > 0 ? (
|
||||
<p>
|
||||
Cannot delete: in use by{" "}
|
||||
{proxyUsage[proxy.id]} profile
|
||||
{proxyUsage[proxy.id] > 1 ? "s" : ""}
|
||||
</p>
|
||||
) : (
|
||||
<p>Delete proxy</p>
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{!isCloud && (
|
||||
<>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEditProxy(proxy)}
|
||||
>
|
||||
<LuPencil className="w-4 h-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Edit proxy</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
handleDeleteProxy(proxy)
|
||||
}
|
||||
disabled={
|
||||
(proxyUsage[proxy.id] ?? 0) > 0
|
||||
}
|
||||
>
|
||||
<LuTrash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{(proxyUsage[proxy.id] ?? 0) > 0 ? (
|
||||
<p>
|
||||
Cannot delete: in use by{" "}
|
||||
{proxyUsage[proxy.id]} profile
|
||||
{proxyUsage[proxy.id] > 1
|
||||
? "s"
|
||||
: ""}
|
||||
</p>
|
||||
) : (
|
||||
<p>Delete proxy</p>
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -256,6 +256,17 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
{user.proxyBandwidthLimitMb > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">
|
||||
Proxy Bandwidth
|
||||
</span>
|
||||
<span>
|
||||
{user.proxyBandwidthUsedMb} /{" "}
|
||||
{user.proxyBandwidthLimitMb} MB
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 pt-2">
|
||||
|
||||
@@ -67,12 +67,15 @@ export interface ProxyCheckResult {
|
||||
is_valid: boolean;
|
||||
}
|
||||
|
||||
export const CLOUD_PROXY_ID = "cloud-included-proxy";
|
||||
|
||||
export interface StoredProxy {
|
||||
id: string;
|
||||
name: string;
|
||||
proxy_settings: ProxySettings;
|
||||
sync_enabled?: boolean;
|
||||
last_sync?: number;
|
||||
is_cloud_managed?: boolean;
|
||||
}
|
||||
|
||||
export interface ProfileGroup {
|
||||
|
||||
Reference in New Issue
Block a user