diff --git a/src/site/src/components/routes/settings/alerts-history-data-table.tsx b/src/site/src/components/routes/settings/alerts-history-data-table.tsx index 5f064d7..2cdd94f 100644 --- a/src/site/src/components/routes/settings/alerts-history-data-table.tsx +++ b/src/site/src/components/routes/settings/alerts-history-data-table.tsx @@ -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 ( diff --git a/src/site/src/components/routes/settings/config-yaml.tsx b/src/site/src/components/routes/settings/config-yaml.tsx index 2a3b8d8..4cbd251 100644 --- a/src/site/src/components/routes/settings/config-yaml.tsx +++ b/src/site/src/components/routes/settings/config-yaml.tsx @@ -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() { diff --git a/src/site/src/components/routes/settings/general.tsx b/src/site/src/components/routes/settings/general.tsx index 7b10214..6d8e4cd 100644 --- a/src/site/src/components/routes/settings/general.tsx +++ b/src/site/src/components/routes/settings/general.tsx @@ -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 Adjust display options for charts.

- - -

- Sets the default time range for charts when a system is viewed. -

+
+
+ + +
+
+ + +
+
diff --git a/src/site/src/components/routes/settings/layout.tsx b/src/site/src/components/routes/settings/layout.tsx index d64ce6b..e1ea4b8 100644 --- a/src/site/src/components/routes/settings/layout.tsx +++ b/src/site/src/components/routes/settings/layout.tsx @@ -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" }) } diff --git a/src/site/src/components/routes/settings/notifications.tsx b/src/site/src/components/routes/settings/notifications.tsx index 9bc3082..e9ec35f 100644 --- a/src/site/src/components/routes/settings/notifications.tsx +++ b/src/site/src/components/routes/settings/notifications.tsx @@ -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

Beszel uses{" "} - + Shoutrrr {" "} to integrate with popular notification services. diff --git a/src/site/src/components/routes/settings/sidebar-nav.tsx b/src/site/src/components/routes/settings/sidebar-nav.tsx index f77f5b9..5dc6805 100644 --- a/src/site/src/components/routes/settings/sidebar-nav.tsx +++ b/src/site/src/components/routes/settings/sidebar-nav.tsx @@ -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 { items: { diff --git a/src/site/src/components/routes/settings/tokens-fingerprints.tsx b/src/site/src/components/routes/settings/tokens-fingerprints.tsx index d734ac9..502525b 100644 --- a/src/site/src/components/routes/settings/tokens-fingerprints.tsx +++ b/src/site/src/components/routes/settings/tokens-fingerprints.tsx @@ -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", diff --git a/src/site/src/lib/enums.ts b/src/site/src/lib/enums.ts index 7fd219b..178c6b0 100644 --- a/src/site/src/lib/enums.ts +++ b/src/site/src/lib/enums.ts @@ -46,3 +46,10 @@ export enum BatteryState { Discharging, Idle, } + +/** Time format */ +export enum HourFormat { + // Default = "Default", + "12h" = "12h", + "24h" = "24h", +} diff --git a/src/site/src/lib/stores.ts b/src/site/src/lib/stores.ts index 917ac66..9cc564b 100644 --- a/src/site/src/lib/stores.ts +++ b/src/site/src/lib/stores.ts @@ -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({ 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("") diff --git a/src/site/src/lib/time.ts b/src/site/src/lib/time.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/site/src/lib/utils.ts b/src/site/src/lib/utils.ts index e7d42e0..fd24866 100644 --- a/src/site/src/lib/utils.ts +++ b/src/site/src/lib/utils.ts @@ -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", diff --git a/src/site/src/types.d.ts b/src/site/src/types.d.ts index cf47909..ea76f57 100644 --- a/src/site/src/types.d.ts +++ b/src/site/src/types.d.ts @@ -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>