diff --git a/.gitignore b/.gitignore index 86aac31..2861de9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ monitor-site .idea.md pb_data data -temp \ No newline at end of file +temp +.vscode \ No newline at end of file diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..9bcce5d --- /dev/null +++ b/dockerfile @@ -0,0 +1,29 @@ +FROM --platform=$BUILDPLATFORM golang:alpine as builder + +WORKDIR /app + +# Download Go modules +COPY go.mod go.sum ./ +RUN go mod download + +COPY *.go ./ +COPY migrations ./migrations + +# Build +ARG TARGETOS TARGETARCH +RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /server . + +# ? ------------------------- +FROM alpine:latest + +RUN apk add --no-cache \ + unzip \ + ca-certificates + +COPY --from=builder /server / + +COPY ./site/dist /site/dist + +EXPOSE 8080 + +CMD ["/server", "serve", "--http=0.0.0.0:8080"] \ No newline at end of file diff --git a/go.mod b/go.mod index 620379b..f86c18e 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,11 @@ module monitor-site go 1.22.4 -require github.com/pocketbase/pocketbase v0.22.16 +require ( + github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61 + github.com/pocketbase/dbx v1.10.1 + github.com/pocketbase/pocketbase v0.22.16 +) require ( github.com/AlecAivazis/survey/v2 v2.3.7 // indirect @@ -42,13 +46,11 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect - github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/ncruces/go-strftime v0.1.9 // indirect - github.com/pocketbase/dbx v1.10.1 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/spf13/cast v1.6.0 // indirect github.com/spf13/cobra v1.8.1 // indirect diff --git a/main.go b/main.go index f6b1812..d628320 100644 --- a/main.go +++ b/main.go @@ -3,17 +3,19 @@ package main import ( "fmt" "log" + _ "monitor-site/migrations" + "net/http/httputil" + "net/url" "os" "strings" "time" + "github.com/labstack/echo/v5" "github.com/pocketbase/pocketbase" "github.com/pocketbase/pocketbase/apis" "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/plugins/migratecmd" "github.com/pocketbase/pocketbase/tools/cron" - - _ "monitor-site/migrations" ) func main() { @@ -21,12 +23,29 @@ func main() { // loosely check if it was executed using "go run" isGoRun := strings.HasPrefix(os.Args[0], os.TempDir()) + // enable auto creation of migration files when making collection changes in the Admin UI migratecmd.MustRegister(app, app.RootCmd, migratecmd.Config{ // (the isGoRun check is to enable it only during development) Automigrate: isGoRun, }) + // serve site + app.OnBeforeServe().Add(func(e *core.ServeEvent) error { + switch isGoRun { + case true: + proxy := httputil.NewSingleHostReverseProxy(&url.URL{ + Scheme: "http", + Host: "localhost:5173", + }) + e.Router.Any("/*", echo.WrapHandler(proxy)) + e.Router.Any("/", echo.WrapHandler(proxy)) + default: + e.Router.GET("/*", apis.StaticDirectoryHandler(os.DirFS("./site/dist"), true)) + } + return nil + }) + // set up cron job to delete records older than 30 days app.OnBeforeServe().Add(func(e *core.ServeEvent) error { scheduler := cron.New() @@ -65,12 +84,6 @@ func main() { return nil }) - // serves static files from the provided public dir (if exists) - app.OnBeforeServe().Add(func(e *core.ServeEvent) error { - e.Router.GET("/*", apis.StaticDirectoryHandler(os.DirFS("./pb_public"), false)) - return nil - }) - if err := app.Start(); err != nil { log.Fatal(err) } diff --git a/site/.gitignore b/site/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/site/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/site/bun.lockb b/site/bun.lockb new file mode 100755 index 0000000..890127a Binary files /dev/null and b/site/bun.lockb differ diff --git a/site/components.json b/site/components.json new file mode 100644 index 0000000..5b4a89e --- /dev/null +++ b/site/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "gray", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} \ No newline at end of file diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..40366c3 --- /dev/null +++ b/site/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Preact + TS + + +
+ + + diff --git a/site/package.json b/site/package.json new file mode 100644 index 0000000..369a7d7 --- /dev/null +++ b/site/package.json @@ -0,0 +1,37 @@ +{ + "name": "site", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@nanostores/preact": "^0.5.1", + "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-slot": "^1.1.0", + "@tanstack/react-table": "^8.19.2", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "lucide-react": "^0.401.0", + "nanostores": "^0.10.3", + "pocketbase": "^0.21.3", + "preact": "^10.22.0", + "tailwind-merge": "^2.4.0", + "tailwindcss-animate": "^1.0.7", + "valibot": "^0.36.0", + "wouter-preact": "^3.3.1" + }, + "devDependencies": { + "@preact/preset-vite": "^2.8.2", + "@types/bun": "^1.1.6", + "autoprefixer": "^10.4.19", + "postcss": "^8.4.39", + "tailwindcss": "^3.4.4", + "typescript": "^5.2.2", + "vite": "^5.3.1" + } +} diff --git a/site/postcss.config.js b/site/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/site/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/site/public/vite.svg b/site/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/site/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/site/src/components/login.tsx b/site/src/components/login.tsx new file mode 100644 index 0000000..afea7ea --- /dev/null +++ b/site/src/components/login.tsx @@ -0,0 +1,62 @@ +import { Link } from 'wouter-preact' + +import { cn } from '@/lib/utils' +import { buttonVariants } from '@/components/ui/button' +import { UserAuthForm } from '@/components/user-auth-form' +import { ChevronLeft } from 'lucide-react' + +export default function LoginPage() { + return ( +
+
+
+
+ +

Welcome back

+

+ Enter your email to sign in to your account +

+
+ +

+ + Don't have an account? Sign Up + +

+
+
+ +
+ ) +} diff --git a/site/src/components/mode-toggle.tsx b/site/src/components/mode-toggle.tsx new file mode 100644 index 0000000..5ab0bcf --- /dev/null +++ b/site/src/components/mode-toggle.tsx @@ -0,0 +1,31 @@ +import { Moon, Sun } from 'lucide-react' + +import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' +import { useTheme } from '@/components/theme-provider' + +export function ModeToggle() { + const { setTheme } = useTheme() + + return ( + + + + + + setTheme('light')}>Light + setTheme('dark')}>Dark + setTheme('system')}>System + + + ) +} diff --git a/site/src/components/routes/home.tsx b/site/src/components/routes/home.tsx new file mode 100644 index 0000000..40b45cd --- /dev/null +++ b/site/src/components/routes/home.tsx @@ -0,0 +1,38 @@ +import { useEffect, useState } from 'preact/hooks' +import { pb } from '@/lib/stores' +import { SystemRecord } from '@/types' +import { DataTable } from '../server-table/data-table' + +export function Home() { + const [systems, setSystems] = useState([] as SystemRecord[]) + + useEffect(() => { + pb.collection('systems') + .getList(1, 20) + .then(({ items }) => { + setSystems(items) + }) + + pb.collection('systems').subscribe('*', (e) => { + setSystems((curSystems) => { + const i = curSystems.findIndex((s) => s.id === e.record.id) + if (i > -1) { + const newSystems = [...systems] + newSystems[i] = e.record + return newSystems + } else { + return [...curSystems, e.record] + } + }) + }) + return () => pb.collection('systems').unsubscribe('*') + }, []) + + return ( + <> +

Dashboard

+ {systems.length && } +
{JSON.stringify(systems, null, 2)}
+ + ) +} diff --git a/site/src/components/routes/server.tsx b/site/src/components/routes/server.tsx new file mode 100644 index 0000000..255d0f3 --- /dev/null +++ b/site/src/components/routes/server.tsx @@ -0,0 +1,12 @@ +import { useEffect } from 'preact/hooks' +import { useRoute } from 'wouter-preact' + +export function ServerDetail() { + const [_, params] = useRoute('/server/:name') + + useEffect(() => { + document.title = `Server: ${params!.name}` + }, []) + + return <>Info for {params!.name} +} diff --git a/site/src/components/server-table/columns.tsx b/site/src/components/server-table/columns.tsx new file mode 100644 index 0000000..e69de29 diff --git a/site/src/components/server-table/data-table.tsx b/site/src/components/server-table/data-table.tsx new file mode 100644 index 0000000..6dce061 --- /dev/null +++ b/site/src/components/server-table/data-table.tsx @@ -0,0 +1,111 @@ +import { + CellContext, + ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, +} from '@tanstack/react-table' + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' + +import { SystemRecord } from '@/types' + +function CellFormatter(info: CellContext) { + const val = info.getValue() as number + let background = '#42b768' + if (val > 25) { + background = '#da2a49' + } else if (val > 10) { + background = '#daa42a' + } + return ( +
+ {val.toFixed(2)}% + + + +
+ ) +} + +export function DataTable({ data }: { data: SystemRecord[] }) { + // console.log('data', data) + const columns: ColumnDef[] = [ + { + header: 'Node', + accessorKey: 'name', + }, + { + header: 'CPU Load', + accessorKey: 'stats.cpu', + cell: CellFormatter, + }, + { + header: 'RAM', + accessorKey: 'stats.memPct', + cell: CellFormatter, + }, + { + header: 'Disk Usage', + accessorKey: 'stats.diskPct', + cell: CellFormatter, + }, + ] + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + }) + + return ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+ ) +} diff --git a/site/src/components/theme-provider.tsx b/site/src/components/theme-provider.tsx new file mode 100644 index 0000000..02afb13 --- /dev/null +++ b/site/src/components/theme-provider.tsx @@ -0,0 +1,71 @@ +import { createContext, useContext, useEffect, useState } from 'react' + +type Theme = 'dark' | 'light' | 'system' + +type ThemeProviderProps = { + children: React.ReactNode + defaultTheme?: Theme + storageKey?: string +} + +type ThemeProviderState = { + theme: Theme + setTheme: (theme: Theme) => void +} + +const initialState: ThemeProviderState = { + theme: 'system', + setTheme: () => null, +} + +const ThemeProviderContext = createContext(initialState) + +export function ThemeProvider({ + children, + defaultTheme = 'system', + storageKey = 'vite-ui-theme', + ...props +}: ThemeProviderProps) { + const [theme, setTheme] = useState( + () => (localStorage.getItem(storageKey) as Theme) || defaultTheme + ) + + useEffect(() => { + const root = window.document.documentElement + + root.classList.remove('light', 'dark') + + if (theme === 'system') { + const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'dark' + : 'light' + + root.classList.add(systemTheme) + return + } + + root.classList.add(theme) + }, [theme]) + + const value = { + theme, + setTheme: (theme: Theme) => { + localStorage.setItem(storageKey, theme) + setTheme(theme) + }, + } + + return ( + + {children} + + ) +} + +export const useTheme = () => { + const context = useContext(ThemeProviderContext) + + if (context === undefined) throw new Error('useTheme must be used within a ThemeProvider') + + return context +} diff --git a/site/src/components/ui/button.tsx b/site/src/components/ui/button.tsx new file mode 100644 index 0000000..d7e2ae7 --- /dev/null +++ b/site/src/components/ui/button.tsx @@ -0,0 +1,49 @@ +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' + +import { cn } from '@/lib/utils' + +const buttonVariants = cva( + 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground hover:bg-primary/90', + destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', + outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', + secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-10 px-4 py-2', + sm: 'h-9 rounded-md px-3', + lg: 'h-11 rounded-md px-8', + icon: 'h-10 w-10', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button' + return ( + + ) + } +) +Button.displayName = 'Button' + +export { Button, buttonVariants } diff --git a/site/src/components/ui/dropdown-menu.tsx b/site/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..769ff7a --- /dev/null +++ b/site/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,198 @@ +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { Check, ChevronRight, Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/site/src/components/ui/input.tsx b/site/src/components/ui/input.tsx new file mode 100644 index 0000000..677d05f --- /dev/null +++ b/site/src/components/ui/input.tsx @@ -0,0 +1,25 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +export interface InputProps + extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/site/src/components/ui/label.tsx b/site/src/components/ui/label.tsx new file mode 100644 index 0000000..683faa7 --- /dev/null +++ b/site/src/components/ui/label.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/site/src/components/ui/table.tsx b/site/src/components/ui/table.tsx new file mode 100644 index 0000000..7f3502f --- /dev/null +++ b/site/src/components/ui/table.tsx @@ -0,0 +1,117 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)) +Table.displayName = "Table" + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = "TableHeader" + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = "TableBody" + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className + )} + {...props} + /> +)) +TableFooter.displayName = "TableFooter" + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = "TableRow" + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableHead.displayName = "TableHead" + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableCell.displayName = "TableCell" + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableCaption.displayName = "TableCaption" + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/site/src/components/user-auth-form.tsx b/site/src/components/user-auth-form.tsx new file mode 100644 index 0000000..bee950f --- /dev/null +++ b/site/src/components/user-auth-form.tsx @@ -0,0 +1,125 @@ +'use client' + +import * as React from 'react' +// import { useSearchParams } from 'next/navigation' +// import { zodResolver } from '@hookform/resolvers/zod' +// import { signIn } from 'next-auth/react' +// import { useForm } from 'react-hook-form' +// import * as z from 'zod' + +import { cn } from '@/lib/utils' +import { userAuthSchema } from '@/lib/validations/auth' +import { buttonVariants } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +// import { toast } from '@/components/ui/use-toast' +import { Github, LoaderCircle } from 'lucide-react' + +interface UserAuthFormProps extends React.HTMLAttributes {} + +type FormData = z.infer + +export function UserAuthForm({ className, ...props }: UserAuthFormProps) { + const signIn = (s: string) => console.log(s) + const handleSubmit = (e: React.FormEvent) => { + // e.preventDefault() + signIn('github') + } + + const errors = { + email: 'This field is required', + password: 'This field is required', + } + + // const { + // register, + // handleSubmit, + // formState: { errors }, + // } = useForm({ + // resolver: zodResolver(userAuthSchema), + // }) + const [isLoading, setIsLoading] = React.useState(false) + const [isGitHubLoading, setIsGitHubLoading] = React.useState(false) + // const searchParams = useSearchParams() + + async function onSubmit(data: FormData) { + setIsLoading(true) + + alert('do pb stuff') + + // const signInResult = await signIn('email', { + // email: data.email.toLowerCase(), + // redirect: false, + // callbackUrl: searchParams?.get('from') || '/dashboard', + // }) + + setIsLoading(false) + + if (!signInResult?.ok) { + alert('Your sign in request failed. Please try again.') + // return toast({ + // title: 'Something went wrong.', + // description: 'Your sign in request failed. Please try again.', + // variant: 'destructive', + // }) + } + + // return toast({ + // title: 'Check your email', + // description: 'We sent you a login link. Be sure to check your spam too.', + // }) + } + + return ( +
+
+
+
+ + + {errors?.email &&

{errors.email.message}

} +
+ +
+
+
+
+ +
+
+ Or continue with +
+
+ +
+ ) +} diff --git a/site/src/index.css b/site/src/index.css new file mode 100644 index 0000000..929b866 --- /dev/null +++ b/site/src/index.css @@ -0,0 +1,76 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 224 71.4% 4.1%; + + --card: 0 0% 100%; + --card-foreground: 224 71.4% 4.1%; + + --popover: 0 0% 100%; + --popover-foreground: 224 71.4% 4.1%; + + --primary: 220.9 39.3% 11%; + --primary-foreground: 210 20% 98%; + + --secondary: 220 14.3% 95.9%; + --secondary-foreground: 220.9 39.3% 11%; + + --muted: 220 14.3% 95.9%; + --muted-foreground: 220 8.9% 46.1%; + + --accent: 220 14.3% 95.9%; + --accent-foreground: 220.9 39.3% 11%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 20% 98%; + + --border: 220 13% 91%; + --input: 220 13% 91%; + --ring: 224 71.4% 4.1%; + + --radius: 0.5rem; + } + + .dark { + --background: 224 71.4% 4.1%; + --foreground: 210 20% 98%; + + --card: 224 71.4% 4.1%; + --card-foreground: 210 20% 98%; + + --popover: 224 71.4% 4.1%; + --popover-foreground: 210 20% 98%; + + --primary: 210 20% 98%; + --primary-foreground: 220.9 39.3% 11%; + + --secondary: 215 27.9% 16.9%; + --secondary-foreground: 210 20% 98%; + + --muted: 215 27.9% 16.9%; + --muted-foreground: 217.9 10.6% 64.9%; + + --accent: 215 27.9% 16.9%; + --accent-foreground: 210 20% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 20% 98%; + + --border: 215 27.9% 16.9%; + --input: 215 27.9% 16.9%; + --ring: 216 12.2% 83.9%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/site/src/lib/stores.ts b/site/src/lib/stores.ts new file mode 100644 index 0000000..165c7dc --- /dev/null +++ b/site/src/lib/stores.ts @@ -0,0 +1,3 @@ +import PocketBase from 'pocketbase' + +export const pb = new PocketBase('/') diff --git a/site/src/lib/utils.ts b/site/src/lib/utils.ts new file mode 100644 index 0000000..d084cca --- /dev/null +++ b/site/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/site/src/main.tsx b/site/src/main.tsx new file mode 100644 index 0000000..47c29a2 --- /dev/null +++ b/site/src/main.tsx @@ -0,0 +1,45 @@ +import './index.css' +import { render } from 'preact' +import { Link, Route, Switch } from 'wouter-preact' +import { Home } from './components/routes/home.tsx' +import { ThemeProvider } from './components/theme-provider.tsx' +import LoginPage from './components/login.tsx' +import { pb } from './lib/stores.ts' +import { ServerDetail } from './components/routes/server.tsx' + +// import { ModeToggle } from './components/mode-toggle.tsx' + +// const ls = localStorage.getItem('auth') +// console.log('ls', ls) +// @ts-ignore +pb.authStore.storageKey = 'pb_admin_auth' + +console.log('pb.authStore', pb.authStore) + +const App = () => {pb.authStore.isValid ?
: } + +const Main = () => ( +
+ + + {/* + Routes below are matched exclusively - + the first matched route gets rendered + */} + + + + + + {/* Default route in a switch */} + 404: No such page! + +
+) + +render(, document.getElementById('app')!) diff --git a/site/src/types.d.ts b/site/src/types.d.ts new file mode 100644 index 0000000..9788f77 --- /dev/null +++ b/site/src/types.d.ts @@ -0,0 +1,16 @@ +import { RecordModel } from 'pocketbase' + +export interface SystemRecord extends RecordModel { + name: string + stats: SystemStats +} + +export interface SystemStats { + cpu: number + disk: number + diskPct: number + diskUsed: number + mem: number + memPct: number + memUsed: number +} diff --git a/site/src/vite-env.d.ts b/site/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/site/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/site/tailwind.config.js b/site/tailwind.config.js new file mode 100644 index 0000000..7cb7e37 --- /dev/null +++ b/site/tailwind.config.js @@ -0,0 +1,77 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + darkMode: ["class"], + content: [ + './pages/**/*.{ts,tsx}', + './components/**/*.{ts,tsx}', + './app/**/*.{ts,tsx}', + './src/**/*.{ts,tsx}', + ], + prefix: "", + theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + keyframes: { + "accordion-down": { + from: { height: "0" }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: "0" }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + }, + }, + }, + plugins: [require("tailwindcss-animate")], +} \ No newline at end of file diff --git a/site/tsconfig.app.json b/site/tsconfig.app.json new file mode 100644 index 0000000..585f0ee --- /dev/null +++ b/site/tsconfig.app.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "baseUrl": ".", + "paths": { + "react": ["./node_modules/preact/compat/"], + "react-dom": ["./node_modules/preact/compat/"], + "@/*": ["./src/*"] + }, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "preact", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/site/tsconfig.json b/site/tsconfig.json new file mode 100644 index 0000000..a5b06bf --- /dev/null +++ b/site/tsconfig.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/site/tsconfig.node.json b/site/tsconfig.node.json new file mode 100644 index 0000000..3afdd6e --- /dev/null +++ b/site/tsconfig.node.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true, + "noEmit": true + }, + "include": ["vite.config.ts"] +} diff --git a/site/vite.config.ts b/site/vite.config.ts new file mode 100644 index 0000000..53d3e35 --- /dev/null +++ b/site/vite.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vite' +import preact from '@preact/preset-vite' +import path from 'path' + +export default defineConfig({ + plugins: [preact()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, +})