From 89b06d00aa86e9630719c7b15b5df6a0691355e2 Mon Sep 17 00:00:00 2001 From: Henry Dollman Date: Mon, 8 Jul 2024 15:53:12 -0400 Subject: [PATCH] progress on client site --- .gitignore | 3 +- dockerfile | 29 +++ go.mod | 8 +- main.go | 29 ++- site/.gitignore | 24 +++ site/bun.lockb | Bin 0 -> 118234 bytes site/components.json | 17 ++ site/index.html | 13 ++ site/package.json | 37 ++++ site/postcss.config.js | 6 + site/public/vite.svg | 1 + site/src/components/login.tsx | 62 ++++++ site/src/components/mode-toggle.tsx | 31 +++ site/src/components/routes/home.tsx | 38 ++++ site/src/components/routes/server.tsx | 12 ++ site/src/components/server-table/columns.tsx | 0 .../components/server-table/data-table.tsx | 111 ++++++++++ site/src/components/theme-provider.tsx | 71 +++++++ site/src/components/ui/button.tsx | 49 +++++ site/src/components/ui/dropdown-menu.tsx | 198 ++++++++++++++++++ site/src/components/ui/input.tsx | 25 +++ site/src/components/ui/label.tsx | 24 +++ site/src/components/ui/table.tsx | 117 +++++++++++ site/src/components/user-auth-form.tsx | 125 +++++++++++ site/src/index.css | 76 +++++++ site/src/lib/stores.ts | 3 + site/src/lib/utils.ts | 6 + site/src/main.tsx | 45 ++++ site/src/types.d.ts | 16 ++ site/src/vite-env.d.ts | 1 + site/tailwind.config.js | 77 +++++++ site/tsconfig.app.json | 34 +++ site/tsconfig.json | 11 + site/tsconfig.node.json | 13 ++ site/vite.config.ts | 12 ++ 35 files changed, 1312 insertions(+), 12 deletions(-) create mode 100644 dockerfile create mode 100644 site/.gitignore create mode 100755 site/bun.lockb create mode 100644 site/components.json create mode 100644 site/index.html create mode 100644 site/package.json create mode 100644 site/postcss.config.js create mode 100644 site/public/vite.svg create mode 100644 site/src/components/login.tsx create mode 100644 site/src/components/mode-toggle.tsx create mode 100644 site/src/components/routes/home.tsx create mode 100644 site/src/components/routes/server.tsx create mode 100644 site/src/components/server-table/columns.tsx create mode 100644 site/src/components/server-table/data-table.tsx create mode 100644 site/src/components/theme-provider.tsx create mode 100644 site/src/components/ui/button.tsx create mode 100644 site/src/components/ui/dropdown-menu.tsx create mode 100644 site/src/components/ui/input.tsx create mode 100644 site/src/components/ui/label.tsx create mode 100644 site/src/components/ui/table.tsx create mode 100644 site/src/components/user-auth-form.tsx create mode 100644 site/src/index.css create mode 100644 site/src/lib/stores.ts create mode 100644 site/src/lib/utils.ts create mode 100644 site/src/main.tsx create mode 100644 site/src/types.d.ts create mode 100644 site/src/vite-env.d.ts create mode 100644 site/tailwind.config.js create mode 100644 site/tsconfig.app.json create mode 100644 site/tsconfig.json create mode 100644 site/tsconfig.node.json create mode 100644 site/vite.config.ts 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 0000000000000000000000000000000000000000..890127ad3f50602a0b6c6dd6134cc28fd03088fb GIT binary patch literal 118234 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�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 + + + + + + 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'), + }, + }, +})