mirror of
https://github.com/fankes/beszel.git
synced 2025-10-19 17:59:28 +08:00
feature: support serving from subpath (#33)
Co-authored-by: Karthik T <karthikt.holmes+github@gmail.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
|||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -40,15 +41,18 @@ type Hub struct {
|
|||||||
rm *records.RecordManager
|
rm *records.RecordManager
|
||||||
systemStats *core.Collection
|
systemStats *core.Collection
|
||||||
containerStats *core.Collection
|
containerStats *core.Collection
|
||||||
|
appURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHub(app *pocketbase.PocketBase) *Hub {
|
func NewHub(app *pocketbase.PocketBase) *Hub {
|
||||||
return &Hub{
|
hub := &Hub{
|
||||||
app: app,
|
app: app,
|
||||||
am: alerts.NewAlertManager(app),
|
am: alerts.NewAlertManager(app),
|
||||||
um: users.NewUserManager(app),
|
um: users.NewUserManager(app),
|
||||||
rm: records.NewRecordManager(app),
|
rm: records.NewRecordManager(app),
|
||||||
}
|
}
|
||||||
|
hub.appURL, _ = GetEnv("APP_URL")
|
||||||
|
return hub
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetEnv retrieves an environment variable with a "BESZEL_HUB_" prefix, or falls back to the unprefixed key.
|
// GetEnv retrieves an environment variable with a "BESZEL_HUB_" prefix, or falls back to the unprefixed key.
|
||||||
@@ -82,6 +86,10 @@ func (h *Hub) Run() {
|
|||||||
settings := h.app.Settings()
|
settings := h.app.Settings()
|
||||||
// batch requests (for global alerts)
|
// batch requests (for global alerts)
|
||||||
settings.Batch.Enabled = true
|
settings.Batch.Enabled = true
|
||||||
|
// set URL if BASE_URL env is set
|
||||||
|
if h.appURL != "" {
|
||||||
|
settings.Meta.AppURL = h.appURL
|
||||||
|
}
|
||||||
// set auth settings
|
// set auth settings
|
||||||
usersCollection, err := h.app.FindCollectionByNameOrId("users")
|
usersCollection, err := h.app.FindCollectionByNameOrId("users")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -118,19 +126,39 @@ func (h *Hub) Run() {
|
|||||||
Scheme: "http",
|
Scheme: "http",
|
||||||
Host: "localhost:5173",
|
Host: "localhost:5173",
|
||||||
})
|
})
|
||||||
se.Router.Any("/", func(e *core.RequestEvent) error {
|
se.Router.Any("/{path...}", func(e *core.RequestEvent) error {
|
||||||
proxy.ServeHTTP(e.Response, e.Request)
|
proxy.ServeHTTP(e.Response, e.Request)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
default:
|
default:
|
||||||
|
// parse app url
|
||||||
|
parsedURL, err := url.Parse(h.appURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// fix base paths in html if using subpath
|
||||||
|
basePath := strings.TrimSuffix(parsedURL.Path, "/") + "/"
|
||||||
|
indexFile, _ := fs.ReadFile(site.DistDirFS, "index.html")
|
||||||
|
indexContent := strings.ReplaceAll(string(indexFile), "./", basePath)
|
||||||
|
// set up static asset serving
|
||||||
|
staticPaths := [2]string{"/static/", "/assets/"}
|
||||||
|
serveStatic := apis.Static(site.DistDirFS, false)
|
||||||
|
// get CSP configuration
|
||||||
csp, cspExists := GetEnv("CSP")
|
csp, cspExists := GetEnv("CSP")
|
||||||
s := apis.Static(site.DistDirFS, true)
|
// add route
|
||||||
se.Router.Any("/{path...}", func(e *core.RequestEvent) error {
|
se.Router.Any("/{path...}", func(e *core.RequestEvent) error {
|
||||||
|
// serve static assets if path is in staticPaths
|
||||||
|
for i := range staticPaths {
|
||||||
|
if strings.Contains(e.Request.URL.Path, staticPaths[i]) {
|
||||||
|
e.Response.Header().Set("Cache-Control", "public, max-age=2592000")
|
||||||
|
return serveStatic(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
if cspExists {
|
if cspExists {
|
||||||
e.Response.Header().Del("X-Frame-Options")
|
e.Response.Header().Del("X-Frame-Options")
|
||||||
e.Response.Header().Set("Content-Security-Policy", csp)
|
e.Response.Header().Set("Content-Security-Policy", csp)
|
||||||
}
|
}
|
||||||
return s(e)
|
return e.HTML(http.StatusOK, indexContent)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return se.Next()
|
return se.Next()
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en" dir="ltr">
|
<html lang="en" dir="ltr">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Beszel</title>
|
<title>Beszel</title>
|
||||||
|
<script>window.BASE_PATH = "%BASE_URL%"</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
@@ -18,7 +18,7 @@ import { Copy, PlusIcon } from "lucide-react"
|
|||||||
import { useState, useRef, MutableRefObject } from "react"
|
import { useState, useRef, MutableRefObject } from "react"
|
||||||
import { useStore } from "@nanostores/react"
|
import { useStore } from "@nanostores/react"
|
||||||
import { cn, copyToClipboard, isReadOnlyUser } from "@/lib/utils"
|
import { cn, copyToClipboard, isReadOnlyUser } from "@/lib/utils"
|
||||||
import { navigate } from "./router"
|
import { basePath, navigate } from "./router"
|
||||||
import { Trans } from "@lingui/macro"
|
import { Trans } from "@lingui/macro"
|
||||||
import { i18n } from "@lingui/core"
|
import { i18n } from "@lingui/core"
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ export function AddSystemButton({ className }: { className?: string }) {
|
|||||||
try {
|
try {
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
await pb.collection("systems").create(data)
|
await pb.collection("systems").create(data)
|
||||||
navigate("/")
|
navigate(basePath)
|
||||||
// console.log(record)
|
// console.log(record)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
|
@@ -23,8 +23,9 @@ import { useEffect } from "react"
|
|||||||
import { useStore } from "@nanostores/react"
|
import { useStore } from "@nanostores/react"
|
||||||
import { $systems } from "@/lib/stores"
|
import { $systems } from "@/lib/stores"
|
||||||
import { isAdmin } from "@/lib/utils"
|
import { isAdmin } from "@/lib/utils"
|
||||||
import { navigate } from "./router"
|
import { $router, basePath, navigate } from "./router"
|
||||||
import { Trans, t } from "@lingui/macro"
|
import { Trans, t } from "@lingui/macro"
|
||||||
|
import { getPagePath } from "@nanostores/router"
|
||||||
|
|
||||||
export default function CommandPalette({ open, setOpen }: { open: boolean; setOpen: (open: boolean) => void }) {
|
export default function CommandPalette({ open, setOpen }: { open: boolean; setOpen: (open: boolean) => void }) {
|
||||||
const systems = useStore($systems)
|
const systems = useStore($systems)
|
||||||
@@ -55,7 +56,7 @@ export default function CommandPalette({ open, setOpen }: { open: boolean; setOp
|
|||||||
<CommandItem
|
<CommandItem
|
||||||
key={system.id}
|
key={system.id}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
navigate(`/system/${encodeURIComponent(system.name)}`)
|
navigate(getPagePath($router, "system", { name: system.name }))
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -72,7 +73,7 @@ export default function CommandPalette({ open, setOpen }: { open: boolean; setOp
|
|||||||
<CommandItem
|
<CommandItem
|
||||||
keywords={["home"]}
|
keywords={["home"]}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
navigate("/")
|
navigate(basePath)
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -86,7 +87,7 @@ export default function CommandPalette({ open, setOpen }: { open: boolean; setOp
|
|||||||
</CommandItem>
|
</CommandItem>
|
||||||
<CommandItem
|
<CommandItem
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
navigate("/settings/general")
|
navigate(getPagePath($router, "settings", { name: "general" }))
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -101,7 +102,7 @@ export default function CommandPalette({ open, setOpen }: { open: boolean; setOp
|
|||||||
<CommandItem
|
<CommandItem
|
||||||
keywords={["alerts"]}
|
keywords={["alerts"]}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
navigate("/settings/notifications")
|
navigate(getPagePath($router, "settings", { name: "notifications" }))
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@@ -9,8 +9,9 @@ import { toast } from "../ui/use-toast"
|
|||||||
import { Dialog, DialogContent, DialogTrigger, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
import { Dialog, DialogContent, DialogTrigger, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||||
import { useCallback, useState } from "react"
|
import { useCallback, useState } from "react"
|
||||||
import { AuthMethodsList, OAuth2AuthConfig } from "pocketbase"
|
import { AuthMethodsList, OAuth2AuthConfig } from "pocketbase"
|
||||||
import { Link } from "../router"
|
import { $router, Link, prependBasePath } from "../router"
|
||||||
import { Trans, t } from "@lingui/macro"
|
import { Trans, t } from "@lingui/macro"
|
||||||
|
import { getPagePath } from "@nanostores/router"
|
||||||
|
|
||||||
const honeypot = v.literal("")
|
const honeypot = v.literal("")
|
||||||
const emailSchema = v.pipe(v.string(), v.email(t`Invalid email address.`))
|
const emailSchema = v.pipe(v.string(), v.email(t`Invalid email address.`))
|
||||||
@@ -260,11 +261,11 @@ export function UserAuthForm({
|
|||||||
) : (
|
) : (
|
||||||
<img
|
<img
|
||||||
className="me-2 h-4 w-4 dark:brightness-0 dark:invert"
|
className="me-2 h-4 w-4 dark:brightness-0 dark:invert"
|
||||||
src={`/_/images/oauth2/${provider.name}.svg`}
|
src={prependBasePath(`/_/images/oauth2/${provider.name}.svg`)}
|
||||||
alt=""
|
alt=""
|
||||||
onError={(e) => {
|
// onError={(e) => {
|
||||||
e.currentTarget.src = "/static/lock.svg"
|
// e.currentTarget.src = "/static/lock.svg"
|
||||||
}}
|
// }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span className="translate-y-[1px]">{provider.displayName}</span>
|
<span className="translate-y-[1px]">{provider.displayName}</span>
|
||||||
@@ -278,7 +279,7 @@ export function UserAuthForm({
|
|||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<button type="button" className={cn(buttonVariants({ variant: "outline" }))}>
|
<button type="button" className={cn(buttonVariants({ variant: "outline" }))}>
|
||||||
<img className="me-2 h-4 w-4 dark:invert" src="/_/images/oauth2/github.svg" alt="" />
|
<img className="me-2 h-4 w-4 dark:invert" src={prependBasePath("/_/images/oauth2/github.svg")} alt="" />
|
||||||
<span className="translate-y-[1px]">GitHub</span>
|
<span className="translate-y-[1px]">GitHub</span>
|
||||||
</button>
|
</button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
@@ -311,7 +312,7 @@ export function UserAuthForm({
|
|||||||
|
|
||||||
{passwordEnabled && !isFirstRun && (
|
{passwordEnabled && !isFirstRun && (
|
||||||
<Link
|
<Link
|
||||||
href="/forgot-password"
|
href={getPagePath($router, "forgot_password")}
|
||||||
className="text-sm mx-auto hover:text-brand underline underline-offset-4 opacity-70 hover:opacity-100 transition-opacity"
|
className="text-sm mx-auto hover:text-brand underline underline-offset-4 opacity-70 hover:opacity-100 transition-opacity"
|
||||||
>
|
>
|
||||||
<Trans>Forgot password?</Trans>
|
<Trans>Forgot password?</Trans>
|
||||||
|
@@ -34,7 +34,7 @@ export default function () {
|
|||||||
const subtitle = useMemo(() => {
|
const subtitle = useMemo(() => {
|
||||||
if (isFirstRun) {
|
if (isFirstRun) {
|
||||||
return t`Please create an admin account`
|
return t`Please create an admin account`
|
||||||
} else if (page?.path === "/forgot-password") {
|
} else if (page?.route === "forgot_password") {
|
||||||
return t`Enter email address to reset password`
|
return t`Enter email address to reset password`
|
||||||
} else {
|
} else {
|
||||||
return t`Please sign in to your account`
|
return t`Please sign in to your account`
|
||||||
@@ -59,7 +59,7 @@ export default function () {
|
|||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-muted-foreground">{subtitle}</p>
|
<p className="text-sm text-muted-foreground">{subtitle}</p>
|
||||||
</div>
|
</div>
|
||||||
{page?.path === "/forgot-password" ? (
|
{page?.route === "forgot_password" ? (
|
||||||
<ForgotPassword />
|
<ForgotPassword />
|
||||||
) : (
|
) : (
|
||||||
<UserAuthForm isFirstRun={isFirstRun} authMethods={authMethods} />
|
<UserAuthForm isFirstRun={isFirstRun} authMethods={authMethods} />
|
||||||
|
@@ -10,7 +10,7 @@ import {
|
|||||||
UserIcon,
|
UserIcon,
|
||||||
UsersIcon,
|
UsersIcon,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { Link } from "./router"
|
import { $router, basePath, Link, prependBasePath } from "./router"
|
||||||
import { LangToggle } from "./lang-toggle"
|
import { LangToggle } from "./lang-toggle"
|
||||||
import { ModeToggle } from "./mode-toggle"
|
import { ModeToggle } from "./mode-toggle"
|
||||||
import { Logo } from "./logo"
|
import { Logo } from "./logo"
|
||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
} from "@/components/ui/dropdown-menu"
|
} from "@/components/ui/dropdown-menu"
|
||||||
import { AddSystemButton } from "./add-system"
|
import { AddSystemButton } from "./add-system"
|
||||||
import { Trans } from "@lingui/macro"
|
import { Trans } from "@lingui/macro"
|
||||||
|
import { getPagePath } from "@nanostores/router"
|
||||||
|
|
||||||
const CommandPalette = lazy(() => import("./command-palette"))
|
const CommandPalette = lazy(() => import("./command-palette"))
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0
|
|||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center h-14 md:h-16 bg-card px-4 pe-3 sm:px-6 border border-border/60 bt-0 rounded-md my-4">
|
<div className="flex items-center h-14 md:h-16 bg-card px-4 pe-3 sm:px-6 border border-border/60 bt-0 rounded-md my-4">
|
||||||
<Link href="/" aria-label="Home" className="p-2 ps-0 me-3">
|
<Link href={basePath} aria-label="Home" className="p-2 ps-0 me-3">
|
||||||
<Logo className="h-[1.1rem] md:h-5 fill-foreground" />
|
<Logo className="h-[1.1rem] md:h-5 fill-foreground" />
|
||||||
</Link>
|
</Link>
|
||||||
<SearchButton />
|
<SearchButton />
|
||||||
@@ -44,7 +45,7 @@ export default function Navbar() {
|
|||||||
<LangToggle />
|
<LangToggle />
|
||||||
<ModeToggle />
|
<ModeToggle />
|
||||||
<Link
|
<Link
|
||||||
href="/settings/general"
|
href={getPagePath($router, "settings", { name: "general" })}
|
||||||
aria-label="Settings"
|
aria-label="Settings"
|
||||||
className={cn("", buttonVariants({ variant: "ghost", size: "icon" }))}
|
className={cn("", buttonVariants({ variant: "ghost", size: "icon" }))}
|
||||||
>
|
>
|
||||||
@@ -63,7 +64,7 @@ export default function Navbar() {
|
|||||||
{isAdmin() && (
|
{isAdmin() && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<a href="/_/" target="_blank">
|
<a href={prependBasePath("/_/")} target="_blank">
|
||||||
<UsersIcon className="me-2.5 h-4 w-4" />
|
<UsersIcon className="me-2.5 h-4 w-4" />
|
||||||
<span>
|
<span>
|
||||||
<Trans>Users</Trans>
|
<Trans>Users</Trans>
|
||||||
@@ -71,7 +72,7 @@ export default function Navbar() {
|
|||||||
</a>
|
</a>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<a href="/_/#/collections?collection=systems" target="_blank">
|
<a href={prependBasePath("/_/#/collections?collection=systems")} target="_blank">
|
||||||
<ServerIcon className="me-2.5 h-4 w-4" />
|
<ServerIcon className="me-2.5 h-4 w-4" />
|
||||||
<span>
|
<span>
|
||||||
<Trans>Systems</Trans>
|
<Trans>Systems</Trans>
|
||||||
@@ -79,7 +80,7 @@ export default function Navbar() {
|
|||||||
</a>
|
</a>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<a href="/_/#/logs" target="_blank">
|
<a href={prependBasePath("/_/#/logs")} target="_blank">
|
||||||
<LogsIcon className="me-2.5 h-4 w-4" />
|
<LogsIcon className="me-2.5 h-4 w-4" />
|
||||||
<span>
|
<span>
|
||||||
<Trans>Logs</Trans>
|
<Trans>Logs</Trans>
|
||||||
@@ -87,7 +88,7 @@ export default function Navbar() {
|
|||||||
</a>
|
</a>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<a href="/_/#/settings/backups" target="_blank">
|
<a href={prependBasePath("/_/#/settings/backups")} target="_blank">
|
||||||
<DatabaseBackupIcon className="me-2.5 h-4 w-4" />
|
<DatabaseBackupIcon className="me-2.5 h-4 w-4" />
|
||||||
<span>
|
<span>
|
||||||
<Trans>Backups</Trans>
|
<Trans>Backups</Trans>
|
||||||
|
@@ -1,16 +1,36 @@
|
|||||||
import { createRouter } from "@nanostores/router"
|
import { createRouter } from "@nanostores/router"
|
||||||
|
|
||||||
export const $router = createRouter(
|
const routes = {
|
||||||
{
|
home: "/",
|
||||||
home: "/",
|
system: `/system/:name`,
|
||||||
server: "/system/:name",
|
settings: `/settings/:name?`,
|
||||||
settings: "/settings/:name?",
|
forgot_password: `/forgot-password`,
|
||||||
forgot_password: "/forgot-password",
|
} as const
|
||||||
},
|
|
||||||
{ links: false }
|
|
||||||
)
|
|
||||||
|
|
||||||
/** Navigate to url using router */
|
/**
|
||||||
|
* The base path of the application.
|
||||||
|
* This is used to prepend the base path to all routes.
|
||||||
|
*/
|
||||||
|
export const basePath = window.BASE_PATH || ""
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepends the base path to the given path.
|
||||||
|
* @param path The path to prepend the base path to.
|
||||||
|
* @returns The path with the base path prepended.
|
||||||
|
*/
|
||||||
|
export const prependBasePath = (path: string) => (basePath + path).replaceAll("//", "/")
|
||||||
|
|
||||||
|
// prepend base path to routes
|
||||||
|
for (const route in routes) {
|
||||||
|
// @ts-ignore need as const above to get nanostores to parse types properly
|
||||||
|
routes[route] = prependBasePath(routes[route])
|
||||||
|
}
|
||||||
|
|
||||||
|
export const $router = createRouter(routes, { links: false })
|
||||||
|
|
||||||
|
/** Navigate to url using router
|
||||||
|
* Base path is automatically prepended if serving from subpath
|
||||||
|
*/
|
||||||
export const navigate = (urlString: string) => {
|
export const navigate = (urlString: string) => {
|
||||||
$router.open(urlString)
|
$router.open(urlString)
|
||||||
}
|
}
|
||||||
|
@@ -7,8 +7,9 @@ import { Separator } from "../ui/separator"
|
|||||||
import { alertInfo, updateRecordList, updateSystemList } from "@/lib/utils"
|
import { alertInfo, updateRecordList, updateSystemList } from "@/lib/utils"
|
||||||
import { AlertRecord, SystemRecord } from "@/types"
|
import { AlertRecord, SystemRecord } from "@/types"
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
||||||
import { Link } from "../router"
|
import { $router, Link } from "../router"
|
||||||
import { Plural, t, Trans } from "@lingui/macro"
|
import { Plural, t, Trans } from "@lingui/macro"
|
||||||
|
import { getPagePath } from "@nanostores/router"
|
||||||
|
|
||||||
const SystemsTable = lazy(() => import("../systems-table/systems-table"))
|
const SystemsTable = lazy(() => import("../systems-table/systems-table"))
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@ export default function Home() {
|
|||||||
</Trans>
|
</Trans>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
<Link
|
<Link
|
||||||
href={`/system/${encodeURIComponent(alert.sysname!)}`}
|
href={getPagePath($router, "system", { name: alert.sysname! })}
|
||||||
className="absolute inset-0 w-full h-full"
|
className="absolute inset-0 w-full h-full"
|
||||||
aria-label="View system"
|
aria-label="View system"
|
||||||
></Link>
|
></Link>
|
||||||
|
@@ -4,7 +4,7 @@ import { SidebarNav } from "./sidebar-nav.tsx"
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card.tsx"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card.tsx"
|
||||||
import { useStore } from "@nanostores/react"
|
import { useStore } from "@nanostores/react"
|
||||||
import { $router } from "@/components/router.tsx"
|
import { $router } from "@/components/router.tsx"
|
||||||
import { redirectPage } from "@nanostores/router"
|
import { getPagePath, redirectPage } from "@nanostores/router"
|
||||||
import { BellIcon, FileSlidersIcon, SettingsIcon } from "lucide-react"
|
import { BellIcon, FileSlidersIcon, SettingsIcon } from "lucide-react"
|
||||||
import { $userSettings, pb } from "@/lib/stores.ts"
|
import { $userSettings, pb } from "@/lib/stores.ts"
|
||||||
import { toast } from "@/components/ui/use-toast.ts"
|
import { toast } from "@/components/ui/use-toast.ts"
|
||||||
@@ -49,17 +49,17 @@ export default function SettingsLayout() {
|
|||||||
const sidebarNavItems = [
|
const sidebarNavItems = [
|
||||||
{
|
{
|
||||||
title: _(t({ message: `General`, comment: "Context: General settings" })),
|
title: _(t({ message: `General`, comment: "Context: General settings" })),
|
||||||
href: "/settings/general",
|
href: getPagePath($router, "settings", { name: "general" }),
|
||||||
icon: SettingsIcon,
|
icon: SettingsIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t`Notifications`,
|
title: t`Notifications`,
|
||||||
href: "/settings/notifications",
|
href: getPagePath($router, "settings", { name: "notifications" }),
|
||||||
icon: BellIcon,
|
icon: BellIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t`YAML Config`,
|
title: t`YAML Config`,
|
||||||
href: "/settings/config",
|
href: getPagePath($router, "settings", { name: "config" }),
|
||||||
icon: FileSlidersIcon,
|
icon: FileSlidersIcon,
|
||||||
admin: true,
|
admin: true,
|
||||||
},
|
},
|
||||||
@@ -69,8 +69,8 @@ export default function SettingsLayout() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = t`Settings` + " / Beszel"
|
document.title = t`Settings` + " / Beszel"
|
||||||
// redirect to account page if no page is specified
|
// @ts-ignore redirect to account page if no page is specified
|
||||||
if (page?.path === "/settings") {
|
if (!page?.params?.name) {
|
||||||
redirectPage($router, "settings", { name: "general" })
|
redirectPage($router, "settings", { name: "general" })
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
@@ -13,6 +13,7 @@ import { saveSettings } from "./layout"
|
|||||||
import * as v from "valibot"
|
import * as v from "valibot"
|
||||||
import { isAdmin } from "@/lib/utils"
|
import { isAdmin } from "@/lib/utils"
|
||||||
import { Trans, t } from "@lingui/macro"
|
import { Trans, t } from "@lingui/macro"
|
||||||
|
import { prependBasePath } from "@/components/router"
|
||||||
|
|
||||||
interface ShoutrrrUrlCardProps {
|
interface ShoutrrrUrlCardProps {
|
||||||
url: string
|
url: string
|
||||||
@@ -94,7 +95,7 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
|
|||||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||||
<Trans>
|
<Trans>
|
||||||
Please{" "}
|
Please{" "}
|
||||||
<a href="/_/#/settings/mail" className="link" target="_blank">
|
<a href={prependBasePath("/_/#/settings/mail")} className="link" target="_blank">
|
||||||
configure an SMTP server
|
configure an SMTP server
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
to ensure alerts are delivered.
|
to ensure alerts are delivered.
|
||||||
|
@@ -22,7 +22,7 @@ export function SidebarNav({ className, items, ...props }: SidebarNavProps) {
|
|||||||
<>
|
<>
|
||||||
{/* Mobile View */}
|
{/* Mobile View */}
|
||||||
<div className="md:hidden">
|
<div className="md:hidden">
|
||||||
<Select onValueChange={(value: string) => navigate(value)} value={page?.path}>
|
<Select onValueChange={navigate} value={page?.path}>
|
||||||
<SelectTrigger className="w-full my-3.5">
|
<SelectTrigger className="w-full my-3.5">
|
||||||
<SelectValue placeholder="Select page" />
|
<SelectValue placeholder="Select page" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
@@ -65,13 +65,14 @@ import { $hubVersion, $systems, pb } from "@/lib/stores"
|
|||||||
import { useStore } from "@nanostores/react"
|
import { useStore } from "@nanostores/react"
|
||||||
import { cn, copyToClipboard, decimalString, isReadOnlyUser, useLocalStorage } from "@/lib/utils"
|
import { cn, copyToClipboard, decimalString, isReadOnlyUser, useLocalStorage } from "@/lib/utils"
|
||||||
import AlertsButton from "../alerts/alert-button"
|
import AlertsButton from "../alerts/alert-button"
|
||||||
import { Link, navigate } from "../router"
|
import { $router, Link, navigate } from "../router"
|
||||||
import { EthernetIcon } from "../ui/icons"
|
import { EthernetIcon } from "../ui/icons"
|
||||||
import { Trans, t } from "@lingui/macro"
|
import { Trans, t } from "@lingui/macro"
|
||||||
import { useLingui } from "@lingui/react"
|
import { useLingui } from "@lingui/react"
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
|
||||||
import { Input } from "../ui/input"
|
import { Input } from "../ui/input"
|
||||||
import { ClassValue } from "clsx"
|
import { ClassValue } from "clsx"
|
||||||
|
import { getPagePath } from "@nanostores/router"
|
||||||
|
|
||||||
type ViewMode = "table" | "grid"
|
type ViewMode = "table" | "grid"
|
||||||
|
|
||||||
@@ -406,7 +407,7 @@ export default function SystemsTable() {
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
const target = e.target as HTMLElement
|
const target = e.target as HTMLElement
|
||||||
if (!target.closest("[data-nolink]") && e.currentTarget.contains(target)) {
|
if (!target.closest("[data-nolink]") && e.currentTarget.contains(target)) {
|
||||||
navigate(`/system/${encodeURIComponent(row.original.name)}`)
|
navigate(getPagePath($router, "system", { name: row.original.name }))
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -489,7 +490,7 @@ export default function SystemsTable() {
|
|||||||
})}
|
})}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<Link
|
<Link
|
||||||
href={`/system/${encodeURIComponent(row.original.name)}`}
|
href={getPagePath($router, "system", { name: row.original.name })}
|
||||||
className="inset-0 absolute w-full h-full"
|
className="inset-0 absolute w-full h-full"
|
||||||
>
|
>
|
||||||
<span className="sr-only">{row.original.name}</span>
|
<span className="sr-only">{row.original.name}</span>
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
import PocketBase from "pocketbase"
|
import PocketBase from "pocketbase"
|
||||||
import { atom, map, WritableAtom } from "nanostores"
|
import { atom, map, WritableAtom } from "nanostores"
|
||||||
import { AlertRecord, ChartTimes, SystemRecord, UserSettings } from "@/types"
|
import { AlertRecord, ChartTimes, SystemRecord, UserSettings } from "@/types"
|
||||||
|
import { basePath } from "@/components/router"
|
||||||
|
|
||||||
/** PocketBase JS Client */
|
/** PocketBase JS Client */
|
||||||
export const pb = new PocketBase("/")
|
export const pb = new PocketBase(basePath)
|
||||||
|
|
||||||
/** Store if user is authenticated */
|
/** Store if user is authenticated */
|
||||||
export const $authenticated = atom(pb.authStore.isValid)
|
export const $authenticated = atom(pb.authStore.isValid)
|
||||||
|
@@ -63,9 +63,9 @@ const App = () => {
|
|||||||
|
|
||||||
if (!page) {
|
if (!page) {
|
||||||
return <h1 className="text-3xl text-center my-14">404</h1>
|
return <h1 className="text-3xl text-center my-14">404</h1>
|
||||||
} else if (page.path === "/") {
|
} else if (page.route === "home") {
|
||||||
return <Home />
|
return <Home />
|
||||||
} else if (page.route === "server") {
|
} else if (page.route === "system") {
|
||||||
return <SystemDetail name={page.params.name} />
|
return <SystemDetail name={page.params.name} />
|
||||||
} else if (page.route === "settings") {
|
} else if (page.route === "settings") {
|
||||||
return (
|
return (
|
||||||
|
7
beszel/site/src/types.d.ts
vendored
7
beszel/site/src/types.d.ts
vendored
@@ -1,5 +1,12 @@
|
|||||||
import { RecordModel } from "pocketbase"
|
import { RecordModel } from "pocketbase"
|
||||||
|
|
||||||
|
// global window properties
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
BASE_PATH: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface SystemRecord extends RecordModel {
|
export interface SystemRecord extends RecordModel {
|
||||||
name: string
|
name: string
|
||||||
host: string
|
host: string
|
||||||
|
@@ -4,6 +4,7 @@ import react from "@vitejs/plugin-react-swc"
|
|||||||
import { lingui } from "@lingui/vite-plugin"
|
import { lingui } from "@lingui/vite-plugin"
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
base: "./",
|
||||||
plugins: [
|
plugins: [
|
||||||
react({
|
react({
|
||||||
plugins: [["@lingui/swc-plugin", {}]],
|
plugins: [["@lingui/swc-plugin", {}]],
|
||||||
|
Reference in New Issue
Block a user