feat: responsive fm card (#11)

* feat: responsive fm card

* delete meaningless words

* fix joinIP
This commit is contained in:
UUBulb
2024-11-22 22:15:41 +08:00
committed by GitHub
parent 87e17a07df
commit 2991b91f35
6 changed files with 98 additions and 30 deletions

View File

@@ -213,7 +213,7 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
name="servers"
render={({ field }) => (
<FormItem>
<FormLabel>Specific Servers (Separate with comma)</FormLabel>
<FormLabel>Specific Servers</FormLabel>
<FormControl>
<MultiSelect
options={serverList}

View File

@@ -37,6 +37,14 @@ import { TableRow, TableCell } from "./ui/table"
import { DataTable } from "./xui/virtulized-data-table"
import { Input } from "@/components/ui/input"
import { Filepath } from "./xui/filepath"
import { useMediaQuery } from "@/hooks/useMediaQuery"
import {
Drawer,
DrawerContent,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer"
interface FMProps {
wsUrl: string;
@@ -346,6 +354,8 @@ export const FMCard = ({ id }: { id?: string }) => {
const [fm, setFM] = useState<ModelCreateFMResponse | null>(null);
const [init, setInit] = useState(false);
const isDesktop = useMediaQuery("(min-width: 640px)");
const fetchFM = async () => {
if (id) {
try {
@@ -363,25 +373,55 @@ export const FMCard = ({ id }: { id?: string }) => {
}
}
return (
<Sheet modal={false} open={open} onOpenChange={(isOpen) => { if (isOpen) setOpen(true); }}>
<SheetTrigger asChild>
<IconButton icon="folder-closed" onClick={fetchFM} />
</SheetTrigger>
<SheetContent setOpen={setOpen} className="sm:min-w-[35%]">
<div className="overflow-auto">
<SheetTitle />
<SheetHeader className="pb-2">
<SheetDescription />
</SheetHeader>
{fm?.session_id && init
?
<FMComponent className="p-1 space-y-5" wsUrl={`/api/v1/ws/file/${fm.session_id}`} />
:
<p>The server does not exist, or have not been connected yet.</p>
}
</div>
</SheetContent>
</Sheet>
return (isDesktop ?
(
<Sheet
modal={false}
open={open}
onOpenChange={(isOpen) => { if (isOpen) setOpen(true); }}
>
<SheetTrigger asChild>
<IconButton icon="folder-closed" onClick={fetchFM} />
</SheetTrigger>
<SheetContent
setOpen={setOpen}
className="min-w-[35%]"
>
<div className="overflow-auto">
<SheetTitle />
<SheetHeader className="pb-2">
<SheetDescription />
</SheetHeader>
{fm?.session_id && init
?
<FMComponent className="p-1 space-y-5" wsUrl={`/api/v1/ws/file/${fm.session_id}`} />
:
<p>The server does not exist, or have not been connected yet.</p>
}
</div>
</SheetContent>
</Sheet>
)
: (
<Drawer>
<DrawerTrigger asChild>
<IconButton icon="folder-closed" onClick={fetchFM} />
</DrawerTrigger>
<DrawerContent className="min-h-[60%] p-4">
<div className="overflow-auto">
<DrawerTitle />
<DrawerHeader className="pb-2">
<SheetDescription />
</DrawerHeader>
{fm?.session_id && init
?
<FMComponent className="p-1 space-y-5" wsUrl={`/api/v1/ws/file/${fm.session_id}`} />
:
<p>The server does not exist, or have not been connected yet.</p>
}
</div>
</DrawerContent>
</Drawer>
)
)
}

View File

@@ -16,6 +16,7 @@ import { HTMLAttributes, forwardRef, useState, useRef, useEffect } from "react";
import { TableVirtuoso } from "react-virtuoso";
import { cn } from "@/lib/utils"
import { ScrollArea } from "@/components/ui/scroll-area";
import { useMediaQuery } from "@/hooks/useMediaQuery";
// Original Table is wrapped with a <div> (see https://ui.shadcn.com/docs/components/table#radix-:r24:-content-manual),
// but here we don't want it, so let's use a new component with only <table> tag
@@ -99,6 +100,8 @@ export function DataTable<TData, TValue>({
const [heightState, setHeight] = useState(0)
const ref = useRef(null);
const isDesktop = useMediaQuery("(min-width: 640px)");
useEffect(() => {
const calculateHeight = () => {
if (ref.current) {
@@ -118,11 +121,14 @@ export function DataTable<TData, TValue>({
setHeight(calculatedHeight);
}
};
window.addEventListener('resize', calculateHeight);
calculateHeight(); // Initial calculation
return () => window.removeEventListener('resize', calculateHeight);
}, []);
if (isDesktop) {
window.addEventListener('resize', calculateHeight);
}
return () => { if (isDesktop) window.removeEventListener('resize', calculateHeight); }
}, [isDesktop]);
return (
<div className="rounded-md border" ref={ref} style={{ height: heightState }}>

View File

@@ -1,7 +1,7 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
import { z } from "zod"
import { FMEntry, FMOpcode } from "@/types"
import { FMEntry, FMOpcode, ModelIP } from "@/types"
import FMWorker from "./fm?worker"
export function cn(...inputs: ClassValue[]) {
@@ -127,3 +127,15 @@ export const fmWorker = new FMWorker();
export function formatPath(path: string) {
return path.replace(/\/{2,}/g, '/');
}
export function joinIP(p?: ModelIP) {
if (p) {
if (p.ipv4_addr && p.ipv6_addr) {
return `${p.ipv4_addr}/${p.ipv6_addr}`;
} else if (p.ipv4_addr) {
return p.ipv4_addr;
}
return p.ipv6_addr;
}
return '';
}

View File

@@ -16,6 +16,7 @@ import { InstallCommandsMenu } from "@/components/install-commands"
import { NoteMenu } from "@/components/note-menu"
import { TerminalButton } from "@/components/terminal"
import { useServer } from "@/hooks/useServer"
import { joinIP } from "@/lib/utils"
export default function ServerPage() {
const { data, mutate, error, isLoading } = useSWR<Server[]>('/api/v1/server', swrFetcher);
@@ -81,13 +82,11 @@ export default function ServerPage() {
{
id: "ip",
header: "IP",
accessorKey: "host.ip",
accessorFn: row => row.host?.ip,
cell: ({ row }) => {
const s = row.original;
return (
<div className="max-w-24 whitespace-normal break-words">
{s.host.ip}
{joinIP(s.geoip?.ip)}
</div>
)
}

View File

@@ -180,6 +180,8 @@ export interface ModelConfig {
listen_port: number;
/** 时区,默认为 Asia/Shanghai */
location: string;
/** 真实IP */
real_ip_header: string;
site_name: string;
tls: boolean;
}
@@ -291,14 +293,17 @@ export interface ModelForceUpdateResponse {
success?: number[];
}
export interface ModelGeoIP {
country_code: string;
ip: ModelIP;
}
export interface ModelHost {
arch: string;
boot_time: number;
country_code: string;
cpu: string[];
disk_total: number;
gpu: string[];
ip: string;
mem_total: number;
platform: string;
platform_version: string;
@@ -327,6 +332,11 @@ export interface ModelHostState {
uptime: number;
}
export interface ModelIP {
ipv4_addr: string;
ipv6_addr: string;
}
export interface ModelLoginRequest {
password: string;
username: string;
@@ -439,6 +449,7 @@ export interface ModelServer {
display_index: number;
/** 启用DDNS */
enable_ddns: boolean;
geoip: ModelGeoIP;
/** 对游客隐藏 */
hide_for_guest: boolean;
host: ModelHost;