✨ 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("/telegrams/change").handler(this::handleTelegramChange);
|
||||||
router.post("/telegram/:telegramId/toggle-proxy").handler(this::handleTelegramToggleProxy);
|
router.post("/telegram/:telegramId/toggle-proxy").handler(this::handleTelegramToggleProxy);
|
||||||
router.get("/telegram/:telegramId/ping").handler(this::handleTelegramPing);
|
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.get("/file/preview").handler(this::handleFilePreview);
|
||||||
router.post("/file/start-download").handler(this::handleFileStartDownload);
|
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) {
|
private void handleTelegramApiMethods(RoutingContext ctx) {
|
||||||
Map<String, Class<TdApi.Function<?>>> functions = TdApiHelp.getFunctions();
|
Map<String, Class<TdApi.Function<?>>> functions = TdApiHelp.getFunctions();
|
||||||
ctx.json(JsonObject.of("methods", functions.keySet()));
|
ctx.json(JsonObject.of("methods", functions.keySet()));
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import {
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { request } from "@/lib/api";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import TGDuck16HeyOut from "@/components/animations/tg-duck16_hey_out.json";
|
import TGDuck16HeyOut from "@/components/animations/tg-duck16_hey_out.json";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
|
@ -27,7 +26,6 @@ const Lottie = dynamic(() => import("lottie-react"), { ssr: false });
|
||||||
export default function About() {
|
export default function About() {
|
||||||
const { data: apiData, error: apiError } = useSWR<VersionData, Error>(
|
const { data: apiData, error: apiError } = useSWR<VersionData, Error>(
|
||||||
"/version",
|
"/version",
|
||||||
request,
|
|
||||||
);
|
);
|
||||||
const { data: githubData, error: githubError } = useSWR<
|
const { data: githubData, error: githubError } = useSWR<
|
||||||
GitHubReleaseData,
|
GitHubReleaseData,
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ export function AccountDialog({
|
||||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
aria-describedby={undefined}
|
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>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
|
@ -50,20 +50,22 @@ export function AccountDialog({
|
||||||
)}
|
)}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="relative h-full w-full">
|
<div className="flex flex-col">
|
||||||
<AccountCreator
|
<AccountCreator
|
||||||
isAdd={isAdd}
|
isAdd={isAdd}
|
||||||
proxyName={proxyName}
|
proxyName={proxyName}
|
||||||
onCreated={setNewAccountId}
|
onCreated={setNewAccountId}
|
||||||
onLoginSuccess={() => setOpen(false)}
|
onLoginSuccess={() => setOpen(false)}
|
||||||
/>
|
/>
|
||||||
<ProxysDialog
|
<div className="flex justify-end">
|
||||||
telegramId={newAccountId}
|
<ProxysDialog
|
||||||
proxyName={proxyName}
|
telegramId={account ? account.id : newAccountId}
|
||||||
onProxyNameChange={setProxyName}
|
proxyName={proxyName}
|
||||||
enableSelect={true}
|
onProxyNameChange={setProxyName}
|
||||||
className="absolute bottom-1 right-1 md:-bottom-14 md:-right-4"
|
enableSelect={true}
|
||||||
/>
|
className="mt-3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ export default function DebugTelegramMethod() {
|
||||||
const { data: methodData, isLoading: isMethodsLoading } = useSWR<
|
const { data: methodData, isLoading: isMethodsLoading } = useSWR<
|
||||||
TelegramMethodData,
|
TelegramMethodData,
|
||||||
Error
|
Error
|
||||||
>("/telegram/api/methods", request, {
|
>("/telegram/api/methods", {
|
||||||
// 添加缓存以提升性能
|
// 添加缓存以提升性能
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
revalidateOnReconnect: false,
|
revalidateOnReconnect: false,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import { AccountDialog } from "@/components/account-dialog";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { BorderBeam } from "@/components/ui/border-beam";
|
import { BorderBeam } from "@/components/ui/border-beam";
|
||||||
import ProxysDialog from "@/components/proxys-dialog";
|
|
||||||
|
|
||||||
interface EmptyStateProps {
|
interface EmptyStateProps {
|
||||||
isLoadingAccount?: boolean;
|
isLoadingAccount?: boolean;
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ import {
|
||||||
XAxis,
|
XAxis,
|
||||||
YAxis,
|
YAxis,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
import { request } from "@/lib/api";
|
|
||||||
import prettyBytes from "pretty-bytes";
|
import prettyBytes from "pretty-bytes";
|
||||||
|
|
||||||
// Type definitions
|
// Type definitions
|
||||||
|
|
@ -92,7 +91,6 @@ const TelegramStats: React.FC<TelegramStatsProps> = ({ telegramId }) => {
|
||||||
|
|
||||||
const { data, error, isLoading } = useSWR<ApiResponse, Error>(
|
const { data, error, isLoading } = useSWR<ApiResponse, Error>(
|
||||||
`/telegram/${telegramId}/download-statistics?type=phase&timeRange=${timeRange}`,
|
`/telegram/${telegramId}/download-statistics?type=phase&timeRange=${timeRange}`,
|
||||||
request,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import {
|
||||||
Upload,
|
Upload,
|
||||||
Video,
|
Video,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { request, telegramApi, type TelegramApiArg } from "@/lib/api";
|
import { telegramApi, type TelegramApiArg } from "@/lib/api";
|
||||||
import { formatDistanceToNow } from "date-fns";
|
import { formatDistanceToNow } from "date-fns";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import useSWRMutation from "swr/mutation";
|
import useSWRMutation from "swr/mutation";
|
||||||
|
|
@ -60,7 +60,6 @@ const FileStatistics: React.FC<FileStatisticsProps> = ({ telegramId }) => {
|
||||||
// Use SWR for data fetching and caching
|
// Use SWR for data fetching and caching
|
||||||
const { data, error, mutate } = useSWR<StatisticsData, Error>(
|
const { data, error, mutate } = useSWR<StatisticsData, Error>(
|
||||||
`/telegram/${telegramId}/download-statistics`,
|
`/telegram/${telegramId}/download-statistics`,
|
||||||
request,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const { trigger: triggerReset, isMutating: isResetMutating } = useSWRMutation<
|
const { trigger: triggerReset, isMutating: isResetMutating } = useSWRMutation<
|
||||||
|
|
@ -248,8 +247,7 @@ const FileStatistics: React.FC<FileStatisticsProps> = ({ telegramId }) => {
|
||||||
{avgStatFields.map((stat, index) => (
|
{avgStatFields.map((stat, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
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
|
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"
|
||||||
dark:hover:border-gray-600"
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div className={`rounded-lg p-2 ${stat.bgColor}`}>
|
<div className={`rounded-lg p-2 ${stat.bgColor}`}>
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export default function ProxyPing({ accountId }: { accountId: string }) {
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="h-6 w-16 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
|
<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 && (
|
{!isLoading && data && (
|
||||||
<div className="flex h-6 items-center space-x-2">
|
<div className="flex h-6 items-center space-x-2">
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,10 @@ import { Button } from "@/components/ui/button";
|
||||||
import { EarthLock } from "lucide-react";
|
import { EarthLock } from "lucide-react";
|
||||||
import Proxys, { type ProxysProps } from "@/components/proxys";
|
import Proxys, { type ProxysProps } from "@/components/proxys";
|
||||||
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
||||||
import React from "react";
|
import React, { useMemo } from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { TooltipWrapper } from "@/components/ui/tooltip";
|
||||||
|
|
||||||
type ProxysDialogProps = {
|
type ProxysDialogProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|
@ -17,21 +19,56 @@ type ProxysDialogProps = {
|
||||||
|
|
||||||
export default function ProxysDialog({
|
export default function ProxysDialog({
|
||||||
className,
|
className,
|
||||||
|
telegramId,
|
||||||
...proxyProps
|
...proxyProps
|
||||||
}: ProxysDialogProps) {
|
}: 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 (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<TooltipWrapper content={triggerTooltipContent}>
|
||||||
<Button
|
<DialogTrigger asChild>
|
||||||
variant="ghost"
|
<Button
|
||||||
className={cn(
|
variant="ghost"
|
||||||
"h-auto rounded-full bg-primary px-2 text-accent hover:bg-primary/90 hover:text-accent",
|
size="icon"
|
||||||
className,
|
className={cn(
|
||||||
)}
|
"rounded-full bg-primary px-2 text-accent hover:bg-primary/90 hover:text-accent",
|
||||||
>
|
triggerClassName,
|
||||||
<EarthLock className="h-4 w-4" />
|
className,
|
||||||
</Button>
|
)}
|
||||||
</DialogTrigger>
|
>
|
||||||
|
<EarthLock className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
</TooltipWrapper>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="md:max-w-2/3 h-full w-full max-w-full md:h-3/4 md:w-2/3"
|
className="md:max-w-2/3 h-full w-full max-w-full md:h-3/4 md:w-2/3"
|
||||||
aria-describedby={undefined}
|
aria-describedby={undefined}
|
||||||
|
|
@ -40,7 +77,7 @@ export default function ProxysDialog({
|
||||||
<DialogTitle>Proxys</DialogTitle>
|
<DialogTitle>Proxys</DialogTitle>
|
||||||
</VisuallyHidden>
|
</VisuallyHidden>
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
<Proxys {...proxyProps} />
|
<Proxys telegramId={telegramId} {...proxyProps} />
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import { SWRConfig } from "swr";
|
import { SWRConfig } from "swr";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { request } from "@/lib/api";
|
import { request, RequestParsedError } from "@/lib/api";
|
||||||
|
|
||||||
export const SWRProvider = ({ children }: { children: React.ReactNode }) => {
|
export const SWRProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
@ -13,8 +13,36 @@ export const SWRProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
refreshInterval: 0,
|
refreshInterval: 0,
|
||||||
errorRetryCount: 1,
|
errorRetryCount: 1,
|
||||||
fetcher: request,
|
fetcher: request,
|
||||||
onError: (err: Error) => {
|
onError: (err: Error, key: string) => {
|
||||||
toast({ title: "Error", description: err.message });
|
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;
|
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 */
|
/* eslint-disable */
|
||||||
export async function request(
|
export async function request<T = any>(
|
||||||
api: string,
|
api: string,
|
||||||
requestInit?: RequestInit,
|
requestInit?: RequestInit,
|
||||||
): Promise<any> {
|
): Promise<T> {
|
||||||
return fetch(`${getApiUrl()}${api}`, {
|
const defaultHeaders = {
|
||||||
credentials: "include",
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(`${getApiUrl()}${api}`, {
|
||||||
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
...defaultHeaders,
|
||||||
|
...requestInit?.headers,
|
||||||
},
|
},
|
||||||
...requestInit,
|
...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() {
|
export function localStorageProvider() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue