mirror of
https://github.com/henrygd/beszel.git
synced 2026-09-21 08:57:48 +02:00
feat: persist view preferences and language to user settings (#1831)
Co-authored-by: ChangkeunJ <reiot92@gmail.com>
This commit is contained in:
@@ -63,7 +63,7 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
|
|||||||
<Label className="block" htmlFor="lang">
|
<Label className="block" htmlFor="lang">
|
||||||
<Trans>Preferred Language</Trans>
|
<Trans>Preferred Language</Trans>
|
||||||
</Label>
|
</Label>
|
||||||
<Select value={i18n.locale} onValueChange={(lang: string) => dynamicActivate(lang)}>
|
<Select name="lang" value={i18n.locale} onValueChange={(lang: string) => dynamicActivate(lang)}>
|
||||||
<SelectTrigger id="lang">
|
<SelectTrigger id="lang">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { lazy, useEffect } from "react"
|
|||||||
import { $router } from "@/components/router.tsx"
|
import { $router } from "@/components/router.tsx"
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card.tsx"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card.tsx"
|
||||||
import { toast } from "@/components/ui/use-toast.ts"
|
import { toast } from "@/components/ui/use-toast.ts"
|
||||||
import { pb } from "@/lib/api"
|
import { saveUserSettings } from "@/lib/api"
|
||||||
import { $userSettings } from "@/lib/stores.ts"
|
import { $userSettings } from "@/lib/stores.ts"
|
||||||
import type { UserSettings } from "@/types"
|
import type { UserSettings } from "@/types"
|
||||||
import { Separator } from "../../ui/separator"
|
import { Separator } from "../../ui/separator"
|
||||||
@@ -36,24 +36,13 @@ const HeartbeatSettings = lazy(heartbeatSettingsImport)
|
|||||||
|
|
||||||
export async function saveSettings(newSettings: Partial<UserSettings>) {
|
export async function saveSettings(newSettings: Partial<UserSettings>) {
|
||||||
try {
|
try {
|
||||||
// get fresh copy of settings
|
await saveUserSettings(newSettings)
|
||||||
const req = await pb.collection("user_settings").getFirstListItem("", {
|
|
||||||
fields: "id,settings",
|
|
||||||
})
|
|
||||||
// update user settings
|
|
||||||
const updatedSettings = await pb.collection("user_settings").update(req.id, {
|
|
||||||
settings: {
|
|
||||||
...req.settings,
|
|
||||||
...newSettings,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
$userSettings.set(updatedSettings.settings)
|
|
||||||
toast({
|
toast({
|
||||||
title: t`Settings saved`,
|
title: t`Settings saved`,
|
||||||
description: t`Your user settings have been updated.`,
|
description: t`Your user settings have been updated.`,
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// console.error('update settings', e)
|
console.error("save settings", e)
|
||||||
toast({
|
toast({
|
||||||
title: t`Failed to save settings`,
|
title: t`Failed to save settings`,
|
||||||
description: t`Check logs for more details.`,
|
description: t`Check logs for more details.`,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useStore } from "@nanostores/react"
|
import { useStore } from "@nanostores/react"
|
||||||
import { getPagePath } from "@nanostores/router"
|
import { getPagePath } from "@nanostores/router"
|
||||||
import { subscribeKeys } from "nanostores"
|
import { subscribeKeys } from "nanostores"
|
||||||
import { useEffect, useMemo, useRef, useState } from "react"
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||||
import { useContainerChartConfigs } from "@/components/charts/hooks"
|
import { useContainerChartConfigs } from "@/components/charts/hooks"
|
||||||
import { pb } from "@/lib/api"
|
import { pb, queueUserSettings } from "@/lib/api"
|
||||||
import { SystemStatus } from "@/lib/enums"
|
import { SystemStatus } from "@/lib/enums"
|
||||||
import {
|
import {
|
||||||
$allSystemsById,
|
$allSystemsById,
|
||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
$systems,
|
$systems,
|
||||||
$userSettings,
|
$userSettings,
|
||||||
} from "@/lib/stores"
|
} from "@/lib/stores"
|
||||||
import { chartTimeData, listen, parseSemVer, useBrowserStorage } from "@/lib/utils"
|
import { chartTimeData, listen, parseSemVer } from "@/lib/utils"
|
||||||
import type {
|
import type {
|
||||||
ChartData,
|
ChartData,
|
||||||
ContainerStatsRecord,
|
ContainerStatsRecord,
|
||||||
@@ -35,8 +35,42 @@ export function useSystemData(id: string) {
|
|||||||
const systems = useStore($systems)
|
const systems = useStore($systems)
|
||||||
const chartTime = useStore($chartTime)
|
const chartTime = useStore($chartTime)
|
||||||
const maxValues = useStore($maxValues)
|
const maxValues = useStore($maxValues)
|
||||||
const [grid, setGrid] = useBrowserStorage("grid", true)
|
const [grid, _setGrid] = useState<boolean>(
|
||||||
const [displayMode, setDisplayMode] = useBrowserStorage<"default" | "tabs">("displayMode", "default")
|
() => $userSettings.get().grid ?? JSON.parse(localStorage.getItem("besz-grid") ?? "null") ?? true
|
||||||
|
)
|
||||||
|
const [displayMode, _setDisplayMode] = useState<"default" | "tabs">(
|
||||||
|
() =>
|
||||||
|
$userSettings.get().displayMode ??
|
||||||
|
(JSON.parse(localStorage.getItem("besz-displayMode") || "null") as "default" | "tabs" | null) ??
|
||||||
|
"default"
|
||||||
|
)
|
||||||
|
|
||||||
|
const applied = useRef(new Set<string>())
|
||||||
|
useEffect(() => {
|
||||||
|
return subscribeKeys($userSettings, ["grid", "displayMode"], (vals) => {
|
||||||
|
if (!applied.current.has("grid") && vals.grid !== undefined) {
|
||||||
|
applied.current.add("grid")
|
||||||
|
_setGrid(vals.grid)
|
||||||
|
}
|
||||||
|
if (!applied.current.has("displayMode") && vals.displayMode !== undefined) {
|
||||||
|
applied.current.add("displayMode")
|
||||||
|
_setDisplayMode(vals.displayMode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const setGrid = useCallback((v: boolean) => {
|
||||||
|
_setGrid(v)
|
||||||
|
localStorage.setItem("besz-grid", JSON.stringify(v))
|
||||||
|
$userSettings.setKey("grid", v)
|
||||||
|
queueUserSettings({ grid: v })
|
||||||
|
}, [])
|
||||||
|
const setDisplayMode = useCallback((v: "default" | "tabs") => {
|
||||||
|
_setDisplayMode(v)
|
||||||
|
localStorage.setItem("besz-displayMode", JSON.stringify(v))
|
||||||
|
$userSettings.setKey("displayMode", v)
|
||||||
|
queueUserSettings({ displayMode: v })
|
||||||
|
}, [])
|
||||||
const [activeTab, setActiveTabRaw] = useState("core")
|
const [activeTab, setActiveTabRaw] = useState("core")
|
||||||
const [mountedTabs, setMountedTabs] = useState(() => new Set<string>(["core"]))
|
const [mountedTabs, setMountedTabs] = useState(() => new Set<string>(["core"]))
|
||||||
const tabsRef = useRef<string[]>(["core", "disk"])
|
const tabsRef = useRef<string[]>(["core", "disk"])
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Trans, useLingui } from "@lingui/react/macro"
|
import { Trans, useLingui } from "@lingui/react/macro"
|
||||||
import { useStore } from "@nanostores/react"
|
import { useStore } from "@nanostores/react"
|
||||||
|
import { subscribeKeys } from "nanostores"
|
||||||
import { getPagePath } from "@nanostores/router"
|
import { getPagePath } from "@nanostores/router"
|
||||||
import {
|
import {
|
||||||
type ColumnDef,
|
type ColumnDef,
|
||||||
@@ -26,7 +27,7 @@ import {
|
|||||||
Settings2Icon,
|
Settings2Icon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { memo, useEffect, useMemo, useRef, useState } from "react"
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -42,8 +43,9 @@ import {
|
|||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
import { TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||||
import { SystemStatus } from "@/lib/enums"
|
import { SystemStatus } from "@/lib/enums"
|
||||||
import { $downSystems, $pausedSystems, $systems, $upSystems } from "@/lib/stores"
|
import { queueUserSettings } from "@/lib/api"
|
||||||
import { cn, runOnce, useBrowserStorage } from "@/lib/utils"
|
import { $downSystems, $pausedSystems, $systems, $upSystems, $userSettings } from "@/lib/stores"
|
||||||
|
import { cn, runOnce } from "@/lib/utils"
|
||||||
import type { SystemRecord } from "@/types"
|
import type { SystemRecord } from "@/types"
|
||||||
import AlertButton from "../alerts/alert-button"
|
import AlertButton from "../alerts/alert-button"
|
||||||
import { $router, Link } from "../router"
|
import { $router, Link } from "../router"
|
||||||
@@ -62,14 +64,83 @@ export default function SystemsTable() {
|
|||||||
const pausedSystems = $pausedSystems.get()
|
const pausedSystems = $pausedSystems.get()
|
||||||
const { i18n, t } = useLingui()
|
const { i18n, t } = useLingui()
|
||||||
const [filter, setFilter] = useState<string>("")
|
const [filter, setFilter] = useState<string>("")
|
||||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all")
|
const [statusFilter, setStatusFilter] = useState<StatusFilter>(
|
||||||
const [sorting, setSorting] = useBrowserStorage<SortingState>(
|
() =>
|
||||||
"sortMode",
|
$userSettings.get().statusFilter ??
|
||||||
[{ id: "system", desc: false }],
|
(JSON.parse(localStorage.getItem("besz-statusFilter") || "null") as StatusFilter | null) ??
|
||||||
sessionStorage
|
"all"
|
||||||
|
)
|
||||||
|
const [sorting, setSorting] = useState<SortingState>(
|
||||||
|
() =>
|
||||||
|
$userSettings.get().sortMode ??
|
||||||
|
JSON.parse(sessionStorage.getItem("besz-sortMode") || "null") ?? [{ id: "system", desc: false }]
|
||||||
)
|
)
|
||||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
||||||
const [columnVisibility, setColumnVisibility] = useBrowserStorage<VisibilityState>("cols", {})
|
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
|
||||||
|
() => $userSettings.get().cols ?? JSON.parse(localStorage.getItem("besz-cols") || "{}")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Apply settings from server once they load (handles incognito / new devices)
|
||||||
|
const applied = useRef(new Set<string>())
|
||||||
|
useEffect(() => {
|
||||||
|
return subscribeKeys($userSettings, ["cols", "statusFilter", "viewMode", "sortMode"], (vals) => {
|
||||||
|
if (!applied.current.has("cols") && vals.cols !== undefined) {
|
||||||
|
applied.current.add("cols")
|
||||||
|
setColumnVisibility(vals.cols)
|
||||||
|
}
|
||||||
|
if (!applied.current.has("statusFilter") && vals.statusFilter !== undefined) {
|
||||||
|
applied.current.add("statusFilter")
|
||||||
|
setStatusFilter(vals.statusFilter)
|
||||||
|
}
|
||||||
|
if (!applied.current.has("viewMode") && vals.viewMode !== undefined) {
|
||||||
|
applied.current.add("viewMode")
|
||||||
|
setViewMode(vals.viewMode)
|
||||||
|
}
|
||||||
|
if (!applied.current.has("sortMode") && vals.sortMode !== undefined) {
|
||||||
|
applied.current.add("sortMode")
|
||||||
|
setSorting(vals.sortMode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleColumnVisibilityChange = useCallback(
|
||||||
|
(updater: VisibilityState | ((prev: VisibilityState) => VisibilityState)) => {
|
||||||
|
setColumnVisibility((prev) => {
|
||||||
|
const next = typeof updater === "function" ? updater(prev) : updater
|
||||||
|
localStorage.setItem("besz-cols", JSON.stringify(next))
|
||||||
|
$userSettings.setKey("cols", next)
|
||||||
|
queueUserSettings({ cols: next })
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleStatusFilterChange = useCallback((value: string) => {
|
||||||
|
const next = value as StatusFilter
|
||||||
|
setStatusFilter(next)
|
||||||
|
localStorage.setItem("besz-statusFilter", JSON.stringify(next))
|
||||||
|
$userSettings.setKey("statusFilter", next)
|
||||||
|
queueUserSettings({ statusFilter: next })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleViewModeChange = useCallback((view: string) => {
|
||||||
|
const next = view as ViewMode
|
||||||
|
setViewMode(next)
|
||||||
|
localStorage.setItem("besz-viewMode", JSON.stringify(next))
|
||||||
|
$userSettings.setKey("viewMode", next)
|
||||||
|
queueUserSettings({ viewMode: next })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleSortingChange = useCallback((updater: SortingState | ((prev: SortingState) => SortingState)) => {
|
||||||
|
setSorting((prev) => {
|
||||||
|
const next = typeof updater === "function" ? updater(prev) : updater
|
||||||
|
sessionStorage.setItem("besz-sortMode", JSON.stringify(next))
|
||||||
|
$userSettings.setKey("sortMode", next)
|
||||||
|
queueUserSettings({ sortMode: next })
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
const locale = i18n.locale
|
const locale = i18n.locale
|
||||||
|
|
||||||
@@ -87,10 +158,12 @@ export default function SystemsTable() {
|
|||||||
return Object.values(pausedSystems) ?? []
|
return Object.values(pausedSystems) ?? []
|
||||||
}, [data, statusFilter])
|
}, [data, statusFilter])
|
||||||
|
|
||||||
const [viewMode, setViewMode] = useBrowserStorage<ViewMode>(
|
const [viewMode, setViewMode] = useState<ViewMode>(
|
||||||
"viewMode",
|
() =>
|
||||||
// show grid view on mobile if there are less than 200 systems (looks better but table is more efficient)
|
$userSettings.get().viewMode ??
|
||||||
window.innerWidth < 1024 && filteredData.length < 200 ? "grid" : "table"
|
(JSON.parse(localStorage.getItem("besz-viewMode") || "null") as ViewMode | null) ??
|
||||||
|
// show grid view on mobile if there are less than 200 systems (looks better but table is more efficient)
|
||||||
|
(window.innerWidth < 1024 && filteredData.length < 200 ? "grid" : "table")
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -105,11 +178,11 @@ export default function SystemsTable() {
|
|||||||
data: filteredData,
|
data: filteredData,
|
||||||
columns: columnDefs,
|
columns: columnDefs,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
onSortingChange: setSorting,
|
onSortingChange: handleSortingChange,
|
||||||
getSortedRowModel: getSortedRowModel(),
|
getSortedRowModel: getSortedRowModel(),
|
||||||
onColumnFiltersChange: setColumnFilters,
|
onColumnFiltersChange: setColumnFilters,
|
||||||
getFilteredRowModel: getFilteredRowModel(),
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
onColumnVisibilityChange: setColumnVisibility,
|
onColumnVisibilityChange: handleColumnVisibilityChange,
|
||||||
state: {
|
state: {
|
||||||
sorting,
|
sorting,
|
||||||
columnFilters,
|
columnFilters,
|
||||||
@@ -181,11 +254,7 @@ export default function SystemsTable() {
|
|||||||
<Trans>Layout</Trans>
|
<Trans>Layout</Trans>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuRadioGroup
|
<DropdownMenuRadioGroup className="px-1 pb-1" value={viewMode} onValueChange={handleViewModeChange}>
|
||||||
className="px-1 pb-1"
|
|
||||||
value={viewMode}
|
|
||||||
onValueChange={(view) => setViewMode(view as ViewMode)}
|
|
||||||
>
|
|
||||||
<DropdownMenuRadioItem value="table" onSelect={(e) => e.preventDefault()} className="gap-2">
|
<DropdownMenuRadioItem value="table" onSelect={(e) => e.preventDefault()} className="gap-2">
|
||||||
<LayoutListIcon className="size-4" />
|
<LayoutListIcon className="size-4" />
|
||||||
<Trans>Table</Trans>
|
<Trans>Table</Trans>
|
||||||
@@ -206,7 +275,7 @@ export default function SystemsTable() {
|
|||||||
<DropdownMenuRadioGroup
|
<DropdownMenuRadioGroup
|
||||||
className="px-1 pb-1"
|
className="px-1 pb-1"
|
||||||
value={statusFilter}
|
value={statusFilter}
|
||||||
onValueChange={(value) => setStatusFilter(value as StatusFilter)}
|
onValueChange={handleStatusFilterChange}
|
||||||
>
|
>
|
||||||
<DropdownMenuRadioItem value="all" onSelect={(e) => e.preventDefault()}>
|
<DropdownMenuRadioItem value="all" onSelect={(e) => e.preventDefault()}>
|
||||||
<Trans>All Systems</Trans>
|
<Trans>All Systems</Trans>
|
||||||
@@ -245,7 +314,9 @@ export default function SystemsTable() {
|
|||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onSelect={(e) => {
|
onSelect={(e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setSorting([{ id: column.id, desc: sorting[0]?.id === column.id && !sorting[0]?.desc }])
|
handleSortingChange([
|
||||||
|
{ id: column.id, desc: sorting[0]?.id === column.id && !sorting[0]?.desc },
|
||||||
|
])
|
||||||
}}
|
}}
|
||||||
key={column.id}
|
key={column.id}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { t } from "@lingui/core/macro"
|
|||||||
import PocketBase from "pocketbase"
|
import PocketBase from "pocketbase"
|
||||||
import { basePath } from "@/components/router"
|
import { basePath } from "@/components/router"
|
||||||
import { toast } from "@/components/ui/use-toast"
|
import { toast } from "@/components/ui/use-toast"
|
||||||
|
import { dynamicActivate, getLocale } from "@/lib/i18n"
|
||||||
import type { ChartTimes, UserSettings } from "@/types"
|
import type { ChartTimes, UserSettings } from "@/types"
|
||||||
import { $alerts, $allSystemsById, $allSystemsByName, $userSettings } from "./stores"
|
import { $alerts, $allSystemsById, $allSystemsByName, $userSettings } from "./stores"
|
||||||
import { chartTimeData, debounce } from "./utils"
|
import { chartTimeData, debounce } from "./utils"
|
||||||
@@ -52,11 +53,45 @@ export function logOut() {
|
|||||||
pb.realtime.unsubscribe()
|
pb.realtime.unsubscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Save a partial update to user settings in database immediately */
|
||||||
|
export async function saveUserSettings(newSettings: Partial<UserSettings>) {
|
||||||
|
// get fresh copy of settings so concurrent changes aren't overwritten
|
||||||
|
const req = await pb.collection("user_settings").getFirstListItem("", { fields: "id,settings" })
|
||||||
|
const updatedSettings = await pb.collection("user_settings").update(req.id, {
|
||||||
|
settings: {
|
||||||
|
...req.settings,
|
||||||
|
...newSettings,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
$userSettings.set(updatedSettings.settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
// keys queued by queueUserSettings, flushed together in a single request so that
|
||||||
|
// two debounced saves for different keys can't race each other's read-modify-write
|
||||||
|
// and silently drop one of the changes
|
||||||
|
let queuedSettings: Partial<UserSettings> = {}
|
||||||
|
|
||||||
|
const flushQueuedSettings = debounce(() => {
|
||||||
|
const toSave = queuedSettings
|
||||||
|
queuedSettings = {}
|
||||||
|
if (Object.keys(toSave).length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
saveUserSettings(toSave).catch(console.error)
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
/** Queue a partial user settings update, merging with any other pending keys and saving them together after a debounce window */
|
||||||
|
export function queueUserSettings(newSettings: Partial<UserSettings>) {
|
||||||
|
queuedSettings = { ...queuedSettings, ...newSettings }
|
||||||
|
flushQueuedSettings()
|
||||||
|
}
|
||||||
|
|
||||||
/** Fetch or create user settings in database */
|
/** Fetch or create user settings in database */
|
||||||
export async function updateUserSettings() {
|
export async function updateUserSettings() {
|
||||||
try {
|
try {
|
||||||
const req = await pb.collection("user_settings").getFirstListItem("", { fields: "settings" })
|
const req = await pb.collection("user_settings").getFirstListItem("", { fields: "settings" })
|
||||||
$userSettings.set(req.settings)
|
$userSettings.set(req.settings)
|
||||||
|
dynamicActivate(req.settings.lang || getLocale())
|
||||||
return
|
return
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("get settings", e)
|
console.error("get settings", e)
|
||||||
@@ -65,6 +100,7 @@ export async function updateUserSettings() {
|
|||||||
try {
|
try {
|
||||||
const createdSettings = await pb.collection("user_settings").create({ user: pb.authStore.record?.id })
|
const createdSettings = await pb.collection("user_settings").create({ user: pb.authStore.record?.id })
|
||||||
$userSettings.set(createdSettings.settings)
|
$userSettings.set(createdSettings.settings)
|
||||||
|
dynamicActivate(createdSettings.settings.lang || getLocale())
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("create settings", e)
|
console.error("create settings", e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ const Layout = () => {
|
|||||||
|
|
||||||
const I18nApp = () => {
|
const I18nApp = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Activate a locale so I18nProvider can mount App and load the account settings.
|
||||||
dynamicActivate(getLocale())
|
dynamicActivate(getLocale())
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
7
internal/site/src/types.d.ts
vendored
7
internal/site/src/types.d.ts
vendored
@@ -370,6 +370,13 @@ export interface UserSettings {
|
|||||||
colorCrit?: number
|
colorCrit?: number
|
||||||
hourFormat?: HourFormat
|
hourFormat?: HourFormat
|
||||||
layoutWidth?: number
|
layoutWidth?: number
|
||||||
|
lang?: string
|
||||||
|
cols?: Record<string, boolean>
|
||||||
|
statusFilter?: "all" | "up" | "down" | "paused" | "pending"
|
||||||
|
viewMode?: "table" | "grid"
|
||||||
|
sortMode?: Array<{ id: string; desc: boolean }>
|
||||||
|
grid?: boolean
|
||||||
|
displayMode?: "default" | "tabs"
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChartDataContainer = {
|
type ChartDataContainer = {
|
||||||
|
|||||||
Reference in New Issue
Block a user