mirror of
https://github.com/Buriburizaem0n/admin-frontend-domain.git
synced 2026-02-04 12:40:08 +00:00
feat: responsive fm card (#11)
* feat: responsive fm card * delete meaningless words * fix joinIP
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }}>
|
||||||
|
|||||||
@@ -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 '';
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user