refactor: refactoring login (#173)

* chore: add tips to login

* refactor: refactoring login

* chore: update release notes
This commit is contained in:
Medcl
2025-02-23 19:08:43 +08:00
committed by GitHub
parent f63bc753f8
commit a06888554b
7 changed files with 414 additions and 475 deletions

View File

@@ -16,6 +16,8 @@ Information about release notes of Coco Server is provided here.
### Improvements
- Improve app startup, init application search in background #172
- Refactoring login #173
## 0.1.0 (2015-02-16)

View File

@@ -7,9 +7,9 @@ use crate::server::search::CocoSearchSource;
use crate::server::servers::{get_server_by_id, persist_servers, persist_servers_token, save_access_token, save_server};
use reqwest::{Client, StatusCode};
use tauri::{AppHandle, Manager, Runtime};
fn request_access_token_url(request_id: &str) -> String {
fn request_access_token_url(request_id: &str, code: &str) -> String {
// Remove the endpoint part and keep just the path for the request
format!("/auth/request_access_token?request_id={}", request_id)
format!("/auth/request_access_token?request_id={}&token={}", request_id, code)
}
#[tauri::command]
@@ -24,8 +24,8 @@ pub async fn handle_sso_callback<R: Runtime>(
if let Some(mut server) = server {
// Prepare the URL for requesting the access token (endpoint is base URL, path is relative)
save_access_token(server_id.clone(), ServerAccessToken::new(server_id.clone(), code.clone(), 60 * 15));
let path = request_access_token_url(&request_id);
// save_access_token(server_id.clone(), ServerAccessToken::new(server_id.clone(), code.clone(), 60 * 15));
let path = request_access_token_url(&request_id, &code);
// Send the request for the access token using the util::http::HttpClient::get method
let response = HttpClient::get(&server_id, &path)
@@ -47,7 +47,7 @@ pub async fn handle_sso_callback<R: Runtime>(
token.access_token.clone(),
token.expire_at,
);
dbg!(&server_id, &request_id, &code, &token);
// dbg!(&server_id, &request_id, &code, &token);
save_access_token(server_id.clone(), access_token);
persist_servers_token(&app_handle)?;

View File

@@ -29,8 +29,6 @@ pub fn get_connector_by_id(server_id: &str, connector_id: &str) -> Option<Connec
}
pub async fn refresh_all_connectors<R: Runtime>(app_handle: &AppHandle<R>) -> Result<(), String> {
// dbg!("Attempting to refresh all connectors");
let servers = get_all_servers();
// Collect all the tasks for fetching and refreshing connectors
@@ -47,13 +45,11 @@ pub async fn refresh_all_connectors<R: Runtime>(app_handle: &AppHandle<R>) -> Re
connectors_map
}
Err(_e) => {
// dbg!("Failed to get connectors for server {}: {}", &server.id, e);
HashMap::new() // Return empty map on failure
}
};
server_map.insert(server.id.clone(), connectors);
// dbg!("end fetch connectors for server: {}", &server.id);
}
// After all tasks have finished, perform a read operation on the cache
@@ -66,8 +62,6 @@ pub async fn refresh_all_connectors<R: Runtime>(app_handle: &AppHandle<R>) -> Re
cache.len()
};
// dbg!("finished refresh connectors: {:?}", cache_size);
Ok(())
}
@@ -101,8 +95,6 @@ pub async fn get_connectors_from_cache_or_remote(
}
pub async fn fetch_connectors_by_server(id: &str) -> Result<Vec<Connector>, String> {
// dbg!("start get_connectors_by_server: id =", &id);
// Use the generic GET method from HttpClient
let resp = HttpClient::get(&id, "/connector/_search")
.await
@@ -111,34 +103,15 @@ pub async fn fetch_connectors_by_server(id: &str) -> Result<Vec<Connector>, Stri
format!("Error fetching connector: {}", e)
})?;
// // Log the raw response status and headers
// dbg!("Response status: {:?}", resp.status());
// dbg!("Response headers: {:?}", resp.headers());
// Ensure the response body is not empty or invalid
if resp.status().is_success() {
dbg!("Received successful response for id: {}", &id);
} else {
dbg!(
"Failed to fetch connectors. Response status: {}",
resp.status()
);
}
// Parse the search results directly from the response body
let datasources: Vec<Connector> = parse_search_results(resp).await.map_err(|e| {
// dbg!("Error parsing search results for id {}: {}", &id, &e);
let datasource: Vec<Connector> = parse_search_results(resp).await.map_err(|e| {
e.to_string()
})?;
// Log the parsed results
// dbg!("Parsed connectors: {:?}", &datasources);
// Save the connectors to the cache
save_connectors_to_cache(&id, datasources.clone());
save_connectors_to_cache(&id, datasource.clone());
// dbg!("end get_connectors_by_server: id =", &id);
return Ok(datasources);
Ok(datasource)
}
#[tauri::command]
@@ -146,8 +119,6 @@ pub async fn get_connectors_by_server<R: Runtime>(
_app_handle: AppHandle<R>,
id: String,
) -> Result<Vec<Connector>, String> {
//fetch_connectors_by_server
let connectors = fetch_connectors_by_server(&id).await?;
Ok(connectors)
}

View File

@@ -1,8 +1,8 @@
// use std::future::Future;
use std::time::Duration;
// use lazy_static::lazy_static;
// use tauri::AppHandle;
use crate::server::servers::{get_server_by_id, get_server_token};
// use std::future::Future;
use std::time::Duration;
use once_cell::sync::Lazy;
use reqwest::{Client, Method, RequestBuilder};
@@ -84,9 +84,6 @@ impl HttpClient {
// Construct the URL
let url = HttpClient::join_url(&s.endpoint, path);
// dbg!(&url);
// dbg!(&server_id);
// Retrieve the token for the server (token is optional)
let token = get_server_token(server_id).map(|t| t.access_token.clone());
@@ -99,6 +96,8 @@ impl HttpClient {
);
}
// dbg!(&server_id);
// dbg!(&url);
// dbg!(&headers);
Self::send_raw_request(method, &url, Some(headers), body).await

View File

@@ -56,32 +56,22 @@ pub async fn connect_to_server(
state: tauri::State<'_, WebSocketManager>,
app_handle: tauri::AppHandle,
) -> Result<(), String> {
dbg!("Connecting to server",id.as_str());
// Disconnect any existing connection first
disconnect(state.clone()).await?;
dbg!("Disconnected from previous server",id.as_str());
// Retrieve server details
let server = get_server_by_id(id.as_str())
.ok_or_else(|| format!("Server with ID {} not found", id))?;
let mut endpoint = convert_to_websocket(server.endpoint.as_str())?;
dbg!("Server endpoint",endpoint.as_str());
let endpoint = convert_to_websocket(server.endpoint.as_str())?;
// Retrieve the token for the server (token is optional)
let token = get_server_token(id.as_str()).map(|t| t.access_token.clone());
dbg!("Server token",token.as_ref().unwrap_or(&"".to_string()).as_str());
// Create the WebSocket request
let mut request = tokio_tungstenite::tungstenite::client::IntoClientRequest::into_client_request(
&endpoint
).map_err(|e| format!("Failed to create WebSocket request: {}", e))?;
dbg!("WebSocket request");
// Add necessary headers
request.headers_mut().insert("Connection", "Upgrade".parse().unwrap());
request.headers_mut().insert("Upgrade", "websocket".parse().unwrap());
@@ -91,20 +81,15 @@ pub async fn connect_to_server(
generate_key().parse().unwrap(),
);
dbg!("WebSocket headers",request.headers().iter().map(|(k, v)| format!("{}: {}", k.as_str(), v.to_str().unwrap())).collect::<Vec<String>>().join("\n"));
// If a token exists, add it to the headers
if let Some(token) = token {
request.headers_mut().insert("X-API-TOKEN", token.parse().unwrap());
}
dbg!("WebSocket headers with token",request.headers().iter().map(|(k, v)| format!("{}: {}", k.as_str(), v.to_str().unwrap())).collect::<Vec<String>>().join("\n"));
// Establish the WebSocket connection
dbg!(&request);
// dbg!(&request);
let (mut ws_remote, _) = connect_async(request).await
.map_err(|e| {
dbg!("WebSocket connection error",&e);
match e {
Error::ConnectionClosed => "WebSocket connection was closed".to_string(),
Error::Protocol(protocol_error) => format!("Protocol error: {}", protocol_error),
@@ -113,19 +98,12 @@ pub async fn connect_to_server(
}
})?;
dbg!("Connected to server1234",id.as_str());
// Create cancellation channel
let (cancel_tx, mut cancel_rx) = mpsc::channel(1);
dbg!("try to lock Connected to server",id.as_str());
// Store connection and cancellation sender
*state.ws_connection.lock().await = Some(ws_remote);
*state.cancel_tx.lock().await = Some(cancel_tx);
dbg!("locked Connected to server",id.as_str());
// Spawn listener task with cancellation
let app_handle_clone = app_handle.clone();
let connection_clone = state.ws_connection.clone();
@@ -133,8 +111,6 @@ pub async fn connect_to_server(
let mut connection = connection_clone.lock().await;
if let Some(ws) = connection.as_mut() {
loop {
dbg!("try to select Connected to server",id.as_str());
tokio::select! {
msg = ws.next() => {
match msg {
@@ -153,8 +129,6 @@ pub async fn connect_to_server(
}
});
dbg!("END Connected to server");
Ok(())
}

View File

@@ -138,7 +138,7 @@ fn _register_shortcut_upon_start(app: &App, shortcut: Shortcut) {
if window.is_visible().unwrap() {
window.hide().unwrap();
} else {
dbg!("showing window");
// dbg!("showing window");
move_window_to_active_monitor(&window);
window.set_visible_on_all_workspaces(true).unwrap();
window.set_always_on_top(true).unwrap();

View File

@@ -1,446 +1,439 @@
import { useState, useEffect, useCallback, useRef } from "react";
import {
RefreshCcw,
Globe,
PackageOpen,
GitFork,
CalendarSync,
Trash2,
Copy,
} from "lucide-react";
import { v4 as uuidv4 } from "uuid";
import { getCurrentWindow } from "@tauri-apps/api/window";
import {
onOpenUrl,
getCurrent as getCurrentDeepLinkUrls,
} 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";
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 endpoint = useAppStore((state) => state.endpoint);
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);
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);
});
};
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 {
useEffect(() => {
console.log("currentService", currentService);
setLoading(false);
}
},
[ssoRequestID, endpoint]
);
setRefreshLoading(false);
setError("");
setIsConnect(true);
}, [JSON.stringify(currentService)]);
const handleUrl = (url: string) => {
try {
const urlObject = new URL(url);
console.log("handle urlObject:", urlObject);
// TODO, 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");
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 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);
});
};
// 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])) {
handleUrl(urls[0]);
}
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");
}
})
.catch((err) => {
console.error("Failed to get initial URLs:", err);
setError("Failed to get initial URLs: " + err);
});
const unlisten = onOpenUrl((urls) => handleUrl(urls[0]));
setRefreshLoading(true);
return () => {
unlisten.then((fn) => fn());
document.removeEventListener("paste", handlePaste);
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);
});
};
}, [ssoRequestID]);
const LoginClick = useCallback(() => {
if (loading) return; // Prevent multiple clicks if already loading
const handleOAuthCallback = useCallback(
async (code: string | null, serverId: string | null) => {
if (!code) {
setError("No authorization code received");
return;
}
let requestID = uuidv4();
setSSORequestID(requestID);
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,
});
// Generate the login URL with the current appUid
const url = `${currentService?.auth_provider?.sso?.url}/?provider=${currentService?.id}&product=coco&request_id=${requestID}`;
if (serverId != null) {
refreshClick(serverId);
}
console.log("Open SSO link, requestID:", ssoRequestID, url);
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,]
);
// Open the URL in a browser
OpenURLWithBrowser(url);
const handleUrl = (url: string) => {
try {
const urlObject = new URL(url.trim());
console.log("handle urlObject:", urlObject);
// Start loading state
setLoading(true);
}, [ssoRequestID, loading, currentService]);
// 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");
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);
});
};
if (reqId != ssoRequestID) {
console.log("Request ID not matched, skip");
setError("Request ID not matched, skip");
return;
}
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 serverId = currentService?.id;
handleOAuthCallback(code, serverId);
} catch (err) {
console.error("Failed to parse URL:", err);
setError("Invalid URL format: " + err);
}
};
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">
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);
}
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);
});
};
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 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>
{!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>
<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}
<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 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 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>
</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}
/>
{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>
) : (
<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" />{" "}
{/* Lucide Copy Icon */}
</button>
</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>
<Connect setIsConnect={setIsConnect} onAddServer={add_coco_server}/>
)}
</div>
) : null}
{currentService?.profile ? (
<DataSourcesList server={currentService?.id} />
) : null}
</div>
) : (
<Connect setIsConnect={setIsConnect} onAddServer={add_coco_server} />
)}
</main>
</div>
);
</main>
</div>
);
}