feat: Support network testing when adding an account, optimize error tip.

This commit is contained in:
jarvis2f 2025-01-21 14:09:42 +08:00
parent b2042ecdee
commit ca08df92fd
12 changed files with 170 additions and 58 deletions

View file

@ -144,6 +144,7 @@ public class HttpVerticle extends AbstractVerticle {
router.post("/telegrams/change").handler(this::handleTelegramChange);
router.post("/telegram/:telegramId/toggle-proxy").handler(this::handleTelegramToggleProxy);
router.get("/telegram/:telegramId/ping").handler(this::handleTelegramPing);
router.get("/telegram/:telegramId/test-network").handler(this::handleTelegramTestNetwork);
router.get("/file/preview").handler(this::handleFilePreview);
router.post("/file/start-download").handler(this::handleFileStartDownload);
@ -493,6 +494,21 @@ public class HttpVerticle extends AbstractVerticle {
);
}
private void handleTelegramTestNetwork(RoutingContext ctx) {
String telegramId = ctx.pathParam("telegramId");
if (StrUtil.isBlank(telegramId)) {
ctx.fail(400);
return;
}
getTelegramVerticle(telegramId)
.ifPresentOrElse(telegramVerticle ->
telegramVerticle.client.execute(new TdApi.TestNetwork())
.onComplete(r ->
ctx.json(JsonObject.of("success", r.succeeded()))),
() -> ctx.fail(404)
);
}
private void handleTelegramApiMethods(RoutingContext ctx) {
Map<String, Class<TdApi.Function<?>>> functions = TdApiHelp.getFunctions();
ctx.json(JsonObject.of("methods", functions.keySet()));

View file

@ -8,7 +8,6 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { request } from "@/lib/api";
import Link from "next/link";
import TGDuck16HeyOut from "@/components/animations/tg-duck16_hey_out.json";
import dynamic from "next/dynamic";
@ -27,7 +26,6 @@ const Lottie = dynamic(() => import("lottie-react"), { ssr: false });
export default function About() {
const { data: apiData, error: apiError } = useSWR<VersionData, Error>(
"/version",
request,
);
const { data: githubData, error: githubError } = useSWR<
GitHubReleaseData,

View file

@ -38,7 +38,7 @@ export function AccountDialog({
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent
aria-describedby={undefined}
className="h-full w-full pb-16 md:h-auto md:min-h-40 md:min-w-[550px]"
className="h-full w-full md:h-auto md:min-h-40 md:min-w-[550px]"
>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
@ -50,20 +50,22 @@ export function AccountDialog({
)}
</DialogTitle>
</DialogHeader>
<div className="relative h-full w-full">
<div className="flex flex-col">
<AccountCreator
isAdd={isAdd}
proxyName={proxyName}
onCreated={setNewAccountId}
onLoginSuccess={() => setOpen(false)}
/>
<ProxysDialog
telegramId={newAccountId}
proxyName={proxyName}
onProxyNameChange={setProxyName}
enableSelect={true}
className="absolute bottom-1 right-1 md:-bottom-14 md:-right-4"
/>
<div className="flex justify-end">
<ProxysDialog
telegramId={account ? account.id : newAccountId}
proxyName={proxyName}
onProxyNameChange={setProxyName}
enableSelect={true}
className="mt-3"
/>
</div>
</div>
</DialogContent>
</Dialog>

View file

@ -59,7 +59,7 @@ export default function DebugTelegramMethod() {
const { data: methodData, isLoading: isMethodsLoading } = useSWR<
TelegramMethodData,
Error
>("/telegram/api/methods", request, {
>("/telegram/api/methods", {
// 添加缓存以提升性能
revalidateOnFocus: false,
revalidateOnReconnect: false,

View file

@ -6,7 +6,6 @@ import { AccountDialog } from "@/components/account-dialog";
import React from "react";
import { Button } from "@/components/ui/button";
import { BorderBeam } from "@/components/ui/border-beam";
import ProxysDialog from "@/components/proxys-dialog";
interface EmptyStateProps {
isLoadingAccount?: boolean;

View file

@ -20,7 +20,6 @@ import {
XAxis,
YAxis,
} from "recharts";
import { request } from "@/lib/api";
import prettyBytes from "pretty-bytes";
// Type definitions
@ -92,7 +91,6 @@ const TelegramStats: React.FC<TelegramStatsProps> = ({ telegramId }) => {
const { data, error, isLoading } = useSWR<ApiResponse, Error>(
`/telegram/${telegramId}/download-statistics?type=phase&timeRange=${timeRange}`,
request,
);
if (error) {

View file

@ -19,7 +19,7 @@ import {
Upload,
Video,
} from "lucide-react";
import { request, telegramApi, type TelegramApiArg } from "@/lib/api";
import { telegramApi, type TelegramApiArg } from "@/lib/api";
import { formatDistanceToNow } from "date-fns";
import { Button } from "@/components/ui/button";
import useSWRMutation from "swr/mutation";
@ -60,7 +60,6 @@ const FileStatistics: React.FC<FileStatisticsProps> = ({ telegramId }) => {
// Use SWR for data fetching and caching
const { data, error, mutate } = useSWR<StatisticsData, Error>(
`/telegram/${telegramId}/download-statistics`,
request,
);
const { trigger: triggerReset, isMutating: isResetMutating } = useSWRMutation<
@ -248,8 +247,7 @@ const FileStatistics: React.FC<FileStatisticsProps> = ({ telegramId }) => {
{avgStatFields.map((stat, index) => (
<div
key={index}
className="flex flex-col space-y-3 rounded-xl border border-gray-100 p-4 transition-colors hover:border-gray-200 dark:border-gray-700
dark:hover:border-gray-600"
className="flex flex-col space-y-3 rounded-xl border border-gray-100 p-4 transition-colors hover:border-gray-200 dark:border-gray-700 dark:hover:border-gray-600"
>
<div className="flex items-center space-x-2">
<div className={`rounded-lg p-2 ${stat.bgColor}`}>

View file

@ -32,7 +32,7 @@ export default function ProxyPing({ accountId }: { accountId: string }) {
{isLoading && (
<div className="h-6 w-16 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
)}
{!isLoading && error && <span>Connection error</span>}
{!isLoading && error && <span>Failed</span>}
{!isLoading && data && (
<div className="flex h-6 items-center space-x-2">
<span className="text-sm">

View file

@ -8,8 +8,10 @@ import { Button } from "@/components/ui/button";
import { EarthLock } from "lucide-react";
import Proxys, { type ProxysProps } from "@/components/proxys";
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
import React from "react";
import React, { useMemo } from "react";
import { cn } from "@/lib/utils";
import useSWR from "swr";
import { TooltipWrapper } from "@/components/ui/tooltip";
type ProxysDialogProps = {
className?: string;
@ -17,21 +19,56 @@ type ProxysDialogProps = {
export default function ProxysDialog({
className,
telegramId,
...proxyProps
}: ProxysDialogProps) {
const { data, isLoading } = useSWR<{
success: boolean;
}>(telegramId ? `/telegram/${telegramId}/test-network` : undefined);
const triggerTooltipContent = useMemo(() => {
if (!telegramId) {
return "You can add proxys for telegram connection.";
}
return (
<>
{isLoading && "Testing network..."}
{!isLoading && data?.success && "Successfully connect to telegram"}
{!isLoading && !data?.success && "Failed to connect to telegram"}
</>
);
}, [data, isLoading, telegramId]);
const triggerClassName = telegramId
? cn(
isLoading &&
"animate-pulse border-4 border-blue-400 text-blue-400 dark:border-blue-500 dark:text-blue-500",
!isLoading &&
data?.success &&
"border-4 border-green-400 text-green-400 dark:border-green-500 dark:text-green-500",
!isLoading &&
!data?.success &&
"border-4 border-red-400 text-red-400 dark:border-red-500 dark:text-red-500",
)
: "";
return (
<Dialog>
<DialogTrigger asChild>
<Button
variant="ghost"
className={cn(
"h-auto rounded-full bg-primary px-2 text-accent hover:bg-primary/90 hover:text-accent",
className,
)}
>
<EarthLock className="h-4 w-4" />
</Button>
</DialogTrigger>
<TooltipWrapper content={triggerTooltipContent}>
<DialogTrigger asChild>
<Button
variant="ghost"
size="icon"
className={cn(
"rounded-full bg-primary px-2 text-accent hover:bg-primary/90 hover:text-accent",
triggerClassName,
className,
)}
>
<EarthLock className="h-4 w-4" />
</Button>
</DialogTrigger>
</TooltipWrapper>
<DialogContent
className="md:max-w-2/3 h-full w-full max-w-full md:h-3/4 md:w-2/3"
aria-describedby={undefined}
@ -40,7 +77,7 @@ export default function ProxysDialog({
<DialogTitle>Proxys</DialogTitle>
</VisuallyHidden>
<div className="mt-3">
<Proxys {...proxyProps} />
<Proxys telegramId={telegramId} {...proxyProps} />
</div>
</DialogContent>
</Dialog>

View file

@ -2,7 +2,7 @@
import { SWRConfig } from "swr";
import React from "react";
import { useToast } from "@/hooks/use-toast";
import { request } from "@/lib/api";
import { request, RequestParsedError } from "@/lib/api";
export const SWRProvider = ({ children }: { children: React.ReactNode }) => {
const { toast } = useToast();
@ -13,8 +13,36 @@ export const SWRProvider = ({ children }: { children: React.ReactNode }) => {
refreshInterval: 0,
errorRetryCount: 1,
fetcher: request,
onError: (err: Error) => {
toast({ title: "Error", description: err.message });
onError: (err: Error, key: string) => {
let message;
if (err instanceof RequestParsedError) {
const responseText = err.responseText;
if (/<\/?[a-z][\s\S]*>/i.test(responseText)) {
message = (
<div dangerouslySetInnerHTML={{ __html: responseText }}></div>
);
} else {
message = responseText;
}
} else {
message = err.message;
}
toast({
description: (
<div className="w-full flex flex-col space-y-2 overflow-hidden">
<div className="flex items-center space-x-1 text-nowrap">
<span className="text-xs text-muted-foreground">
Request Failed
</span>
<p className="rounded-md bg-gray-100 p-1 text-xs text-muted-foreground dark:bg-gray-800">
{key}
</p>
</div>
<div className="line-clamp-3 text-wrap">{message}</div>
</div>
),
});
},
}}
>

View file

@ -29,4 +29,27 @@ const TooltipContent = React.forwardRef<
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
const TooltipWrapper = ({
content,
children,
}: {
content: React.ReactNode;
children: React.ReactNode;
}) => {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>{children}</TooltipTrigger>
<TooltipContent>{content}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
};
export {
Tooltip,
TooltipTrigger,
TooltipContent,
TooltipProvider,
TooltipWrapper,
};

View file

@ -26,33 +26,46 @@ export function getWsUrl(): string {
}
/* eslint-disable */
export async function request(
export async function request<T = any>(
api: string,
requestInit?: RequestInit,
): Promise<any> {
return fetch(`${getApiUrl()}${api}`, {
credentials: "include",
): Promise<T> {
const defaultHeaders = {
'Content-Type': 'application/json',
};
const response = await fetch(`${getApiUrl()}${api}`, {
credentials: 'include',
headers: {
"Content-Type": "application/json",
...defaultHeaders,
...requestInit?.headers,
},
...requestInit,
}).then(async (res) => {
let responseText = "Failed to fetch";
try {
responseText = await res.text();
if (!responseText || responseText === "") {
return;
}
const data = JSON.parse(responseText);
if (res.ok) {
return data;
}
throw new Error(data.error ?? responseText);
} catch (err: Error | any) {
toast({ title: "Error", description: err.message });
throw err;
}
});
const responseText = await response.text();
if (!responseText) {
return undefined as T;
}
let data;
try {
data = JSON.parse(responseText);
} catch (e) {
throw new RequestParsedError(responseText);
}
if (!response.ok) {
throw new Error(data.error ?? `Request failed with status ${response.status}`);
}
return data as T;
}
export class RequestParsedError extends Error {
responseText: string;
constructor(responseText: string) {
super('Parse JSON Error');
this.responseText = responseText;
}
}
export function localStorageProvider() {