Skip to content

Commit 0566188

Browse files
committed
feat(ui): show separate upload and download speeds
replace the combined network speed summary with dedicated upload and download values in the home system stats cards. improve stat rendering for responsive layouts by hiding network on mobile, stacking network directions on desktop, and refining text-like value styling with directional icons. also improve secondary disk selection by avoiding duplicate matches to the primary disk and using the disk key in the secondary disk description.
1 parent d74ed80 commit 0566188

4 files changed

Lines changed: 113 additions & 40 deletions

File tree

src/components/home/SystemStatValue.tsx

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
1+
import { IconArrowDown, IconArrowUp } from '@tabler/icons-react'
12
import { type FieldPath, Render } from 'juststore'
2-
import { formatDuration } from '@/lib/format'
3+
import { formatBytes, formatDuration } from '@/lib/format'
34
import { cn } from '@/lib/utils'
45
import { Progress } from '../ui/progress'
56
import { type Store, store } from './store'
67

8+
export type SystemStatValueType = 'text' | 'progress' | 'duration' | 'upload' | 'download'
9+
710
export default function SystemStatValue({
811
valueKey,
912
descriptionKey,
1013
type,
1114
}: {
1215
valueKey: FieldPath<Store['systemInfo']>
1316
descriptionKey?: FieldPath<Store['systemInfo']>
14-
type: 'text' | 'progress' | 'duration'
17+
type: SystemStatValueType
1518
}) {
1619
const state = store.systemInfo[valueKey]
1720
return (
@@ -35,7 +38,7 @@ function DisplayValue({
3538
type,
3639
}: {
3740
valueKey: FieldPath<Store['systemInfo']>
38-
type: 'text' | 'progress' | 'duration'
41+
type: SystemStatValueType
3942
}) {
4043
const displayValue = store.systemInfo[valueKey].useCompute(value => {
4144
if (type === 'duration') {
@@ -44,10 +47,37 @@ function DisplayValue({
4447
if (type === 'progress') {
4548
return `${value}%`
4649
}
50+
if (type === 'upload') {
51+
type = 'text'
52+
return (
53+
<>
54+
<IconArrowUp className="size-4 text-green-500" />{' '}
55+
{formatBytes(Number(value), { precision: 0, unit: '/s' })}
56+
</>
57+
)
58+
}
59+
if (type === 'download') {
60+
type = 'text'
61+
return (
62+
<>
63+
<IconArrowDown className="size-4 text-red-500" />{' '}
64+
{formatBytes(Number(value), { precision: 0, unit: '/s' })}
65+
</>
66+
)
67+
}
4768
return String(value)
4869
})
70+
const isTextLike = type === 'text' || type === 'upload' || type === 'download'
4971
return (
50-
<div className="text-base sm:text-xl leading-none font-semibold tracking-tight tabular-nums">
72+
<div
73+
className={cn(
74+
'font-semibold tracking-tight tabular-nums',
75+
isTextLike
76+
? 'text-base sm:text-lg leading-tight whitespace-nowrap'
77+
: 'text-base sm:text-xl leading-none',
78+
isTextLike && 'flex items-center gap-1'
79+
)}
80+
>
5181
{displayValue}
5282
</div>
5383
)
@@ -58,7 +88,7 @@ function Description({
5888
type,
5989
}: {
6090
descriptionKey?: FieldPath<Store['systemInfo']>
61-
type: 'text' | 'progress' | 'duration'
91+
type: SystemStatValueType
6292
}) {
6393
const value = store.systemInfo[descriptionKey ?? 'uptime'].use()
6494
return (

src/components/home/SystemStats.tsx

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import { Clock, Cpu, HardDrive, type LucideIcon, MemoryStick, Wifi } from 'lucid
33
import { Suspense } from 'react'
44
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
55
import { formatDuration } from '@/lib/format'
6+
import { cn } from '@/lib/utils'
67
import SystemStatsProvider from './SystemStatsProvider'
7-
import SystemStatValue from './SystemStatValue'
8+
import SystemStatValue, { type SystemStatValueType } from './SystemStatValue'
89
import { type Store, store } from './store'
910

1011
export default function SystemStats() {
@@ -20,10 +21,13 @@ export default function SystemStats() {
2021
}
2122

2223
function SystemStatsMobile() {
24+
const stats = statsProps.filter(stat => {
25+
return !('hideOnMobile' in stat && stat.hideOnMobile)
26+
})
2327
return (
2428
<Card size="sm" className="mx-1 block sm:hidden">
2529
<CardContent className="grid grid-cols-5 gap-y-2 [&>*:nth-child(odd)]:col-start-1 [&>*:nth-child(even)]:col-start-4">
26-
{statsProps.map(stat => (
30+
{stats.map(stat => (
2731
<MobileStatRow key={stat.key} stat={stat} />
2832
))}
2933
</CardContent>
@@ -35,9 +39,13 @@ function SystemStatsDesktop() {
3539
const hasSecondaryDisk = Boolean(store.systemInfo.secondaryPartitionUsageDesc.use())
3640

3741
return (
38-
<div className="hidden sm:grid grid-cols-2 gap-2 px-1 sm:grid-cols-5 sm:gap-4">
42+
<div className="hidden sm:grid grid-cols-2 gap-2 px-1 sm:grid-cols-4 md:grid-cols-5 sm:gap-4">
3943
{statsProps.map(stat => (
40-
<Card key={stat.key} size="sm" className="h-full">
44+
<Card
45+
key={stat.key}
46+
size="sm"
47+
className={cn('h-full', stat.key === 'networkSpeedUpload' && 'hidden md:flex')}
48+
>
4149
<CardHeader>
4250
<div className="flex items-center justify-between gap-2">
4351
<CardTitle className="text-xs font-medium text-muted-foreground sm:text-sm">
@@ -47,21 +55,28 @@ function SystemStatsDesktop() {
4755
</div>
4856
</CardHeader>
4957
<CardContent className="space-y-1.5 sm:space-y-2">
50-
{stat.key === 'rootPartitionUsage' ? (
51-
<>
52-
<SystemStatValue
53-
valueKey={stat.key}
54-
descriptionKey={stat.descriptionKey}
55-
type={stat.type}
56-
/>
57-
{hasSecondaryDisk && (
58+
{stat.key === 'rootPartitionUsage' && hasSecondaryDisk ? (
59+
<div className="flex justify-between gap-3">
60+
<div className="space-y-1.5 sm:space-y-2 w-full xl:w-auto">
5861
<SystemStatValue
59-
valueKey="secondaryPartitionUsage"
60-
descriptionKey="secondaryPartitionUsageDesc"
61-
type="progress"
62+
valueKey={stat.key}
63+
descriptionKey={stat.descriptionKey}
64+
type={stat.type}
6265
/>
63-
)}
64-
</>
66+
</div>
67+
<div className="space-y-1.5 sm:space-y-2 hidden xl:block">
68+
<SystemStatValue
69+
valueKey={stat.secondaryKey}
70+
descriptionKey={stat.secondaryDescriptionKey}
71+
type={stat.type}
72+
/>
73+
</div>
74+
</div>
75+
) : stat.key === 'networkSpeedUpload' ? (
76+
<div className="flex flex-col gap-0.5">
77+
<SystemStatValue valueKey={stat.key} type="upload" />
78+
<SystemStatValue valueKey={stat.secondaryKey} type="download" />
79+
</div>
6580
) : (
6681
<SystemStatValue
6782
valueKey={stat.key}
@@ -77,14 +92,17 @@ function SystemStatsDesktop() {
7792
}
7893

7994
type StatProp = {
95+
hideOnMobile?: boolean
8096
label: string
8197
mobileLabel: string
8298
icon: LucideIcon
83-
mobileValueMode?: 'percent' | 'description' | 'duration' | 'text'
84-
type: 'text' | 'progress' | 'duration'
99+
mobileValueMode?: 'percent' | 'description' | 'duration' | 'text' | 'networkSpeed'
100+
type: SystemStatValueType
85101
color: string
86102
key: FieldPath<Store['systemInfo']>
87103
descriptionKey?: FieldPath<Store['systemInfo']>
104+
secondaryKey?: FieldPath<Store['systemInfo']>
105+
secondaryDescriptionKey?: FieldPath<Store['systemInfo']>
88106
format?: (value: number) => string
89107
}
90108

@@ -98,6 +116,9 @@ function MobileStatRow({ stat }: { stat: StatProp }) {
98116
if (stat.mobileValueMode === 'text') {
99117
return String(value)
100118
}
119+
if (stat.mobileValueMode === 'networkSpeed') {
120+
return null
121+
}
101122
return `${value}%`
102123
})()
103124

@@ -112,7 +133,7 @@ function MobileStatRow({ stat }: { stat: StatProp }) {
112133
)
113134
}
114135

115-
const statsProps: StatProp[] = [
136+
const statsProps = [
116137
{
117138
label: 'Uptime',
118139
mobileLabel: 'Up',
@@ -121,6 +142,7 @@ const statsProps: StatProp[] = [
121142
type: 'duration',
122143
color: 'text-primary',
123144
key: 'uptime',
145+
descriptionKey: undefined,
124146
},
125147
{
126148
label: 'CPU Usage',
@@ -130,6 +152,7 @@ const statsProps: StatProp[] = [
130152
type: 'progress',
131153
color: 'bg-chart-1',
132154
key: 'cpuAverage',
155+
descriptionKey: undefined,
133156
},
134157
{
135158
label: 'Memory',
@@ -150,14 +173,19 @@ const statsProps: StatProp[] = [
150173
color: 'bg-chart-3',
151174
key: 'rootPartitionUsage',
152175
descriptionKey: 'rootPartitionUsageDesc',
176+
secondaryKey: 'secondaryPartitionUsage',
177+
secondaryDescriptionKey: 'secondaryPartitionUsageDesc',
153178
},
154179
{
180+
hideOnMobile: true,
155181
label: 'Network',
156182
mobileLabel: 'Net',
157183
icon: Wifi,
158-
mobileValueMode: 'text',
184+
mobileValueMode: 'networkSpeed',
159185
type: 'text',
160186
color: 'bg-chart-4',
161-
key: 'networkSpeedSummary',
187+
key: 'networkSpeedUpload',
188+
secondaryKey: 'networkSpeedDownload',
189+
descriptionKey: undefined,
162190
},
163-
] as const
191+
] as const satisfies StatProp[]

src/components/home/SystemStatsProvider.tsx

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useWebSocketApi } from '@/hooks/websocket'
22
import type { DiskUsageStat, MemVirtualMemoryStat, StatsResponse, SystemInfo } from '@/lib/api'
3-
import { store } from './store'
43
import { formatBytes } from '@/lib/format'
4+
import { store } from './store'
55

66
export default function SystemStatsProvider() {
77
useWebSocketApi<SystemInfo>({
@@ -19,7 +19,8 @@ export default function SystemStatsProvider() {
1919
secondaryPartitionUsageDesc: getSecondaryDiskUsageDesc(data.disks, '/'),
2020
memoryUsage: Math.round(data.memory.used_percent * 100) / 100,
2121
memoryUsageDesc: getMemoryUsageDesc(data.memory),
22-
networkSpeedSummary: getNetworkSpeedSummary(data),
22+
networkSpeedUpload: data.network?.upload_speed ?? 0,
23+
networkSpeedDownload: data.network?.download_speed ?? 0,
2324
}),
2425
})
2526

@@ -50,7 +51,24 @@ function getDiskUsageDesc(disks: Record<string, DiskUsageStat>, path: string) {
5051
function getSecondaryDisk(disks: Record<string, DiskUsageStat>, path: string) {
5152
const allDisks = Object.values(disks)
5253
const primaryDisk = getDisk(disks, path)
53-
return allDisks.find(d => d.path !== primaryDisk?.path)
54+
if (!primaryDisk) {
55+
return undefined
56+
}
57+
return allDisks.find(d => !isSameDisk(d, primaryDisk))
58+
}
59+
60+
function isSameDisk(disk: DiskUsageStat, otherDisk: DiskUsageStat) {
61+
if (disk.path === otherDisk.path) {
62+
return true
63+
}
64+
if (
65+
disk.fstype === otherDisk.fstype &&
66+
disk.total === otherDisk.total &&
67+
disk.used === otherDisk.used
68+
) {
69+
return true
70+
}
71+
return false
5472
}
5573

5674
function getSecondaryDiskUsage(disks: Record<string, DiskUsageStat>, path: string) {
@@ -62,15 +80,10 @@ function getSecondaryDiskUsageDesc(disks: Record<string, DiskUsageStat>, path: s
6280
if (!disk) {
6381
return ''
6482
}
65-
return `${disk.path}: ${formatBytes(disk.used, { precision: 1 })} / ${formatBytes(disk.total, { precision: 1 })}`
83+
const key = Object.entries(disks).find(([_, d]) => Object.is(d, disk))?.[0]
84+
return `${key}: ${formatBytes(disk.used, { precision: 1 })} / ${formatBytes(disk.total, { precision: 1 })}`
6685
}
6786

6887
function getMemoryUsageDesc(memory: MemVirtualMemoryStat) {
6988
return `${formatBytes(memory.used, { precision: 1 })} / ${formatBytes(memory.total, { precision: 1 })}`
7089
}
71-
72-
function getNetworkSpeedSummary(data: SystemInfo) {
73-
const uploadSpeed = data.network?.upload_speed ?? 0
74-
const downloadSpeed = data.network?.download_speed ?? 0
75-
return `↑ ${formatBytes(uploadSpeed, { precision: 0, unit: '/s' })}${formatBytes(downloadSpeed, { precision: 0, unit: '/s' })}`
76-
}

src/components/home/store.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ type SystemInfoSimple = {
4444
secondaryPartitionUsageDesc: string
4545
memoryUsage: number
4646
memoryUsageDesc: string
47-
networkSpeedSummary: string
47+
networkSpeedUpload: number
48+
networkSpeedDownload: number
4849
}
4950

5051
export const store = createStore<Store>('homepage', {
@@ -57,7 +58,8 @@ export const store = createStore<Store>('homepage', {
5758
secondaryPartitionUsageDesc: '',
5859
memoryUsage: 0,
5960
memoryUsageDesc: '',
60-
networkSpeedSummary: '—',
61+
networkSpeedUpload: 0,
62+
networkSpeedDownload: 0,
6163
},
6264
homepageCategories: [],
6365
searchQuery: '',

0 commit comments

Comments
 (0)