mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-04-05 00:06:17 +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> {
|
async getSystemInfo(): Promise<SystemInformationResponse | undefined> {
|
||||||
try {
|
try {
|
||||||
const [cpu, mem, os, disk] = await Promise.all([
|
const [cpu, mem, os, disk, currentLoad, fsSize, uptime] = await Promise.all([
|
||||||
si.cpu(),
|
si.cpu(),
|
||||||
si.mem(),
|
si.mem(),
|
||||||
si.osInfo(),
|
si.osInfo(),
|
||||||
si.diskLayout(),
|
si.diskLayout(),
|
||||||
|
si.currentLoad(),
|
||||||
|
si.fsSize(),
|
||||||
|
si.time(),
|
||||||
])
|
])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -87,6 +90,9 @@ export class SystemService {
|
||||||
mem,
|
mem,
|
||||||
os,
|
os,
|
||||||
disk,
|
disk,
|
||||||
|
currentLoad,
|
||||||
|
fsSize,
|
||||||
|
uptime,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error getting system info:', 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 { ListRemoteZimFilesResponse, ListZimFilesResponse } from '../../types/zim'
|
||||||
import { ServiceSlim } from '../../types/services'
|
import { ServiceSlim } from '../../types/services'
|
||||||
import { FileEntry } from '../../types/files'
|
import { FileEntry } from '../../types/files'
|
||||||
|
import { SystemInformationResponse } from '../../types/system'
|
||||||
|
|
||||||
class API {
|
class API {
|
||||||
private client
|
private client: AxiosInstance
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.client = axios.create({
|
this.client = axios.create({
|
||||||
|
|
@ -117,6 +118,16 @@ class API {
|
||||||
throw error
|
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()
|
export default new API()
|
||||||
|
|
|
||||||
|
|
@ -2,69 +2,218 @@ import { Head } from '@inertiajs/react'
|
||||||
import SettingsLayout from '~/layouts/SettingsLayout'
|
import SettingsLayout from '~/layouts/SettingsLayout'
|
||||||
import { SystemInformationResponse } from '../../../types/system'
|
import { SystemInformationResponse } from '../../../types/system'
|
||||||
import { formatBytes } from '~/lib/util'
|
import { formatBytes } from '~/lib/util'
|
||||||
|
import CircularGauge from '~/components/systeminfo/CircularGauge'
|
||||||
const Section = ({ title, children }: { title: string; children: React.ReactNode }) => {
|
import HorizontalBarChart from '~/components/systeminfo/HorizontalBarChart'
|
||||||
return (
|
import InfoCard from '~/components/systeminfo/InfoCard'
|
||||||
<div className="px-4 sm:px-0 mt-8">
|
import {
|
||||||
<h3 className="text-base/7 font-semibold text-gray-900">{title}</h3>
|
CpuChipIcon,
|
||||||
<div className="mt-1 border-t border-gray-300">
|
CircleStackIcon,
|
||||||
<dl className="divide-y divide-gray-200">{children}</dl>
|
ServerIcon,
|
||||||
</div>
|
ComputerDesktopIcon,
|
||||||
</div>
|
} from '@heroicons/react/24/outline'
|
||||||
)
|
import Alert from '~/components/Alert'
|
||||||
}
|
import { useSystemInfo } from '~/hooks/useSystemInfo'
|
||||||
|
import StatusCard from '~/components/systeminfo/StatusCard'
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SettingsPage(props: {
|
export default function SettingsPage(props: {
|
||||||
system: { info: SystemInformationResponse | undefined }
|
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 (
|
return (
|
||||||
<SettingsLayout>
|
<SettingsLayout>
|
||||||
<Head title="Settings" />
|
<Head title="System Information" />
|
||||||
<div className="xl:pl-72 w-full">
|
<div className="xl:pl-72 w-full">
|
||||||
<main className="px-12 py-6">
|
<main className="px-6 lg:px-12 py-6 lg:py-8">
|
||||||
<h1 className="text-4xl font-semibold mb-6">System Information</h1>
|
<div className="mb-8">
|
||||||
<div>
|
<h1 className="text-4xl font-bold text-desert-green mb-2">System Information</h1>
|
||||||
<Section title="OS Information">
|
<p className="text-desert-stone-dark">
|
||||||
<Row label="Distro" value={props.system.info?.os.distro} />
|
Real-time monitoring and diagnostics • Last updated: {new Date().toLocaleString()} •
|
||||||
<Row label="Kernel" value={props.system.info?.os.kernel} />
|
Refreshing data every 30 seconds
|
||||||
<Row label="Architecture" value={props.system.info?.os.arch} />
|
</p>
|
||||||
<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>
|
|
||||||
</div>
|
</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>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</SettingsLayout>
|
</SettingsLayout>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,9 @@ export type SystemInformationResponse = {
|
||||||
mem: Systeminformation.MemData
|
mem: Systeminformation.MemData
|
||||||
os: Systeminformation.OsData
|
os: Systeminformation.OsData
|
||||||
disk: Systeminformation.DiskLayoutData[]
|
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
|
// 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