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",
|
"short_name": "CupControl",
|
||||||
"name": "Create TanStack App Sample",
|
"name": "Mange your Netcup Servers",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "favicon.ico",
|
"src": "logo.png",
|
||||||
"sizes": "64x64 32x32 24x24 16x16",
|
"type": "image/png"
|
||||||
"type": "image/x-icon"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "logo192.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "192x192"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "logo512.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "512x512"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"start_url": ".",
|
"start_url": ".",
|
||||||
|
|||||||
@@ -1,25 +1,14 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {
|
import {
|
||||||
IconCamera,
|
IconCamera,
|
||||||
IconChartBar,
|
|
||||||
IconDashboard,
|
IconDashboard,
|
||||||
IconDatabase,
|
|
||||||
IconFileAi,
|
IconFileAi,
|
||||||
IconFileDescription,
|
IconFileDescription,
|
||||||
IconFileWord,
|
|
||||||
IconFolder,
|
|
||||||
IconHelp,
|
|
||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
IconListDetails,
|
IconServer,
|
||||||
IconReport,
|
|
||||||
IconSearch,
|
|
||||||
IconSettings,
|
|
||||||
IconUsers,
|
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
|
|
||||||
import { NavDocuments } from "@/components/nav-documents";
|
|
||||||
import { NavMain } from "@/components/nav-main";
|
import { NavMain } from "@/components/nav-main";
|
||||||
import { NavSecondary } from "@/components/nav-secondary";
|
|
||||||
import { NavUser } from "@/components/nav-user";
|
import { NavUser } from "@/components/nav-user";
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
@@ -32,33 +21,19 @@ import {
|
|||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import type { User } from "@/models/user";
|
import type { User } from "@/models/user";
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
navMain: [
|
navMain: [
|
||||||
{
|
{
|
||||||
title: "Dashboard",
|
title: "Dashboard",
|
||||||
url: "#",
|
url: "/",
|
||||||
icon: IconDashboard,
|
icon: IconDashboard,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Lifecycle",
|
title: "Server List",
|
||||||
url: "#",
|
url: "/server_list",
|
||||||
icon: IconListDetails,
|
icon: IconServer,
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Analytics",
|
|
||||||
url: "#",
|
|
||||||
icon: IconChartBar,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Projects",
|
|
||||||
url: "#",
|
|
||||||
icon: IconFolder,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Team",
|
|
||||||
url: "#",
|
|
||||||
icon: IconUsers,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
navClouds: [
|
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>) {
|
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||||
@@ -164,18 +105,20 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||||||
asChild
|
asChild
|
||||||
className="data-[slot=sidebar-menu-button]:!p-1.5"
|
className="data-[slot=sidebar-menu-button]:!p-1.5"
|
||||||
>
|
>
|
||||||
<a href="#">
|
<Link to="/">
|
||||||
<IconInnerShadowTop className="!size-5" />
|
<img
|
||||||
<span className="text-base font-semibold">Acme Inc.</span>
|
src="/logo.svg"
|
||||||
</a>
|
alt="CupControl Logo"
|
||||||
|
className="w-5 h-5 mr-2"
|
||||||
|
/>
|
||||||
|
<span className="text-base font-semibold">CupControl</span>
|
||||||
|
</Link>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<NavMain items={data.navMain} />
|
<NavMain items={data.navMain} />
|
||||||
<NavDocuments items={data.documents} />
|
|
||||||
<NavSecondary items={data.navSecondary} className="mt-auto" />
|
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<NavUser
|
<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 {
|
import {
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
SidebarGroupContent,
|
SidebarGroupContent,
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
} from "@/components/ui/sidebar"
|
} from "@/components/ui/sidebar";
|
||||||
|
import { Link, useLocation } from "@tanstack/react-router";
|
||||||
|
|
||||||
export function NavMain({
|
export function NavMain({
|
||||||
items,
|
items,
|
||||||
}: {
|
}: {
|
||||||
items: {
|
items: {
|
||||||
title: string
|
title: string;
|
||||||
url: string
|
url: string;
|
||||||
icon?: Icon
|
icon?: Icon;
|
||||||
}[]
|
}[];
|
||||||
}) {
|
}) {
|
||||||
|
const location = useLocation();
|
||||||
return (
|
return (
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarGroupContent className="flex flex-col gap-2">
|
<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>
|
<SidebarMenu>
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<SidebarMenuItem key={item.title}>
|
<SidebarMenuItem key={item.title}>
|
||||||
<SidebarMenuButton tooltip={item.title}>
|
<Link to={item.url}>
|
||||||
{item.icon && <item.icon />}
|
<SidebarMenuButton
|
||||||
<span>{item.title}</span>
|
tooltip={item.title}
|
||||||
</SidebarMenuButton>
|
className={
|
||||||
|
location.pathname === item.url
|
||||||
|
? "bg-pink-900 text-white"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{item.icon && <item.icon />}
|
||||||
|
<span>{item.title}</span>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</Link>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
))}
|
))}
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroupContent>
|
</SidebarGroupContent>
|
||||||
</SidebarGroup>
|
</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 { Separator } from "@/components/ui/separator";
|
||||||
import { SidebarTrigger } from "@/components/ui/sidebar";
|
import { SidebarTrigger } from "@/components/ui/sidebar";
|
||||||
import { ModeToggle } from "./mode-toggle";
|
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 { Button } from "./ui/button";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { Spinner } from "./ui/spinner";
|
||||||
|
|
||||||
export function SiteHeader() {
|
export function SiteHeader() {
|
||||||
const [oldLoginState, setOldLoginState] = useState(false);
|
const [oldLoginState, setOldLoginState] = useState(false);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const isFetching = useIsFetching();
|
||||||
|
|
||||||
const { data: pingResult } = useQuery({
|
const { data: pingResult } = useQuery({
|
||||||
queryKey: ["ping"],
|
queryKey: ["ping"],
|
||||||
@@ -44,6 +46,10 @@ export function SiteHeader() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
queryClient.invalidateQueries();
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
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)">
|
<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">
|
<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>
|
<h1 className="text-base font-medium">Documents</h1>
|
||||||
<div className="ml-auto flex items-center gap-2">
|
<div className="ml-auto flex items-center gap-2">
|
||||||
|
{isFetching ? <Spinner /> : null}
|
||||||
|
<Button variant="outline" size="sm" onClick={refresh}>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
<ModeToggle />
|
<ModeToggle />
|
||||||
</div>
|
</div>
|
||||||
</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.
|
// 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 rootRouteImport } from './routes/__root'
|
||||||
|
import { Route as Server_listRouteImport } from './routes/server_list'
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
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({
|
const IndexRoute = IndexRouteImport.update({
|
||||||
id: '/',
|
id: '/',
|
||||||
path: '/',
|
path: '/',
|
||||||
@@ -19,28 +25,39 @@ const IndexRoute = IndexRouteImport.update({
|
|||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/server_list': typeof Server_listRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/server_list': typeof Server_listRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/server_list': typeof Server_listRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths: '/'
|
fullPaths: '/' | '/server_list'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to: '/'
|
to: '/' | '/server_list'
|
||||||
id: '__root__' | '/'
|
id: '__root__' | '/' | '/server_list'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
|
Server_listRoute: typeof Server_listRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
interface FileRoutesByPath {
|
interface FileRoutesByPath {
|
||||||
|
'/server_list': {
|
||||||
|
id: '/server_list'
|
||||||
|
path: '/server_list'
|
||||||
|
fullPath: '/server_list'
|
||||||
|
preLoaderRoute: typeof Server_listRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/': {
|
'/': {
|
||||||
id: '/'
|
id: '/'
|
||||||
path: '/'
|
path: '/'
|
||||||
@@ -53,6 +70,7 @@ declare module '@tanstack/react-router' {
|
|||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
|
Server_listRoute: Server_listRoute,
|
||||||
}
|
}
|
||||||
export const routeTree = rootRouteImport
|
export const routeTree = rootRouteImport
|
||||||
._addFileChildren(rootRouteChildren)
|
._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>
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,8 @@
|
|||||||
use std::{env, str::FromStr, sync::Mutex, time::Duration};
|
use std::{env, str::FromStr, sync::Mutex, time::Duration};
|
||||||
|
|
||||||
use sqlx::{
|
use sqlx::{
|
||||||
ConnectOptions, Sqlite, SqlitePool,
|
SqlitePool,
|
||||||
pool::PoolConnection,
|
sqlite::{SqliteConnectOptions, SqliteJournalMode, SqliteSynchronous},
|
||||||
sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions, SqliteSynchronous},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static DATABASE_POOL: Mutex<Option<SqlitePool>> = Mutex::new(None);
|
static DATABASE_POOL: Mutex<Option<SqlitePool>> = Mutex::new(None);
|
||||||
|
|||||||
@@ -19,14 +19,15 @@ pub async fn get_access_token() -> Result<String, Box<dyn Error>> {
|
|||||||
let pool = db::get_pool().await?;
|
let pool = db::get_pool().await?;
|
||||||
let tok = query!("SELECT * FROM tokens WHERE is_refresh = 0 ORDER BY expires_at DESC LIMIT 1")
|
let tok = query!("SELECT * FROM tokens WHERE is_refresh = 0 ORDER BY expires_at DESC LIMIT 1")
|
||||||
.fetch_one(&pool)
|
.fetch_one(&pool)
|
||||||
.await?
|
.await?;
|
||||||
.token;
|
|
||||||
|
|
||||||
Ok(tok)
|
dbg!(&tok);
|
||||||
|
Ok(tok.token)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_authed_api_config() -> Result<configuration::Configuration, Box<dyn Error>> {
|
pub async fn get_authed_api_config() -> Result<configuration::Configuration, Box<dyn Error>> {
|
||||||
let mut conf = configuration::Configuration::default();
|
let mut conf = configuration::Configuration::default();
|
||||||
conf.bearer_access_token = Some(get_access_token().await?);
|
conf.bearer_access_token = Some(get_access_token().await?);
|
||||||
|
|
||||||
Ok(conf)
|
Ok(conf)
|
||||||
}
|
}
|
||||||
|
|||||||
10
src/jobs.rs
10
src/jobs.rs
@@ -8,11 +8,13 @@ use crate::{auth, db};
|
|||||||
pub fn init_scheduler(rt: Handle) -> Scheduler {
|
pub fn init_scheduler(rt: Handle) -> Scheduler {
|
||||||
let scheduler = Scheduler::new();
|
let scheduler = Scheduler::new();
|
||||||
|
|
||||||
let trigger_every_5min =
|
let trigger_every_5min = Trigger::with_identity("5min_trigger", "default_group")
|
||||||
Trigger::with_identity("5min_trigger", "default_group").every(Duration::from_mins(5));
|
.every(Duration::from_mins(5))
|
||||||
|
.repeat(u32::max_value());
|
||||||
|
|
||||||
let trigger_every_3min =
|
let trigger_every_3min = Trigger::with_identity("3min_trigger", "default_group")
|
||||||
Trigger::with_identity("3min_trigger", "default_group").every(Duration::from_mins(3));
|
.every(Duration::from_mins(3))
|
||||||
|
.repeat(u32::max_value());
|
||||||
|
|
||||||
let rt_cleanup = rt.clone();
|
let rt_cleanup = rt.clone();
|
||||||
let cleanup_tokens_job = Job::with_identity("cleanup_tokens_job", "default_group", move || {
|
let cleanup_tokens_job = Job::with_identity("cleanup_tokens_job", "default_group", move || {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ mod db;
|
|||||||
mod helper;
|
mod helper;
|
||||||
mod jobs;
|
mod jobs;
|
||||||
mod models;
|
mod models;
|
||||||
|
mod servers;
|
||||||
|
|
||||||
#[get("/api/hello")]
|
#[get("/api/hello")]
|
||||||
async fn hello() -> impl Responder {
|
async fn hello() -> impl Responder {
|
||||||
@@ -47,6 +48,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
.service(auth::is_scp_logged_in)
|
.service(auth::is_scp_logged_in)
|
||||||
.service(auth::start_flow)
|
.service(auth::start_flow)
|
||||||
.service(auth::get_user)
|
.service(auth::get_user)
|
||||||
|
.service(servers::list_servers)
|
||||||
.service(
|
.service(
|
||||||
Files::new("/", "./frontend/dist")
|
Files::new("/", "./frontend/dist")
|
||||||
.index_file("index.html")
|
.index_file("index.html")
|
||||||
|
|||||||
14
src/servers.rs
Normal file
14
src/servers.rs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
use actix_web::{Responder, get};
|
||||||
|
|
||||||
|
use crate::helper;
|
||||||
|
|
||||||
|
#[get("/api/servers")]
|
||||||
|
pub async fn list_servers() -> impl Responder {
|
||||||
|
let config = helper::get_authed_api_config().await.unwrap();
|
||||||
|
let response =
|
||||||
|
scp_core::apis::default_api::api_v1_servers_get(&config, None, None, None, None, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
serde_json::to_string(&response).unwrap()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user