Skip to content

Commit e13c24a

Browse files
zhravanraghavyuva
andauthored
feat: dashboard with draggable layout, charts, and extended system metrics (#536)
* chore: .gitignore updated * feat: draggable cards * chore: updated file * feat: redesign graphical representation of existing items * feat: system stats CPU core count * feat: redesign graphical representation of existing items * fix: logic cpu_core count * feat: doughnut chart for memory usage * fix: css alignment of the card for dashboard pages * fix: yarn.lock file added * feat: reusable system-metric-card, centralized loading state mgmt * refactor: move out changes * feat: cpu usage for per core load * style: fix load avg bar graph alignment * refactor: separate out view and hooks * refactor: fix typoe metric hook * fix: reset layout button shown only when user has custom layout * refactor: skeletons to specific files * refactor: dashboard metric components * refactor: dashboard components --------- Co-authored-by: raghavyuva <vikramnbhat15@gmail.com> Co-authored-by: Raghavendra Bhat <53376933+raghavyuva@users.noreply.github.com>
1 parent e20a96f commit e13c24a

35 files changed

+1869
-468
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,5 @@ poetry.lock
6666
artifacts/
6767
cli/nixopus.spec
6868

69-
**/.DS_Store
69+
**/.DS_Store
70+
api/nixopus-api

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-22T16:45:33.662757+05:30",
6+
"release_date": "2025-10-27T17:11:46.599575+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: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ func formatBytes(bytes uint64, unit string) string {
3131
}
3232
}
3333

34+
// TODO: Add support for multi server management
35+
// solution: create a bridge between the gopsutil and the ssh client
3436
func (m *DashboardMonitor) GetSystemStats() {
3537
osType, err := m.getCommandOutput("uname -s")
3638
if err != nil {
@@ -42,24 +44,48 @@ func (m *DashboardMonitor) GetSystemStats() {
4244
stats := SystemStats{
4345
OSType: osType,
4446
Timestamp: time.Now(),
47+
CPU: CPUStats{PerCore: []CPUCore{}},
4548
Memory: MemoryStats{},
4649
Load: LoadStats{},
4750
Disk: DiskStats{AllMounts: []DiskMount{}},
4851
}
4952

53+
if hostname, err := m.getCommandOutput("hostname"); err == nil {
54+
stats.Hostname = strings.TrimSpace(hostname)
55+
}
56+
57+
if kernelVersion, err := m.getCommandOutput("uname -r"); err == nil {
58+
stats.KernelVersion = strings.TrimSpace(kernelVersion)
59+
}
60+
61+
if architecture, err := m.getCommandOutput("uname -m"); err == nil {
62+
stats.Architecture = strings.TrimSpace(architecture)
63+
}
64+
65+
var uptime string
5066
if hostInfo, err := host.Info(); err == nil {
51-
stats.Load.Uptime = time.Duration(hostInfo.Uptime * uint64(time.Second)).String()
67+
uptime = time.Duration(hostInfo.Uptime * uint64(time.Second)).String()
5268
}
5369

5470
if loadAvg, err := m.getCommandOutput("uptime"); err == nil {
5571
loadAvgStr := strings.TrimSpace(loadAvg)
5672
stats.Load = parseLoadAverage(loadAvgStr)
5773
}
5874

75+
stats.Load.Uptime = uptime
76+
5977
if cpuInfo, err := cpu.Info(); err == nil && len(cpuInfo) > 0 {
6078
stats.CPUInfo = cpuInfo[0].ModelName
6179
}
6280

81+
if stats.CPUCores == 0 {
82+
if coreCount, err := cpu.Counts(true); err == nil {
83+
stats.CPUCores = coreCount
84+
}
85+
}
86+
87+
stats.CPU = m.getCPUStats()
88+
6389
if memInfo, err := mem.VirtualMemory(); err == nil {
6490
stats.Memory = MemoryStats{
6591
Total: float64(memInfo.Total) / bytesInGB,
@@ -126,6 +152,36 @@ func parseLoadAverage(loadStr string) LoadStats {
126152
return loadStats
127153
}
128154

155+
func (m *DashboardMonitor) getCPUStats() CPUStats {
156+
cpuStats := CPUStats{
157+
Overall: 0.0,
158+
PerCore: []CPUCore{},
159+
}
160+
161+
perCorePercent, err := cpu.Percent(time.Second, true)
162+
if err == nil && len(perCorePercent) > 0 {
163+
cpuStats.PerCore = make([]CPUCore, len(perCorePercent))
164+
var totalUsage float64 = 0
165+
166+
for i, usage := range perCorePercent {
167+
cpuStats.PerCore[i] = CPUCore{
168+
CoreID: i,
169+
Usage: usage,
170+
}
171+
totalUsage += usage
172+
}
173+
174+
cpuStats.Overall = totalUsage / float64(len(perCorePercent))
175+
} else {
176+
177+
if overallPercent, err := cpu.Percent(time.Second, false); err == nil && len(overallPercent) > 0 {
178+
cpuStats.Overall = overallPercent[0]
179+
}
180+
}
181+
182+
return cpuStats
183+
}
184+
129185
func (m *DashboardMonitor) getCommandOutput(cmd string) (string, error) {
130186
session, err := m.client.NewSession()
131187
if err != nil {

api/internal/features/dashboard/types.go

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

4545
type SystemStats struct {
46-
OSType string `json:"os_type"`
47-
CPUInfo string `json:"cpu_info"`
48-
Memory MemoryStats `json:"memory"`
49-
Load LoadStats `json:"load"`
50-
Disk DiskStats `json:"disk"`
51-
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+
KernelVersion string `json:"kernel_version"`
55+
Architecture string `json:"architecture"`
56+
Timestamp time.Time `json:"timestamp"`
57+
}
58+
59+
type CPUCore struct {
60+
CoreID int `json:"core_id"`
61+
Usage float64 `json:"usage"`
62+
}
63+
64+
type CPUStats struct {
65+
Overall float64 `json:"overall"`
66+
PerCore []CPUCore `json:"per_core"`
5267
}
5368

5469
type MemoryStats struct {

view/app/dashboard/components/smtp-banner.tsx

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,13 @@
11
'use client';
22

3-
import React from 'react';
43
import { Alert, AlertDescription } from '@/components/ui/alert';
54
import { Button } from '@/components/ui/button';
65
import { X } from 'lucide-react';
7-
import { useTranslation } from '@/hooks/use-translation';
8-
import { useRouter } from 'next/navigation';
96
import { TypographyMuted } from '@/components/ui/typography';
10-
11-
const SMTP_BANNER_KEY = 'smtp_banner_dismissed';
7+
import useSmtpBanner from '../hooks/use-smtp-banner';
128

139
export function SMTPBanner() {
14-
const { t } = useTranslation();
15-
const router = useRouter();
16-
const [isVisible, setIsVisible] = React.useState(false);
17-
18-
React.useEffect(() => {
19-
const dismissed = localStorage.getItem(SMTP_BANNER_KEY);
20-
if (!dismissed) {
21-
setIsVisible(true);
22-
}
23-
}, []);
24-
25-
const handleDismiss = () => {
26-
localStorage.setItem(SMTP_BANNER_KEY, 'true');
27-
setIsVisible(false);
28-
};
29-
30-
const handleConfigure = () => {
31-
router.push('/settings/notifications');
32-
};
10+
const { handleDismiss, handleConfigure, t, isVisible } = useSmtpBanner();
3311

3412
if (!isVisible) return null;
3513

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
'use client';
2+
3+
import React from 'react';
4+
import { Cpu } from 'lucide-react';
5+
import { SystemStatsType } from '@/redux/types/monitor';
6+
import { TypographySmall, TypographyMuted } from '@/components/ui/typography';
7+
import { BarChartComponent } from '@/components/ui/bar-chart-component';
8+
import { SystemMetricCard } from './system-metric-card';
9+
import { useSystemMetric } from '../../hooks/use-system-metric';
10+
import { createCPUChartData, createCPUChartConfig, formatPercentage } from '../utils/utils';
11+
import { DEFAULT_METRICS, CHART_COLORS } from '../utils/constants';
12+
import { CPUUsageCardSkeletonContent } from './skeletons/cpu-usage';
13+
14+
interface CPUUsageCardProps {
15+
systemStats: SystemStatsType | null;
16+
}
17+
18+
interface CPUUsageHeaderProps {
19+
overallUsage: number;
20+
label: string;
21+
}
22+
23+
interface CPUUsageChartProps {
24+
chartData: ReturnType<typeof createCPUChartData>;
25+
chartConfig: ReturnType<typeof createCPUChartConfig>;
26+
yAxisLabel: string;
27+
xAxisLabel: string;
28+
}
29+
30+
interface TopCoresListProps {
31+
cores: Array<{ core_id: number; usage: number }>;
32+
}
33+
34+
interface CoreItemProps {
35+
coreId: number;
36+
usage: number;
37+
color: string;
38+
}
39+
40+
const CPU_COLORS = [
41+
CHART_COLORS.blue,
42+
CHART_COLORS.green,
43+
CHART_COLORS.orange,
44+
CHART_COLORS.purple,
45+
CHART_COLORS.red,
46+
CHART_COLORS.yellow,
47+
];
48+
49+
const CPUUsageHeader: React.FC<CPUUsageHeaderProps> = ({ overallUsage, label }) => {
50+
return (
51+
<div className="text-center">
52+
<TypographyMuted className="text-xs">{label}</TypographyMuted>
53+
<div className="text-3xl font-bold text-primary mt-1">
54+
{formatPercentage(overallUsage)}%
55+
</div>
56+
</div>
57+
);
58+
};
59+
60+
const CPUUsageChart: React.FC<CPUUsageChartProps> = ({
61+
chartData,
62+
chartConfig,
63+
yAxisLabel,
64+
xAxisLabel,
65+
}) => {
66+
return (
67+
<div>
68+
<BarChartComponent
69+
data={chartData}
70+
chartConfig={chartConfig}
71+
height="h-[180px]"
72+
yAxisLabel={yAxisLabel}
73+
xAxisLabel={xAxisLabel}
74+
showAxisLabels={true}
75+
/>
76+
</div>
77+
);
78+
};
79+
80+
const CoreItem: React.FC<CoreItemProps> = ({ coreId, usage, color }) => {
81+
return (
82+
<div className="flex flex-col items-center gap-1">
83+
<div className="flex items-center gap-1">
84+
<div className="h-2 w-2 rounded-full" style={{ backgroundColor: color }} />
85+
<TypographyMuted className="text-xs">Core {coreId}</TypographyMuted>
86+
</div>
87+
<TypographySmall className="text-sm font-bold">
88+
{formatPercentage(usage)}%
89+
</TypographySmall>
90+
</div>
91+
);
92+
};
93+
94+
const TopCoresList: React.FC<TopCoresListProps> = ({ cores }) => {
95+
return (
96+
<div className="grid grid-cols-3 gap-2 text-center">
97+
{cores.map((core) => {
98+
const color = CPU_COLORS[core.core_id % CPU_COLORS.length];
99+
return (
100+
<CoreItem
101+
key={core.core_id}
102+
coreId={core.core_id}
103+
usage={core.usage}
104+
color={color}
105+
/>
106+
);
107+
})}
108+
</div>
109+
);
110+
};
111+
112+
const CPUUsageCard: React.FC<CPUUsageCardProps> = ({ systemStats }) => {
113+
const { data: cpu, isLoading, t } = useSystemMetric({
114+
systemStats,
115+
extractData: (stats) => stats.cpu,
116+
defaultData: DEFAULT_METRICS.cpu,
117+
});
118+
119+
const perCoreData = cpu.per_core;
120+
const chartData = createCPUChartData(perCoreData);
121+
const chartConfig = createCPUChartConfig(perCoreData.length);
122+
const topCores = [...perCoreData].sort((a, b) => b.usage - a.usage).slice(0, 3);
123+
124+
return (
125+
<SystemMetricCard
126+
title={t('dashboard.cpu.title')}
127+
icon={Cpu}
128+
isLoading={isLoading}
129+
skeletonContent={<CPUUsageCardSkeletonContent />}
130+
>
131+
<div className="space-y-4">
132+
<CPUUsageHeader
133+
overallUsage={cpu.overall}
134+
label={t('dashboard.cpu.overall')}
135+
/>
136+
137+
<CPUUsageChart
138+
chartData={chartData}
139+
chartConfig={chartConfig}
140+
yAxisLabel={t('dashboard.cpu.usage')}
141+
xAxisLabel={t('dashboard.cpu.cores')}
142+
/>
143+
144+
<TopCoresList cores={topCores} />
145+
</div>
146+
</SystemMetricCard>
147+
);
148+
};
149+
150+
export default CPUUsageCard;

0 commit comments

Comments
 (0)