diff --git a/src/components/Unraid/Layout/UnraidLayout.tsx b/src/components/Unraid/Layout/UnraidLayout.tsx new file mode 100644 index 000000000..e0c22bb85 --- /dev/null +++ b/src/components/Unraid/Layout/UnraidLayout.tsx @@ -0,0 +1,279 @@ +/** + * Unraid Layout Component + * Main layout wrapper with sidebar navigation for Unraid pages + */ + +import { useState } from 'react'; +import { + AppShell, + Burger, + Group, + Header, + MediaQuery, + Navbar, + NavLink, + ScrollArea, + Text, + ThemeIcon, + Title, + useMantineColorScheme, + useMantineTheme, + ActionIcon, + Tooltip, + Badge, + Divider, + Box, +} from '@mantine/core'; +import { + IconDashboard, + IconDatabase, + IconBrandDocker, + IconServer2, + IconFolders, + IconSettings, + IconTools, + IconBell, + IconMoon, + IconSun, + IconChevronRight, + IconServer, + IconUsers, + IconNetwork, + IconShield, + IconCpu, + IconPlug, + IconFileText, + IconInfoCircle, + IconTerminal2, +} from '@tabler/icons-react'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; + +interface UnraidLayoutProps { + children: React.ReactNode; + notifications?: number; +} + +interface NavItem { + icon: React.FC<{ size?: number }>; + label: string; + href: string; + badge?: number | string; + children?: NavItem[]; +} + +const mainNavItems: NavItem[] = [ + { icon: IconDashboard, label: 'Dashboard', href: '/unraid' }, + { icon: IconDatabase, label: 'Array', href: '/unraid/array' }, + { icon: IconBrandDocker, label: 'Docker', href: '/unraid/docker' }, + { icon: IconServer2, label: 'VMs', href: '/unraid/vms' }, + { icon: IconFolders, label: 'Shares', href: '/unraid/shares' }, + { icon: IconUsers, label: 'Users', href: '/unraid/users' }, +]; + +const settingsNavItems: NavItem[] = [ + { icon: IconServer, label: 'Identification', href: '/unraid/settings/identification' }, + { icon: IconDatabase, label: 'Disk Settings', href: '/unraid/settings/disk' }, + { icon: IconNetwork, label: 'Network', href: '/unraid/settings/network' }, + { icon: IconBrandDocker, label: 'Docker', href: '/unraid/settings/docker' }, + { icon: IconServer2, label: 'VM Manager', href: '/unraid/settings/vm' }, + { icon: IconShield, label: 'Management Access', href: '/unraid/settings/management' }, + { icon: IconCpu, label: 'CPU Pinning', href: '/unraid/settings/cpu' }, + { icon: IconBell, label: 'Notifications', href: '/unraid/settings/notifications' }, +]; + +const toolsNavItems: NavItem[] = [ + { icon: IconFileText, label: 'System Log', href: '/unraid/tools/syslog' }, + { icon: IconInfoCircle, label: 'Diagnostics', href: '/unraid/tools/diagnostics' }, + { icon: IconCpu, label: 'System Devices', href: '/unraid/tools/devices' }, + { icon: IconTerminal2, label: 'Terminal', href: '/unraid/tools/terminal' }, + { icon: IconPlug, label: 'Plugins', href: '/unraid/tools/plugins' }, +]; + +function NavSection({ + title, + items, + currentPath, +}: { + title?: string; + items: NavItem[]; + currentPath: string; +}) { + return ( + <> + {title && ( + + {title} + + )} + {items.map((item) => ( + + + + } + active={currentPath === item.href} + rightSection={ + item.badge ? ( + + {item.badge} + + ) : ( + + ) + } + sx={(theme) => ({ + borderRadius: '0 9999px 9999px 0', + marginRight: theme.spacing.sm, + '&[data-active]': { + backgroundColor: + theme.colorScheme === 'dark' + ? 'rgba(255, 255, 255, 0.1)' + : 'rgba(0, 0, 0, 0.05)', + color: theme.colors.blue[theme.colorScheme === 'dark' ? 4 : 7], + fontWeight: 500, + }, + })} + /> + ))} + + ); +} + +export function UnraidLayout({ children, notifications = 0 }: UnraidLayoutProps) { + const theme = useMantineTheme(); + const { colorScheme, toggleColorScheme } = useMantineColorScheme(); + const [opened, setOpened] = useState(false); + const router = useRouter(); + + return ( + + ); +} + +export default UnraidLayout; diff --git a/src/components/Unraid/Layout/index.ts b/src/components/Unraid/Layout/index.ts new file mode 100644 index 000000000..3a471e0e4 --- /dev/null +++ b/src/components/Unraid/Layout/index.ts @@ -0,0 +1 @@ +export { UnraidLayout } from './UnraidLayout'; diff --git a/src/pages/unraid/array/index.tsx b/src/pages/unraid/array/index.tsx new file mode 100644 index 000000000..13a5a8b76 --- /dev/null +++ b/src/pages/unraid/array/index.tsx @@ -0,0 +1,726 @@ +/** + * Array Management Page + * Detailed view of array devices, parity, and pools + */ + +import { useState } from 'react'; +import { + Alert, + Badge, + Button, + Card, + Container, + Divider, + Grid, + Group, + Loader, + Menu, + Paper, + Progress, + RingProgress, + SegmentedControl, + SimpleGrid, + Stack, + Table, + Tabs, + Text, + ThemeIcon, + Title, + Tooltip, +} from '@mantine/core'; +import { notifications } from '@mantine/notifications'; +import { + IconDatabase, + IconHardDrive, + IconTemperature, + IconPlayerPlay, + IconPlayerStop, + IconPlayerPause, + IconRefresh, + IconAlertCircle, + IconCheck, + IconChevronDown, + IconShield, + IconCpu, + IconArrowsUpDown, +} from '@tabler/icons-react'; +import { GetServerSidePropsContext } from 'next'; + +import { UnraidLayout } from '~/components/Unraid/Layout'; +import { getServerAuthSession } from '~/server/auth'; +import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations'; +import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder'; +import { api } from '~/utils/api'; +import type { ArrayDisk } from '~/lib/unraid/types'; + +function formatBytes(bytes: number): string { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +} + +function getStatusColor(status: string): string { + switch (status) { + case 'DISK_OK': + return 'green'; + case 'DISK_INVALID': + case 'DISK_WRONG': + return 'red'; + case 'DISK_DSBL': + case 'DISK_DSBL_NEW': + return 'orange'; + case 'DISK_NEW': + return 'blue'; + case 'DISK_NP': + return 'gray'; + default: + return 'gray'; + } +} + +function DiskDetailsRow({ disk }: { disk: ArrayDisk }) { + const usedPercent = + disk.fsSize && disk.fsUsed ? ((disk.fsUsed / disk.fsSize) * 100).toFixed(1) : null; + + return ( + + + + + + +
+ + {disk.name} + + + {disk.device} + +
+
+ + + + {disk.model} + + + {disk.serial} + + + + + {disk.status.replace('DISK_', '')} + + + + {formatBytes(disk.size)} + + + + {disk.fsType || 'N/A'} + + + + {disk.temp !== null ? ( + + + 50 ? 'red' : disk.temp > 40 ? 'orange' : undefined} + > + {disk.temp}°C + + + ) : ( + + {disk.spunDown ? 'Standby' : '-'} + + )} + + + + + + R: {disk.numReads.toLocaleString()} + + + + + W: {disk.numWrites.toLocaleString()} + + + + + + {disk.numErrors > 0 ? ( + + {disk.numErrors} + + ) : ( + + 0 + + )} + + + {usedPercent ? ( + + 90 + ? 'red' + : parseFloat(usedPercent) > 75 + ? 'orange' + : 'blue' + } + /> + + ) : ( + + - + + )} + + + ); +} + +export default function ArrayPage() { + const [arrayLoading, setArrayLoading] = useState(false); + const [parityLoading, setParityLoading] = useState(false); + + const { + data: array, + isLoading, + error, + refetch, + } = api.unraid.array.useQuery(undefined, { + refetchInterval: 5000, + }); + + const startArray = api.unraid.startArray.useMutation({ + onMutate: () => setArrayLoading(true), + onSettled: () => setArrayLoading(false), + onSuccess: () => { + notifications.show({ + title: 'Array Starting', + message: 'Array is starting...', + color: 'green', + icon: , + }); + refetch(); + }, + onError: (err) => { + notifications.show({ + title: 'Error', + message: err.message, + color: 'red', + }); + }, + }); + + const stopArray = api.unraid.stopArray.useMutation({ + onMutate: () => setArrayLoading(true), + onSettled: () => setArrayLoading(false), + onSuccess: () => { + notifications.show({ + title: 'Array Stopping', + message: 'Array is stopping...', + color: 'green', + icon: , + }); + refetch(); + }, + }); + + const startParityCheck = api.unraid.startParityCheck.useMutation({ + onMutate: () => setParityLoading(true), + onSettled: () => setParityLoading(false), + onSuccess: () => { + notifications.show({ + title: 'Parity Check Started', + message: 'Parity check has started', + color: 'green', + }); + refetch(); + }, + }); + + const pauseParityCheck = api.unraid.pauseParityCheck.useMutation({ + onMutate: () => setParityLoading(true), + onSettled: () => setParityLoading(false), + onSuccess: () => { + notifications.show({ + title: 'Parity Check Paused', + message: 'Parity check has been paused', + color: 'yellow', + }); + refetch(); + }, + }); + + const cancelParityCheck = api.unraid.cancelParityCheck.useMutation({ + onMutate: () => setParityLoading(true), + onSettled: () => setParityLoading(false), + onSuccess: () => { + notifications.show({ + title: 'Parity Check Cancelled', + message: 'Parity check has been cancelled', + color: 'orange', + }); + refetch(); + }, + }); + + if (isLoading) { + return ( + + + + + Loading array data... + + + + ); + } + + if (error) { + return ( + + + } title="Error" color="red"> + {error.message} + + + + ); + } + + if (!array) { + return ( + + + } title="No Data" color="yellow"> + No array data available + + + + ); + } + + const isStarted = array.state === 'STARTED'; + const usedPercent = ((array.capacity.used / array.capacity.total) * 100).toFixed(1); + + return ( + + + + {/* Header */} + + + + + +
+ Array Devices + + Manage array, parity, and cache pools + +
+
+ + + + {array.state} + + + {isStarted ? ( + + ) : ( + + )} + +
+ + {/* Capacity Overview */} + + + + +
+ + Total Capacity + + + {formatBytes(array.capacity.total)} + +
+ 90 + ? 'red' + : parseFloat(usedPercent) > 75 + ? 'orange' + : 'blue', + }, + ]} + label={ + + {usedPercent}% + + } + /> +
+
+
+ + + + + Used Space + + + {formatBytes(array.capacity.used)} + + + across {array.disks.length} data disks + + + + + + + + Free Space + + + {formatBytes(array.capacity.free)} + + + available for new data + + + +
+ + {/* Parity Check Status */} + {array.parityCheckStatus && ( + + + + + + +
+ Parity Check {array.parityCheckStatus.running ? 'In Progress' : 'Status'} + + {array.parityCheckStatus.errors} errors found + +
+
+ + + {array.parityCheckStatus.running ? ( + <> + + + + ) : ( + + + + + + startParityCheck.mutate({ correct: false })}> + Check Only + + startParityCheck.mutate({ correct: true })}> + Check + Correct + + + + )} + +
+ + {array.parityCheckStatus.running && ( +
+ + + Progress: {array.parityCheckStatus.progress.toFixed(1)}% + + + ETA:{' '} + {array.parityCheckStatus.eta > 0 + ? `${Math.floor(array.parityCheckStatus.eta / 3600)}h ${Math.floor((array.parityCheckStatus.eta % 3600) / 60)}m` + : 'Calculating...'} + + + +
+ )} +
+ )} + + {/* Disk Tables */} + + + }> + Parity ({array.parities.length}) + + }> + Data Disks ({array.disks.length}) + + }> + Cache Pools ({array.caches.length}) + + + + + + + + + + + + + + + + + + + + + {array.parities.map((disk) => ( + + ))} + +
DeviceModelStatusSizeFSTempI/OErrorsUsage
+
+
+ + + + + + + + + + + + + + + + + + + {array.disks.map((disk) => ( + + ))} + +
DeviceModelStatusSizeFSTempI/OErrorsUsage
+
+
+ + + + {array.caches.map((cache) => { + const cacheUsedPercent = ((cache.fsUsed / cache.fsSize) * 100).toFixed(1); + return ( + + +
+ {cache.name} + + {cache.fsType} • {cache.devices.length} device(s) + +
+ + + {formatBytes(cache.fsUsed)} / {formatBytes(cache.fsSize)} + + 90 + ? 'red' + : parseFloat(cacheUsedPercent) > 75 + ? 'orange' + : 'teal' + } + > + {cacheUsedPercent}% + + +
+ + 90 + ? 'red' + : parseFloat(cacheUsedPercent) > 75 + ? 'orange' + : 'teal' + } + /> + + {cache.devices.length > 0 && ( + + + + + + + + + + + {cache.devices.map((device) => ( + + + + + + + ))} + +
DeviceModelSizeTemp
+ + + {device.name} + + + {device.model} + + {formatBytes(device.size)} + + {device.temp !== null ? ( + 50 + ? 'red' + : device.temp > 40 + ? 'orange' + : undefined + } + > + {device.temp}°C + + ) : ( + + {device.spunDown ? 'Standby' : '-'} + + )} +
+ )} +
+ ); + })} + + {array.caches.length === 0 && ( + + No cache pools configured + + )} +
+
+
+
+
+
+ ); +} + +export const getServerSideProps = async (context: GetServerSidePropsContext) => { + const session = await getServerAuthSession(context); + const translations = await getServerSideTranslations( + ['common'], + context.locale, + context.req, + context.res + ); + + const result = checkForSessionOrAskForLogin( + context, + session, + () => session?.user != undefined + ); + if (result) { + return result; + } + + return { + props: { + ...translations, + }, + }; +}; diff --git a/src/pages/unraid/docker/index.tsx b/src/pages/unraid/docker/index.tsx new file mode 100644 index 000000000..db0ec815d --- /dev/null +++ b/src/pages/unraid/docker/index.tsx @@ -0,0 +1,476 @@ +/** + * Docker Management Page + * Full Docker container management with details + */ + +import { useState } from 'react'; +import { + ActionIcon, + Alert, + Badge, + Button, + Card, + Container, + Grid, + Group, + Loader, + Menu, + Paper, + ScrollArea, + SegmentedControl, + SimpleGrid, + Stack, + Table, + Text, + TextInput, + ThemeIcon, + Title, + Tooltip, +} from '@mantine/core'; +import { useDebouncedValue } from '@mantine/hooks'; +import { notifications } from '@mantine/notifications'; +import { + IconBrandDocker, + IconBox, + IconPlayerPlay, + IconPlayerStop, + IconDots, + IconSearch, + IconAlertCircle, + IconCheck, + IconNetwork, + IconRefresh, +} from '@tabler/icons-react'; +import { GetServerSidePropsContext } from 'next'; + +import { UnraidLayout } from '~/components/Unraid/Layout'; +import { getServerAuthSession } from '~/server/auth'; +import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations'; +import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder'; +import { api } from '~/utils/api'; +import type { DockerContainer, ContainerState } from '~/lib/unraid/types'; + +function getStateColor(state: ContainerState): string { + switch (state) { + case 'RUNNING': + return 'green'; + case 'EXITED': + return 'red'; + case 'PAUSED': + return 'yellow'; + case 'RESTARTING': + return 'orange'; + case 'CREATED': + return 'blue'; + case 'DEAD': + return 'gray'; + default: + return 'gray'; + } +} + +function formatDate(timestamp: number): string { + return new Date(timestamp * 1000).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); +} + +function ContainerCard({ + container, + onStart, + onStop, + isLoading, +}: { + container: DockerContainer; + onStart: () => void; + onStop: () => void; + isLoading: boolean; +}) { + const isRunning = container.state === 'RUNNING'; + const containerName = container.names[0]?.replace(/^\//, '') || 'Unknown'; + + return ( + + + + + + + +
+ + {containerName} + + + {container.image} + +
+
+ + + + {container.state} + + + {isRunning ? ( + + + + + + ) : ( + + + + + + )} + + + + + + + + + View Logs + Console + Edit + + + Remove + + + + +
+
+ + + {/* Network */} + + + + {container.networkMode} + + + + {/* Ports */} + {container.ports.length > 0 && ( + + + Ports: + + {container.ports.slice(0, 3).map((port, idx) => ( + + {port.publicPort ? `${port.publicPort}:` : ''} + {port.privatePort}/{port.type} + + ))} + {container.ports.length > 3 && ( + + +{container.ports.length - 3} more + + )} + + )} + + {/* Status */} + + + {container.status} + + {container.autoStart && ( + + Auto-start + + )} + + +
+ ); +} + +export default function DockerPage() { + const [search, setSearch] = useState(''); + const [debouncedSearch] = useDebouncedValue(search, 200); + const [filter, setFilter] = useState<'all' | 'running' | 'stopped'>('all'); + const [loadingContainers, setLoadingContainers] = useState([]); + + const { + data: docker, + isLoading, + error, + refetch, + } = api.unraid.docker.useQuery(undefined, { + refetchInterval: 5000, + }); + + const startContainer = api.unraid.startContainer.useMutation({ + onMutate: ({ id }) => setLoadingContainers((prev) => [...prev, id]), + onSettled: (_, __, { id }) => + setLoadingContainers((prev) => prev.filter((cid) => cid !== id)), + onSuccess: () => { + notifications.show({ + title: 'Container Started', + message: 'Container started successfully', + color: 'green', + icon: , + }); + refetch(); + }, + onError: (err) => { + notifications.show({ + title: 'Error', + message: err.message, + color: 'red', + }); + }, + }); + + const stopContainer = api.unraid.stopContainer.useMutation({ + onMutate: ({ id }) => setLoadingContainers((prev) => [...prev, id]), + onSettled: (_, __, { id }) => + setLoadingContainers((prev) => prev.filter((cid) => cid !== id)), + onSuccess: () => { + notifications.show({ + title: 'Container Stopped', + message: 'Container stopped successfully', + color: 'green', + icon: , + }); + refetch(); + }, + }); + + const filteredContainers = docker?.containers.filter((container) => { + const name = container.names[0]?.replace(/^\//, '') || ''; + const matchesSearch = + name.toLowerCase().includes(debouncedSearch.toLowerCase()) || + container.image.toLowerCase().includes(debouncedSearch.toLowerCase()); + + const matchesFilter = + filter === 'all' || + (filter === 'running' && container.state === 'RUNNING') || + (filter === 'stopped' && container.state !== 'RUNNING'); + + return matchesSearch && matchesFilter; + }); + + const runningCount = docker?.containers.filter((c) => c.state === 'RUNNING').length || 0; + const stoppedCount = (docker?.containers.length || 0) - runningCount; + + if (isLoading) { + return ( + + + + + Loading Docker data... + + + + ); + } + + if (error) { + return ( + + + } title="Error" color="red"> + {error.message} + + + + ); + } + + return ( + + + + {/* Header */} + + + + + +
+ Docker Containers + + {runningCount} running, {stoppedCount} stopped + +
+
+ + +
+ + {/* Stats */} + + + + Total Containers + + + {docker?.containers.length || 0} + + + + + + Running + + + {runningCount} + + + + + + Networks + + + {docker?.networks.length || 0} + + + + + {/* Filters */} + + } + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} + style={{ flex: 1, maxWidth: 300 }} + /> + + setFilter(value as any)} + data={[ + { label: 'All', value: 'all' }, + { label: 'Running', value: 'running' }, + { label: 'Stopped', value: 'stopped' }, + ]} + /> + + + {/* Container Grid */} + + {filteredContainers?.map((container) => ( + + startContainer.mutate({ id: container.id })} + onStop={() => stopContainer.mutate({ id: container.id })} + isLoading={loadingContainers.includes(container.id)} + /> + + ))} + + + {filteredContainers?.length === 0 && ( + + No containers found + + )} + + {/* Networks */} + {docker?.networks && docker.networks.length > 0 && ( +
+ + Networks + + + + + + + + + + + + + {docker.networks.map((network) => ( + + + + + + + ))} + +
NameDriverScopeID
+ + + {network.name} + + + + {network.driver} + + + + {network.scope} + + + + {network.id.substring(0, 12)} + +
+
+
+ )} +
+
+
+ ); +} + +export const getServerSideProps = async (context: GetServerSidePropsContext) => { + const session = await getServerAuthSession(context); + const translations = await getServerSideTranslations( + ['common'], + context.locale, + context.req, + context.res + ); + + const result = checkForSessionOrAskForLogin( + context, + session, + () => session?.user != undefined + ); + if (result) { + return result; + } + + return { + props: { + ...translations, + }, + }; +}; diff --git a/src/pages/unraid/index.tsx b/src/pages/unraid/index.tsx index 4a4e38864..b76802a77 100644 --- a/src/pages/unraid/index.tsx +++ b/src/pages/unraid/index.tsx @@ -21,6 +21,7 @@ import { IconServer, IconAlertCircle, IconCheck } from '@tabler/icons-react'; import { GetServerSidePropsContext } from 'next'; import { SystemInfoCard, ArrayCard, DockerCard, VmsCard } from '~/components/Unraid/Dashboard'; +import { UnraidLayout } from '~/components/Unraid/Layout'; import { getServerAuthSession } from '~/server/auth'; import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations'; import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder'; @@ -219,66 +220,75 @@ export default function UnraidDashboardPage() { }, }); + const unreadNotifications = dashboard?.notifications?.filter((n) => !n.read).length || 0; + // Loading state if (isLoading) { return ( - - - - Connecting to Unraid server... - - + + + + + Connecting to Unraid server... + + + ); } // Error state if (error) { return ( - - } - title="Connection Error" - color="red" - variant="filled" - > - {error.message} - - + + + } + title="Connection Error" + color="red" + variant="filled" + > + {error.message} + + + ); } // No data if (!dashboard) { return ( - - } - title="No Data" - color="yellow" - > - No data received from Unraid server. Please check your configuration. - - + + + } + title="No Data" + color="yellow" + > + No data received from Unraid server. Please check your configuration. + + + ); } return ( - - - {/* Header */} - - - - - -
- Unraid Dashboard - - {dashboard.vars.name} - Unraid {dashboard.info.versions.unraid} - -
+ + + + {/* Header */} + + + + + +
+ Unraid Dashboard + + {dashboard.vars.name} - Unraid {dashboard.info.versions.unraid} + +
+
-
{/* Dashboard Grid */} @@ -326,6 +336,7 @@ export default function UnraidDashboardPage() {
+ ); } diff --git a/src/pages/unraid/settings/identification.tsx b/src/pages/unraid/settings/identification.tsx new file mode 100644 index 000000000..5b67cc9a7 --- /dev/null +++ b/src/pages/unraid/settings/identification.tsx @@ -0,0 +1,288 @@ +/** + * Identification Settings Page + * Server name, description, and basic settings + */ + +import { + Alert, + Button, + Card, + Container, + Divider, + Group, + Loader, + Stack, + Switch, + Text, + TextInput, + Textarea, + ThemeIcon, + Title, +} from '@mantine/core'; +import { useForm } from '@mantine/form'; +import { notifications } from '@mantine/notifications'; +import { + IconServer, + IconAlertCircle, + IconCheck, + IconDeviceFloppy, +} from '@tabler/icons-react'; +import { GetServerSidePropsContext } from 'next'; + +import { UnraidLayout } from '~/components/Unraid/Layout'; +import { getServerAuthSession } from '~/server/auth'; +import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations'; +import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder'; +import { api } from '~/utils/api'; + +export default function IdentificationSettingsPage() { + const { + data: vars, + isLoading, + error, + } = api.unraid.vars.useQuery(); + + const { + data: info, + } = api.unraid.info.useQuery(); + + const form = useForm({ + initialValues: { + name: '', + description: '', + model: '', + timezone: '', + }, + }); + + // Update form when data loads + if (vars && !form.isTouched()) { + form.setValues({ + name: vars.name || '', + description: vars.comment || '', + model: vars.flashProduct || '', + timezone: vars.timezone || '', + }); + } + + if (isLoading) { + return ( + + + + + Loading settings... + + + + ); + } + + if (error) { + return ( + + + } title="Error" color="red"> + {error.message} + + + + ); + } + + return ( + + + + {/* Header */} + + + + +
+ Identification + + Server name and basic settings + +
+
+ + {/* Server Identity */} + + + Server Identity + + + + + +