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" name="servers"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Specific Servers (Separate with comma)</FormLabel> <FormLabel>Specific Servers</FormLabel>
<FormControl> <FormControl>
<MultiSelect <MultiSelect
options={serverList} options={serverList}

View File

@@ -37,6 +37,14 @@ import { TableRow, TableCell } from "./ui/table"
import { DataTable } from "./xui/virtulized-data-table" import { DataTable } from "./xui/virtulized-data-table"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Filepath } from "./xui/filepath" import { Filepath } from "./xui/filepath"
import { useMediaQuery } from "@/hooks/useMediaQuery"
import {
Drawer,
DrawerContent,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer"
interface FMProps { interface FMProps {
wsUrl: string; wsUrl: string;
@@ -346,6 +354,8 @@ export const FMCard = ({ id }: { id?: string }) => {
const [fm, setFM] = useState<ModelCreateFMResponse | null>(null); const [fm, setFM] = useState<ModelCreateFMResponse | null>(null);
const [init, setInit] = useState(false); const [init, setInit] = useState(false);
const isDesktop = useMediaQuery("(min-width: 640px)");
const fetchFM = async () => { const fetchFM = async () => {
if (id) { if (id) {
try { try {
@@ -363,25 +373,55 @@ export const FMCard = ({ id }: { id?: string }) => {
} }
} }
return ( return (isDesktop ?
<Sheet modal={false} open={open} onOpenChange={(isOpen) => { if (isOpen) setOpen(true); }}> (
<SheetTrigger asChild> <Sheet
<IconButton icon="folder-closed" onClick={fetchFM} /> modal={false}
</SheetTrigger> open={open}
<SheetContent setOpen={setOpen} className="sm:min-w-[35%]"> onOpenChange={(isOpen) => { if (isOpen) setOpen(true); }}
<div className="overflow-auto"> >
<SheetTitle /> <SheetTrigger asChild>
<SheetHeader className="pb-2"> <IconButton icon="folder-closed" onClick={fetchFM} />
<SheetDescription /> </SheetTrigger>
</SheetHeader> <SheetContent
{fm?.session_id && init setOpen={setOpen}
? className="min-w-[35%]"
<FMComponent className="p-1 space-y-5" wsUrl={`/api/v1/ws/file/${fm.session_id}`} /> >
: <div className="overflow-auto">
<p>The server does not exist, or have not been connected yet.</p> <SheetTitle />
} <SheetHeader className="pb-2">
</div> <SheetDescription />
</SheetContent> </SheetHeader>
</Sheet> {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 { TableVirtuoso } from "react-virtuoso";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { ScrollArea } from "@/components/ui/scroll-area"; 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), // 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 // 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 [heightState, setHeight] = useState(0)
const ref = useRef(null); const ref = useRef(null);
const isDesktop = useMediaQuery("(min-width: 640px)");
useEffect(() => { useEffect(() => {
const calculateHeight = () => { const calculateHeight = () => {
if (ref.current) { if (ref.current) {
@@ -118,11 +121,14 @@ export function DataTable<TData, TValue>({
setHeight(calculatedHeight); setHeight(calculatedHeight);
} }
}; };
window.addEventListener('resize', calculateHeight);
calculateHeight(); // Initial calculation calculateHeight(); // Initial calculation
return () => window.removeEventListener('resize', calculateHeight); if (isDesktop) {
}, []); window.addEventListener('resize', calculateHeight);
}
return () => { if (isDesktop) window.removeEventListener('resize', calculateHeight); }
}, [isDesktop]);
return ( return (
<div className="rounded-md border" ref={ref} style={{ height: heightState }}> <div className="rounded-md border" ref={ref} style={{ height: heightState }}>

View File

@@ -1,7 +1,7 @@
import { clsx, type ClassValue } from "clsx" import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge"
import { z } from "zod" import { z } from "zod"
import { FMEntry, FMOpcode } from "@/types" import { FMEntry, FMOpcode, ModelIP } from "@/types"
import FMWorker from "./fm?worker" import FMWorker from "./fm?worker"
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
@@ -127,3 +127,15 @@ export const fmWorker = new FMWorker();
export function formatPath(path: string) { export function formatPath(path: string) {
return path.replace(/\/{2,}/g, '/'); 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 { NoteMenu } from "@/components/note-menu"
import { TerminalButton } from "@/components/terminal" import { TerminalButton } from "@/components/terminal"
import { useServer } from "@/hooks/useServer" import { useServer } from "@/hooks/useServer"
import { joinIP } from "@/lib/utils"
export default function ServerPage() { export default function ServerPage() {
const { data, mutate, error, isLoading } = useSWR<Server[]>('/api/v1/server', swrFetcher); const { data, mutate, error, isLoading } = useSWR<Server[]>('/api/v1/server', swrFetcher);
@@ -81,13 +82,11 @@ export default function ServerPage() {
{ {
id: "ip", id: "ip",
header: "IP", header: "IP",
accessorKey: "host.ip",
accessorFn: row => row.host?.ip,
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original; const s = row.original;
return ( return (
<div className="max-w-24 whitespace-normal break-words"> <div className="max-w-24 whitespace-normal break-words">
{s.host.ip} {joinIP(s.geoip?.ip)}
</div> </div>
) )
} }

View File

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