feat: add server list page
This commit is contained in:
BIN
frontend/public/logo.png
Normal file
BIN
frontend/public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
1
frontend/public/logo.svg
Normal file
1
frontend/public/logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><ellipse cx="18" cy="26" fill="#99AAB5" rx="18" ry="10"/><ellipse cx="18" cy="24" fill="#CCD6DD" rx="18" ry="10"/><path fill="#F5F8FA" d="M18 31C3.042 31 1 16 1 12h34c0 2-1.958 19-17 19"/><path fill="#CCD6DD" d="M34.385 9.644c2.442-10.123-9.781-7.706-12.204-5.799A38 38 0 0 0 18 3.611c-9.389 0-17 3.229-17 8.444S8.611 21.5 18 21.5s17-4.229 17-9.444c0-.863-.226-1.664-.615-2.412m-2.503-2.692c-1.357-.938-3.102-1.694-5.121-2.25 1.875-.576 4.551-.309 5.121 2.25"/><ellipse cx="18" cy="13" fill="#8A4B38" rx="15" ry="7"/><path fill="#D99E82" d="M20 17a1 1 0 0 1-.707-.293c-2.337-2.337-2.376-4.885-.125-8.262.739-1.109.9-2.246.478-3.377-.461-1.236-1.438-1.996-1.731-2.077-.553 0-.958-.443-.958-.996S17.448 1 18 1c.997 0 2.395 1.153 3.183 2.625 1.034 1.933.91 4.039-.351 5.929-1.961 2.942-1.531 4.332-.125 5.738A.999.999 0 0 1 20 17m-6-2a1 1 0 0 1-.707-.293c-2.337-2.337-2.376-4.885-.125-8.262.727-1.091.893-2.083.494-2.947-.444-.961-1.431-1.469-1.684-1.499a.99.99 0 0 1-.989-1c0-.552.458-1 1.011-1 .997 0 2.585.974 3.36 2.423.481.899 1.052 2.761-.528 5.131-1.961 2.942-1.531 4.332-.125 5.738a1 1 0 0 1 0 1.414A1 1 0 0 1 14 15"/></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,21 +1,10 @@
|
||||
{
|
||||
"short_name": "TanStack App",
|
||||
"name": "Create TanStack App Sample",
|
||||
"short_name": "CupControl",
|
||||
"name": "Mange your Netcup Servers",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
"src": "logo.png",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
|
||||
@@ -1,25 +1,14 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
IconCamera,
|
||||
IconChartBar,
|
||||
IconDashboard,
|
||||
IconDatabase,
|
||||
IconFileAi,
|
||||
IconFileDescription,
|
||||
IconFileWord,
|
||||
IconFolder,
|
||||
IconHelp,
|
||||
IconInnerShadowTop,
|
||||
IconListDetails,
|
||||
IconReport,
|
||||
IconSearch,
|
||||
IconSettings,
|
||||
IconUsers,
|
||||
IconServer,
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
import { NavDocuments } from "@/components/nav-documents";
|
||||
import { NavMain } from "@/components/nav-main";
|
||||
import { NavSecondary } from "@/components/nav-secondary";
|
||||
import { NavUser } from "@/components/nav-user";
|
||||
import {
|
||||
Sidebar,
|
||||
@@ -32,33 +21,19 @@ import {
|
||||
} from "@/components/ui/sidebar";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { User } from "@/models/user";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
|
||||
const data = {
|
||||
navMain: [
|
||||
{
|
||||
title: "Dashboard",
|
||||
url: "#",
|
||||
url: "/",
|
||||
icon: IconDashboard,
|
||||
},
|
||||
{
|
||||
title: "Lifecycle",
|
||||
url: "#",
|
||||
icon: IconListDetails,
|
||||
},
|
||||
{
|
||||
title: "Analytics",
|
||||
url: "#",
|
||||
icon: IconChartBar,
|
||||
},
|
||||
{
|
||||
title: "Projects",
|
||||
url: "#",
|
||||
icon: IconFolder,
|
||||
},
|
||||
{
|
||||
title: "Team",
|
||||
url: "#",
|
||||
icon: IconUsers,
|
||||
title: "Server List",
|
||||
url: "/server_list",
|
||||
icon: IconServer,
|
||||
},
|
||||
],
|
||||
navClouds: [
|
||||
@@ -109,40 +84,6 @@ const data = {
|
||||
],
|
||||
},
|
||||
],
|
||||
navSecondary: [
|
||||
{
|
||||
title: "Settings",
|
||||
url: "#",
|
||||
icon: IconSettings,
|
||||
},
|
||||
{
|
||||
title: "Get Help",
|
||||
url: "#",
|
||||
icon: IconHelp,
|
||||
},
|
||||
{
|
||||
title: "Search",
|
||||
url: "#",
|
||||
icon: IconSearch,
|
||||
},
|
||||
],
|
||||
documents: [
|
||||
{
|
||||
name: "Data Library",
|
||||
url: "#",
|
||||
icon: IconDatabase,
|
||||
},
|
||||
{
|
||||
name: "Reports",
|
||||
url: "#",
|
||||
icon: IconReport,
|
||||
},
|
||||
{
|
||||
name: "Word Assistant",
|
||||
url: "#",
|
||||
icon: IconFileWord,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
@@ -164,18 +105,20 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
asChild
|
||||
className="data-[slot=sidebar-menu-button]:!p-1.5"
|
||||
>
|
||||
<a href="#">
|
||||
<IconInnerShadowTop className="!size-5" />
|
||||
<span className="text-base font-semibold">Acme Inc.</span>
|
||||
</a>
|
||||
<Link to="/">
|
||||
<img
|
||||
src="/logo.svg"
|
||||
alt="CupControl Logo"
|
||||
className="w-5 h-5 mr-2"
|
||||
/>
|
||||
<span className="text-base font-semibold">CupControl</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<NavMain items={data.navMain} />
|
||||
<NavDocuments items={data.documents} />
|
||||
<NavSecondary items={data.navSecondary} className="mt-auto" />
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<NavUser
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
IconDots,
|
||||
IconFolder,
|
||||
IconShare3,
|
||||
IconTrash,
|
||||
type Icon,
|
||||
} from "@tabler/icons-react"
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar"
|
||||
|
||||
export function NavDocuments({
|
||||
items,
|
||||
}: {
|
||||
items: {
|
||||
name: string
|
||||
url: string
|
||||
icon: Icon
|
||||
}[]
|
||||
}) {
|
||||
const { isMobile } = useSidebar()
|
||||
|
||||
return (
|
||||
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
||||
<SidebarGroupLabel>Documents</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<SidebarMenuItem key={item.name}>
|
||||
<SidebarMenuButton asChild>
|
||||
<a href={item.url}>
|
||||
<item.icon />
|
||||
<span>{item.name}</span>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuAction
|
||||
showOnHover
|
||||
className="data-[state=open]:bg-accent rounded-sm"
|
||||
>
|
||||
<IconDots />
|
||||
<span className="sr-only">More</span>
|
||||
</SidebarMenuAction>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-24 rounded-lg"
|
||||
side={isMobile ? "bottom" : "right"}
|
||||
align={isMobile ? "end" : "start"}
|
||||
>
|
||||
<DropdownMenuItem>
|
||||
<IconFolder />
|
||||
<span>Open</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconShare3 />
|
||||
<span>Share</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem variant="destructive">
|
||||
<IconTrash />
|
||||
<span>Delete</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton className="text-sidebar-foreground/70">
|
||||
<IconDots className="text-sidebar-foreground/70" />
|
||||
<span>More</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
)
|
||||
}
|
||||
@@ -1,56 +1,48 @@
|
||||
import { IconCirclePlusFilled, IconMail, type Icon } from "@tabler/icons-react"
|
||||
import { IconCirclePlusFilled, IconMail, type Icon } from "@tabler/icons-react";
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar"
|
||||
} from "@/components/ui/sidebar";
|
||||
import { Link, useLocation } from "@tanstack/react-router";
|
||||
|
||||
export function NavMain({
|
||||
items,
|
||||
}: {
|
||||
items: {
|
||||
title: string
|
||||
url: string
|
||||
icon?: Icon
|
||||
}[]
|
||||
title: string;
|
||||
url: string;
|
||||
icon?: Icon;
|
||||
}[];
|
||||
}) {
|
||||
const location = useLocation();
|
||||
return (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupContent className="flex flex-col gap-2">
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem className="flex items-center gap-2">
|
||||
<SidebarMenuButton
|
||||
tooltip="Quick Create"
|
||||
className="bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground active:bg-primary/90 active:text-primary-foreground min-w-8 duration-200 ease-linear"
|
||||
>
|
||||
<IconCirclePlusFilled />
|
||||
<span>Quick Create</span>
|
||||
</SidebarMenuButton>
|
||||
<Button
|
||||
size="icon"
|
||||
className="size-8 group-data-[collapsible=icon]:opacity-0"
|
||||
variant="outline"
|
||||
>
|
||||
<IconMail />
|
||||
<span className="sr-only">Inbox</span>
|
||||
</Button>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton tooltip={item.title}>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
</SidebarMenuButton>
|
||||
<Link to={item.url}>
|
||||
<SidebarMenuButton
|
||||
tooltip={item.title}
|
||||
className={
|
||||
location.pathname === item.url
|
||||
? "bg-pink-900 text-white"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
</SidebarMenuButton>
|
||||
</Link>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { type Icon } from "@tabler/icons-react"
|
||||
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar"
|
||||
|
||||
export function NavSecondary({
|
||||
items,
|
||||
...props
|
||||
}: {
|
||||
items: {
|
||||
title: string
|
||||
url: string
|
||||
icon: Icon
|
||||
}[]
|
||||
} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
|
||||
return (
|
||||
<SidebarGroup {...props}>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton asChild>
|
||||
<a href={item.url}>
|
||||
<item.icon />
|
||||
<span>{item.title}</span>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { SidebarTrigger } from "@/components/ui/sidebar";
|
||||
import { ModeToggle } from "./mode-toggle";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useIsFetching, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Button } from "./ui/button";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Spinner } from "./ui/spinner";
|
||||
|
||||
export function SiteHeader() {
|
||||
const [oldLoginState, setOldLoginState] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
const isFetching = useIsFetching();
|
||||
|
||||
const { data: pingResult } = useQuery({
|
||||
queryKey: ["ping"],
|
||||
@@ -44,6 +46,10 @@ export function SiteHeader() {
|
||||
}
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
queryClient.invalidateQueries();
|
||||
}
|
||||
|
||||
return (
|
||||
<header className="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
|
||||
<div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
|
||||
@@ -77,6 +83,10 @@ export function SiteHeader() {
|
||||
/>
|
||||
<h1 className="text-base font-medium">Documents</h1>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
{isFetching ? <Spinner /> : null}
|
||||
<Button variant="outline" size="sm" onClick={refresh}>
|
||||
Refresh
|
||||
</Button>
|
||||
<ModeToggle />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
16
frontend/src/components/ui/spinner.tsx
Normal file
16
frontend/src/components/ui/spinner.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Loader2Icon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
|
||||
return (
|
||||
<Loader2Icon
|
||||
role="status"
|
||||
aria-label="Loading"
|
||||
className={cn("size-4 animate-spin", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Spinner }
|
||||
15
frontend/src/models/minimal_servers.ts
Normal file
15
frontend/src/models/minimal_servers.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export type MinimalServers = MinimalServer[];
|
||||
|
||||
export interface MinimalServer {
|
||||
id: number;
|
||||
name: string;
|
||||
hostname: string;
|
||||
nickname: string;
|
||||
disabled: boolean;
|
||||
template: Template;
|
||||
}
|
||||
|
||||
export interface Template {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
@@ -9,8 +9,14 @@
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as Server_listRouteImport } from './routes/server_list'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
|
||||
const Server_listRoute = Server_listRouteImport.update({
|
||||
id: '/server_list',
|
||||
path: '/server_list',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
@@ -19,28 +25,39 @@ const IndexRoute = IndexRouteImport.update({
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/server_list': typeof Server_listRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/server_list': typeof Server_listRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/server_list': typeof Server_listRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/'
|
||||
fullPaths: '/' | '/server_list'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/'
|
||||
id: '__root__' | '/'
|
||||
to: '/' | '/server_list'
|
||||
id: '__root__' | '/' | '/server_list'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
Server_listRoute: typeof Server_listRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/server_list': {
|
||||
id: '/server_list'
|
||||
path: '/server_list'
|
||||
fullPath: '/server_list'
|
||||
preLoaderRoute: typeof Server_listRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
@@ -53,6 +70,7 @@ declare module '@tanstack/react-router' {
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
Server_listRoute: Server_listRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
|
||||
62
frontend/src/routes/server_list.tsx
Normal file
62
frontend/src/routes/server_list.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Card } from "@/components/ui/card";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import type { MinimalServers } from "@/models/minimal_servers";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/server_list")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const { data: servers } = useQuery({
|
||||
queryKey: ["servers"],
|
||||
queryFn: async () => {
|
||||
let res = await fetch("/api/servers");
|
||||
return (await res.json()) as MinimalServers;
|
||||
},
|
||||
});
|
||||
return (
|
||||
<div className="p-2 flex justify-center items-center w-full">
|
||||
<Card className="w-full max-w-225 p-2">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[100px]">Name</TableHead>
|
||||
<TableHead>Hostname</TableHead>
|
||||
<TableHead>Nickame</TableHead>
|
||||
<TableHead className="text-right">Type/Template</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{servers?.map((server) => (
|
||||
<TableRow key={server.id}>
|
||||
<TableCell className="font-medium">{server.name}</TableCell>
|
||||
<TableCell>{server.hostname}</TableCell>
|
||||
<TableCell>{server.nickname}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{server.template.name}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TableCell colSpan={3}>Total Servers</TableCell>
|
||||
<TableCell className="text-right">{servers?.length}</TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user