mirror of
https://github.com/fankes/beszel.git
synced 2025-10-19 17:59:28 +08:00
updates
This commit is contained in:
16
main.go
16
main.go
@@ -41,20 +41,6 @@ func main() {
|
|||||||
Automigrate: isGoRun,
|
Automigrate: isGoRun,
|
||||||
})
|
})
|
||||||
|
|
||||||
app.OnAfterBootstrap().Add(func(e *core.BootstrapEvent) error {
|
|
||||||
// update app settings on first run
|
|
||||||
settings := app.Settings()
|
|
||||||
if app.Settings().Meta.AppName == "Acme" {
|
|
||||||
app.Settings().Meta.AppName = "Qoma"
|
|
||||||
app.Settings().Meta.HideControls = true
|
|
||||||
err := app.Dao().SaveSettings(settings)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
// serve site
|
// serve site
|
||||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||||
switch isGoRun {
|
switch isGoRun {
|
||||||
@@ -116,7 +102,7 @@ func main() {
|
|||||||
// api route to return public key
|
// api route to return public key
|
||||||
e.Router.GET("/api/qoma/getkey", func(c echo.Context) error {
|
e.Router.GET("/api/qoma/getkey", func(c echo.Context) error {
|
||||||
requestData := apis.RequestInfo(c)
|
requestData := apis.RequestInfo(c)
|
||||||
if requestData.Admin == nil {
|
if requestData.AuthRecord == nil {
|
||||||
return apis.NewForbiddenError("Forbidden", nil)
|
return apis.NewForbiddenError("Forbidden", nil)
|
||||||
}
|
}
|
||||||
key, err := os.ReadFile("./pb_data/id_ed25519.pub")
|
key, err := os.ReadFile("./pb_data/id_ed25519.pub")
|
||||||
|
@@ -12,73 +12,10 @@ import (
|
|||||||
func init() {
|
func init() {
|
||||||
m.Register(func(db dbx.Builder) error {
|
m.Register(func(db dbx.Builder) error {
|
||||||
jsonData := `[
|
jsonData := `[
|
||||||
{
|
|
||||||
"id": "_pb_users_auth_",
|
|
||||||
"created": "2024-07-07 15:59:04.262Z",
|
|
||||||
"updated": "2024-07-09 23:42:40.542Z",
|
|
||||||
"name": "users",
|
|
||||||
"type": "auth",
|
|
||||||
"system": false,
|
|
||||||
"schema": [
|
|
||||||
{
|
|
||||||
"system": false,
|
|
||||||
"id": "users_name",
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"required": false,
|
|
||||||
"presentable": false,
|
|
||||||
"unique": false,
|
|
||||||
"options": {
|
|
||||||
"min": null,
|
|
||||||
"max": null,
|
|
||||||
"pattern": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"system": false,
|
|
||||||
"id": "users_avatar",
|
|
||||||
"name": "avatar",
|
|
||||||
"type": "file",
|
|
||||||
"required": false,
|
|
||||||
"presentable": false,
|
|
||||||
"unique": false,
|
|
||||||
"options": {
|
|
||||||
"mimeTypes": [
|
|
||||||
"image/jpeg",
|
|
||||||
"image/png",
|
|
||||||
"image/svg+xml",
|
|
||||||
"image/gif",
|
|
||||||
"image/webp"
|
|
||||||
],
|
|
||||||
"thumbs": null,
|
|
||||||
"maxSelect": 1,
|
|
||||||
"maxSize": 5242880,
|
|
||||||
"protected": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"indexes": [],
|
|
||||||
"listRule": "id = @request.auth.id",
|
|
||||||
"viewRule": "id = @request.auth.id",
|
|
||||||
"createRule": "",
|
|
||||||
"updateRule": "id = @request.auth.id",
|
|
||||||
"deleteRule": "id = @request.auth.id",
|
|
||||||
"options": {
|
|
||||||
"allowEmailAuth": true,
|
|
||||||
"allowOAuth2Auth": true,
|
|
||||||
"allowUsernameAuth": true,
|
|
||||||
"exceptEmailDomains": null,
|
|
||||||
"manageRule": null,
|
|
||||||
"minPasswordLength": 8,
|
|
||||||
"onlyEmailDomains": null,
|
|
||||||
"onlyVerified": false,
|
|
||||||
"requireEmail": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "2hz5ncl8tizk5nx",
|
"id": "2hz5ncl8tizk5nx",
|
||||||
"created": "2024-07-07 16:08:20.979Z",
|
"created": "2024-07-07 16:08:20.979Z",
|
||||||
"updated": "2024-07-13 23:20:50.678Z",
|
"updated": "2024-07-14 03:36:23.090Z",
|
||||||
"name": "systems",
|
"name": "systems",
|
||||||
"type": "base",
|
"type": "base",
|
||||||
"system": false,
|
"system": false,
|
||||||
@@ -156,17 +93,17 @@ func init() {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"indexes": [],
|
"indexes": [],
|
||||||
"listRule": null,
|
"listRule": "",
|
||||||
"viewRule": null,
|
"viewRule": "@request.auth.id != \"\"",
|
||||||
"createRule": null,
|
"createRule": "@request.auth.id != \"\" && @request.auth.admin = true",
|
||||||
"updateRule": null,
|
"updateRule": "",
|
||||||
"deleteRule": null,
|
"deleteRule": "@request.auth.id != \"\" && @request.auth.admin = true",
|
||||||
"options": {}
|
"options": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ej9oowivz8b2mht",
|
"id": "ej9oowivz8b2mht",
|
||||||
"created": "2024-07-07 16:09:09.179Z",
|
"created": "2024-07-07 16:09:09.179Z",
|
||||||
"updated": "2024-07-09 23:42:40.542Z",
|
"updated": "2024-07-14 03:36:23.089Z",
|
||||||
"name": "system_stats",
|
"name": "system_stats",
|
||||||
"type": "base",
|
"type": "base",
|
||||||
"system": false,
|
"system": false,
|
||||||
@@ -203,7 +140,7 @@ func init() {
|
|||||||
"indexes": [
|
"indexes": [
|
||||||
"CREATE INDEX ` + "`" + `idx_GxIee0j` + "`" + ` ON ` + "`" + `system_stats` + "`" + ` (` + "`" + `system` + "`" + `)"
|
"CREATE INDEX ` + "`" + `idx_GxIee0j` + "`" + ` ON ` + "`" + `system_stats` + "`" + ` (` + "`" + `system` + "`" + `)"
|
||||||
],
|
],
|
||||||
"listRule": null,
|
"listRule": "@request.auth.id != \"\"",
|
||||||
"viewRule": null,
|
"viewRule": null,
|
||||||
"createRule": null,
|
"createRule": null,
|
||||||
"updateRule": null,
|
"updateRule": null,
|
||||||
@@ -213,7 +150,7 @@ func init() {
|
|||||||
{
|
{
|
||||||
"id": "juohu4jipgc13v7",
|
"id": "juohu4jipgc13v7",
|
||||||
"created": "2024-07-07 16:09:57.976Z",
|
"created": "2024-07-07 16:09:57.976Z",
|
||||||
"updated": "2024-07-09 23:42:40.542Z",
|
"updated": "2024-07-14 03:36:23.090Z",
|
||||||
"name": "container_stats",
|
"name": "container_stats",
|
||||||
"type": "base",
|
"type": "base",
|
||||||
"system": false,
|
"system": false,
|
||||||
@@ -248,12 +185,71 @@ func init() {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"indexes": [],
|
"indexes": [],
|
||||||
"listRule": null,
|
"listRule": "@request.auth.id != \"\"",
|
||||||
"viewRule": null,
|
"viewRule": null,
|
||||||
"createRule": null,
|
"createRule": null,
|
||||||
"updateRule": null,
|
"updateRule": null,
|
||||||
"deleteRule": null,
|
"deleteRule": null,
|
||||||
"options": {}
|
"options": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "_pb_users_auth_",
|
||||||
|
"created": "2024-07-14 03:36:23.076Z",
|
||||||
|
"updated": "2024-07-14 03:36:23.087Z",
|
||||||
|
"name": "users",
|
||||||
|
"type": "auth",
|
||||||
|
"system": false,
|
||||||
|
"schema": [
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "users_avatar",
|
||||||
|
"name": "avatar",
|
||||||
|
"type": "file",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"mimeTypes": [
|
||||||
|
"image/jpeg",
|
||||||
|
"image/png",
|
||||||
|
"image/svg+xml",
|
||||||
|
"image/gif",
|
||||||
|
"image/webp"
|
||||||
|
],
|
||||||
|
"thumbs": null,
|
||||||
|
"maxSelect": 1,
|
||||||
|
"maxSize": 5242880,
|
||||||
|
"protected": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "ebyl7gfs",
|
||||||
|
"name": "admin",
|
||||||
|
"type": "bool",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"indexes": [],
|
||||||
|
"listRule": "id = @request.auth.id",
|
||||||
|
"viewRule": "id = @request.auth.id",
|
||||||
|
"createRule": null,
|
||||||
|
"updateRule": null,
|
||||||
|
"deleteRule": null,
|
||||||
|
"options": {
|
||||||
|
"allowEmailAuth": true,
|
||||||
|
"allowOAuth2Auth": true,
|
||||||
|
"allowUsernameAuth": true,
|
||||||
|
"exceptEmailDomains": null,
|
||||||
|
"manageRule": null,
|
||||||
|
"minPasswordLength": 8,
|
||||||
|
"onlyEmailDomains": null,
|
||||||
|
"onlyVerified": false,
|
||||||
|
"requireEmail": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]`
|
]`
|
||||||
|
|
||||||
|
19
migrations/initial-settings.go
Normal file
19
migrations/initial-settings.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pocketbase/dbx"
|
||||||
|
"github.com/pocketbase/pocketbase/daos"
|
||||||
|
m "github.com/pocketbase/pocketbase/migrations"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
m.Register(func(db dbx.Builder) error {
|
||||||
|
dao := daos.New(db)
|
||||||
|
|
||||||
|
settings, _ := dao.FindSettings()
|
||||||
|
settings.Meta.AppName = "Qoma"
|
||||||
|
settings.Meta.HideControls = true
|
||||||
|
|
||||||
|
return dao.SaveSettings(settings)
|
||||||
|
}, nil)
|
||||||
|
}
|
@@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 75.7 81.8"><path fill="#16a34a" d="M76 29v14a33 33 0 0 1-1 7 30 30 0 0 1-1 2 29 29 0 0 1-3 7 27 27 0 0 1 0 1 27 27 0 0 1-6 6 27 27 0 0 1-7 4l14 12H54L42 72H29a32 32 0 0 1-8-1 29 29 0 0 1-3-1q-6-2-10-6a28 28 0 0 1-6-10 30 30 0 0 1-2-9 35 35 0 0 1 0-2V29a32 32 0 0 1 1-8 28 28 0 0 1 1-3 28 28 0 0 1 5-9 27 27 0 0 1 1-1 28 28 0 0 1 10-6 32 32 0 0 1 0 0 30 30 0 0 1 9-2 35 35 0 0 1 2 0h17a32 32 0 0 1 9 1 28 28 0 0 1 3 1 28 28 0 0 1 9 6 27 27 0 0 1 6 9 31 31 0 0 1 0 1 30 30 0 0 1 3 9 35 35 0 0 1 0 2ZM63 43V29a20 20 0 0 0 0-4 17 17 0 0 0-1-3 15 15 0 0 0-3-4 14 14 0 0 0-1-1 15 15 0 0 0-4-3 17 17 0 0 0-1 0 17 17 0 0 0-5-1 21 21 0 0 0-2 0H29a20 20 0 0 0-4 0 17 17 0 0 0-2 1l-6 3a15 15 0 0 0-3 5 17 17 0 0 0 0 0 17 17 0 0 0-1 5 22 22 0 0 0 0 2v14a20 20 0 0 0 0 4 17 17 0 0 0 1 2 15 15 0 0 0 3 5 14 14 0 0 0 0 1 15 15 0 0 0 5 3 17 17 0 0 0 1 0 17 17 0 0 0 4 1 21 21 0 0 0 2 0h17a20 20 0 0 0 4 0 17 17 0 0 0 3-1l5-3a15 15 0 0 0 4-5 17 17 0 0 0 0-1 17 17 0 0 0 1-4 22 22 0 0 0 0-2Z"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 75.7 81.8"><path fill="#22c55e" d="M76 29v14a33 33 0 0 1-1 7 30 30 0 0 1-1 2 29 29 0 0 1-3 7 27 27 0 0 1 0 1 27 27 0 0 1-6 6 27 27 0 0 1-7 4l14 12H54L42 72H29a32 32 0 0 1-8-1 29 29 0 0 1-3-1q-6-2-10-6a28 28 0 0 1-6-10 30 30 0 0 1-2-9 35 35 0 0 1 0-2V29a32 32 0 0 1 1-8 28 28 0 0 1 1-3 28 28 0 0 1 5-9 27 27 0 0 1 1-1 28 28 0 0 1 10-6 32 32 0 0 1 0 0 30 30 0 0 1 9-2 35 35 0 0 1 2 0h17a32 32 0 0 1 9 1 28 28 0 0 1 3 1 28 28 0 0 1 9 6 27 27 0 0 1 6 9 31 31 0 0 1 0 1 30 30 0 0 1 3 9 35 35 0 0 1 0 2ZM63 43V29a20 20 0 0 0 0-4 17 17 0 0 0-1-3 15 15 0 0 0-3-4 14 14 0 0 0-1-1 15 15 0 0 0-4-3 17 17 0 0 0-1 0 17 17 0 0 0-5-1 21 21 0 0 0-2 0H29a20 20 0 0 0-4 0 17 17 0 0 0-2 1l-6 3a15 15 0 0 0-3 5 17 17 0 0 0 0 0 17 17 0 0 0-1 5 22 22 0 0 0 0 2v14a20 20 0 0 0 0 4 17 17 0 0 0 1 2 15 15 0 0 0 3 5 14 14 0 0 0 0 1 15 15 0 0 0 5 3 17 17 0 0 0 1 0 17 17 0 0 0 4 1 21 21 0 0 0 2 0h17a20 20 0 0 0 4 0 17 17 0 0 0 3-1l5-3a15 15 0 0 0 4-5 17 17 0 0 0 0-1 17 17 0 0 0 1-4 22 22 0 0 0 0-2Z"/></svg>
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -79,17 +79,17 @@ export function AddServerButton() {
|
|||||||
Add <span className="hidden sm:inline">System</span>
|
Add <span className="hidden sm:inline">System</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
<DialogContent className="w-[90%] sm:max-w-[425px] rounded-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Add New System</DialogTitle>
|
<DialogTitle className="mb-2">Add New System</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
The agent must be running on the server to connect. Copy the{' '}
|
The agent must be running on the server to connect. Copy the{' '}
|
||||||
<code className="bg-muted px-1 rounded-sm">docker-compose.yml</code> for the agent
|
<code className="bg-muted px-1 rounded-sm">docker-compose.yml</code> for the agent
|
||||||
below.
|
below.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<form name="testing" action="/" onSubmit={handleSubmit as any}>
|
<form onSubmit={handleSubmit as any}>
|
||||||
<div className="grid gap-4 py-4">
|
<div className="grid gap-4 mt-1 mb-4">
|
||||||
<div className="grid grid-cols-4 items-center gap-4">
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
<Label htmlFor="name" className="text-right">
|
<Label htmlFor="name" className="text-right">
|
||||||
Name
|
Name
|
||||||
@@ -116,7 +116,7 @@ export function AddServerButton() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-4 items-center gap-4 relative">
|
<div className="grid grid-cols-4 items-center gap-4 relative">
|
||||||
<Label htmlFor="pkey" className="text-right">
|
<Label htmlFor="pkey" className="text-right whitespace-pre">
|
||||||
Public Key
|
Public Key
|
||||||
</Label>
|
</Label>
|
||||||
<Input readOnly id="pkey" value={publicKey} className="col-span-3" required></Input>
|
<Input readOnly id="pkey" value={publicKey} className="col-span-3" required></Input>
|
||||||
@@ -144,7 +144,7 @@ export function AddServerButton() {
|
|||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter className="flex justify-end gap-2">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant={'ghost'}
|
variant={'ghost'}
|
||||||
|
@@ -19,7 +19,7 @@ const chartConfig = {
|
|||||||
},
|
},
|
||||||
} satisfies ChartConfig
|
} satisfies ChartConfig
|
||||||
|
|
||||||
export default function ({ chartData }: { chartData: { time: string; cpu: number }[] }) {
|
export default function CpuChart({ chartData }: { chartData: { time: string; cpu: number }[] }) {
|
||||||
if (!chartData?.length) {
|
if (!chartData?.length) {
|
||||||
return <Spinner />
|
return <Spinner />
|
||||||
}
|
}
|
||||||
|
@@ -20,7 +20,7 @@ const chartConfig = {
|
|||||||
},
|
},
|
||||||
} satisfies ChartConfig
|
} satisfies ChartConfig
|
||||||
|
|
||||||
export default function ({
|
export default function DiskChart({
|
||||||
chartData,
|
chartData,
|
||||||
}: {
|
}: {
|
||||||
chartData: { time: string; disk: number; diskUsed: number }[]
|
chartData: { time: string; disk: number; diskUsed: number }[]
|
||||||
|
@@ -22,6 +22,7 @@ import {
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { $servers, navigate } from '@/lib/stores'
|
import { $servers, navigate } from '@/lib/stores'
|
||||||
|
import { isAdmin } from '@/lib/utils'
|
||||||
|
|
||||||
export default function CommandPalette() {
|
export default function CommandPalette() {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
@@ -82,11 +83,13 @@ export default function CommandPalette() {
|
|||||||
</CommandItem>
|
</CommandItem>
|
||||||
))}
|
))}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
|
{isAdmin() && (
|
||||||
|
<>
|
||||||
<CommandSeparator />
|
<CommandSeparator />
|
||||||
<CommandGroup heading="Admin">
|
<CommandGroup heading="Admin">
|
||||||
<CommandItem
|
<CommandItem
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
window.location.href = '/_/#/collections?collectionId=2hz5ncl8tizk5nx'
|
window.location.href = '/_/'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Database className="mr-2 h-4 w-4" />
|
<Database className="mr-2 h-4 w-4" />
|
||||||
@@ -114,6 +117,8 @@ export default function CommandPalette() {
|
|||||||
<CommandShortcut>Admin</CommandShortcut>
|
<CommandShortcut>Admin</CommandShortcut>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</CommandDialog>
|
</CommandDialog>
|
||||||
)
|
)
|
||||||
|
@@ -24,7 +24,7 @@ export default function () {
|
|||||||
<span className="sr-only">Qoma</span>
|
<span className="sr-only">Qoma</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{isFirstRun ? 'Please create your admin account' : 'Please sign in to your account'}
|
{isFirstRun ? 'Please create an admin account' : 'Please sign in to your account'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<UserAuthForm isFirstRun={isFirstRun} />
|
<UserAuthForm isFirstRun={isFirstRun} />
|
||||||
|
@@ -21,7 +21,7 @@ export function ModeToggle() {
|
|||||||
<span className="sr-only">Toggle theme</span>
|
<span className="sr-only">Toggle theme</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent>
|
||||||
<DropdownMenuItem onClick={() => setTheme('light')}>Light</DropdownMenuItem>
|
<DropdownMenuItem onClick={() => setTheme('light')}>Light</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => setTheme('dark')}>Dark</DropdownMenuItem>
|
<DropdownMenuItem onClick={() => setTheme('dark')}>Dark</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => setTheme('system')}>System</DropdownMenuItem>
|
<DropdownMenuItem onClick={() => setTheme('system')}>System</DropdownMenuItem>
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import { Suspense, lazy, useEffect } from 'react'
|
import { Suspense, lazy, useEffect } from 'react'
|
||||||
// import { DataTable } from '../server-table/data-table'
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'
|
||||||
|
|
||||||
const SystemsTable = lazy(() => import('../server-table/systems-table'))
|
const SystemsTable = lazy(() => import('../server-table/systems-table'))
|
||||||
|
@@ -4,14 +4,18 @@ import { Suspense, lazy, useEffect, useState } from 'react'
|
|||||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/card'
|
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/card'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import Spinner from '../spinner'
|
import Spinner from '../spinner'
|
||||||
import CpuChart from '../charts/cpu-chart'
|
// import CpuChart from '../charts/cpu-chart'
|
||||||
import MemChart from '../charts/mem-chart'
|
// import MemChart from '../charts/mem-chart'
|
||||||
import DiskChart from '../charts/disk-chart'
|
// import DiskChart from '../charts/disk-chart'
|
||||||
import ContainerCpuChart from '../charts/container-cpu-chart'
|
// import ContainerCpuChart from '../charts/container-cpu-chart'
|
||||||
|
// import ContainerMemChart from '../charts/container-mem-chart'
|
||||||
import { CpuIcon, MemoryStickIcon } from 'lucide-react'
|
import { CpuIcon, MemoryStickIcon } from 'lucide-react'
|
||||||
import ContainerMemChart from '../charts/container-mem-chart'
|
|
||||||
|
|
||||||
// const CpuChart = lazy(() => import('../cpu-chart'))
|
const CpuChart = lazy(() => import('../charts/cpu-chart'))
|
||||||
|
const ContainerCpuChart = lazy(() => import('../charts/container-cpu-chart'))
|
||||||
|
const MemChart = lazy(() => import('../charts/mem-chart'))
|
||||||
|
const ContainerMemChart = lazy(() => import('../charts/container-mem-chart'))
|
||||||
|
const DiskChart = lazy(() => import('../charts/disk-chart'))
|
||||||
|
|
||||||
function timestampToBrowserTime(timestamp: string) {
|
function timestampToBrowserTime(timestamp: string) {
|
||||||
const date = new Date(timestamp)
|
const date = new Date(timestamp)
|
||||||
@@ -138,6 +142,9 @@ export default function ServerDetail({ name }: { name: string }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="grid gap-6 mb-10">
|
<div className="grid gap-6 mb-10">
|
||||||
|
<div className="p-6">
|
||||||
|
<h1 className="text-3xl font-semibold">{name}</h1>
|
||||||
|
</div>
|
||||||
<Card className="pb-2">
|
<Card className="pb-2">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex gap-2 justify-between">
|
<CardTitle className="flex gap-2 justify-between">
|
||||||
@@ -203,9 +210,9 @@ export default function ServerDetail({ name }: { name: string }) {
|
|||||||
<CardDescription>Precise usage at the recorded time</CardDescription>
|
<CardDescription>Precise usage at the recorded time</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className={'pl-1 w-[calc(100%-2em)] h-52 relative'}>
|
<CardContent className={'pl-1 w-[calc(100%-2em)] h-52 relative'}>
|
||||||
{/* <Suspense fallback={<Spinner />}> */}
|
<Suspense fallback={<Spinner />}>
|
||||||
<DiskChart chartData={diskChartData} />
|
<DiskChart chartData={diskChartData} />
|
||||||
{/* </Suspense> */}
|
</Suspense>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -61,7 +61,7 @@ import { useMemo, useState } from 'react'
|
|||||||
import { $servers, pb, navigate } from '@/lib/stores'
|
import { $servers, pb, navigate } from '@/lib/stores'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { AddServerButton } from '../add-server'
|
import { AddServerButton } from '../add-server'
|
||||||
import { cn, copyToClipboard } from '@/lib/utils'
|
import { cn, copyToClipboard, isAdmin } from '@/lib/utils'
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -75,7 +75,7 @@ function CellFormatter(info: CellContext<SystemRecord, unknown>) {
|
|||||||
const val = info.getValue() as number
|
const val = info.getValue() as number
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2 items-center tabular-nums tracking-tight">
|
<div className="flex gap-2 items-center tabular-nums tracking-tight">
|
||||||
<span className="grow min-w-10 block bg-muted h-4 relative rounded-sm overflow-hidden">
|
<span className="grow min-w-10 block bg-muted h-[1em] relative rounded-sm overflow-hidden">
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'absolute inset-0 w-full h-full origin-left',
|
'absolute inset-0 w-full h-full origin-left',
|
||||||
@@ -288,9 +288,11 @@ export default function SystemsTable() {
|
|||||||
onChange={(event) => table.getColumn('name')?.setFilterValue(event.target.value)}
|
onChange={(event) => table.getColumn('name')?.setFilterValue(event.target.value)}
|
||||||
className="max-w-sm"
|
className="max-w-sm"
|
||||||
/>
|
/>
|
||||||
|
{isAdmin() && (
|
||||||
<div className="ml-auto flex gap-2">
|
<div className="ml-auto flex gap-2">
|
||||||
<AddServerButton />
|
<AddServerButton />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-md border overflow-hidden">
|
<div className="rounded-md border overflow-hidden">
|
||||||
<Table>
|
<Table>
|
||||||
|
@@ -3,8 +3,8 @@ import { cn } from '@/lib/utils'
|
|||||||
import { buttonVariants } from '@/components/ui/button'
|
import { buttonVariants } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Github, LoaderCircle, LogInIcon } from 'lucide-react'
|
import { Github, LoaderCircle, LockIcon, LogInIcon, MailIcon, UserIcon } from 'lucide-react'
|
||||||
import { pb } from '@/lib/stores'
|
import { $authenticated, pb } from '@/lib/stores'
|
||||||
import * as v from 'valibot'
|
import * as v from 'valibot'
|
||||||
import { toast } from './ui/use-toast'
|
import { toast } from './ui/use-toast'
|
||||||
import {
|
import {
|
||||||
@@ -24,13 +24,21 @@ const passwordSchema = v.pipe(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const LoginSchema = v.looseObject({
|
const LoginSchema = v.looseObject({
|
||||||
username: honeypot,
|
name: honeypot,
|
||||||
email: emailSchema,
|
email: emailSchema,
|
||||||
password: passwordSchema,
|
password: passwordSchema,
|
||||||
})
|
})
|
||||||
|
|
||||||
const RegisterSchema = v.looseObject({
|
const RegisterSchema = v.looseObject({
|
||||||
username: honeypot,
|
name: honeypot,
|
||||||
|
username: v.pipe(
|
||||||
|
v.string(),
|
||||||
|
v.regex(
|
||||||
|
/^(?=.*[a-zA-Z])[a-zA-Z0-9_-]+$/,
|
||||||
|
'Invalid username. You may use alphanumeric characters, underscores, and hyphens.'
|
||||||
|
),
|
||||||
|
v.minLength(3, 'Username must be at least 3 characters long.')
|
||||||
|
),
|
||||||
email: emailSchema,
|
email: emailSchema,
|
||||||
password: passwordSchema,
|
password: passwordSchema,
|
||||||
passwordConfirm: passwordSchema,
|
passwordConfirm: passwordSchema,
|
||||||
@@ -68,7 +76,7 @@ export function UserAuthForm({
|
|||||||
setErrors(errors)
|
setErrors(errors)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const { email, password, passwordConfirm } = result.output
|
const { email, password, passwordConfirm, username } = result.output
|
||||||
if (isFirstRun) {
|
if (isFirstRun) {
|
||||||
// check that passwords match
|
// check that passwords match
|
||||||
if (password !== passwordConfirm) {
|
if (password !== passwordConfirm) {
|
||||||
@@ -82,9 +90,19 @@ export function UserAuthForm({
|
|||||||
passwordConfirm: password,
|
passwordConfirm: password,
|
||||||
})
|
})
|
||||||
await pb.admins.authWithPassword(email, password)
|
await pb.admins.authWithPassword(email, password)
|
||||||
|
await pb.collection('users').create({
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
passwordConfirm: password,
|
||||||
|
admin: true,
|
||||||
|
verified: true,
|
||||||
|
})
|
||||||
|
await pb.collection('users').authWithPassword(email, password)
|
||||||
} else {
|
} else {
|
||||||
await pb.admins.authWithPassword(email, password)
|
await pb.collection('users').authWithPassword(email, password)
|
||||||
}
|
}
|
||||||
|
$authenticated.set(true)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return toast({
|
return toast({
|
||||||
title: 'Login attempt failed',
|
title: 'Login attempt failed',
|
||||||
@@ -100,30 +118,49 @@ export function UserAuthForm({
|
|||||||
<div className={cn('grid gap-6', className)} {...props}>
|
<div className={cn('grid gap-6', className)} {...props}>
|
||||||
<form onSubmit={handleSubmit} onChange={() => setErrors({})}>
|
<form onSubmit={handleSubmit} onChange={() => setErrors({})}>
|
||||||
<div className="grid gap-2.5">
|
<div className="grid gap-2.5">
|
||||||
<div className="sr-only">
|
{isFirstRun && (
|
||||||
{/* honeypot */}
|
<div className="grid gap-1 relative">
|
||||||
<label htmlFor="username"></label>
|
<UserIcon className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||||||
<input id="username" type="text" name="username" tabIndex={-1} />
|
<Label className="sr-only" htmlFor="username">
|
||||||
|
Username
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
autoFocus={true}
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
|
required
|
||||||
|
placeholder="username"
|
||||||
|
type="username"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoComplete="username"
|
||||||
|
autoCorrect="off"
|
||||||
|
disabled={isLoading || isGitHubLoading}
|
||||||
|
className="pl-9"
|
||||||
|
/>
|
||||||
|
{errors?.username && <p className="px-1 text-xs text-red-600">{errors.username}</p>}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-1">
|
)}
|
||||||
|
<div className="grid gap-1 relative">
|
||||||
|
<MailIcon className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||||||
<Label className="sr-only" htmlFor="email">
|
<Label className="sr-only" htmlFor="email">
|
||||||
Email
|
Email
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
autoFocus={true}
|
|
||||||
id="email"
|
id="email"
|
||||||
name="email"
|
name="email"
|
||||||
required
|
required
|
||||||
placeholder="name@example.com"
|
placeholder={isFirstRun ? 'email' : 'name@example.com'}
|
||||||
type="email"
|
type="email"
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
disabled={isLoading || isGitHubLoading}
|
disabled={isLoading || isGitHubLoading}
|
||||||
|
className="pl-9"
|
||||||
/>
|
/>
|
||||||
{errors?.email && <p className="px-1 text-xs text-red-600">{errors.email}</p>}
|
{errors?.email && <p className="px-1 text-xs text-red-600">{errors.email}</p>}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-1">
|
<div className="grid gap-1 relative">
|
||||||
|
<LockIcon className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||||||
<Label className="sr-only" htmlFor="pass">
|
<Label className="sr-only" htmlFor="pass">
|
||||||
Password
|
Password
|
||||||
</Label>
|
</Label>
|
||||||
@@ -135,11 +172,13 @@ export function UserAuthForm({
|
|||||||
type="password"
|
type="password"
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
disabled={isLoading || isGitHubLoading}
|
disabled={isLoading || isGitHubLoading}
|
||||||
|
className="pl-9"
|
||||||
/>
|
/>
|
||||||
{errors?.password && <p className="px-1 text-xs text-red-600">{errors.password}</p>}
|
{errors?.password && <p className="px-1 text-xs text-red-600">{errors.password}</p>}
|
||||||
</div>
|
</div>
|
||||||
{isFirstRun && (
|
{isFirstRun && (
|
||||||
<div className="grid gap-1">
|
<div className="grid gap-1 relative">
|
||||||
|
<LockIcon className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||||||
<Label className="sr-only" htmlFor="pass2">
|
<Label className="sr-only" htmlFor="pass2">
|
||||||
Confirm password
|
Confirm password
|
||||||
</Label>
|
</Label>
|
||||||
@@ -151,12 +190,18 @@ export function UserAuthForm({
|
|||||||
type="password"
|
type="password"
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
disabled={isLoading || isGitHubLoading}
|
disabled={isLoading || isGitHubLoading}
|
||||||
|
className="pl-9"
|
||||||
/>
|
/>
|
||||||
{errors?.passwordConfirm && (
|
{errors?.passwordConfirm && (
|
||||||
<p className="px-1 text-xs text-red-600">{errors.passwordConfirm}</p>
|
<p className="px-1 text-xs text-red-600">{errors.passwordConfirm}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div className="sr-only">
|
||||||
|
{/* honeypot */}
|
||||||
|
<label htmlFor="name"></label>
|
||||||
|
<input id="name" type="text" name="name" tabIndex={-1} />
|
||||||
|
</div>
|
||||||
<button className={cn(buttonVariants())} disabled={isLoading}>
|
<button className={cn(buttonVariants())} disabled={isLoading}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<LoaderCircle className="mr-2 h-4 w-4 animate-spin" />
|
<LoaderCircle className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
@@ -4,8 +4,6 @@ import { SystemRecord } from '@/types'
|
|||||||
import { createRouter } from '@nanostores/router'
|
import { createRouter } from '@nanostores/router'
|
||||||
|
|
||||||
export const pb = new PocketBase('/')
|
export const pb = new PocketBase('/')
|
||||||
// @ts-ignore
|
|
||||||
pb.authStore.storageKey = 'pb_admin_auth'
|
|
||||||
|
|
||||||
export const $router = createRouter(
|
export const $router = createRouter(
|
||||||
{
|
{
|
||||||
@@ -20,9 +18,6 @@ export const navigate = (urlString: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const $authenticated = atom(pb.authStore.isValid)
|
export const $authenticated = atom(pb.authStore.isValid)
|
||||||
pb.authStore.onChange(() => {
|
|
||||||
$authenticated.set(pb.authStore.isValid)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const $servers = atom([] as SystemRecord[])
|
export const $servers = atom([] as SystemRecord[])
|
||||||
|
|
||||||
|
@@ -54,3 +54,5 @@ export const formatShortDate = (timestamp: string) => shortDateFormatter.format(
|
|||||||
|
|
||||||
export const updateFavicon = (newIconUrl: string) =>
|
export const updateFavicon = (newIconUrl: string) =>
|
||||||
((document.querySelector("link[rel='icon']") as HTMLLinkElement).href = newIconUrl)
|
((document.querySelector("link[rel='icon']") as HTMLLinkElement).href = newIconUrl)
|
||||||
|
|
||||||
|
export const isAdmin = () => pb.authStore.model?.admin
|
||||||
|
@@ -5,7 +5,7 @@ import Home from './components/routes/home.tsx'
|
|||||||
import { ThemeProvider } from './components/theme-provider.tsx'
|
import { ThemeProvider } from './components/theme-provider.tsx'
|
||||||
import { $authenticated, $router, $servers, navigate, pb } from './lib/stores.ts'
|
import { $authenticated, $router, $servers, navigate, pb } from './lib/stores.ts'
|
||||||
import { ModeToggle } from './components/mode-toggle.tsx'
|
import { ModeToggle } from './components/mode-toggle.tsx'
|
||||||
import { cn, updateFavicon, updateServerList } from './lib/utils.ts'
|
import { cn, isAdmin, updateFavicon, updateServerList } from './lib/utils.ts'
|
||||||
import { buttonVariants } from './components/ui/button.tsx'
|
import { buttonVariants } from './components/ui/button.tsx'
|
||||||
import { DatabaseBackupIcon, Github, LogOutIcon, LogsIcon, MailIcon, UserIcon } from 'lucide-react'
|
import { DatabaseBackupIcon, Github, LogOutIcon, LogsIcon, MailIcon, UserIcon } from 'lucide-react'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
@@ -35,8 +35,14 @@ const App = () => {
|
|||||||
const authenticated = useStore($authenticated)
|
const authenticated = useStore($authenticated)
|
||||||
const servers = useStore($servers)
|
const servers = useStore($servers)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
// get servers
|
// get servers
|
||||||
useEffect(updateServerList, [])
|
updateServerList()
|
||||||
|
// change auth store on auth change
|
||||||
|
pb.authStore.onChange(() => {
|
||||||
|
$authenticated.set(pb.authStore.isValid)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
pb.collection<SystemRecord>('systems').subscribe('*', (e) => {
|
pb.collection<SystemRecord>('systems').subscribe('*', (e) => {
|
||||||
@@ -128,36 +134,7 @@ const Layout = () => {
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div className={'flex ml-auto'}>
|
<div className={'flex ml-auto'}>
|
||||||
<DropdownMenu>
|
<ModeToggle />
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<a
|
|
||||||
aria-label="User Actions"
|
|
||||||
href={'https://github.com/henrygd'}
|
|
||||||
className={cn('', buttonVariants({ variant: 'ghost', size: 'icon' }))}
|
|
||||||
>
|
|
||||||
<UserIcon className="h-[1.2rem] w-[1.2rem]" />
|
|
||||||
</a>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent>
|
|
||||||
<DropdownMenuItem onSelect={() => pb.authStore.clear()}>
|
|
||||||
<LogOutIcon className="mr-2.5 h-4 w-4" />
|
|
||||||
<span>Log out</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem asChild>
|
|
||||||
<a href="/_/#/logs">
|
|
||||||
<LogsIcon className="mr-2.5 h-4 w-4" />
|
|
||||||
<span>Logs</span>
|
|
||||||
</a>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem asChild>
|
|
||||||
<a href="/_/#/settings/backups">
|
|
||||||
<DatabaseBackupIcon className="mr-2.5 h-4 w-4" />
|
|
||||||
<span>Backups</span>
|
|
||||||
</a>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
<TooltipProvider delayDuration={300}>
|
<TooltipProvider delayDuration={300}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
@@ -175,7 +152,40 @@ const Layout = () => {
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
<ModeToggle />
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<a
|
||||||
|
aria-label="User Actions"
|
||||||
|
href={'https://github.com/henrygd'}
|
||||||
|
className={cn('', buttonVariants({ variant: 'ghost', size: 'icon' }))}
|
||||||
|
>
|
||||||
|
<UserIcon className="h-[1.2rem] w-[1.2rem]" />
|
||||||
|
</a>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem onSelect={() => pb.authStore.clear()}>
|
||||||
|
<LogOutIcon className="mr-2.5 h-4 w-4" />
|
||||||
|
<span>Log out</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{isAdmin() && (
|
||||||
|
<>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<a href="/_/#/logs">
|
||||||
|
<LogsIcon className="mr-2.5 h-4 w-4" />
|
||||||
|
<span>Logs</span>
|
||||||
|
</a>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<a href="/_/#/settings/backups">
|
||||||
|
<DatabaseBackupIcon className="mr-2.5 h-4 w-4" />
|
||||||
|
<span>Backups</span>
|
||||||
|
</a>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user