feat: support for enable and disable services (#189)

This commit is contained in:
ayangweb
2025-02-24 18:41:06 +08:00
committed by GitHub
parent 3b6c9e31d5
commit 8da843adae
5 changed files with 447 additions and 420 deletions

View File

@@ -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<R: Runtime>(app_handle: &AppHandle<R>) {
// Remove any `None` values if `home_dir()` fails
let app_dirs: Vec<PathBuf> = 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::<SearchSourceRegistry>();

View File

@@ -437,10 +437,9 @@ pub async fn remove_coco_server<R: Runtime>(
}
#[tauri::command]
pub async fn enable_server<R: Runtime>(
app_handle: AppHandle<R>,
id: String,
) -> Result<(), ()> {
pub async fn enable_server<R: Runtime>(app_handle: AppHandle<R>, 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<R: Runtime>(
}
#[tauri::command]
pub async fn disable_server<R: Runtime>(
app_handle: AppHandle<R>,
id: String,
) -> Result<(), ()> {
pub async fn disable_server<R: Runtime>(app_handle: AppHandle<R>, 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);

View File

@@ -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 { 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 { 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 { t } = useTranslation();
const SidebarRef = useRef<{ refreshData: () => void }>(null);
const SidebarRef = useRef<{ refreshData: () => void }>(null);
const error = useAppStore((state) => state.error);
const setError = useAppStore((state) => state.setError);
const error = useAppStore((state) => state.error);
const setError = useAppStore((state) => state.setError);
const [isConnect, setIsConnect] = useState(true);
const [isConnect, setIsConnect] = useState(true);
const ssoRequestID = useAppStore((state) => state.ssoRequestID);
const setSSORequestID = useAppStore((state) => state.setSSORequestID);
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 currentService = useConnectStore((state) => state.currentService);
const setCurrentService = useConnectStore((state) => state.setCurrentService);
const serverList = useConnectStore((state) => state.serverList);
const setServerList = useConnectStore((state) => state.setServerList);
const serverList = useConnectStore((state) => state.serverList);
const setServerList = useConnectStore((state) => state.setServerList);
const [loading, setLoading] = useState(false);
const [refreshLoading, setRefreshLoading] = useState(false);
const [loading, setLoading] = useState(false);
const [refreshLoading, setRefreshLoading] = useState(false);
// fetch the servers
useEffect(() => {
fetchServers(true);
}, []);
// fetch the servers
useEffect(() => {
fetchServers(true);
}, []);
useEffect(() => {
console.log("currentService", currentService);
setLoading(false);
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");
}
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 handleOAuthCallback = useCallback(
async (code: string | null, serverId: string | null) => {
if (!code) {
setError("No authorization code received");
return;
}
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");
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);
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]
);
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);
});
};
const handleUrl = (url: string) => {
try {
const urlObject = new URL(url.trim());
console.log("handle urlObject:", urlObject);
const handleOAuthCallback = useCallback(
async (code: string | null, serverId: string | null) => {
if (!code) {
setError("No authorization code received");
return;
}
// 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");
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 (reqId != ssoRequestID) {
console.log("Request ID not matched, skip");
setError("Request ID not matched, skip");
return;
}
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);
}
};
// 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);
});
};
function onAddServer() {
setIsConnect(false);
const serverId = currentService?.id;
handleOAuthCallback(code, serverId);
} catch (err) {
console.error("Failed to parse URL:", err);
setError("Invalid URL format: " + 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 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);
});
// 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);
}
};
return (
<div className="flex bg-gray-50 dark:bg-gray-900">
<Sidebar
ref={SidebarRef}
onAddServer={onAddServer}
serverList={serverList}
/>
// 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");
};
<main className="flex-1 p-4 py-8">
{isConnect ? (
<div className="max-w-4xl mx-auto">
<div className="w-full rounded-[4px] bg-[rgba(229,229,229,1)] dark:bg-gray-800 mb-6">
<img
width="100%"
src={currentService?.provider?.banner || bannerImg}
alt="banner"
/>
</div>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-3">
<div className="flex items-center text-gray-900 dark:text-white font-medium">
{currentService?.name}
</div>
</div>
<div className="flex gap-2">
<button
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
onClick={() =>
OpenURLWithBrowser(currentService?.provider?.website)
}
>
<Globe className="w-3.5 h-3.5"/>
</button>
<button
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
onClick={() => refreshClick(currentService?.id)}
>
<RefreshCcw
className={`w-3.5 h-3.5 ${
refreshLoading ? "animate-spin" : ""
}`}
/>
</button>
// Adding event listener for paste events
document.addEventListener("paste", handlePaste);
{!currentService?.builtin && (
<button
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
onClick={() => remove_coco_server(currentService?.id)}
>
<Trash2 className="w-3.5 h-3.5 text-[#ff4747]"/>
</button>
)}
</div>
</div>
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);
});
<div className="mb-8">
<div className="text-sm text-gray-500 dark:text-gray-400 mb-2 flex">
<span className="flex items-center gap-1">
<PackageOpen className="w-4 h-4"/>{" "}
{currentService?.provider?.name}
</span>
<span className="mx-4">|</span>
<span className="flex items-center gap-1">
<GitFork className="w-4 h-4"/>{" "}
{currentService?.version?.number}
</span>
<span className="mx-4">|</span>
<span className="flex items-center gap-1">
<CalendarSync className="w-4 h-4"/> {currentService?.updated}
</span>
</div>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
{currentService?.provider?.description}
</p>
</div>
const unlisten = onOpenUrl((urls) => handleUrl(urls[0]));
{currentService?.auth_provider?.sso?.url ? (
<div className="mb-8">
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
{t('cloud.accountInfo')}
</h2>
{currentService?.profile ? (
<UserProfile
server={currentService?.id}
userInfo={currentService?.profile}
onLogout={onLogout}
/>
) : (
<div>
{/* Login Button (conditionally rendered when not loading) */}
{!loading && (
<button
className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors mb-3"
onClick={LoginClick}
>
{t('cloud.login')}
</button>
)}
return () => {
unlisten.then((fn) => fn());
document.removeEventListener("paste", handlePaste);
};
}, [ssoRequestID]);
{/* Cancel Button and Copy URL button while loading */}
{loading && (
<div className="flex items-center space-x-2">
<button
className="px-6 py-2 text-white bg-red-500 rounded-md hover:bg-red-600 transition-colors mb-3"
onClick={() => setLoading(false)} // Reset loading state
>
{t('cloud.cancel')}
</button>
<button
onClick={() => {
navigator.clipboard.writeText(
`${currentService?.auth_provider?.sso?.url}/?provider=${currentService?.id}&product=coco&request_id=${ssoRequestID}`
);
}}
className="text-xl text-blue-500 hover:text-blue-600"
>
<Copy className="inline mr-2"/>{" "}
</button>
<div className="text-justify italic text-xs">If the link did not
open
automatically, please
copy and
paste it into your browser manually.
</div>
</div>
)}
const LoginClick = useCallback(() => {
if (loading) return; // Prevent multiple clicks if already loading
{/* Privacy Policy Link */}
<button
className="text-xs text-[#0096FB] dark:text-blue-400 block"
onClick={() =>
OpenURLWithBrowser(
currentService?.provider?.privacy_policy
)
}
>
{t('cloud.privacyPolicy')}
</button>
</div>
)}
</div>
) : null}
let requestID = uuidv4();
setSSORequestID(requestID);
{currentService?.profile ? (
<DataSourcesList server={currentService?.id}/>
) : null}
</div>
) : (
<Connect setIsConnect={setIsConnect} onAddServer={add_coco_server}/>
// 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 (
<div className="flex bg-gray-50 dark:bg-gray-900">
<Sidebar
ref={SidebarRef}
onAddServer={onAddServer}
serverList={serverList}
/>
<main className="flex-1 p-4 py-8">
{isConnect ? (
<div className="max-w-4xl mx-auto">
<div className="w-full rounded-[4px] bg-[rgba(229,229,229,1)] dark:bg-gray-800 mb-6">
<img
width="100%"
src={currentService?.provider?.banner || bannerImg}
alt="banner"
/>
</div>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-3">
<div className="flex items-center text-gray-900 dark:text-white font-medium">
{currentService?.name}
</div>
</div>
<div className="flex items-center gap-2">
<SettingsToggle
checked={currentService?.enabled}
label={
currentService?.enabled
? t("cloud.enable_server")
: t("cloud.disable_server")
}
onChange={(value) => {
const command = value ? "enable_server" : "disable_server";
invoke(command, { id: currentService?.id });
setCurrentService({ ...currentService, enabled: value });
}}
/>
<button
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
onClick={() =>
OpenURLWithBrowser(currentService?.provider?.website)
}
>
<Globe className="w-3.5 h-3.5" />
</button>
<button
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
onClick={() => refreshClick(currentService?.id)}
>
<RefreshCcw
className={`w-3.5 h-3.5 ${
refreshLoading ? "animate-spin" : ""
}`}
/>
</button>
{!currentService?.builtin && (
<button
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
onClick={() => remove_coco_server(currentService?.id)}
>
<Trash2 className="w-3.5 h-3.5 text-[#ff4747]" />
</button>
)}
</main>
</div>
);
</div>
</div>
<div className="mb-8">
<div className="text-sm text-gray-500 dark:text-gray-400 mb-2 flex">
<span className="flex items-center gap-1">
<PackageOpen className="w-4 h-4" />{" "}
{currentService?.provider?.name}
</span>
<span className="mx-4">|</span>
<span className="flex items-center gap-1">
<GitFork className="w-4 h-4" />{" "}
{currentService?.version?.number}
</span>
<span className="mx-4">|</span>
<span className="flex items-center gap-1">
<CalendarSync className="w-4 h-4" /> {currentService?.updated}
</span>
</div>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
{currentService?.provider?.description}
</p>
</div>
{currentService?.auth_provider?.sso?.url ? (
<div className="mb-8">
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
{t("cloud.accountInfo")}
</h2>
{currentService?.profile ? (
<UserProfile
server={currentService?.id}
userInfo={currentService?.profile}
onLogout={onLogout}
/>
) : (
<div>
{/* Login Button (conditionally rendered when not loading) */}
{!loading && (
<button
className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors mb-3"
onClick={LoginClick}
>
{t("cloud.login")}
</button>
)}
{/* Cancel Button and Copy URL button while loading */}
{loading && (
<div className="flex items-center space-x-2">
<button
className="px-6 py-2 text-white bg-red-500 rounded-md hover:bg-red-600 transition-colors mb-3"
onClick={() => setLoading(false)} // Reset loading state
>
{t("cloud.cancel")}
</button>
<button
onClick={() => {
navigator.clipboard.writeText(
`${currentService?.auth_provider?.sso?.url}/?provider=${currentService?.id}&product=coco&request_id=${ssoRequestID}`
);
}}
className="text-xl text-blue-500 hover:text-blue-600"
>
<Copy className="inline mr-2" />{" "}
</button>
<div className="text-justify italic text-xs">
If the link did not open automatically, please copy
and paste it into your browser manually.
</div>
</div>
)}
{/* Privacy Policy Link */}
<button
className="text-xs text-[#0096FB] dark:text-blue-400 block"
onClick={() =>
OpenURLWithBrowser(
currentService?.provider?.privacy_policy
)
}
>
{t("cloud.privacyPolicy")}
</button>
</div>
)}
</div>
) : null}
{currentService?.profile ? (
<DataSourcesList server={currentService?.id} />
) : null}
</div>
) : (
<Connect setIsConnect={setIsConnect} onAddServer={add_coco_server} />
)}
</main>
</div>
);
}

View File

@@ -162,6 +162,8 @@
"serverOffline": "Server Offline",
"yourServers": "Your Coco-Servers",
"addServer": "Add New Server"
}
},
"enable_server": "Enable Server",
"disable_server": "Disable Server"
}
}
}

View File

@@ -162,6 +162,8 @@
"serverOffline": "服务器离线",
"yourServers": "您的 Coco-Servers",
"addServer": "添加新服务器"
}
},
"enable_server": "启用服务",
"disable_server": "禁用服务"
}
}
}