mirror of
https://github.com/fankes/beszel.git
synced 2025-10-18 17:29:28 +08:00
add time format (12h, 24h) settings (#424)
- Some biome formatting :/ - Changed chartTime update to use listenkeys
This commit is contained in:
@@ -1,26 +1,16 @@
|
||||
import { pb } from "@/lib/api"
|
||||
import { cn, formatDuration, formatShortDate } from "@/lib/utils"
|
||||
import { alertInfo } from "@/lib/alerts"
|
||||
import { AlertsHistoryRecord } from "@/types"
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import {
|
||||
type ColumnFiltersState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
getFilteredRowModel,
|
||||
type SortingState,
|
||||
useReactTable,
|
||||
flexRender,
|
||||
ColumnFiltersState,
|
||||
SortingState,
|
||||
VisibilityState,
|
||||
type VisibilityState,
|
||||
} from "@tanstack/react-table"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { alertsHistoryColumns } from "../../alerts-history-columns"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { memo, useEffect, useState } from "react"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select"
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
@@ -29,9 +19,7 @@ import {
|
||||
DownloadIcon,
|
||||
Trash2Icon,
|
||||
} from "lucide-react"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
import { memo, useEffect, useState } from "react"
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -43,6 +31,18 @@ import {
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
import { alertInfo } from "@/lib/alerts"
|
||||
import { pb } from "@/lib/api"
|
||||
import { cn, formatDuration, formatShortDate } from "@/lib/utils"
|
||||
import type { AlertsHistoryRecord } from "@/types"
|
||||
import { alertsHistoryColumns } from "../../alerts-history-columns"
|
||||
|
||||
const SectionIntro = memo(() => {
|
||||
return (
|
||||
|
@@ -1,15 +1,15 @@
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { redirectPage } from "@nanostores/router"
|
||||
import { $router } from "@/components/router"
|
||||
import clsx from "clsx"
|
||||
import { AlertCircleIcon, FileSlidersIcon, LoaderCircleIcon } from "lucide-react"
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
||||
import { useState } from "react"
|
||||
import { $router } from "@/components/router"
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import clsx from "clsx"
|
||||
import { isAdmin, pb } from "@/lib/api"
|
||||
|
||||
export default function ConfigYaml() {
|
||||
|
@@ -1,18 +1,18 @@
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
/** biome-ignore-all lint/correctness/useUniqueElementIds: component is only rendered once */
|
||||
import { Trans, useLingui } from "@lingui/react/macro"
|
||||
import { LanguagesIcon, LoaderCircleIcon, SaveIcon } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { chartTimeData } from "@/lib/utils"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { LanguagesIcon, LoaderCircleIcon, SaveIcon } from "lucide-react"
|
||||
import { UserSettings } from "@/types"
|
||||
import { saveSettings } from "./layout"
|
||||
import { useState } from "react"
|
||||
import languages from "@/lib/languages"
|
||||
import { HourFormat, Unit } from "@/lib/enums"
|
||||
import { dynamicActivate } from "@/lib/i18n"
|
||||
import { useLingui } from "@lingui/react/macro"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Unit } from "@/lib/enums"
|
||||
import languages from "@/lib/languages"
|
||||
import { chartTimeData, currentHour12 } from "@/lib/utils"
|
||||
import type { UserSettings } from "@/types"
|
||||
import { saveSettings } from "./layout"
|
||||
|
||||
export default function SettingsProfilePage({ userSettings }: { userSettings: UserSettings }) {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
@@ -82,24 +82,46 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
|
||||
<Trans>Adjust display options for charts.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<Label className="block" htmlFor="chartTime">
|
||||
<Trans>Default time period</Trans>
|
||||
</Label>
|
||||
<Select name="chartTime" key={userSettings.chartTime} defaultValue={userSettings.chartTime}>
|
||||
<SelectTrigger id="chartTime">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(chartTimeData).map(([value, { label }]) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{label()}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-[0.8rem] text-muted-foreground">
|
||||
<Trans>Sets the default time range for charts when a system is viewed.</Trans>
|
||||
</p>
|
||||
<div className="grid sm:grid-cols-3 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label className="block" htmlFor="chartTime">
|
||||
<Trans>Default time period</Trans>
|
||||
</Label>
|
||||
<Select name="chartTime" key={userSettings.chartTime} defaultValue={userSettings.chartTime}>
|
||||
<SelectTrigger id="chartTime">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(chartTimeData).map(([value, { label }]) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{label()}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label className="block" htmlFor="hourFormat">
|
||||
<Trans>Time format</Trans>
|
||||
</Label>
|
||||
<Select
|
||||
name="hourFormat"
|
||||
key={userSettings.hourFormat}
|
||||
defaultValue={userSettings.hourFormat ?? (currentHour12() ? HourFormat["12h"] : HourFormat["24h"])}
|
||||
>
|
||||
<SelectTrigger id="hourFormat">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.keys(HourFormat).map((value) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="grid gap-2">
|
||||
|
@@ -1,18 +1,17 @@
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Trans, useLingui } from "@lingui/react/macro"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { getPagePath, redirectPage } from "@nanostores/router"
|
||||
import { AlertOctagonIcon, BellIcon, FileSlidersIcon, FingerprintIcon, SettingsIcon } from "lucide-react"
|
||||
import { lazy, useEffect } from "react"
|
||||
import { $router } from "@/components/router.tsx"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card.tsx"
|
||||
import { toast } from "@/components/ui/use-toast.ts"
|
||||
import { pb } from "@/lib/api"
|
||||
import { $userSettings } from "@/lib/stores.ts"
|
||||
import type { UserSettings } from "@/types"
|
||||
import { Separator } from "../../ui/separator"
|
||||
import { SidebarNav } from "./sidebar-nav.tsx"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card.tsx"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { $router } from "@/components/router.tsx"
|
||||
import { getPagePath, redirectPage } from "@nanostores/router"
|
||||
import { BellIcon, FileSlidersIcon, FingerprintIcon, SettingsIcon, AlertOctagonIcon } from "lucide-react"
|
||||
import { $userSettings } from "@/lib/stores.ts"
|
||||
import { toast } from "@/components/ui/use-toast.ts"
|
||||
import { UserSettings } from "@/types"
|
||||
import { useLingui } from "@lingui/react/macro"
|
||||
import { pb } from "@/lib/api"
|
||||
|
||||
const generalSettingsImport = () => import("./general.tsx")
|
||||
const notificationsSettingsImport = () => import("./notifications.tsx")
|
||||
@@ -93,9 +92,10 @@ export default function SettingsLayout() {
|
||||
|
||||
const page = useStore($router)
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: no dependencies
|
||||
useEffect(() => {
|
||||
document.title = t`Settings` + " / Beszel"
|
||||
// @ts-ignore redirect to account page if no page is specified
|
||||
document.title = `${t`Settings`} / Beszel`
|
||||
// @ts-expect-error redirect to account page if no page is specified
|
||||
if (!page?.params?.name) {
|
||||
redirectPage($router, "settings", { name: "general" })
|
||||
}
|
||||
|
@@ -1,19 +1,19 @@
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { BellIcon, LoaderCircleIcon, PlusIcon, SaveIcon, Trash2Icon } from "lucide-react"
|
||||
import { ChangeEventHandler, useEffect, useState } from "react"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import { InputTags } from "@/components/ui/input-tags"
|
||||
import { UserSettings } from "@/types"
|
||||
import { saveSettings } from "./layout"
|
||||
import { type ChangeEventHandler, useEffect, useState } from "react"
|
||||
import * as v from "valibot"
|
||||
import { prependBasePath } from "@/components/router"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { InputTags } from "@/components/ui/input-tags"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import { isAdmin, pb } from "@/lib/api"
|
||||
import type { UserSettings } from "@/types"
|
||||
import { saveSettings } from "./layout"
|
||||
|
||||
interface ShoutrrrUrlCardProps {
|
||||
url: string
|
||||
@@ -127,7 +127,7 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
<Trans>
|
||||
Beszel uses{" "}
|
||||
<a href="https://beszel.dev/guide/notifications" target="_blank" className="link">
|
||||
<a href="https://beszel.dev/guide/notifications" target="_blank" className="link" rel="noopener">
|
||||
Shoutrrr
|
||||
</a>{" "}
|
||||
to integrate with popular notification services.
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { isAdmin, isReadOnlyUser } from "@/lib/api"
|
||||
import { buttonVariants } from "../../ui/button"
|
||||
import { $router, Link, navigate } from "../../router"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import type React from "react"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { isAdmin, isReadOnlyUser } from "@/lib/api"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { $router, Link, navigate } from "../../router"
|
||||
import { buttonVariants } from "../../ui/button"
|
||||
|
||||
interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
|
||||
items: {
|
||||
|
@@ -1,9 +1,6 @@
|
||||
import { Trans, useLingui } from "@lingui/react/macro"
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { $publicKey } from "@/lib/stores"
|
||||
import { memo, useEffect, useMemo, useState } from "react"
|
||||
import { Table, TableCell, TableHead, TableBody, TableRow, TableHeader } from "@/components/ui/table"
|
||||
import { FingerprintRecord } from "@/types"
|
||||
import { Trans, useLingui } from "@lingui/react/macro"
|
||||
import { redirectPage } from "@nanostores/router"
|
||||
import {
|
||||
CopyIcon,
|
||||
FingerprintIcon,
|
||||
@@ -13,9 +10,17 @@ import {
|
||||
ServerIcon,
|
||||
Trash2Icon,
|
||||
} from "lucide-react"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import { cn, copyToClipboard, generateToken, getHubURL, tokenMap } from "@/lib/utils"
|
||||
import { isReadOnlyUser, pb } from "@/lib/api"
|
||||
import { memo, useEffect, useMemo, useState } from "react"
|
||||
import {
|
||||
copyDockerCompose,
|
||||
copyDockerRun,
|
||||
copyLinuxCommand,
|
||||
copyWindowsCommand,
|
||||
type DropdownItem,
|
||||
InstallDropdown,
|
||||
} from "@/components/install-dropdowns"
|
||||
import { $router } from "@/components/router"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -23,20 +28,15 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { AppleIcon, DockerIcon, TuxIcon, WindowsIcon } from "@/components/ui/icons"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import {
|
||||
copyDockerCompose,
|
||||
copyDockerRun,
|
||||
copyLinuxCommand,
|
||||
copyWindowsCommand,
|
||||
DropdownItem,
|
||||
InstallDropdown,
|
||||
} from "@/components/install-dropdowns"
|
||||
import { AppleIcon, DockerIcon, TuxIcon, WindowsIcon } from "@/components/ui/icons"
|
||||
import { redirectPage } from "@nanostores/router"
|
||||
import { $router } from "@/components/router"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import { isReadOnlyUser, pb } from "@/lib/api"
|
||||
import { $publicKey } from "@/lib/stores"
|
||||
import { cn, copyToClipboard, generateToken, getHubURL, tokenMap } from "@/lib/utils"
|
||||
import type { FingerprintRecord } from "@/types"
|
||||
|
||||
const pbFingerprintOptions = {
|
||||
expand: "system",
|
||||
|
@@ -46,3 +46,10 @@ export enum BatteryState {
|
||||
Discharging,
|
||||
Idle,
|
||||
}
|
||||
|
||||
/** Time format */
|
||||
export enum HourFormat {
|
||||
// Default = "Default",
|
||||
"12h" = "12h",
|
||||
"24h" = "24h",
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { atom, computed, map, type ReadableAtom } from "nanostores"
|
||||
import { atom, computed, listenKeys, map, type ReadableAtom } from "nanostores"
|
||||
import type { AlertMap, ChartTimes, SystemRecord, UserSettings } from "@/types"
|
||||
import { pb } from "./api"
|
||||
import { Unit } from "./enums"
|
||||
@@ -50,7 +50,7 @@ export const $userSettings = map<UserSettings>({
|
||||
unitTemp: Unit.Celsius,
|
||||
})
|
||||
// update chart time on change
|
||||
$userSettings.subscribe((value) => $chartTime.set(value.chartTime))
|
||||
listenKeys($userSettings, ["chartTime"], ({ chartTime }) => $chartTime.set(chartTime))
|
||||
|
||||
/** Container chart filter */
|
||||
export const $containerFilter = atom("")
|
||||
|
0
src/site/src/lib/time.ts
Normal file
0
src/site/src/lib/time.ts
Normal file
@@ -6,8 +6,9 @@ import { twMerge } from "tailwind-merge"
|
||||
import { prependBasePath } from "@/components/router"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import type { ChartTimeData, FingerprintRecord, SemVer, SystemRecord } from "@/types"
|
||||
import { MeterState, Unit } from "./enums"
|
||||
import { HourFormat, MeterState, Unit } from "./enums"
|
||||
import { $copyContent, $userSettings } from "./stores"
|
||||
import { listenKeys } from "nanostores"
|
||||
|
||||
export const FAVICON_DEFAULT = "favicon.svg"
|
||||
export const FAVICON_GREEN = "favicon-green.svg"
|
||||
@@ -36,24 +37,47 @@ export async function copyToClipboard(content: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const hourWithMinutesFormatter = new Intl.DateTimeFormat(undefined, {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
})
|
||||
// Create formatters directly without intermediate containers
|
||||
const createHourWithMinutesFormatter = (hour12?: boolean) =>
|
||||
new Intl.DateTimeFormat(undefined, {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
hour12,
|
||||
})
|
||||
|
||||
const createShortDateFormatter = (hour12?: boolean) =>
|
||||
new Intl.DateTimeFormat(undefined, {
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
hour12,
|
||||
})
|
||||
|
||||
// Initialize formatters with default values
|
||||
let hourWithMinutesFormatter = createHourWithMinutesFormatter()
|
||||
let shortDateFormatter = createShortDateFormatter()
|
||||
|
||||
export const currentHour12 = () => shortDateFormatter.resolvedOptions().hour12
|
||||
|
||||
export const hourWithMinutes = (timestamp: string) => {
|
||||
return hourWithMinutesFormatter.format(new Date(timestamp))
|
||||
}
|
||||
|
||||
const shortDateFormatter = new Intl.DateTimeFormat(undefined, {
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
})
|
||||
export const formatShortDate = (timestamp: string) => {
|
||||
return shortDateFormatter.format(new Date(timestamp))
|
||||
}
|
||||
|
||||
// Update the time formatters if user changes hourFormat
|
||||
listenKeys($userSettings, ["hourFormat"], ({ hourFormat }) => {
|
||||
if (!hourFormat) return
|
||||
const newHour12 = hourFormat === HourFormat["12h"]
|
||||
if (currentHour12() !== newHour12) {
|
||||
hourWithMinutesFormatter = createHourWithMinutesFormatter(newHour12)
|
||||
shortDateFormatter = createShortDateFormatter(newHour12)
|
||||
}
|
||||
})
|
||||
|
||||
const dayFormatter = new Intl.DateTimeFormat(undefined, {
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
|
29
src/site/src/types.d.ts
vendored
29
src/site/src/types.d.ts
vendored
@@ -1,5 +1,5 @@
|
||||
import { RecordModel } from "pocketbase"
|
||||
import { Unit, Os, BatteryState } from "./lib/enums"
|
||||
import type { RecordModel } from "pocketbase"
|
||||
import type { Unit, Os, BatteryState, HourFormat } from "./lib/enums"
|
||||
|
||||
// global window properties
|
||||
declare global {
|
||||
@@ -238,6 +238,7 @@ export interface UserSettings {
|
||||
unitDisk?: Unit
|
||||
colorWarn?: number
|
||||
colorCrit?: number
|
||||
hourFormat?: HourFormat
|
||||
}
|
||||
|
||||
type ChartDataContainer = {
|
||||
@@ -262,17 +263,17 @@ export interface ChartData {
|
||||
chartTime: ChartTimes
|
||||
}
|
||||
|
||||
interface AlertInfo {
|
||||
name: () => string
|
||||
unit: string
|
||||
icon: any
|
||||
desc: () => string
|
||||
max?: number
|
||||
min?: number
|
||||
step?: number
|
||||
start?: number
|
||||
/** Single value description (when there's only one value, like status) */
|
||||
singleDesc?: () => string
|
||||
}
|
||||
// interface AlertInfo {
|
||||
// name: () => string
|
||||
// unit: string
|
||||
// icon: any
|
||||
// desc: () => string
|
||||
// max?: number
|
||||
// min?: number
|
||||
// step?: number
|
||||
// start?: number
|
||||
// /** Single value description (when there's only one value, like status) */
|
||||
// singleDesc?: () => string
|
||||
// }
|
||||
|
||||
export type AlertMap = Record<string, Map<string, AlertRecord>>
|
||||
|
Reference in New Issue
Block a user