forked from github-starred/komodo
simplify network stats
This commit is contained in:
@@ -28,9 +28,6 @@ pub async fn record_server_stats(ts: i64) {
|
||||
disks: stats.disks.clone(),
|
||||
network_ingress_bytes: stats.network_ingress_bytes,
|
||||
network_egress_bytes: stats.network_egress_bytes,
|
||||
network_usage_interface: stats
|
||||
.network_usage_interface
|
||||
.clone(),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -2,8 +2,7 @@ use std::{cmp::Ordering, sync::OnceLock};
|
||||
|
||||
use async_timing_util::wait_until_timelength;
|
||||
use komodo_client::entities::stats::{
|
||||
SingleDiskUsage, SingleNetworkInterfaceUsage, SystemInformation,
|
||||
SystemProcess, SystemStats,
|
||||
SingleDiskUsage, SystemInformation, SystemProcess, SystemStats,
|
||||
};
|
||||
use sysinfo::{ProcessesToUpdate, System};
|
||||
use tokio::sync::RwLock;
|
||||
@@ -84,40 +83,21 @@ impl StatsClient {
|
||||
let total_mem = self.system.total_memory();
|
||||
let available_mem = self.system.available_memory();
|
||||
|
||||
let mut total_ingress: u64 = 0;
|
||||
let mut total_egress: u64 = 0;
|
||||
let mut network_ingress_bytes: u64 = 0;
|
||||
let mut network_egress_bytes: u64 = 0;
|
||||
|
||||
// Fetch network data (Ingress and Egress)
|
||||
let network_usage: Vec<SingleNetworkInterfaceUsage> = self
|
||||
.networks
|
||||
.iter()
|
||||
.map(|(interface_name, network)| {
|
||||
let ingress = network.received();
|
||||
let egress = network.transmitted();
|
||||
|
||||
// Update total ingress and egress
|
||||
total_ingress += ingress;
|
||||
total_egress += egress;
|
||||
|
||||
// Return per-interface network stats
|
||||
SingleNetworkInterfaceUsage {
|
||||
name: interface_name.clone(),
|
||||
ingress_bytes: ingress as f64,
|
||||
egress_bytes: egress as f64,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
for (_, network) in self.networks.iter() {
|
||||
network_ingress_bytes += network.received();
|
||||
network_egress_bytes += network.transmitted();
|
||||
}
|
||||
|
||||
SystemStats {
|
||||
cpu_perc: self.system.global_cpu_usage(),
|
||||
mem_free_gb: self.system.free_memory() as f64 / BYTES_PER_GB,
|
||||
mem_used_gb: (total_mem - available_mem) as f64 / BYTES_PER_GB,
|
||||
mem_total_gb: total_mem as f64 / BYTES_PER_GB,
|
||||
// Added total ingress and egress
|
||||
network_ingress_bytes: total_ingress as f64,
|
||||
network_egress_bytes: total_egress as f64,
|
||||
network_usage_interface: network_usage,
|
||||
|
||||
network_ingress_bytes: network_ingress_bytes as f64,
|
||||
network_egress_bytes: network_egress_bytes as f64,
|
||||
disks: self.get_disks(),
|
||||
polling_rate: self.stats.polling_rate,
|
||||
refresh_ts: self.stats.refresh_ts,
|
||||
|
||||
@@ -49,17 +49,17 @@ pub struct SystemStatsRecord {
|
||||
pub disk_used_gb: f64,
|
||||
/// Total disk size in GB
|
||||
pub disk_total_gb: f64,
|
||||
/// Breakdown of individual disks, ie their usages, sizes, and mount points
|
||||
/// Breakdown of individual disks, including their usage, total size, and mount point
|
||||
pub disks: Vec<SingleDiskUsage>,
|
||||
/// Network ingress usage in bytes
|
||||
/// Total network ingress in bytes
|
||||
#[serde(default)]
|
||||
pub network_ingress_bytes: f64,
|
||||
/// Network egress usage in bytes
|
||||
/// Total network egress in bytes
|
||||
#[serde(default)]
|
||||
pub network_egress_bytes: f64,
|
||||
/// Network usage by interface name (ingress, egress in bytes)
|
||||
#[serde(default)]
|
||||
pub network_usage_interface: Vec<SingleNetworkInterfaceUsage>, // interface -> (ingress, egress)
|
||||
// /// Network usage by interface name (ingress, egress in bytes)
|
||||
// #[serde(default)]
|
||||
// pub network_usage_interface: Vec<SingleNetworkInterfaceUsage>, // interface -> (ingress, egress)
|
||||
}
|
||||
|
||||
/// Realtime system stats data.
|
||||
@@ -86,9 +86,9 @@ pub struct SystemStats {
|
||||
/// Network egress usage in MB
|
||||
#[serde(default)]
|
||||
pub network_egress_bytes: f64,
|
||||
/// Network usage by interface name (ingress, egress in bytes)
|
||||
#[serde(default)]
|
||||
pub network_usage_interface: Vec<SingleNetworkInterfaceUsage>, // interface -> (ingress, egress)
|
||||
// /// Network usage by interface name (ingress, egress in bytes)
|
||||
// #[serde(default)]
|
||||
// pub network_usage_interface: Vec<SingleNetworkInterfaceUsage>, // interface -> (ingress, egress)
|
||||
// metadata
|
||||
/// The rate the system stats are being polled from the system
|
||||
pub polling_rate: Timelength,
|
||||
|
||||
@@ -1820,16 +1820,6 @@ export interface SingleDiskUsage {
|
||||
total_gb: number;
|
||||
}
|
||||
|
||||
/** Info for network interface usage. */
|
||||
export interface SingleNetworkInterfaceUsage {
|
||||
/** The network interface name */
|
||||
name: string;
|
||||
/** The ingress in bytes */
|
||||
ingress_bytes: number;
|
||||
/** The egress in bytes */
|
||||
egress_bytes: number;
|
||||
}
|
||||
|
||||
export enum Timelength {
|
||||
OneSecond = "1-sec",
|
||||
FiveSeconds = "5-sec",
|
||||
@@ -1875,8 +1865,6 @@ export interface SystemStats {
|
||||
network_ingress_bytes?: number;
|
||||
/** Network egress usage in MB */
|
||||
network_egress_bytes?: number;
|
||||
/** Network usage by interface name (ingress, egress in bytes) */
|
||||
network_usage_interface?: SingleNetworkInterfaceUsage[];
|
||||
/** The rate the system stats are being polled from the system */
|
||||
polling_rate: Timelength;
|
||||
/** Unix timestamp in milliseconds when stats were last polled */
|
||||
@@ -5099,14 +5087,12 @@ export interface SystemStatsRecord {
|
||||
disk_used_gb: number;
|
||||
/** Total disk size in GB */
|
||||
disk_total_gb: number;
|
||||
/** Breakdown of individual disks, ie their usages, sizes, and mount points */
|
||||
/** Breakdown of individual disks, including their usage, total size, and mount point */
|
||||
disks: SingleDiskUsage[];
|
||||
/** Network ingress usage in bytes */
|
||||
/** Total network ingress in bytes */
|
||||
network_ingress_bytes?: number;
|
||||
/** Network egress usage in bytes */
|
||||
/** Total network egress in bytes */
|
||||
network_egress_bytes?: number;
|
||||
/** Network usage by interface name (ingress, egress in bytes) */
|
||||
network_usage_interface?: SingleNetworkInterfaceUsage[];
|
||||
}
|
||||
|
||||
/** Response to [GetHistoricalServerStats]. */
|
||||
@@ -6833,6 +6819,16 @@ export interface SetUsersInUserGroup {
|
||||
users: string[];
|
||||
}
|
||||
|
||||
/** Info for network interface usage. */
|
||||
export interface SingleNetworkInterfaceUsage {
|
||||
/** The network interface name */
|
||||
name: string;
|
||||
/** The ingress in bytes */
|
||||
ingress_bytes: number;
|
||||
/** The egress in bytes */
|
||||
egress_bytes: number;
|
||||
}
|
||||
|
||||
/** Configuration for a Slack alerter. */
|
||||
export interface SlackAlerterEndpoint {
|
||||
/** The Slack app webhook url */
|
||||
|
||||
28
frontend/public/client/types.d.ts
vendored
28
frontend/public/client/types.d.ts
vendored
@@ -1910,15 +1910,6 @@ export interface SingleDiskUsage {
|
||||
/** Total size of the disk in GB */
|
||||
total_gb: number;
|
||||
}
|
||||
/** Info for network interface usage. */
|
||||
export interface SingleNetworkInterfaceUsage {
|
||||
/** The network interface name */
|
||||
name: string;
|
||||
/** The ingress in bytes */
|
||||
ingress_bytes: number;
|
||||
/** The egress in bytes */
|
||||
egress_bytes: number;
|
||||
}
|
||||
export declare enum Timelength {
|
||||
OneSecond = "1-sec",
|
||||
FiveSeconds = "5-sec",
|
||||
@@ -1963,8 +1954,6 @@ export interface SystemStats {
|
||||
network_ingress_bytes?: number;
|
||||
/** Network egress usage in MB */
|
||||
network_egress_bytes?: number;
|
||||
/** Network usage by interface name (ingress, egress in bytes) */
|
||||
network_usage_interface?: SingleNetworkInterfaceUsage[];
|
||||
/** The rate the system stats are being polled from the system */
|
||||
polling_rate: Timelength;
|
||||
/** Unix timestamp in milliseconds when stats were last polled */
|
||||
@@ -4852,14 +4841,12 @@ export interface SystemStatsRecord {
|
||||
disk_used_gb: number;
|
||||
/** Total disk size in GB */
|
||||
disk_total_gb: number;
|
||||
/** Breakdown of individual disks, ie their usages, sizes, and mount points */
|
||||
/** Breakdown of individual disks, including their usage, total size, and mount point */
|
||||
disks: SingleDiskUsage[];
|
||||
/** Network ingress usage in bytes */
|
||||
/** Total network ingress in bytes */
|
||||
network_ingress_bytes?: number;
|
||||
/** Network egress usage in bytes */
|
||||
/** Total network egress in bytes */
|
||||
network_egress_bytes?: number;
|
||||
/** Network usage by interface name (ingress, egress in bytes) */
|
||||
network_usage_interface?: SingleNetworkInterfaceUsage[];
|
||||
}
|
||||
/** Response to [GetHistoricalServerStats]. */
|
||||
export interface GetHistoricalServerStatsResponse {
|
||||
@@ -6411,6 +6398,15 @@ export interface SetUsersInUserGroup {
|
||||
/** The user ids or usernames to hard set as the group's users. */
|
||||
users: string[];
|
||||
}
|
||||
/** Info for network interface usage. */
|
||||
export interface SingleNetworkInterfaceUsage {
|
||||
/** The network interface name */
|
||||
name: string;
|
||||
/** The ingress in bytes */
|
||||
ingress_bytes: number;
|
||||
/** The egress in bytes */
|
||||
egress_bytes: number;
|
||||
}
|
||||
/** Configuration for a Slack alerter. */
|
||||
export interface SlackAlerterEndpoint {
|
||||
/** The Slack app webhook url */
|
||||
|
||||
@@ -8,10 +8,3 @@ const statsGranularityAtom = atomWithStorage<Types.Timelength>(
|
||||
);
|
||||
|
||||
export const useStatsGranularity = () => useAtom(statsGranularityAtom);
|
||||
|
||||
const selectedNetworkInterfaceAtom = atomWithStorage<string | undefined>(
|
||||
"selected-network-interface-v0",
|
||||
undefined // Default value is `undefined` (Global view)
|
||||
);
|
||||
|
||||
export const useSelectedNetworkInterface = () => useAtom(selectedNetworkInterfaceAtom);
|
||||
|
||||
@@ -2,14 +2,14 @@ import { hex_color_by_intention } from "@lib/color";
|
||||
import { useRead } from "@lib/hooks";
|
||||
import { Types } from "komodo_client";
|
||||
import { useMemo } from "react";
|
||||
import { useStatsGranularity, useSelectedNetworkInterface } from "./hooks";
|
||||
import { useStatsGranularity } from "./hooks";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { AxisOptions, Chart } from "react-charts";
|
||||
import { convertTsMsToLocalUnixTsInMs } from "@lib/utils";
|
||||
import { useTheme } from "@ui/theme";
|
||||
import { fmt_utc_date } from "@lib/formatting";
|
||||
|
||||
type StatType = "cpu" | "mem" | "disk" | "network_ingress" | "network_egress" | "network_interface_ingress" | "network_interface_egress";
|
||||
type StatType = "cpu" | "mem" | "disk" | "network_ingress" | "network_egress";
|
||||
|
||||
type StatDatapoint = { date: number; value: number };
|
||||
|
||||
@@ -22,13 +22,11 @@ export const StatChart = ({
|
||||
type: StatType;
|
||||
className?: string;
|
||||
}) => {
|
||||
const [selectedInterface] = useSelectedNetworkInterface();
|
||||
const [granularity] = useStatsGranularity();
|
||||
|
||||
const { data, isPending } = useRead("GetHistoricalServerStats", {
|
||||
server: server_id,
|
||||
granularity,
|
||||
selectedInterface,
|
||||
});
|
||||
|
||||
const stats = useMemo(
|
||||
@@ -37,7 +35,7 @@ export const StatChart = ({
|
||||
.map((stat) => {
|
||||
return {
|
||||
date: convertTsMsToLocalUnixTsInMs(stat.ts),
|
||||
value: getStat(stat, type, selectedInterface),
|
||||
value: getStat(stat, type),
|
||||
};
|
||||
})
|
||||
.reverse(),
|
||||
@@ -58,6 +56,10 @@ export const StatChart = ({
|
||||
);
|
||||
};
|
||||
|
||||
const BYTES_PER_GB = 1073741824.0;
|
||||
const BYTES_PER_MB = 1048576.0;
|
||||
const BYTES_PER_KB = 1024.0;
|
||||
|
||||
export const InnerStatChart = ({
|
||||
type,
|
||||
stats,
|
||||
@@ -72,12 +74,11 @@ export const InnerStatChart = ({
|
||||
? "dark"
|
||||
: "light"
|
||||
: _theme;
|
||||
const BYTES_PER_GB = 1073741824.0;
|
||||
const BYTES_PER_MB = 1048576.0;
|
||||
const BYTES_PER_KB = 1024.0;
|
||||
|
||||
const min = stats?.[0]?.date ?? 0;
|
||||
const max = stats?.[stats.length - 1]?.date ?? 0;
|
||||
const diff = max - min;
|
||||
|
||||
const timeAxis = useMemo((): AxisOptions<StatDatapoint> => {
|
||||
return {
|
||||
getValue: (datum) => new Date(datum.date),
|
||||
@@ -177,27 +178,14 @@ export const InnerStatChart = ({
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
const getStat = (stat: Types.SystemStatsRecord, type: StatType, selectedInterface?: string) => {
|
||||
const getStat = (stat: Types.SystemStatsRecord, type: StatType) => {
|
||||
if (type === "cpu") return stat.cpu_perc || 0;
|
||||
if (type === "mem") return (100 * stat.mem_used_gb) / stat.mem_total_gb;
|
||||
if (type === "disk") return (100 * stat.disk_used_gb) / stat.disk_total_gb;
|
||||
if (type === "network_ingress") return stat.network_ingress_bytes || 0;
|
||||
if (type === "network_egress") return stat.network_egress_bytes || 0;
|
||||
if (type === "network_interface_ingress")
|
||||
return selectedInterface
|
||||
? stat.network_usage_interface?.find(
|
||||
(networkInterface) => networkInterface.name === selectedInterface
|
||||
)?.ingress_bytes || 0
|
||||
: stat.network_ingress_bytes || 0;
|
||||
if (type === "network_interface_egress")
|
||||
return selectedInterface
|
||||
? stat.network_usage_interface?.find(
|
||||
(networkInterface) => networkInterface.name === selectedInterface
|
||||
)?.egress_bytes || 0
|
||||
: stat.network_egress_bytes || 0;
|
||||
return 0;
|
||||
};
|
||||
|
||||
@@ -205,7 +193,7 @@ const getColor = (type: StatType) => {
|
||||
if (type === "cpu") return hex_color_by_intention("Good");
|
||||
if (type === "mem") return hex_color_by_intention("Warning");
|
||||
if (type === "disk") return hex_color_by_intention("Neutral");
|
||||
if (type === "network_interface_ingress") return hex_color_by_intention("Critical");
|
||||
if (type === "network_interface_egress") return hex_color_by_intention("Unknown");
|
||||
if (type === "network_ingress") return hex_color_by_intention("Good");
|
||||
if (type === "network_egress") return hex_color_by_intention("Critical");
|
||||
return hex_color_by_intention("Unknown");
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { Section } from "@components/layouts";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@ui/card";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@ui/card";
|
||||
import { Progress } from "@ui/progress";
|
||||
import { Cpu, Database, Loader2, MemoryStick } from "lucide-react";
|
||||
import { useRead } from "@lib/hooks";
|
||||
@@ -14,7 +8,7 @@ import { DataTable, SortableHeader } from "@ui/data-table";
|
||||
import { ReactNode, useMemo, useState } from "react";
|
||||
import { Input } from "@ui/input";
|
||||
import { StatChart } from "./stat-chart";
|
||||
import { useStatsGranularity, useSelectedNetworkInterface } from "./hooks";
|
||||
import { useStatsGranularity } from "./hooks";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -32,7 +26,6 @@ export const ServerStats = ({
|
||||
titleOther?: ReactNode;
|
||||
}) => {
|
||||
const [interval, setInterval] = useStatsGranularity();
|
||||
const [networkInterface, setNetworkInterface] = useSelectedNetworkInterface();
|
||||
|
||||
const stats = useRead(
|
||||
"GetSystemStats",
|
||||
@@ -75,6 +68,10 @@ export const ServerStats = ({
|
||||
header: "Kernel",
|
||||
accessorKey: "kernel",
|
||||
},
|
||||
{
|
||||
header: "CPU",
|
||||
accessorKey: "cpu_brand",
|
||||
},
|
||||
{
|
||||
header: "Core Count",
|
||||
accessorFn: ({ core_count }) =>
|
||||
@@ -93,11 +90,11 @@ export const ServerStats = ({
|
||||
</Section>
|
||||
|
||||
<Section title="Current">
|
||||
<div className="flex flex-col lg:flex-row gap-4">
|
||||
<div className="flex flex-col xl:flex-row gap-4">
|
||||
<CPU stats={stats} />
|
||||
<RAM stats={stats} />
|
||||
<NETWORK stats={stats} />
|
||||
<DISK stats={stats} />
|
||||
<NETWORK stats={stats} />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
@@ -136,39 +133,6 @@ export const ServerStats = ({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Network Interface Dropdown */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-muted-foreground">Interface:</div>
|
||||
<Select
|
||||
value={networkInterface ?? "all"} // Show "all" if networkInterface is undefined
|
||||
onValueChange={(interfaceName) => {
|
||||
if (interfaceName === "all") {
|
||||
setNetworkInterface(undefined); // Set undefined for "All" option
|
||||
} else {
|
||||
setNetworkInterface(interfaceName);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[150px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All</SelectItem>
|
||||
{/* Iterate over the vector and access the `name` property */}
|
||||
{(stats?.network_usage_interface ?? []).map(
|
||||
(networkInterface) => (
|
||||
<SelectItem
|
||||
key={networkInterface.name}
|
||||
value={networkInterface.name}
|
||||
>
|
||||
{networkInterface.name}
|
||||
</SelectItem>
|
||||
)
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
@@ -355,44 +319,62 @@ const ProcessesInner = ({
|
||||
);
|
||||
};
|
||||
|
||||
const CPU = ({ stats }: { stats: Types.SystemStats | undefined }) => {
|
||||
const perc = stats?.cpu_perc;
|
||||
|
||||
const StatBar = ({
|
||||
title,
|
||||
icon,
|
||||
percentage,
|
||||
}: {
|
||||
title: string;
|
||||
icon: ReactNode;
|
||||
percentage: number | undefined;
|
||||
}) => {
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardHeader className="flex-row justify-between">
|
||||
<CardTitle>CPU Usage</CardTitle>
|
||||
<CardHeader className="flex-row items-center justify-between">
|
||||
<CardTitle>{title}</CardTitle>
|
||||
<div className="flex gap-2 items-center">
|
||||
<CardDescription>{perc?.toFixed(2)}%</CardDescription>
|
||||
<Cpu className="w-4 h-4" />
|
||||
<div className="text-lg">{percentage?.toFixed(2)}%</div>
|
||||
{icon}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Progress value={perc} className="h-4" />
|
||||
<Progress value={percentage} className="h-4" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const CPU = ({ stats }: { stats: Types.SystemStats | undefined }) => {
|
||||
return (
|
||||
<StatBar
|
||||
title="CPU Usage"
|
||||
icon={<Cpu className="w-5 h-5" />}
|
||||
percentage={stats?.cpu_perc}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const RAM = ({ stats }: { stats: Types.SystemStats | undefined }) => {
|
||||
const used = stats?.mem_used_gb;
|
||||
const total = stats?.mem_total_gb;
|
||||
|
||||
const perc = ((used ?? 0) / (total ?? 0)) * 100;
|
||||
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardHeader className="flex-row justify-between">
|
||||
<CardTitle>RAM Usage</CardTitle>
|
||||
<div className="flex gap-2 items-center">
|
||||
<CardDescription>{perc.toFixed(2)}%</CardDescription>
|
||||
<MemoryStick className="w-4 h-4" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Progress value={perc} className="h-4" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<StatBar
|
||||
title="RAM Usage"
|
||||
icon={<MemoryStick className="w-5 h-5" />}
|
||||
percentage={((used ?? 0) / (total ?? 0)) * 100}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const DISK = ({ stats }: { stats: Types.SystemStats | undefined }) => {
|
||||
const used = stats?.disks.reduce((acc, curr) => (acc += curr.used_gb), 0);
|
||||
const total = stats?.disks.reduce((acc, curr) => (acc += curr.total_gb), 0);
|
||||
return (
|
||||
<StatBar
|
||||
title="Disk Usage"
|
||||
icon={<Database className="w-5 h-5" />}
|
||||
percentage={((used ?? 0) / (total ?? 0)) * 100}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -441,25 +423,3 @@ const NETWORK = ({ stats }: { stats: Types.SystemStats | undefined }) => {
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const DISK = ({ stats }: { stats: Types.SystemStats | undefined }) => {
|
||||
const used = stats?.disks.reduce((acc, curr) => (acc += curr.used_gb), 0);
|
||||
const total = stats?.disks.reduce((acc, curr) => (acc += curr.total_gb), 0);
|
||||
|
||||
const perc = ((used ?? 0) / (total ?? 0)) * 100;
|
||||
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardHeader className="flex-row justify-between">
|
||||
<CardTitle>Disk Usage</CardTitle>
|
||||
<div className="flex gap-2 items-center">
|
||||
<CardDescription>{perc?.toFixed(2)}%</CardDescription>
|
||||
<Database className="w-4 h-4" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Progress value={perc} className="h-4" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user