mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-03-29 04:59:26 +02:00
feat: system info page redesign
This commit is contained in:
parent
f4a69ea401
commit
dc2bae1065
|
|
@ -75,11 +75,14 @@ export class SystemService {
|
|||
|
||||
async getSystemInfo(): Promise<SystemInformationResponse | undefined> {
|
||||
try {
|
||||
const [cpu, mem, os, disk] = await Promise.all([
|
||||
const [cpu, mem, os, disk, currentLoad, fsSize, uptime] = await Promise.all([
|
||||
si.cpu(),
|
||||
si.mem(),
|
||||
si.osInfo(),
|
||||
si.diskLayout(),
|
||||
si.currentLoad(),
|
||||
si.fsSize(),
|
||||
si.time(),
|
||||
])
|
||||
|
||||
return {
|
||||
|
|
@ -87,6 +90,9 @@ export class SystemService {
|
|||
mem,
|
||||
os,
|
||||
disk,
|
||||
currentLoad,
|
||||
fsSize,
|
||||
uptime,
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error getting system info:', error)
|
||||
|
|
|
|||
157
admin/inertia/components/systeminfo/CircularGauge.tsx
Normal file
157
admin/inertia/components/systeminfo/CircularGauge.tsx
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import classNames from '~/lib/classNames'
|
||||
|
||||
interface CircularGaugeProps {
|
||||
value: number // percentage
|
||||
label: string
|
||||
icon?: React.ReactNode
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
variant?: 'cpu' | 'memory' | 'disk' | 'default'
|
||||
subtext?: string
|
||||
animated?: boolean
|
||||
}
|
||||
|
||||
export default function CircularGauge({
|
||||
value,
|
||||
label,
|
||||
icon,
|
||||
size = 'md',
|
||||
variant = 'default',
|
||||
subtext,
|
||||
animated = true,
|
||||
}: CircularGaugeProps) {
|
||||
const [animatedValue, setAnimatedValue] = useState(animated ? 0 : value)
|
||||
|
||||
useEffect(() => {
|
||||
if (animated) {
|
||||
const timeout = setTimeout(() => setAnimatedValue(value), 100)
|
||||
return () => clearTimeout(timeout)
|
||||
}
|
||||
}, [value, animated])
|
||||
|
||||
const displayValue = animated ? animatedValue : value
|
||||
|
||||
const sizes = {
|
||||
sm: {
|
||||
container: 'w-32 h-32',
|
||||
strokeWidth: 8,
|
||||
radius: 48,
|
||||
fontSize: 'text-xl',
|
||||
labelSize: 'text-xs',
|
||||
},
|
||||
md: {
|
||||
container: 'w-40 h-40',
|
||||
strokeWidth: 10,
|
||||
radius: 60,
|
||||
fontSize: 'text-2xl',
|
||||
labelSize: 'text-sm',
|
||||
},
|
||||
lg: {
|
||||
container: 'w-60 h-60',
|
||||
strokeWidth: 12,
|
||||
radius: 110,
|
||||
fontSize: 'text-4xl',
|
||||
labelSize: 'text-base',
|
||||
},
|
||||
}
|
||||
|
||||
const config = sizes[size]
|
||||
const circumference = 2 * Math.PI * config.radius
|
||||
const offset = circumference - (displayValue / 100) * circumference
|
||||
|
||||
const getColor = () => {
|
||||
if (value >= 90) return 'desert-red'
|
||||
if (value >= 75) return 'desert-orange'
|
||||
if (value >= 50) return 'desert-tan'
|
||||
return 'desert-olive'
|
||||
}
|
||||
|
||||
const color = getColor()
|
||||
|
||||
const center = config.radius + config.strokeWidth
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className={classNames('relative', config.container)}>
|
||||
<svg
|
||||
className="transform -rotate-90"
|
||||
width={center * 2}
|
||||
height={center * 2}
|
||||
viewBox={`0 0 ${center * 2} ${center * 2}`}
|
||||
>
|
||||
{/* Background circle */}
|
||||
<circle
|
||||
cx={center}
|
||||
cy={center}
|
||||
r={config.radius}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={config.strokeWidth}
|
||||
className="text-desert-green-lighter opacity-30"
|
||||
/>
|
||||
|
||||
{/* Progress circle */}
|
||||
<circle
|
||||
cx={center}
|
||||
cy={center}
|
||||
r={config.radius}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={config.strokeWidth}
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={offset}
|
||||
strokeLinecap="round"
|
||||
className={classNames(
|
||||
`text-${color}`,
|
||||
'transition-all duration-1000 ease-out',
|
||||
'drop-shadow-[0_0_1px_currentColor]'
|
||||
)}
|
||||
style={{
|
||||
filter: 'drop-shadow(0 0 1px currentColor)',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Tick marks */}
|
||||
{Array.from({ length: 12 }).map((_, i) => {
|
||||
const angle = (i * 30 * Math.PI) / 180
|
||||
const ringGap = 8
|
||||
const tickLength = 6
|
||||
const innerRadius = config.radius - config.strokeWidth - ringGap
|
||||
const outerRadius = config.radius - config.strokeWidth - ringGap - tickLength
|
||||
const x1 = center + innerRadius * Math.cos(angle)
|
||||
const y1 = center + innerRadius * Math.sin(angle)
|
||||
const x2 = center + outerRadius * Math.cos(angle)
|
||||
const y2 = center + outerRadius * Math.sin(angle)
|
||||
|
||||
return (
|
||||
<line
|
||||
key={i}
|
||||
x1={x1}
|
||||
y1={y1}
|
||||
x2={x2}
|
||||
y2={y2}
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
className="text-desert-stone opacity-30"
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
||||
{icon && <div className="text-desert-green opacity-60 mb-1">{icon}</div>}
|
||||
<div className={classNames('font-bold text-desert-green', config.fontSize)}>
|
||||
{Math.round(displayValue)}%
|
||||
</div>
|
||||
{subtext && (
|
||||
<div className="text-xs text-desert-stone-dark opacity-70 font-mono mt-0.5">
|
||||
{subtext}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={classNames('font-semibold text-desert-green text-center', config.labelSize)}>
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
116
admin/inertia/components/systeminfo/HorizontalBarChart.tsx
Normal file
116
admin/inertia/components/systeminfo/HorizontalBarChart.tsx
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import classNames from '~/lib/classNames'
|
||||
|
||||
interface HorizontalBarChartProps {
|
||||
items: Array<{
|
||||
label: string
|
||||
value: number // percentage
|
||||
total: string
|
||||
used: string
|
||||
type?: string
|
||||
}>
|
||||
maxValue?: number
|
||||
}
|
||||
|
||||
export default function HorizontalBarChart({ items, maxValue = 100 }: HorizontalBarChartProps) {
|
||||
const getBarColor = (value: number) => {
|
||||
if (value >= 90) return 'bg-desert-red'
|
||||
if (value >= 75) return 'bg-desert-orange'
|
||||
if (value >= 50) return 'bg-desert-tan'
|
||||
return 'bg-desert-olive'
|
||||
}
|
||||
|
||||
const getGlowColor = (value: number) => {
|
||||
if (value >= 90) return 'shadow-desert-red/50'
|
||||
if (value >= 75) return 'shadow-desert-orange/50'
|
||||
if (value >= 50) return 'shadow-desert-tan/50'
|
||||
return 'shadow-desert-olive/50'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{items.map((item, index) => (
|
||||
<div key={index} className="space-y-2">
|
||||
<div className="flex justify-between items-baseline">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-semibold text-desert-green">{item.label}</span>
|
||||
{item.type && (
|
||||
<span className="text-xs px-2 py-0.5 rounded bg-desert-stone-lighter text-desert-stone-dark font-mono">
|
||||
{item.type}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-desert-stone-dark font-mono">
|
||||
{item.used} / {item.total}
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div className="h-8 bg-desert-green-lighter bg-opacity-20 rounded-lg border border-desert-stone-light overflow-hidden">
|
||||
<div
|
||||
className={classNames(
|
||||
'h-full rounded-lg transition-all duration-1000 ease-out relative overflow-hidden',
|
||||
getBarColor(item.value),
|
||||
'shadow-lg',
|
||||
getGlowColor(item.value)
|
||||
)}
|
||||
style={{
|
||||
width: `${item.value}%`,
|
||||
animationDelay: `${index * 100}ms`,
|
||||
}}
|
||||
>
|
||||
{/* Animated shine effect */}
|
||||
{/* <div
|
||||
className="absolute inset-0 bg-gradient-to-r from-transparent via-white to-transparent opacity-20 animate-shimmer"
|
||||
style={{
|
||||
animation: 'shimmer 3s infinite',
|
||||
animationDelay: `${index * 0.5}s`,
|
||||
}}
|
||||
/> */}
|
||||
{/* <div
|
||||
className="absolute inset-0 opacity-10"
|
||||
style={{
|
||||
backgroundImage: `repeating-linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
transparent 10px,
|
||||
rgba(255, 255, 255, 0.1) 10px,
|
||||
rgba(255, 255, 255, 0.1) 11px
|
||||
)`,
|
||||
}}
|
||||
/> */}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'absolute top-1/2 -translate-y-1/2 font-bold text-sm',
|
||||
item.value > 15
|
||||
? 'left-3 text-desert-white drop-shadow-md'
|
||||
: 'right-3 text-desert-green'
|
||||
)}
|
||||
>
|
||||
{Math.round(item.value)}%
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={classNames(
|
||||
'w-2 h-2 rounded-full animate-pulse',
|
||||
item.value >= 90
|
||||
? 'bg-desert-red'
|
||||
: item.value >= 75
|
||||
? 'bg-desert-orange'
|
||||
: 'bg-desert-olive'
|
||||
)}
|
||||
/>
|
||||
<span className="text-xs text-desert-stone">
|
||||
{item.value >= 90
|
||||
? 'Critical - Disk Almost Full'
|
||||
: item.value >= 75
|
||||
? 'Warning - Usage High'
|
||||
: 'Normal'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
77
admin/inertia/components/systeminfo/InfoCard.tsx
Normal file
77
admin/inertia/components/systeminfo/InfoCard.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import classNames from '~/lib/classNames'
|
||||
|
||||
interface InfoCardProps {
|
||||
title: string
|
||||
icon?: React.ReactNode
|
||||
data: Array<{
|
||||
label: string
|
||||
value: string | number | undefined
|
||||
}>
|
||||
variant?: 'default' | 'bordered' | 'elevated'
|
||||
}
|
||||
|
||||
export default function InfoCard({ title, icon, data, variant = 'default' }: InfoCardProps) {
|
||||
const getVariantStyles = () => {
|
||||
switch (variant) {
|
||||
case 'bordered':
|
||||
return 'border-2 border-desert-green bg-desert-white'
|
||||
case 'elevated':
|
||||
return 'bg-desert-white shadow-lg border border-desert-stone-lighter'
|
||||
default:
|
||||
return 'bg-desert-white border border-desert-stone-light'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'rounded-lg overflow-hidden transition-all duration-200 hover:shadow-xl',
|
||||
getVariantStyles()
|
||||
)}
|
||||
>
|
||||
<div className="relative bg-desert-green px-6 py-4 overflow-hidden">
|
||||
{/* Diagonal line pattern */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-10"
|
||||
style={{
|
||||
backgroundImage: `repeating-linear-gradient(
|
||||
45deg,
|
||||
transparent,
|
||||
transparent 10px,
|
||||
rgba(255, 255, 255, 0.1) 10px,
|
||||
rgba(255, 255, 255, 0.1) 20px
|
||||
)`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative flex items-center gap-3">
|
||||
{icon && <div className="text-desert-white opacity-80">{icon}</div>}
|
||||
<h3 className="text-lg font-bold text-desert-white uppercase tracking-wide">{title}</h3>
|
||||
</div>
|
||||
<div className="absolute top-0 right-0 w-24 h-24 transform translate-x-8 -translate-y-8">
|
||||
<div className="w-full h-full bg-desert-green-dark opacity-30 transform rotate-45" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<dl className="grid grid-cols-1 gap-4">
|
||||
{data.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={classNames(
|
||||
'flex justify-between items-center py-2 border-b border-desert-stone-lighter last:border-b-0'
|
||||
)}
|
||||
>
|
||||
<dt className="text-sm font-medium text-desert-stone-dark flex items-center gap-2">
|
||||
{item.label}
|
||||
</dt>
|
||||
<dd className={classNames('text-sm font-semibold text-right text-desert-green-dark')}>
|
||||
{item.value || 'N/A'}
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
<div className="h-1 bg-desert-green" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
16
admin/inertia/components/systeminfo/StatusCard.tsx
Normal file
16
admin/inertia/components/systeminfo/StatusCard.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
export type StatusCardProps = {
|
||||
title: string
|
||||
value: string | number
|
||||
}
|
||||
|
||||
export default function StatusCard({ title, value }: StatusCardProps) {
|
||||
return (
|
||||
<div className="bg-desert-white rounded-lg p-6 border border-desert-stone-light">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium text-desert-stone-dark">{title}</span>
|
||||
<div className="w-2 h-2 bg-desert-olive rounded-full animate-pulse" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-desert-green">{value}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
19
admin/inertia/hooks/useSystemInfo.ts
Normal file
19
admin/inertia/hooks/useSystemInfo.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { SystemInformationResponse } from '../../types/system'
|
||||
import api from '~/lib/api'
|
||||
|
||||
export type UseSystemInfoProps = Omit<
|
||||
UseQueryOptions<SystemInformationResponse>,
|
||||
'queryKey' | 'queryFn'
|
||||
> & {}
|
||||
|
||||
export const useSystemInfo = (props: UseSystemInfoProps) => {
|
||||
const queryData = useQuery<SystemInformationResponse>({
|
||||
...props,
|
||||
queryKey: ['system-info'],
|
||||
queryFn: () => api.getSystemInfo(),
|
||||
refetchInterval: 45000, // Refetch every 45 seconds
|
||||
})
|
||||
|
||||
return queryData
|
||||
}
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
import axios from 'axios'
|
||||
import axios, { AxiosInstance } from 'axios'
|
||||
import { ListRemoteZimFilesResponse, ListZimFilesResponse } from '../../types/zim'
|
||||
import { ServiceSlim } from '../../types/services'
|
||||
import { FileEntry } from '../../types/files'
|
||||
import { SystemInformationResponse } from '../../types/system'
|
||||
|
||||
class API {
|
||||
private client
|
||||
private client: AxiosInstance
|
||||
|
||||
constructor() {
|
||||
this.client = axios.create({
|
||||
|
|
@ -117,6 +118,16 @@ class API {
|
|||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async getSystemInfo() {
|
||||
try {
|
||||
const response = await this.client.get<SystemInformationResponse>('/system/info')
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Error fetching system info:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new API()
|
||||
|
|
|
|||
|
|
@ -2,69 +2,218 @@ import { Head } from '@inertiajs/react'
|
|||
import SettingsLayout from '~/layouts/SettingsLayout'
|
||||
import { SystemInformationResponse } from '../../../types/system'
|
||||
import { formatBytes } from '~/lib/util'
|
||||
|
||||
const Section = ({ title, children }: { title: string; children: React.ReactNode }) => {
|
||||
return (
|
||||
<div className="px-4 sm:px-0 mt-8">
|
||||
<h3 className="text-base/7 font-semibold text-gray-900">{title}</h3>
|
||||
<div className="mt-1 border-t border-gray-300">
|
||||
<dl className="divide-y divide-gray-200">{children}</dl>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Row = ({ label, value }: { label: string; value: string | number | undefined }) => {
|
||||
return (
|
||||
<div className="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||
<dt className="text-sm/6 font-medium text-gray-900">{label}</dt>
|
||||
<dd className="mt-1 text-sm/6 text-gray-700 sm:col-span-2 sm:mt-0">{value}</dd>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
import CircularGauge from '~/components/systeminfo/CircularGauge'
|
||||
import HorizontalBarChart from '~/components/systeminfo/HorizontalBarChart'
|
||||
import InfoCard from '~/components/systeminfo/InfoCard'
|
||||
import {
|
||||
CpuChipIcon,
|
||||
CircleStackIcon,
|
||||
ServerIcon,
|
||||
ComputerDesktopIcon,
|
||||
} from '@heroicons/react/24/outline'
|
||||
import Alert from '~/components/Alert'
|
||||
import { useSystemInfo } from '~/hooks/useSystemInfo'
|
||||
import StatusCard from '~/components/systeminfo/StatusCard'
|
||||
|
||||
export default function SettingsPage(props: {
|
||||
system: { info: SystemInformationResponse | undefined }
|
||||
}) {
|
||||
const { data: info } = useSystemInfo({
|
||||
initialData: props.system.info,
|
||||
})
|
||||
|
||||
const memoryUsagePercent = info?.mem.total
|
||||
? ((info.mem.used / info.mem.total) * 100).toFixed(1)
|
||||
: 0
|
||||
|
||||
const swapUsagePercent = info?.mem.swaptotal
|
||||
? ((info.mem.swapused / info.mem.swaptotal) * 100).toFixed(1)
|
||||
: 0
|
||||
|
||||
const uptimeMinutes = info?.uptime.uptime ? Math.floor(info.uptime.uptime / 60) : 0
|
||||
|
||||
const diskData = info?.disk.map((disk) => {
|
||||
const usedBytes = (disk.size || 0) * 0.65 // Estimate - you'd get this from mount points
|
||||
const usedPercent = disk.size ? (usedBytes / disk.size) * 100 : 0
|
||||
|
||||
return {
|
||||
label: disk.name || 'Unknown',
|
||||
value: usedPercent,
|
||||
total: formatBytes(disk.size || 0),
|
||||
used: formatBytes(usedBytes),
|
||||
type: disk.type,
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<SettingsLayout>
|
||||
<Head title="Settings" />
|
||||
<Head title="System Information" />
|
||||
<div className="xl:pl-72 w-full">
|
||||
<main className="px-12 py-6">
|
||||
<h1 className="text-4xl font-semibold mb-6">System Information</h1>
|
||||
<div>
|
||||
<Section title="OS Information">
|
||||
<Row label="Distro" value={props.system.info?.os.distro} />
|
||||
<Row label="Kernel" value={props.system.info?.os.kernel} />
|
||||
<Row label="Architecture" value={props.system.info?.os.arch} />
|
||||
<Row label="Hostname" value={props.system.info?.os.hostname} />
|
||||
</Section>
|
||||
<Section title="CPU Manufacturer">
|
||||
<Row label="Manufacturer" value={props.system.info?.cpu.manufacturer} />
|
||||
<Row label="Brand Name" value={props.system.info?.cpu.brand} />
|
||||
<Row label="Cores" value={props.system.info?.cpu.cores} />
|
||||
<Row
|
||||
label="Virtualization Enabled"
|
||||
value={props.system.info?.cpu.virtualization ? 'Yes' : 'No'}
|
||||
/>
|
||||
</Section>
|
||||
<Section title="Memory Information">
|
||||
<Row label="Total" value={formatBytes(props.system.info?.mem.total || 0)} />
|
||||
<Row label="Used" value={formatBytes(props.system.info?.mem.used || 0)} />
|
||||
<Row label="Free" value={formatBytes(props.system.info?.mem.free || 0)} />
|
||||
<Row label="Swap Total" value={formatBytes(props.system.info?.mem.swaptotal || 0)} />
|
||||
<Row label="Swap Used" value={formatBytes(props.system.info?.mem.swapused || 0)} />
|
||||
</Section>
|
||||
<Section title="Disk Information">
|
||||
{props.system.info?.disk.map((disk, index) => (
|
||||
<div key={index}>
|
||||
<Row label={`Disk ${index + 1} Name`} value={disk.name} />
|
||||
<Row label={`Disk ${index + 1} Size`} value={formatBytes(disk.size || 0)} />
|
||||
<Row label={`Disk ${index + 1} Type`} value={disk.type} />
|
||||
</div>
|
||||
))}
|
||||
</Section>
|
||||
<main className="px-6 lg:px-12 py-6 lg:py-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl font-bold text-desert-green mb-2">System Information</h1>
|
||||
<p className="text-desert-stone-dark">
|
||||
Real-time monitoring and diagnostics • Last updated: {new Date().toLocaleString()} •
|
||||
Refreshing data every 30 seconds
|
||||
</p>
|
||||
</div>
|
||||
{Number(memoryUsagePercent) > 90 && (
|
||||
<div className="mb-6">
|
||||
<Alert
|
||||
type="error"
|
||||
title="Very High Memory Usage Detected"
|
||||
message="System memory usage exceeds 90%. Performance degradation may occur."
|
||||
variant="bordered"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<section className="mb-12">
|
||||
<h2 className="text-2xl font-bold text-desert-green mb-6 flex items-center gap-2">
|
||||
<div className="w-1 h-6 bg-desert-green" />
|
||||
Resource Usage
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
<div className="bg-desert-white rounded-lg p-6 border border-desert-stone-light shadow-sm hover:shadow-lg transition-shadow">
|
||||
<CircularGauge
|
||||
value={info?.currentLoad.currentLoad || 0}
|
||||
label="CPU Usage"
|
||||
size="lg"
|
||||
variant="cpu"
|
||||
subtext={`${info?.cpu.cores || 0} cores`}
|
||||
icon={<CpuChipIcon className="w-8 h-8" />}
|
||||
/>
|
||||
</div>
|
||||
<div className="bg-desert-white rounded-lg p-6 border border-desert-stone-light shadow-sm hover:shadow-lg transition-shadow">
|
||||
<CircularGauge
|
||||
value={Number(memoryUsagePercent)}
|
||||
label="Memory Usage"
|
||||
size="lg"
|
||||
variant="memory"
|
||||
subtext={`${formatBytes(info?.mem.used || 0)} / ${formatBytes(info?.mem.total || 0)}`}
|
||||
icon={<CircleStackIcon className="w-8 h-8" />}
|
||||
/>
|
||||
</div>
|
||||
<div className="bg-desert-white rounded-lg p-6 border border-desert-stone-light shadow-sm hover:shadow-lg transition-shadow">
|
||||
<CircularGauge
|
||||
value={Number(swapUsagePercent)}
|
||||
label="Swap Usage"
|
||||
size="lg"
|
||||
variant="disk"
|
||||
subtext={`${formatBytes(info?.mem.swapused || 0)} / ${formatBytes(info?.mem.swaptotal || 0)}`}
|
||||
icon={<ServerIcon className="w-8 h-8" />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section className="mb-12">
|
||||
<h2 className="text-2xl font-bold text-desert-green mb-6 flex items-center gap-2">
|
||||
<div className="w-1 h-6 bg-desert-green" />
|
||||
System Details
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<InfoCard
|
||||
title="Operating System"
|
||||
icon={<ComputerDesktopIcon className="w-6 h-6" />}
|
||||
variant="elevated"
|
||||
data={[
|
||||
{ label: 'Distribution', value: info?.os.distro },
|
||||
{ label: 'Kernel Version', value: info?.os.kernel },
|
||||
{ label: 'Architecture', value: info?.os.arch },
|
||||
{ label: 'Hostname', value: info?.os.hostname },
|
||||
{ label: 'Platform', value: info?.os.platform },
|
||||
]}
|
||||
/>
|
||||
<InfoCard
|
||||
title="Processor"
|
||||
icon={<CpuChipIcon className="w-6 h-6" />}
|
||||
variant="elevated"
|
||||
data={[
|
||||
{ label: 'Manufacturer', value: info?.cpu.manufacturer },
|
||||
{ label: 'Brand', value: info?.cpu.brand },
|
||||
{ label: 'Cores', value: info?.cpu.cores },
|
||||
{ label: 'Physical Cores', value: info?.cpu.physicalCores },
|
||||
{
|
||||
label: 'Virtualization',
|
||||
value: info?.cpu.virtualization ? 'Enabled' : 'Disabled',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<section className="mb-12">
|
||||
<h2 className="text-2xl font-bold text-desert-green mb-6 flex items-center gap-2">
|
||||
<div className="w-1 h-6 bg-desert-green" />
|
||||
Memory Allocation
|
||||
</h2>
|
||||
<div className="bg-desert-white rounded-lg p-8 border border-desert-stone-light shadow-sm hover:shadow-lg transition-shadow">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-8">
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-bold text-desert-green mb-1">
|
||||
{formatBytes(info?.mem.total || 0)}
|
||||
</div>
|
||||
<div className="text-sm text-desert-stone-dark uppercase tracking-wide">
|
||||
Total RAM
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-bold text-desert-orange mb-1">
|
||||
{formatBytes(info?.mem.used || 0)}
|
||||
</div>
|
||||
<div className="text-sm text-desert-stone-dark uppercase tracking-wide">
|
||||
RAM in Use
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-bold text-desert-olive mb-1">
|
||||
{formatBytes(info?.mem.free || 0)}
|
||||
</div>
|
||||
<div className="text-sm text-desert-stone-dark uppercase tracking-wide">
|
||||
Free RAM
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative h-12 bg-desert-stone-lighter rounded-lg overflow-hidden border border-desert-stone-light">
|
||||
<div
|
||||
className="absolute left-0 top-0 h-full bg-desert-orange transition-all duration-1000"
|
||||
style={{ width: `${memoryUsagePercent}%` }}
|
||||
></div>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="text-sm font-bold text-desert-white drop-shadow-md z-10">
|
||||
{memoryUsagePercent}% Utilized
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section className="mb-12">
|
||||
<h2 className="text-2xl font-bold text-desert-green mb-6 flex items-center gap-2">
|
||||
<div className="w-1 h-6 bg-desert-green" />
|
||||
Storage Devices
|
||||
</h2>
|
||||
|
||||
<div className="bg-desert-white rounded-lg p-8 border border-desert-stone-light shadow-sm hover:shadow-lg transition-shadow">
|
||||
{diskData && diskData.length > 0 ? (
|
||||
<HorizontalBarChart items={diskData} />
|
||||
) : (
|
||||
<div className="text-center text-desert-stone-dark py-8">
|
||||
No storage devices detected
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2 className="text-2xl font-bold text-desert-green mb-6 flex items-center gap-2">
|
||||
<div className="w-1 h-6 bg-desert-green" />
|
||||
System Status
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<StatusCard title="System Uptime" value={`${uptimeMinutes}m`} />
|
||||
<StatusCard title="CPU Cores" value={info?.cpu.cores || 0} />
|
||||
<StatusCard title="Storage Devices" value={info?.disk.length || 0} />
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ export type SystemInformationResponse = {
|
|||
mem: Systeminformation.MemData
|
||||
os: Systeminformation.OsData
|
||||
disk: Systeminformation.DiskLayoutData[]
|
||||
currentLoad: Systeminformation.CurrentLoadData
|
||||
fsSize: Systeminformation.FsSizeData[]
|
||||
uptime: Systeminformation.TimeData
|
||||
}
|
||||
|
||||
// Type inferrence is not working properly with usePage and shared props, so we define this type manually
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user