From 8da843adae71d618157127cfecda7574a22fef67 Mon Sep 17 00:00:00 2001 From: ayangweb <75017711+ayangweb@users.noreply.github.com> Date: Mon, 24 Feb 2025 18:41:06 +0800 Subject: [PATCH] feat: support for enable and disable services (#189) --- src-tauri/src/lib.rs | 5 +- src-tauri/src/server/servers.rs | 15 +- src/components/Cloud/Cloud.tsx | 859 ++++++++++++++++---------------- src/locales/en/translation.json | 6 +- src/locales/zh/translation.json | 6 +- 5 files changed, 459 insertions(+), 432 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 71e616e..80a25db 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -160,7 +160,6 @@ pub fn run() { let settings_window = app.get_webview_window(SETTINGS_WINDOW_LABEL).unwrap(); setup::default(app, main_window.clone(), settings_window.clone()); - Ok(()) }) .on_window_event(|window, event| match event { @@ -174,7 +173,6 @@ pub fn run() { .build(ctx) .expect("error while running tauri application"); - // Create a single Tokio runtime instance let rt = RT::new().expect("Failed to create Tokio runtime"); let app_handle = app.handle().clone(); @@ -237,8 +235,7 @@ async fn init_app_search_source(app_handle: &AppHandle) { // Remove any `None` values if `home_dir()` fails let app_dirs: Vec = dir.into_iter().flatten().collect(); - let application_search = - local::application::ApplicationSearchSource::new(1000f64, app_dirs); + let application_search = local::application::ApplicationSearchSource::new(1000f64, app_dirs); // Register the application search source let registry = app_handle.state::(); diff --git a/src-tauri/src/server/servers.rs b/src-tauri/src/server/servers.rs index 5489c81..e99f07e 100644 --- a/src-tauri/src/server/servers.rs +++ b/src-tauri/src/server/servers.rs @@ -437,10 +437,9 @@ pub async fn remove_coco_server( } #[tauri::command] -pub async fn enable_server( - app_handle: AppHandle, - id: String, -) -> Result<(), ()> { +pub async fn enable_server(app_handle: AppHandle, id: String) -> Result<(), ()> { + println!("enable_server: {}", id); + let server = get_server_by_id(id.as_str()); if let Some(mut server) = server { server.enabled = true; @@ -458,10 +457,9 @@ pub async fn enable_server( } #[tauri::command] -pub async fn disable_server( - app_handle: AppHandle, - id: String, -) -> Result<(), ()> { +pub async fn disable_server(app_handle: AppHandle, id: String) -> Result<(), ()> { + println!("disable_server: {}", id); + let server = get_server_by_id(id.as_str()); if let Some(mut server) = server { server.enabled = false; @@ -571,6 +569,7 @@ fn test_trim_endpoint_last_forward_slash() { }, }, priority: 0, + enabled: true, }; trim_endpoint_last_forward_slash(&mut server); diff --git a/src/components/Cloud/Cloud.tsx b/src/components/Cloud/Cloud.tsx index e0ad397..8f1765b 100644 --- a/src/components/Cloud/Cloud.tsx +++ b/src/components/Cloud/Cloud.tsx @@ -1,439 +1,466 @@ -import {useCallback, useEffect, useRef, useState} from "react"; -import {CalendarSync, Copy, GitFork, Globe, PackageOpen, RefreshCcw, Trash2,} from "lucide-react"; -import {v4 as uuidv4} from "uuid"; -import {getCurrentWindow} from "@tauri-apps/api/window"; -import {getCurrent as getCurrentDeepLinkUrls, onOpenUrl,} from "@tauri-apps/plugin-deep-link"; -import {invoke} from "@tauri-apps/api/core"; -import {useTranslation} from "react-i18next"; - -import {UserProfile} from "./UserProfile"; -import {DataSourcesList} from "./DataSourcesList"; -import {Sidebar} from "./Sidebar"; -import {Connect} from "./Connect"; -import {OpenURLWithBrowser} from "@/utils"; -import {useAppStore} from "@/stores/appStore"; -import {useConnectStore} from "@/stores/connectStore"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { + CalendarSync, + Copy, + GitFork, + Globe, + PackageOpen, + RefreshCcw, + Trash2, +} from "lucide-react"; +import { v4 as uuidv4 } from "uuid"; +import { getCurrentWindow } from "@tauri-apps/api/window"; +import { + getCurrent as getCurrentDeepLinkUrls, + onOpenUrl, +} from "@tauri-apps/plugin-deep-link"; +import { invoke } from "@tauri-apps/api/core"; +import { useTranslation } from "react-i18next"; + +import { UserProfile } from "./UserProfile"; +import { DataSourcesList } from "./DataSourcesList"; +import { Sidebar } from "./Sidebar"; +import { Connect } from "./Connect"; +import { OpenURLWithBrowser } from "@/utils"; +import { useAppStore } from "@/stores/appStore"; +import { useConnectStore } from "@/stores/connectStore"; import bannerImg from "@/assets/images/coco-cloud-banner.jpeg"; +import SettingsToggle from "../Settings/SettingsToggle"; export default function Cloud() { - const {t} = useTranslation(); - - const SidebarRef = useRef<{ refreshData: () => void }>(null); - - const error = useAppStore((state) => state.error); - const setError = useAppStore((state) => state.setError); - - const [isConnect, setIsConnect] = useState(true); - - const ssoRequestID = useAppStore((state) => state.ssoRequestID); - const setSSORequestID = useAppStore((state) => state.setSSORequestID); - - const currentService = useConnectStore((state) => state.currentService); - const setCurrentService = useConnectStore((state) => state.setCurrentService); - - const serverList = useConnectStore((state) => state.serverList); - const setServerList = useConnectStore((state) => state.setServerList); - - const [loading, setLoading] = useState(false); - const [refreshLoading, setRefreshLoading] = useState(false); - - // fetch the servers - useEffect(() => { - fetchServers(true); - }, []); + const { t } = useTranslation(); + + const SidebarRef = useRef<{ refreshData: () => void }>(null); + + const error = useAppStore((state) => state.error); + const setError = useAppStore((state) => state.setError); + + const [isConnect, setIsConnect] = useState(true); + + const ssoRequestID = useAppStore((state) => state.ssoRequestID); + const setSSORequestID = useAppStore((state) => state.setSSORequestID); + + const currentService = useConnectStore((state) => state.currentService); + const setCurrentService = useConnectStore((state) => state.setCurrentService); + + const serverList = useConnectStore((state) => state.serverList); + const setServerList = useConnectStore((state) => state.setServerList); + + const [loading, setLoading] = useState(false); + const [refreshLoading, setRefreshLoading] = useState(false); + + // fetch the servers + useEffect(() => { + fetchServers(true); + }, []); + + useEffect(() => { + console.log("currentService", currentService); + setLoading(false); + setRefreshLoading(false); + setError(""); + setIsConnect(true); + }, [JSON.stringify(currentService)]); + + const fetchServers = async (resetSelection: boolean) => { + invoke("list_coco_servers") + .then((res: any) => { + console.log("list_coco_servers", res); + setServerList(res); + if (resetSelection && res.length > 0) { + console.log("setCurrentService", res[res.length - 1]); + setCurrentService(res[res.length - 1]); + } else { + console.warn("Service list is empty or last item has no id"); + } + }) + .catch((err: any) => { + setError(err); + console.error(err); + }); + }; + + const add_coco_server = (endpointLink: string) => { + if (!endpointLink) { + throw new Error("Endpoint is required"); + } + if ( + !endpointLink.startsWith("http://") && + !endpointLink.startsWith("https://") + ) { + throw new Error("Invalid Endpoint"); + } - useEffect(() => { - console.log("currentService", currentService); - setLoading(false); + setRefreshLoading(true); + + return invoke("add_coco_server", { endpoint: endpointLink }) + .then((res: any) => { + console.log("add_coco_server", res); + fetchServers(false) + .then((r) => { + console.log("fetchServers", r); + setCurrentService(res); + }) + .catch((err: any) => { + console.error("fetchServers failed:", err); + setError(err); + throw err; // Propagate error back up to outer promise chain + }); + }) + .catch((err: any) => { + // Handle the invoke error + console.error("add coco server failed:", err); + setError(err); + throw err; // Propagate error back up + }) + .finally(() => { setRefreshLoading(false); - setError(""); - setIsConnect(true); - }, [JSON.stringify(currentService)]); - - const fetchServers = async (resetSelection: boolean) => { - invoke("list_coco_servers") - .then((res: any) => { - console.log("list_coco_servers", res); - setServerList(res); - if (resetSelection && res.length > 0) { - console.log("setCurrentService", res[res.length - 1]); - setCurrentService(res[res.length - 1]); - } else { - console.warn("Service list is empty or last item has no id"); - } - }) - .catch((err: any) => { - setError(err); - console.error(err); - }); - }; - - const add_coco_server = (endpointLink: string) => { - if (!endpointLink) { - throw new Error("Endpoint is required"); - } - if ( - !endpointLink.startsWith("http://") && - !endpointLink.startsWith("https://") - ) { - throw new Error("Invalid Endpoint"); + }); + }; + + const handleOAuthCallback = useCallback( + async (code: string | null, serverId: string | null) => { + if (!code) { + setError("No authorization code received"); + return; + } + + try { + console.log("Handling OAuth callback:", { code, serverId }); + await invoke("handle_sso_callback", { + serverId: serverId, // Make sure 'server_id' is the correct argument + requestId: ssoRequestID, // Make sure 'request_id' is the correct argument + code: code, + }); + + if (serverId != null) { + refreshClick(serverId); } - setRefreshLoading(true); - - return invoke("add_coco_server", {endpoint: endpointLink}) - .then((res: any) => { - console.log("add_coco_server", res); - fetchServers(false) - .then((r) => { - console.log("fetchServers", r); - setCurrentService(res); - }) - .catch((err: any) => { - console.error("fetchServers failed:", err); - setError(err); - throw err; // Propagate error back up to outer promise chain - }); - }) - .catch((err: any) => { - // Handle the invoke error - console.error("add coco server failed:", err); - setError(err); - throw err; // Propagate error back up - }) - .finally(() => { - setRefreshLoading(false); - }); + getCurrentWindow() + .setFocus() + .catch((err) => { + setError(err); + }); + } catch (e) { + console.error("Sign in failed:", e); + setError("SSO login failed: " + e); + throw error; + } finally { + setLoading(false); + } + }, + [ssoRequestID] + ); + + const handleUrl = (url: string) => { + try { + const urlObject = new URL(url.trim()); + console.log("handle urlObject:", urlObject); + + // pass request_id and check with local, if the request_id are same, then continue + const reqId = urlObject.searchParams.get("request_id"); + const code = urlObject.searchParams.get("code"); + + if (reqId != ssoRequestID) { + console.log("Request ID not matched, skip"); + setError("Request ID not matched, skip"); + return; + } + + const serverId = currentService?.id; + handleOAuthCallback(code, serverId); + } catch (err) { + console.error("Failed to parse URL:", err); + setError("Invalid URL format: " + err); + } + }; + + // Fetch the initial deep link intent + useEffect(() => { + // Test the handleUrl function + // handleUrl("coco://oauth_callback?code=cuq8asc61mdmvii032q0sx1e5akx10zo8bks45znpv3cx1gtyc6wsi0rvplizb34mwbsrbm3jar8jnefg3o5&request_id=3f1acedb-6a5b-4fe1-82fd-e66934e98a55&provider=coco-cloud/"); + // Function to handle pasted URL + const handlePaste = (event: any) => { + const pastedText = event.clipboardData.getData("text").trim(); + console.log("handle paste text:", pastedText); + if (isValidCallbackUrl(pastedText)) { + // Handle the URL as if it's a deep link + console.log("handle callback on paste:", pastedText); + handleUrl(pastedText); + } }; - const handleOAuthCallback = useCallback( - async (code: string | null, serverId: string | null) => { - if (!code) { - setError("No authorization code received"); - return; - } - - try { - console.log("Handling OAuth callback:", {code, serverId}); - await invoke("handle_sso_callback", { - serverId: serverId, // Make sure 'server_id' is the correct argument - requestId: ssoRequestID, // Make sure 'request_id' is the correct argument - code: code, - }); - - if (serverId != null) { - refreshClick(serverId); - } - - getCurrentWindow() - .setFocus() - .catch((err) => { - setError(err); - }); - } catch (e) { - console.error("Sign in failed:", e); - setError("SSO login failed: " + e); - throw error; - } finally { - setLoading(false); - } - }, - [ssoRequestID,] - ); - - const handleUrl = (url: string) => { - try { - const urlObject = new URL(url.trim()); - console.log("handle urlObject:", urlObject); - - // pass request_id and check with local, if the request_id are same, then continue - const reqId = urlObject.searchParams.get("request_id"); - const code = urlObject.searchParams.get("code"); - - if (reqId != ssoRequestID) { - console.log("Request ID not matched, skip"); - setError("Request ID not matched, skip"); - return; - } - - const serverId = currentService?.id; - handleOAuthCallback(code, serverId); - } catch (err) { - console.error("Failed to parse URL:", err); - setError("Invalid URL format: " + err); - } + // Function to check if the pasted URL is valid for our deep link scheme + const isValidCallbackUrl = (url: string) => { + return url && url.startsWith("coco://oauth_callback"); }; - // Fetch the initial deep link intent - useEffect(() => { - // Test the handleUrl function - // handleUrl("coco://oauth_callback?code=cuq8asc61mdmvii032q0sx1e5akx10zo8bks45znpv3cx1gtyc6wsi0rvplizb34mwbsrbm3jar8jnefg3o5&request_id=3f1acedb-6a5b-4fe1-82fd-e66934e98a55&provider=coco-cloud/"); - // Function to handle pasted URL - const handlePaste = (event: any) => { - const pastedText = event.clipboardData.getData("text").trim(); - console.log("handle paste text:", pastedText); - if (isValidCallbackUrl(pastedText)) { - // Handle the URL as if it's a deep link - console.log("handle callback on paste:", pastedText); - handleUrl(pastedText); - } - }; - - // Function to check if the pasted URL is valid for our deep link scheme - const isValidCallbackUrl = (url: string) => { - return url && url.startsWith("coco://oauth_callback"); - }; - - // Adding event listener for paste events - document.addEventListener("paste", handlePaste); - - getCurrentDeepLinkUrls() - .then((urls) => { - console.log("URLs:", urls); - if (urls && urls.length > 0) { - if (isValidCallbackUrl(urls[0].trim())) { - handleUrl(urls[0]); - } - } - }) - .catch((err) => { - console.error("Failed to get initial URLs:", err); - setError("Failed to get initial URLs: " + err); - }); - - const unlisten = onOpenUrl((urls) => handleUrl(urls[0])); - - return () => { - unlisten.then((fn) => fn()); - document.removeEventListener("paste", handlePaste); - }; - }, [ssoRequestID]); - - const LoginClick = useCallback(() => { - if (loading) return; // Prevent multiple clicks if already loading - - let requestID = uuidv4(); - setSSORequestID(requestID); - - // Generate the login URL with the current appUid - const url = `${currentService?.auth_provider?.sso?.url}/?provider=${currentService?.id}&product=coco&request_id=${requestID}`; - - console.log("Open SSO link, requestID:", ssoRequestID, url); - - // Open the URL in a browser - OpenURLWithBrowser(url); - - // Start loading state - setLoading(true); - }, [ssoRequestID, loading, currentService]); - - const refreshClick = (id: string) => { - setRefreshLoading(true); - invoke("refresh_coco_server_info", {id}) - .then((res: any) => { - console.log("refresh_coco_server_info", id, JSON.stringify(res)); - fetchServers(false).then((r) => { - console.log("fetchServers", r); - }); - // update currentService - setCurrentService(res); - }) - .catch((err: any) => { - setError(err); - console.error(err); - }) - .finally(() => { - setRefreshLoading(false); - }); - }; + // Adding event listener for paste events + document.addEventListener("paste", handlePaste); - function onAddServer() { - setIsConnect(false); - } + getCurrentDeepLinkUrls() + .then((urls) => { + console.log("URLs:", urls); + if (urls && urls.length > 0) { + if (isValidCallbackUrl(urls[0].trim())) { + handleUrl(urls[0]); + } + } + }) + .catch((err) => { + console.error("Failed to get initial URLs:", err); + setError("Failed to get initial URLs: " + err); + }); - function onLogout(id: string) { - console.log("onLogout", id); - setRefreshLoading(true); - invoke("logout_coco_server", {id}) - .then((res: any) => { - console.log("logout_coco_server", id, JSON.stringify(res)); - refreshClick(id); - }) - .catch((err: any) => { - setError(err); - console.error(err); - }) - .finally(() => { - setRefreshLoading(false); - }); - } + const unlisten = onOpenUrl((urls) => handleUrl(urls[0])); - const remove_coco_server = (id: string) => { - invoke("remove_coco_server", {id}) - .then((res: any) => { - console.log("remove_coco_server", id, JSON.stringify(res)); - fetchServers(true).then((r) => { - console.log("fetchServers", r); - }); - }) - .catch((err: any) => { - // TODO display the error message - setError(err); - console.error(err); - }); + return () => { + unlisten.then((fn) => fn()); + document.removeEventListener("paste", handlePaste); }; + }, [ssoRequestID]); + + const LoginClick = useCallback(() => { + if (loading) return; // Prevent multiple clicks if already loading + + let requestID = uuidv4(); + setSSORequestID(requestID); + + // Generate the login URL with the current appUid + const url = `${currentService?.auth_provider?.sso?.url}/?provider=${currentService?.id}&product=coco&request_id=${requestID}`; + + console.log("Open SSO link, requestID:", ssoRequestID, url); + + // Open the URL in a browser + OpenURLWithBrowser(url); + + // Start loading state + setLoading(true); + }, [ssoRequestID, loading, currentService]); + + const refreshClick = (id: string) => { + setRefreshLoading(true); + invoke("refresh_coco_server_info", { id }) + .then((res: any) => { + console.log("refresh_coco_server_info", id, JSON.stringify(res)); + fetchServers(false).then((r) => { + console.log("fetchServers", r); + }); + // update currentService + setCurrentService(res); + }) + .catch((err: any) => { + setError(err); + console.error(err); + }) + .finally(() => { + setRefreshLoading(false); + }); + }; + + function onAddServer() { + setIsConnect(false); + } + + function onLogout(id: string) { + console.log("onLogout", id); + setRefreshLoading(true); + invoke("logout_coco_server", { id }) + .then((res: any) => { + console.log("logout_coco_server", id, JSON.stringify(res)); + refreshClick(id); + }) + .catch((err: any) => { + setError(err); + console.error(err); + }) + .finally(() => { + setRefreshLoading(false); + }); + } + + const remove_coco_server = (id: string) => { + invoke("remove_coco_server", { id }) + .then((res: any) => { + console.log("remove_coco_server", id, JSON.stringify(res)); + fetchServers(true).then((r) => { + console.log("fetchServers", r); + }); + }) + .catch((err: any) => { + // TODO display the error message + setError(err); + console.error(err); + }); + }; + + console.log("currentService", currentService); + + return ( +
+ + +
+ {isConnect ? ( +
+
+ banner +
+
+
+
+ {currentService?.name} +
+
+
+ { + const command = value ? "enable_server" : "disable_server"; + + invoke(command, { id: currentService?.id }); + + setCurrentService({ ...currentService, enabled: value }); + }} + /> + + + + {!currentService?.builtin && ( + + )} +
+
- return ( -
- - -
- {isConnect ? ( -
-
- banner -
-
-
-
- {currentService?.name} -
-
-
- - - - {!currentService?.builtin && ( - - )} -
-
- -
-
+
+
- {" "} - {currentService?.provider?.name} + {" "} + {currentService?.provider?.name} - | - - {" "} - {currentService?.version?.number} + | + + {" "} + {currentService?.version?.number} - | - - {currentService?.updated} + | + + {currentService?.updated} -
-

- {currentService?.provider?.description} -

-
- - {currentService?.auth_provider?.sso?.url ? ( -
-

- {t('cloud.accountInfo')} -

- {currentService?.profile ? ( - - ) : ( -
- {/* Login Button (conditionally rendered when not loading) */} - {!loading && ( - - )} - - {/* Cancel Button and Copy URL button while loading */} - {loading && ( -
- - -
If the link did not - open - automatically, please - copy and - paste it into your browser manually. -
-
- )} - - {/* Privacy Policy Link */} - -
- )} -
- ) : null} - - {currentService?.profile ? ( - - ) : null} -
+
+

+ {currentService?.provider?.description} +

+
+ + {currentService?.auth_provider?.sso?.url ? ( +
+

+ {t("cloud.accountInfo")} +

+ {currentService?.profile ? ( + ) : ( - +
+ {/* Login Button (conditionally rendered when not loading) */} + {!loading && ( + + )} + + {/* Cancel Button and Copy URL button while loading */} + {loading && ( +
+ + +
+ If the link did not open automatically, please copy + and paste it into your browser manually. +
+
+ )} + + {/* Privacy Policy Link */} + +
)} -
-
- ); +
+ ) : null} + + {currentService?.profile ? ( + + ) : null} +
+ ) : ( + + )} + + + ); } diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 2e5743a..108fa23 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -162,6 +162,8 @@ "serverOffline": "Server Offline", "yourServers": "Your Coco-Servers", "addServer": "Add New Server" - } + }, + "enable_server": "Enable Server", + "disable_server": "Disable Server" } -} \ No newline at end of file +} diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json index 7d47c58..c3b825c 100644 --- a/src/locales/zh/translation.json +++ b/src/locales/zh/translation.json @@ -162,6 +162,8 @@ "serverOffline": "服务器离线", "yourServers": "您的 Coco-Servers", "addServer": "添加新服务器" - } + }, + "enable_server": "启用服务", + "disable_server": "禁用服务" } -} \ No newline at end of file +}