mirror of
https://github.com/infinilabs/coco-app.git
synced 2026-08-29 10:09:30 +02:00
refactor: service info related components (#537)
* refactor: service info related components * docs: update notes * refactor: chat header service status
This commit is contained in:
@@ -57,6 +57,7 @@ Information about release notes of Coco Server is provided here.
|
||||
- style: search error styles #533
|
||||
- fix: server image loading failure #534
|
||||
- chore: skip register server that not logged in #536
|
||||
- refactor: service info related components #537
|
||||
|
||||
## 0.4.0 (2025-04-27)
|
||||
|
||||
|
||||
@@ -125,8 +125,6 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
|
||||
response = res;
|
||||
}
|
||||
|
||||
console.log("assistant_search", response);
|
||||
|
||||
let assistantList = response?.hits?.hits ?? [];
|
||||
|
||||
console.log("assistantList", assistantList);
|
||||
@@ -143,13 +141,13 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
|
||||
}
|
||||
}
|
||||
|
||||
console.log("state.allAssistants", state.allAssistants);
|
||||
//console.log("state.allAssistants", state.allAssistants);
|
||||
|
||||
const matched = state.allAssistants.find((item: any) => {
|
||||
return item._id === currentAssistant?._id;
|
||||
});
|
||||
|
||||
console.log("matched", matched);
|
||||
//console.log("matched", matched);
|
||||
|
||||
if (matched) {
|
||||
setCurrentAssistant(matched);
|
||||
@@ -318,7 +316,6 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
|
||||
placeholder={t("assistant.popover.search")}
|
||||
className="w-full h-8 px-2 bg-transparent border rounded-md dark:border-white/10"
|
||||
onChange={(event) => {
|
||||
console.log("onChange", event.target.value);
|
||||
setKeyword(event.target.value.trim());
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,7 @@ import HistoryIcon from "@/icons/History";
|
||||
import PinOffIcon from "@/icons/PinOff";
|
||||
import PinIcon from "@/icons/Pin";
|
||||
import WindowsFullIcon from "@/icons/WindowsFull";
|
||||
import { useAppStore, IServer } from "@/stores/appStore";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import type { Chat } from "@/types/chat";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import VisibleKey from "../Common/VisibleKey";
|
||||
@@ -13,6 +13,8 @@ import { useShortcutsStore } from "@/stores/shortcutsStore";
|
||||
import { HISTORY_PANEL_ID } from "@/constants";
|
||||
import { AssistantList } from "./AssistantList";
|
||||
import { ServerList } from "./ServerList";
|
||||
import { Server } from "@/types/server"
|
||||
|
||||
|
||||
interface ChatHeaderProps {
|
||||
onCreateNewChat: () => void;
|
||||
@@ -20,7 +22,7 @@ interface ChatHeaderProps {
|
||||
setIsSidebarOpen: () => void;
|
||||
isSidebarOpen: boolean;
|
||||
activeChat: Chat | undefined;
|
||||
reconnect: (server?: IServer) => void;
|
||||
reconnect: (server?: Server) => void;
|
||||
isLogin: boolean;
|
||||
setIsLogin: (isLogin: boolean) => void;
|
||||
isChatPage?: boolean;
|
||||
|
||||
@@ -3,16 +3,18 @@ import { Settings, RefreshCw, Check, Server } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||
import { useKeyPress } from "ahooks";
|
||||
import { isNil } from "lodash-es";
|
||||
|
||||
import logoImg from "@/assets/icon.svg";
|
||||
import ServerIcon from "@/icons/Server";
|
||||
import VisibleKey from "../Common/VisibleKey";
|
||||
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import { useAppStore, IServer } from "@/stores/appStore";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { useChatStore } from "@/stores/chatStore";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import { isNil } from "lodash-es";
|
||||
import { Server as IServer } from "@/types/server";
|
||||
import StatusIndicator from "@/components/Cloud/StatusIndicator";
|
||||
|
||||
interface ServerListProps {
|
||||
isLogin: boolean;
|
||||
@@ -215,17 +217,16 @@ export function ServerList({
|
||||
{server.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 truncate max-w-[200px]">
|
||||
AI Assistant: {server.assistantCount || 1}
|
||||
AI Assistant: {1}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<span
|
||||
className={`w-3 h-3 rounded-full ${
|
||||
server.health?.status
|
||||
? `bg-[${server.health?.status}]`
|
||||
: "bg-gray-400 dark:bg-gray-600"
|
||||
}`}
|
||||
<StatusIndicator
|
||||
enabled={server.enabled}
|
||||
public={server.public}
|
||||
hasProfile={!!server?.profile}
|
||||
status={server.health?.status}
|
||||
/>
|
||||
<div className="size-4 flex justify-end">
|
||||
{currentService?.id === server.id && (
|
||||
|
||||
@@ -77,7 +77,7 @@ const Splash = () => {
|
||||
});
|
||||
|
||||
const settingsAssistantList = useMemo(() => {
|
||||
console.log("assistantList", assistantList);
|
||||
//console.log("assistantList", assistantList);
|
||||
|
||||
return assistantList.filter((item) => {
|
||||
return settings?.display_assistants?.includes(item?._source?.id);
|
||||
|
||||
@@ -1,64 +1,32 @@
|
||||
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 { useTranslation } from "react-i18next";
|
||||
import clsx from "clsx";
|
||||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
import { emit } from "@tauri-apps/api/event";
|
||||
|
||||
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 "@/components/Settings/SettingsToggle";
|
||||
import Tooltip from "@/components/Common/Tooltip";
|
||||
import {
|
||||
list_coco_servers,
|
||||
add_coco_server,
|
||||
enable_server,
|
||||
disable_server,
|
||||
logout_coco_server,
|
||||
remove_coco_server,
|
||||
refresh_coco_server_info,
|
||||
handle_sso_callback,
|
||||
} from "@/commands";
|
||||
import ServiceInfo from "./ServiceInfo";
|
||||
import ServiceAuth from "./ServiceAuth";
|
||||
|
||||
export default function Cloud() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const SidebarRef = useRef<{ refreshData: () => void }>(null);
|
||||
|
||||
const errors = useAppStore((state) => state.errors);
|
||||
const addError = useAppStore((state) => state.addError);
|
||||
|
||||
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
|
||||
@@ -68,7 +36,6 @@ export default function Cloud() {
|
||||
|
||||
useEffect(() => {
|
||||
console.log("currentService", currentService);
|
||||
setLoading(false);
|
||||
setRefreshLoading(false);
|
||||
setIsConnect(true);
|
||||
}, [JSON.stringify(currentService)]);
|
||||
@@ -131,178 +98,30 @@ export default function Cloud() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleOAuthCallback = useCallback(
|
||||
async (code: string | null, serverId: string | null) => {
|
||||
if (!code || !serverId) {
|
||||
addError("No authorization code received");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("Handling OAuth callback:", { code, serverId });
|
||||
await 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,
|
||||
const refreshClick = useCallback(
|
||||
(id: string) => {
|
||||
setRefreshLoading(true);
|
||||
refresh_coco_server_info(id)
|
||||
.then((res: any) => {
|
||||
console.log("refresh_coco_server_info", id, res);
|
||||
fetchServers(false).then((r) => {
|
||||
console.log("fetchServers", r);
|
||||
});
|
||||
// update currentService
|
||||
setCurrentService(res);
|
||||
emit("login_or_logout", true);
|
||||
})
|
||||
.finally(() => {
|
||||
setRefreshLoading(false);
|
||||
});
|
||||
|
||||
if (serverId != null) {
|
||||
refreshClick(serverId);
|
||||
}
|
||||
|
||||
getCurrentWindow().setFocus();
|
||||
} catch (e) {
|
||||
console.error("Sign in failed:", e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[ssoRequestID]
|
||||
[fetchServers]
|
||||
);
|
||||
|
||||
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");
|
||||
addError("Request ID not matched, skip");
|
||||
return;
|
||||
}
|
||||
|
||||
const serverId = currentService?.id;
|
||||
handleOAuthCallback(code, serverId);
|
||||
} catch (err) {
|
||||
console.error("Failed to parse URL:", err);
|
||||
addError("Invalid URL format: " + err);
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch the initial deep link intent
|
||||
useEffect(() => {
|
||||
// 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);
|
||||
addError("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);
|
||||
refresh_coco_server_info(id)
|
||||
.then((res: any) => {
|
||||
console.log("refresh_coco_server_info", id, res);
|
||||
fetchServers(false).then((r) => {
|
||||
console.log("fetchServers", r);
|
||||
});
|
||||
// update currentService
|
||||
setCurrentService(res);
|
||||
emit("login_or_logout", true);
|
||||
})
|
||||
.finally(() => {
|
||||
setRefreshLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
function onAddServer() {
|
||||
setIsConnect(false);
|
||||
}
|
||||
|
||||
function onLogout(id: string) {
|
||||
console.log("onLogout", id);
|
||||
setRefreshLoading(true);
|
||||
logout_coco_server(id)
|
||||
.then((res: any) => {
|
||||
console.log("logout_coco_server", id, JSON.stringify(res));
|
||||
refreshClick(id);
|
||||
emit("login_or_logout", false);
|
||||
})
|
||||
.finally(() => {
|
||||
setRefreshLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
const removeServer = (id: string) => {
|
||||
remove_coco_server(id).then((res: any) => {
|
||||
console.log("remove_coco_server", id, JSON.stringify(res));
|
||||
fetchServers(true).then((r) => {
|
||||
console.log("fetchServers", r);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const enable_coco_server = useCallback(
|
||||
async (enabled: boolean) => {
|
||||
if (enabled) {
|
||||
await enable_server(currentService?.id);
|
||||
} else {
|
||||
await disable_server(currentService?.id);
|
||||
}
|
||||
|
||||
setCurrentService({ ...currentService, enabled });
|
||||
|
||||
await fetchServers(false);
|
||||
},
|
||||
[currentService?.id]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex bg-gray-50 dark:bg-gray-900">
|
||||
<Sidebar
|
||||
@@ -314,152 +133,16 @@ export default function Cloud() {
|
||||
<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"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.src = bannerImg;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Tooltip content={currentService?.endpoint}>
|
||||
<div className="flex items-center text-gray-900 dark:text-white font-medium cursor-pointer">
|
||||
{currentService?.name}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<SettingsToggle
|
||||
checked={currentService?.enabled}
|
||||
className={clsx({
|
||||
"bg-red-600 focus:ring-red-500": !currentService?.enabled,
|
||||
})}
|
||||
label={
|
||||
currentService?.enabled
|
||||
? t("cloud.enable_server")
|
||||
: t("cloud.disable_server")
|
||||
}
|
||||
onChange={enable_coco_server}
|
||||
/>
|
||||
<ServiceInfo
|
||||
refreshLoading={refreshLoading}
|
||||
refreshClick={refreshClick}
|
||||
fetchServers={fetchServers}
|
||||
/>
|
||||
|
||||
<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={() => removeServer(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}
|
||||
</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">
|
||||
{t("cloud.manualCopyLink")}
|
||||
</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}
|
||||
<ServiceAuth
|
||||
setRefreshLoading={setRefreshLoading}
|
||||
refreshClick={refreshClick}
|
||||
/>
|
||||
|
||||
{currentService?.profile ? (
|
||||
<DataSourcesList server={currentService?.id} />
|
||||
|
||||
253
src/components/Cloud/ServiceAuth.tsx
Normal file
253
src/components/Cloud/ServiceAuth.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
import { memo, useCallback, useEffect, useState } from "react";
|
||||
import { Copy } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { emit } from "@tauri-apps/api/event";
|
||||
import {
|
||||
getCurrent as getCurrentDeepLinkUrls,
|
||||
onOpenUrl,
|
||||
} from "@tauri-apps/plugin-deep-link";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
|
||||
import { UserProfile } from "./UserProfile";
|
||||
import { OpenURLWithBrowser } from "@/utils";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { logout_coco_server, handle_sso_callback } from "@/commands";
|
||||
|
||||
interface ServiceAuthProps {
|
||||
setRefreshLoading: (loading: boolean) => void;
|
||||
refreshClick: (id: string) => void;
|
||||
}
|
||||
|
||||
const ServiceAuth = memo(
|
||||
({ setRefreshLoading, refreshClick }: ServiceAuthProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const ssoRequestID = useAppStore((state) => state.ssoRequestID);
|
||||
const setSSORequestID = useAppStore((state) => state.setSSORequestID);
|
||||
|
||||
const addError = useAppStore((state) => state.addError);
|
||||
|
||||
const currentService = useConnectStore((state) => state.currentService);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
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 onLogout = useCallback(
|
||||
(id: string) => {
|
||||
console.log("onLogout", id);
|
||||
setRefreshLoading(true);
|
||||
logout_coco_server(id)
|
||||
.then((res: any) => {
|
||||
console.log("logout_coco_server", id, JSON.stringify(res));
|
||||
refreshClick(id);
|
||||
emit("login_or_logout", false);
|
||||
})
|
||||
.finally(() => {
|
||||
setRefreshLoading(false);
|
||||
});
|
||||
},
|
||||
[refreshClick]
|
||||
);
|
||||
|
||||
const handleOAuthCallback = useCallback(
|
||||
async (code: string | null, serverId: string | null) => {
|
||||
if (!code || !serverId) {
|
||||
addError("No authorization code received");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("Handling OAuth callback:", { code, serverId });
|
||||
await 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 (e) {
|
||||
console.error("Sign in failed:", e);
|
||||
} 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");
|
||||
addError("Request ID not matched, skip");
|
||||
return;
|
||||
}
|
||||
|
||||
const serverId = currentService?.id;
|
||||
handleOAuthCallback(code, serverId);
|
||||
} catch (err) {
|
||||
console.error("Failed to parse URL:", err);
|
||||
addError("Invalid URL format: " + err);
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch the initial deep link intent
|
||||
useEffect(() => {
|
||||
setLoading(false);
|
||||
// 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);
|
||||
addError("Failed to get initial URLs: " + err);
|
||||
});
|
||||
|
||||
const unlisten = onOpenUrl((urls) => handleUrl(urls[0]));
|
||||
|
||||
return () => {
|
||||
unlisten.then((fn) => fn());
|
||||
document.removeEventListener("paste", handlePaste);
|
||||
};
|
||||
}, [ssoRequestID]);
|
||||
|
||||
if (!currentService?.auth_provider?.sso?.url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<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 && <LoginButton LoginClick={LoginClick} />}
|
||||
|
||||
{/* Cancel Button and Copy URL button while loading */}
|
||||
{loading && (
|
||||
<LoadingState
|
||||
onCancel={() => setLoading(false)}
|
||||
onCopy={() => {
|
||||
navigator.clipboard.writeText(
|
||||
`${currentService?.auth_provider?.sso?.url}/?provider=${currentService?.id}&product=coco&request_id=${ssoRequestID}`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default ServiceAuth;
|
||||
|
||||
const LoginButton = memo(({ LoginClick }: { LoginClick: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<button
|
||||
className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors mb-3"
|
||||
onClick={LoginClick}
|
||||
aria-label={t("cloud.login")}
|
||||
>
|
||||
{t("cloud.login")}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
||||
const LoadingState = memo(
|
||||
({ onCancel, onCopy }: { onCancel: () => void; onCopy: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<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={onCancel}
|
||||
>
|
||||
{t("cloud.cancel")}
|
||||
</button>
|
||||
<button
|
||||
onClick={onCopy}
|
||||
className="text-xl text-blue-500 hover:text-blue-600"
|
||||
>
|
||||
<Copy className="inline mr-2" />{" "}
|
||||
</button>
|
||||
<div className="text-justify italic text-xs">
|
||||
{t("cloud.manualCopyLink")}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
26
src/components/Cloud/ServiceBanner.tsx
Normal file
26
src/components/Cloud/ServiceBanner.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { memo } from "react";
|
||||
|
||||
import bannerImg from "@/assets/images/coco-cloud-banner.jpeg";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
|
||||
interface ServiceBannerProps {}
|
||||
|
||||
const ServiceBanner = memo(({}: ServiceBannerProps) => {
|
||||
const currentService = useConnectStore((state) => state.currentService);
|
||||
|
||||
return (
|
||||
<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"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.src = bannerImg;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default ServiceBanner;
|
||||
104
src/components/Cloud/ServiceHeader.tsx
Normal file
104
src/components/Cloud/ServiceHeader.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { memo, useCallback } from "react";
|
||||
import { Globe, RefreshCcw, Trash2 } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import clsx from "clsx";
|
||||
|
||||
import Tooltip from "@/components/Common/Tooltip";
|
||||
import SettingsToggle from "@/components/Settings/SettingsToggle";
|
||||
import { OpenURLWithBrowser } from "@/utils";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import { enable_server, disable_server, remove_coco_server } from "@/commands";
|
||||
|
||||
interface ServiceHeaderProps {
|
||||
refreshLoading?: boolean;
|
||||
refreshClick: (id: string) => void;
|
||||
fetchServers: (force: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
const ServiceHeader = memo(
|
||||
({ refreshLoading, refreshClick, fetchServers }: ServiceHeaderProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const currentService = useConnectStore((state) => state.currentService);
|
||||
const setCurrentService = useConnectStore(
|
||||
(state) => state.setCurrentService
|
||||
);
|
||||
|
||||
const enable_coco_server = useCallback(
|
||||
async (enabled: boolean) => {
|
||||
if (enabled) {
|
||||
await enable_server(currentService?.id);
|
||||
} else {
|
||||
await disable_server(currentService?.id);
|
||||
}
|
||||
|
||||
setCurrentService({ ...currentService, enabled });
|
||||
|
||||
await fetchServers(false);
|
||||
},
|
||||
[currentService?.id]
|
||||
);
|
||||
|
||||
const removeServer = (id: string) => {
|
||||
remove_coco_server(id).then((res: any) => {
|
||||
console.log("remove_coco_server", id, JSON.stringify(res));
|
||||
fetchServers(true).then((r) => {
|
||||
console.log("fetchServers", r);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Tooltip content={currentService?.endpoint}>
|
||||
<div className="flex items-center text-gray-900 dark:text-white font-medium cursor-pointer">
|
||||
{currentService?.name}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<SettingsToggle
|
||||
checked={currentService?.enabled}
|
||||
className={clsx({
|
||||
"bg-red-600 focus:ring-red-500": !currentService?.enabled,
|
||||
})}
|
||||
label={
|
||||
currentService?.enabled
|
||||
? t("cloud.enable_server")
|
||||
: t("cloud.disable_server")
|
||||
}
|
||||
onChange={enable_coco_server}
|
||||
/>
|
||||
|
||||
<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={() => removeServer(currentService?.id)}
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5 text-[#ff4747]" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default ServiceHeader;
|
||||
31
src/components/Cloud/ServiceInfo.tsx
Normal file
31
src/components/Cloud/ServiceInfo.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { memo } from "react";
|
||||
|
||||
import ServiceBanner from "./ServiceBanner";
|
||||
import ServiceHeader from "./ServiceHeader";
|
||||
import ServiceMetadata from "./ServiceMetadata";
|
||||
|
||||
interface ServiceInfoProps {
|
||||
refreshLoading?: boolean;
|
||||
refreshClick: (id: string) => void;
|
||||
fetchServers: (force: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
const ServiceInfo = memo(
|
||||
({ refreshLoading, refreshClick, fetchServers }: ServiceInfoProps) => {
|
||||
return (
|
||||
<>
|
||||
<ServiceBanner />
|
||||
|
||||
<ServiceHeader
|
||||
refreshLoading={refreshLoading}
|
||||
refreshClick={refreshClick}
|
||||
fetchServers={fetchServers}
|
||||
/>
|
||||
|
||||
<ServiceMetadata />
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default ServiceInfo;
|
||||
33
src/components/Cloud/ServiceMetadata.tsx
Normal file
33
src/components/Cloud/ServiceMetadata.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { memo } from "react";
|
||||
import { PackageOpen, GitFork, CalendarSync } from "lucide-react";
|
||||
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
|
||||
interface ServiceMetadataProps {}
|
||||
|
||||
const ServiceMetadata = memo(({}: ServiceMetadataProps) => {
|
||||
const currentService = useConnectStore((state) => state.currentService);
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
});
|
||||
|
||||
export default ServiceMetadata;
|
||||
@@ -1,13 +1,20 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { forwardRef } from "react";
|
||||
import { forwardRef, useMemo, useCallback } from "react";
|
||||
import { Plus } from "lucide-react";
|
||||
|
||||
import cocoLogoImg from "@/assets/app-icon.png";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import { Server } from "@/types/server";
|
||||
import StatusIndicator from "./StatusIndicator";
|
||||
|
||||
interface SidebarProps {
|
||||
onAddServer: () => void;
|
||||
serverList: any[];
|
||||
serverList: Server[];
|
||||
}
|
||||
|
||||
interface ServerGroups {
|
||||
builtinServers: JSX.Element[];
|
||||
customServers: JSX.Element[];
|
||||
}
|
||||
|
||||
export const Sidebar = forwardRef<{ refreshData: () => void }, SidebarProps>(
|
||||
@@ -18,69 +25,84 @@ export const Sidebar = forwardRef<{ refreshData: () => void }, SidebarProps>(
|
||||
(state) => state.setCurrentService
|
||||
);
|
||||
|
||||
const onAddServerClick = () => {
|
||||
onAddServer();
|
||||
};
|
||||
const getServerItemClassName = useCallback((isSelected: boolean) => {
|
||||
return `flex cursor-pointer items-center space-x-2 px-3 py-2 bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 rounded-lg mb-2 whitespace-nowrap ${
|
||||
isSelected
|
||||
? "dark:bg-blue-900/20 dark:bg-blue-900 border border-[#0087ff]"
|
||||
: "bg-gray-50 dark:bg-gray-900 border border-[#e6e6e6] dark:border-gray-700"
|
||||
}`;
|
||||
}, []);
|
||||
|
||||
// Extracted server item rendering
|
||||
const renderServerItem = (item: any) => {
|
||||
return (
|
||||
<div
|
||||
key={item?.id}
|
||||
className={`flex cursor-pointer items-center space-x-2 px-3 py-2 bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 rounded-lg mb-2 whitespace-nowrap ${
|
||||
currentService?.id === item?.id
|
||||
? "dark:bg-blue-900/20 dark:bg-blue-900 border border-[#0087ff]"
|
||||
: "bg-gray-50 dark:bg-gray-900 border border-[#e6e6e6] dark:border-gray-700"
|
||||
}`}
|
||||
onClick={() => setCurrentService(item)}
|
||||
>
|
||||
<img
|
||||
src={item?.provider?.icon || cocoLogoImg}
|
||||
alt="LogoImg"
|
||||
className="w-5 h-5 flex-shrink-0"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.src = cocoLogoImg;
|
||||
}}
|
||||
/>
|
||||
<span className="font-medium truncate max-w-[140px]">{item?.name}</span>
|
||||
<div className="flex-1" />
|
||||
<button className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300">
|
||||
{item.health?.status ? (
|
||||
<div className={`w-3 h-3 rounded-full bg-[${item.health?.status}]`} />
|
||||
) : (
|
||||
<div className="w-3 h-3 rounded-full bg-gray-400 dark:bg-gray-600" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
const renderServerItem = useCallback(
|
||||
(item: Server) => {
|
||||
const isSelected = currentService?.id === item.id;
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className={getServerItemClassName(isSelected)}
|
||||
onClick={() => setCurrentService(item)}
|
||||
>
|
||||
<img
|
||||
src={item?.provider?.icon || cocoLogoImg}
|
||||
alt={`${item.name} logo`}
|
||||
className="w-5 h-5 flex-shrink-0"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.src = cocoLogoImg;
|
||||
}}
|
||||
/>
|
||||
<span className="font-medium truncate max-w-[140px]">
|
||||
{item.name}
|
||||
</span>
|
||||
<div className="flex-1" />
|
||||
<button className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300">
|
||||
<StatusIndicator
|
||||
enabled={item.enabled}
|
||||
public={item.public}
|
||||
hasProfile={!!item?.profile}
|
||||
status={item.health?.status}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[currentService]
|
||||
);
|
||||
|
||||
const { builtinServers, customServers } = useMemo(() => {
|
||||
const groups = serverList.reduce<ServerGroups>(
|
||||
(acc, item) => {
|
||||
const renderedItem = renderServerItem(item);
|
||||
if (item?.builtin) {
|
||||
acc.builtinServers.push(renderedItem);
|
||||
} else {
|
||||
acc.customServers.push(renderedItem);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{ builtinServers: [], customServers: [] }
|
||||
);
|
||||
};
|
||||
return groups;
|
||||
}, [serverList, renderServerItem]);
|
||||
|
||||
return (
|
||||
<div className="w-64 min-h-[550px] border-r border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
|
||||
<div className="p-4 py-8">
|
||||
{/* Render Built-in Servers */}
|
||||
<div>
|
||||
{serverList
|
||||
.filter((item) => item?.builtin)
|
||||
.map((item) => renderServerItem(item))}
|
||||
</div>
|
||||
{builtinServers}
|
||||
|
||||
<div className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2 mt-6">
|
||||
{t("cloud.sidebar.yourServers")}
|
||||
</div>
|
||||
|
||||
{/* Render Non-Built-in Servers */}
|
||||
<div>
|
||||
{serverList
|
||||
.filter((item) => !item?.builtin)
|
||||
.map((item) => renderServerItem(item))}
|
||||
</div>
|
||||
{customServers}
|
||||
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
className="w-full flex items-center justify-center p-2 border-2 border-dashed border-gray-200 dark:border-gray-700 rounded-lg text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600"
|
||||
onClick={onAddServerClick}
|
||||
onClick={onAddServer}
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
43
src/components/Cloud/StatusIndicator.tsx
Normal file
43
src/components/Cloud/StatusIndicator.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useMemo, memo } from "react";
|
||||
|
||||
import { Status } from "@/types/server";
|
||||
|
||||
type StatusIndicatorProps = {
|
||||
enabled: boolean;
|
||||
public: boolean;
|
||||
hasProfile: boolean;
|
||||
status?: Status;
|
||||
};
|
||||
|
||||
const StatusIndicator = memo(
|
||||
({ enabled, public: isPublic, hasProfile, status }: StatusIndicatorProps) => {
|
||||
// If service is enabled AND (public OR (private AND logged in)) AND service is available
|
||||
// Otherwise show gray status
|
||||
const isActive =
|
||||
enabled && (isPublic || (!isPublic && hasProfile)) && status;
|
||||
|
||||
const statusColorClass = useMemo(() => {
|
||||
if (!isActive) return "bg-gray-400 dark:bg-gray-600";
|
||||
switch (status) {
|
||||
case "green":
|
||||
return "bg-green-500";
|
||||
case "yellow":
|
||||
return "bg-yellow-500";
|
||||
case "red":
|
||||
return "bg-red-500";
|
||||
default:
|
||||
return "bg-gray-400 dark:bg-gray-600";
|
||||
}
|
||||
}, [isActive, status]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`w-3 h-3 rounded-full ${statusColorClass}`}
|
||||
role="status"
|
||||
aria-label={isActive ? "active" : "inactive"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default StatusIndicator;
|
||||
@@ -1,16 +1,6 @@
|
||||
import { User, LogOut } from "lucide-react";
|
||||
|
||||
interface UserPreferences {
|
||||
theme: "dark" | "light";
|
||||
language: string;
|
||||
}
|
||||
interface UserInfo {
|
||||
name: string;
|
||||
email: string;
|
||||
avatar?: string;
|
||||
roles: string[]; // ["admin", "editor"]
|
||||
preferences: UserPreferences;
|
||||
}
|
||||
import { UserProfile as UserInfo } from "@/types/server";
|
||||
|
||||
interface UserProfileProps {
|
||||
server: string; //server's id
|
||||
|
||||
@@ -21,7 +21,7 @@ import { useCreation } from "ahooks";
|
||||
import SettingsItem from "./SettingsItem";
|
||||
import SettingsToggle from "./SettingsToggle";
|
||||
import { ShortcutItem } from "./ShortcutItem";
|
||||
import { Shortcut } from "./shortcut";
|
||||
import { Shortcut } from "../../types/shortcut";
|
||||
import { useShortcutEditor } from "@/hooks/useShortcutEditor";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { AppTheme } from "@/types/index";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
|
||||
import { Shortcut } from "@/components/Settings/shortcut";
|
||||
import { Shortcut } from "@/types/shortcut";
|
||||
import { normalizeKey, isModifierKey, sortKeys } from "@/utils/keyboardUtils";
|
||||
|
||||
const RESERVED_SHORTCUTS = [
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useCallback, useRef } from "react";
|
||||
import { useWebSocket as useWebSocketAHook } from "ahooks";
|
||||
|
||||
import { useAppStore, IServer } from "@/stores/appStore";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import { Server } from "@/types/server";
|
||||
|
||||
enum ReadyState {
|
||||
Connecting = 0,
|
||||
@@ -15,7 +16,7 @@ interface WebSocketProps {
|
||||
clientId: string;
|
||||
connected: boolean;
|
||||
setConnected: (connected: boolean) => void;
|
||||
currentService: IServer | null;
|
||||
currentService: Server | null;
|
||||
dealMsgRef: React.MutableRefObject<((msg: string) => void) | null>;
|
||||
onWebsocketSessionId?: (sessionId: string) => void;
|
||||
}
|
||||
@@ -101,7 +102,7 @@ export default function useWebSocket({
|
||||
// 2. If not connected or disconnected, input box has a connect button, clicking it will connect to WebSocket
|
||||
// src/components/Search/InputBox.tsx
|
||||
const reconnect = useCallback(
|
||||
async (server?: IServer) => {
|
||||
async (server?: Server) => {
|
||||
if (isTauri) {
|
||||
const targetServer = server || currentService;
|
||||
if (!targetServer?.id) return;
|
||||
|
||||
@@ -6,26 +6,6 @@ import platformAdapter from "@/utils/platformAdapter";
|
||||
|
||||
const ENDPOINT_CHANGE_EVENT = "endpoint-changed";
|
||||
|
||||
export interface IServer {
|
||||
id: string;
|
||||
name: string;
|
||||
endpoint: string;
|
||||
provider: {
|
||||
icon: string;
|
||||
};
|
||||
enabled: boolean;
|
||||
public: boolean;
|
||||
profile?: any;
|
||||
available?: boolean;
|
||||
health?: {
|
||||
status: string;
|
||||
};
|
||||
assistantCount?: number;
|
||||
minimal_client_version?: {
|
||||
number: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface ErrorMessage {
|
||||
id: string;
|
||||
type: "error" | "warning" | "info";
|
||||
|
||||
@@ -3,6 +3,7 @@ import { persist, subscribeWithSelector } from "zustand/middleware";
|
||||
import { produce } from "immer";
|
||||
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import { Server } from "@/types/server"
|
||||
|
||||
const CONNECTOR_CHANGE_EVENT = "connector_data_change";
|
||||
const DATASOURCE_CHANGE_EVENT = "datasourceData_change";
|
||||
@@ -12,10 +13,10 @@ type keyArrayObject = {
|
||||
};
|
||||
|
||||
export type IConnectStore = {
|
||||
serverList: any[];
|
||||
setServerList: (servers: []) => void;
|
||||
currentService: any;
|
||||
setCurrentService: (service: any) => void;
|
||||
serverList: Server[];
|
||||
setServerList: (servers: Server[]) => void;
|
||||
currentService: Server;
|
||||
setCurrentService: (service: Server) => void;
|
||||
connector_data: keyArrayObject;
|
||||
setConnectorData: (connector_data: any[], key: string) => void;
|
||||
datasourceData: keyArrayObject;
|
||||
@@ -41,7 +42,7 @@ export const useConnectStore = create<IConnectStore>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
serverList: [],
|
||||
setServerList: (serverList: []) => {
|
||||
setServerList: (serverList: Server[]) => {
|
||||
console.log("set serverList:", serverList);
|
||||
set(
|
||||
produce((draft) => {
|
||||
@@ -49,7 +50,35 @@ export const useConnectStore = create<IConnectStore>()(
|
||||
})
|
||||
);
|
||||
},
|
||||
currentService: "default_coco_server",
|
||||
// ... existing code ...
|
||||
currentService: {
|
||||
id: "default_coco_server",
|
||||
builtin: true,
|
||||
enabled: true,
|
||||
name: "Coco Cloud",
|
||||
endpoint: "https://coco.infini.cloud",
|
||||
provider: {
|
||||
name: "INFINI Labs",
|
||||
icon: "https://coco.infini.cloud/icon.png",
|
||||
website: "http://infinilabs.com",
|
||||
eula: "http://infinilabs.com/eula.txt",
|
||||
privacy_policy: "http://infinilabs.com/privacy_policy.txt",
|
||||
banner: "https://coco.infini.cloud/banner.jpg",
|
||||
description: "Coco AI Server - Search, Connect, Collaborate, AI-powered enterprise search, all in one space."
|
||||
},
|
||||
version: {
|
||||
number: "1.0.0_SNAPSHOT"
|
||||
},
|
||||
public: false,
|
||||
available: true,
|
||||
auth_provider: {
|
||||
sso: {
|
||||
url: "https://coco.infini.cloud/sso/login/"
|
||||
}
|
||||
},
|
||||
priority: 0
|
||||
},
|
||||
// ... existing code ...
|
||||
setCurrentService: (server: any) => {
|
||||
console.log("set default server:", server);
|
||||
set(
|
||||
|
||||
65
src/types/server.ts
Normal file
65
src/types/server.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
interface Provider {
|
||||
name: string;
|
||||
icon?: string;
|
||||
banner?: string;
|
||||
description?: string;
|
||||
eula?: string;
|
||||
privacy_policy?: string;
|
||||
website?: string;
|
||||
auth_provider?: {
|
||||
sso: {
|
||||
url: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface Version {
|
||||
number: string;
|
||||
}
|
||||
|
||||
interface MinimalClientVersion {
|
||||
number: string;
|
||||
}
|
||||
|
||||
export type Status = 'green' | 'yellow' | 'red';
|
||||
|
||||
interface Health {
|
||||
services?: Record<string, Status>;
|
||||
status: Status;
|
||||
}
|
||||
|
||||
interface Preferences {
|
||||
theme?: string;
|
||||
language?: string;
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
avatar?: string;
|
||||
preferences?: Preferences;
|
||||
}
|
||||
|
||||
export interface Server {
|
||||
id: string;
|
||||
builtin: boolean;
|
||||
name: string;
|
||||
endpoint: string;
|
||||
provider: Provider;
|
||||
version: Version;
|
||||
minimal_client_version?: MinimalClientVersion;
|
||||
updated?: string;
|
||||
enabled: boolean;
|
||||
public: boolean;
|
||||
available: boolean;
|
||||
health?: Health;
|
||||
profile?: UserProfile;
|
||||
auth_provider: {
|
||||
sso: {
|
||||
url: string;
|
||||
};
|
||||
};
|
||||
priority: number;
|
||||
|
||||
}
|
||||
@@ -27,7 +27,7 @@ export async function copyToClipboard(text: string) {
|
||||
document.execCommand("copy");
|
||||
console.info("Copy Success");
|
||||
} catch (error) {
|
||||
console.info("Copy Failed");
|
||||
console.error("Copy Failed");
|
||||
}
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
@@ -68,7 +68,7 @@ export const IsTauri = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const OpenURLWithBrowser = async (url: string) => {
|
||||
export const OpenURLWithBrowser = async (url?: string) => {
|
||||
if (!url) return;
|
||||
if (IsTauri()) {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user