✨ feat: Support network testing when adding an account, optimize error tip.
This commit is contained in:
parent
b2042ecdee
commit
ca08df92fd
12 changed files with 170 additions and 58 deletions
|
|
@ -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()));
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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}`}>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
),
|
||||
});
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue