Skip to content

Commit b150d69

Browse files
authored
feat: configurable dashboard widgets with topbar (#541)
* feat: add clock and weather widget for dashboard * feat: add topbar with network speed indicators * refactor: containers widget in dashboard
1 parent 577cf5c commit b150d69

File tree

24 files changed

+1057
-61
lines changed

24 files changed

+1057
-61
lines changed

api/api/versions.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
{
44
"version": "v1",
55
"status": "active",
6-
"release_date": "2025-10-27T17:11:46.599575+05:30",
6+
"release_date": "2025-10-28T22:04:41.913593+05:30",
77
"end_of_life": "0001-01-01T00:00:00Z",
88
"changes": [
99
"Initial API version"

api/internal/features/dashboard/system_stats.go

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/shirou/gopsutil/v3/disk"
1414
"github.com/shirou/gopsutil/v3/host"
1515
"github.com/shirou/gopsutil/v3/mem"
16+
"github.com/shirou/gopsutil/v3/net"
1617
)
1718

1819
const (
@@ -31,7 +32,7 @@ func formatBytes(bytes uint64, unit string) string {
3132
}
3233
}
3334

34-
// TODO: Add support for multi server management
35+
// TODO: Add support for multi server management
3536
// solution: create a bridge between the gopsutil and the ssh client
3637
func (m *DashboardMonitor) GetSystemStats() {
3738
osType, err := m.getCommandOutput("uname -s")
@@ -48,6 +49,7 @@ func (m *DashboardMonitor) GetSystemStats() {
4849
Memory: MemoryStats{},
4950
Load: LoadStats{},
5051
Disk: DiskStats{AllMounts: []DiskMount{}},
52+
Network: NetworkStats{Interfaces: []NetworkInterface{}},
5153
}
5254

5355
if hostname, err := m.getCommandOutput("hostname"); err == nil {
@@ -129,6 +131,8 @@ func (m *DashboardMonitor) GetSystemStats() {
129131
stats.Disk = diskStats
130132
}
131133

134+
stats.Network = m.getNetworkStats()
135+
132136
m.Broadcast(string(GetSystemStats), stats)
133137
}
134138

@@ -182,6 +186,44 @@ func (m *DashboardMonitor) getCPUStats() CPUStats {
182186
return cpuStats
183187
}
184188

189+
func (m *DashboardMonitor) getNetworkStats() NetworkStats {
190+
networkStats := NetworkStats{
191+
Interfaces: []NetworkInterface{},
192+
}
193+
194+
if ioCounters, err := net.IOCounters(true); err == nil {
195+
var totalSent, totalRecv, totalPacketsSent, totalPacketsRecv uint64
196+
197+
for _, counter := range ioCounters {
198+
interfaces := NetworkInterface{
199+
Name: counter.Name,
200+
BytesSent: counter.BytesSent,
201+
BytesRecv: counter.BytesRecv,
202+
PacketsSent: counter.PacketsSent,
203+
PacketsRecv: counter.PacketsRecv,
204+
ErrorIn: counter.Errin,
205+
ErrorOut: counter.Errout,
206+
DropIn: counter.Dropin,
207+
DropOut: counter.Dropout,
208+
}
209+
210+
networkStats.Interfaces = append(networkStats.Interfaces, interfaces)
211+
212+
totalSent += counter.BytesSent
213+
totalRecv += counter.BytesRecv
214+
totalPacketsSent += counter.PacketsSent
215+
totalPacketsRecv += counter.PacketsRecv
216+
}
217+
218+
networkStats.TotalBytesSent = totalSent
219+
networkStats.TotalBytesRecv = totalRecv
220+
networkStats.TotalPacketsSent = totalPacketsSent
221+
networkStats.TotalPacketsRecv = totalPacketsRecv
222+
}
223+
224+
return networkStats
225+
}
226+
185227
func (m *DashboardMonitor) getCommandOutput(cmd string) (string, error) {
186228
session, err := m.client.NewSession()
187229
if err != nil {

api/internal/features/dashboard/types.go

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,27 +43,28 @@ type DashboardMonitor struct {
4343
}
4444

4545
type SystemStats struct {
46-
OSType string `json:"os_type"`
47-
Hostname string `json:"hostname"`
48-
CPUInfo string `json:"cpu_info"`
49-
CPUCores int `json:"cpu_cores"`
50-
CPU CPUStats `json:"cpu"`
51-
Memory MemoryStats `json:"memory"`
52-
Load LoadStats `json:"load"`
53-
Disk DiskStats `json:"disk"`
54-
KernelVersion string `json:"kernel_version"`
55-
Architecture string `json:"architecture"`
56-
Timestamp time.Time `json:"timestamp"`
46+
OSType string `json:"os_type"`
47+
Hostname string `json:"hostname"`
48+
CPUInfo string `json:"cpu_info"`
49+
CPUCores int `json:"cpu_cores"`
50+
CPU CPUStats `json:"cpu"`
51+
Memory MemoryStats `json:"memory"`
52+
Load LoadStats `json:"load"`
53+
Disk DiskStats `json:"disk"`
54+
Network NetworkStats `json:"network"`
55+
KernelVersion string `json:"kernel_version"`
56+
Architecture string `json:"architecture"`
57+
Timestamp time.Time `json:"timestamp"`
5758
}
5859

5960
type CPUCore struct {
60-
CoreID int `json:"core_id"`
61-
Usage float64 `json:"usage"`
61+
CoreID int `json:"core_id"`
62+
Usage float64 `json:"usage"`
6263
}
6364

6465
type CPUStats struct {
65-
Overall float64 `json:"overall"`
66-
PerCore []CPUCore `json:"per_core"`
66+
Overall float64 `json:"overall"`
67+
PerCore []CPUCore `json:"per_core"`
6768
}
6869

6970
type MemoryStats struct {
@@ -97,3 +98,25 @@ type DiskStats struct {
9798
MountPoint string `json:"mountPoint"`
9899
AllMounts []DiskMount `json:"allMounts"`
99100
}
101+
102+
type NetworkInterface struct {
103+
Name string `json:"name"`
104+
BytesSent uint64 `json:"bytesSent"`
105+
BytesRecv uint64 `json:"bytesRecv"`
106+
PacketsSent uint64 `json:"packetsSent"`
107+
PacketsRecv uint64 `json:"packetsRecv"`
108+
ErrorIn uint64 `json:"errorIn"`
109+
ErrorOut uint64 `json:"errorOut"`
110+
DropIn uint64 `json:"dropIn"`
111+
DropOut uint64 `json:"dropOut"`
112+
}
113+
114+
type NetworkStats struct {
115+
TotalBytesSent uint64 `json:"totalBytesSent"`
116+
TotalBytesRecv uint64 `json:"totalBytesRecv"`
117+
TotalPacketsSent uint64 `json:"totalPacketsSent"`
118+
TotalPacketsRecv uint64 `json:"totalPacketsRecv"`
119+
Interfaces []NetworkInterface `json:"interfaces"`
120+
UploadSpeed float64 `json:"uploadSpeed"`
121+
DownloadSpeed float64 `json:"downloadSpeed"`
122+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
'use client';
2+
3+
import React from 'react';
4+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
5+
import { Button } from '@/components/ui/button';
6+
import { Package, ArrowRight } from 'lucide-react';
7+
import { ContainerData } from '@/redux/types/monitor';
8+
import { useTranslation } from '@/hooks/use-translation';
9+
import { TypographySmall } from '@/components/ui/typography';
10+
import { useRouter } from 'next/navigation';
11+
import ContainersTable from './container-table';
12+
13+
interface ContainersWidgetProps {
14+
containersData: ContainerData[];
15+
}
16+
17+
const ContainersWidget: React.FC<ContainersWidgetProps> = ({ containersData }) => {
18+
const { t } = useTranslation();
19+
const router = useRouter();
20+
21+
return (
22+
<Card>
23+
<CardHeader className="flex flex-row items-center justify-between">
24+
<CardTitle className="text-xs sm:text-sm font-bold flex items-center">
25+
<Package className="h-3 w-3 sm:h-4 sm:w-4 mr-1 sm:mr-2 text-muted-foreground" />
26+
<TypographySmall>{t('dashboard.containers.title')}</TypographySmall>
27+
</CardTitle>
28+
<Button variant="outline" size="sm" onClick={() => router.push('/containers')}>
29+
<ArrowRight className="h-3 w-3 sm:h-4 sm:w-4 mr-1 sm:mr-2 text-muted-foreground" />
30+
{t('dashboard.containers.viewAll')}
31+
</Button>
32+
</CardHeader>
33+
<CardContent>
34+
<ContainersTable containersData={containersData} />
35+
</CardContent>
36+
</Card>
37+
);
38+
};
39+
40+
export default ContainersWidget;
41+
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
'use client';
2+
3+
import React from 'react';
4+
import { Clock } from 'lucide-react';
5+
import { SystemMetricCard } from './system-metric-card';
6+
import { ClockCardSkeletonContent } from './skeletons/clock';
7+
import useClock from '../../hooks/use-clock';
8+
9+
const ClockWidget: React.FC = () => {
10+
const { formattedTime, formattedDate } = useClock();
11+
12+
return (
13+
<SystemMetricCard
14+
title="Clock"
15+
icon={Clock}
16+
isLoading={false}
17+
skeletonContent={<ClockCardSkeletonContent />}
18+
>
19+
<div className="flex flex-col items-center justify-center h-full space-y-3">
20+
<div className="text-5xl font-bold text-primary tabular-nums">
21+
{formattedTime}
22+
</div>
23+
<div className="text-sm text-muted-foreground">
24+
{formattedDate}
25+
</div>
26+
</div>
27+
</SystemMetricCard>
28+
);
29+
};
30+
31+
export default ClockWidget;
32+

view/app/dashboard/components/system/load-average.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ const LoadAverageCard: React.FC<LoadAverageCardProps> = ({ systemStats }) => {
4545
/>
4646
</div>
4747

48-
{/* Summary Stats with Color Indicators */}
4948
<div className="grid grid-cols-3 gap-2 text-center">
5049
<div className="flex flex-col items-center gap-1">
5150
<div className="flex items-center gap-1">
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
'use client';
2+
3+
import React from 'react';
4+
import { Network } from 'lucide-react';
5+
import { SystemMetricCard } from './system-metric-card';
6+
import { NetworkCardSkeletonContent } from './skeletons/network';
7+
import { useNetwork } from '../../hooks/use-network';
8+
import { SystemStatsType } from '@/redux/types/monitor';
9+
import { ArrowDownCircle, ArrowUpCircle } from 'lucide-react';
10+
11+
interface NetworkWidgetProps {
12+
systemStats: SystemStatsType | null;
13+
}
14+
15+
const NetworkWidget: React.FC<NetworkWidgetProps> = ({ systemStats }) => {
16+
const { networkData } = useNetwork({ systemStats });
17+
18+
const isLoading = !systemStats || !systemStats.network;
19+
20+
return (
21+
<SystemMetricCard
22+
title="Network Traffic"
23+
icon={Network}
24+
isLoading={isLoading}
25+
skeletonContent={<NetworkCardSkeletonContent />}
26+
>
27+
<div className="flex flex-col items-center justify-center h-full space-y-4">
28+
<div className="grid grid-cols-2 gap-4 w-full">
29+
<div className="flex flex-col items-center text-center">
30+
<ArrowDownCircle className="h-8 w-8 text-blue-500 mb-2" />
31+
<div className="text-xs text-muted-foreground mb-1">Download</div>
32+
<div className="text-2xl font-bold text-primary tabular-nums">
33+
{networkData.downloadSpeed}
34+
</div>
35+
</div>
36+
<div className="flex flex-col items-center text-center">
37+
<ArrowUpCircle className="h-8 w-8 text-green-500 mb-2" />
38+
<div className="text-xs text-muted-foreground mb-1">Upload</div>
39+
<div className="text-2xl font-bold text-primary tabular-nums">
40+
{networkData.uploadSpeed}
41+
</div>
42+
</div>
43+
</div>
44+
<div className="flex gap-4 text-xs text-muted-foreground">
45+
<span>{networkData.totalDownload}</span>
46+
<span>{networkData.totalUpload}</span>
47+
</div>
48+
</div>
49+
</SystemMetricCard>
50+
);
51+
};
52+
53+
export default NetworkWidget;
54+
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Skeleton } from '@/components/ui/skeleton';
2+
3+
export const ClockCardSkeletonContent = () => {
4+
return (
5+
<div className="flex flex-col items-center justify-center space-y-3">
6+
<Skeleton className="h-20 w-64" />
7+
<Skeleton className="h-4 w-56" />
8+
</div>
9+
);
10+
};
11+

view/app/dashboard/components/system/skeletons/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,6 @@ export { LoadAverageCardSkeleton, LoadAverageCardSkeletonContent } from './load-
33
export { MemoryUsageCardSkeleton, MemoryUsageCardSkeletonContent } from './memory-usage';
44
export { DiskUsageCardSkeleton, DiskUsageCardSkeletonContent } from './disk-usage';
55
export { SystemInfoCardSkeleton } from './system-info';
6+
export { ClockCardSkeletonContent } from './clock';
7+
export { WeatherCardSkeletonContent } from './weather';
8+
export { NetworkCardSkeletonContent } from './network';
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use client';
2+
3+
import React from 'react';
4+
import { Skeleton } from '@/components/ui/skeleton';
5+
6+
export const NetworkCardSkeletonContent: React.FC = () => {
7+
return (
8+
<div className="flex flex-col items-center justify-center h-full space-y-4">
9+
<Skeleton className="h-16 w-full" />
10+
<Skeleton className="h-16 w-full" />
11+
<Skeleton className="h-8 w-3/4" />
12+
</div>
13+
);
14+
};
15+

0 commit comments

Comments
 (0)