Refactor: extract shared Dashboard StacksTable component (0.21.3)
The Dashboard's stacks-usage table (CPU/mem meters + inline start/stop/restart) is now a reusable components/stacks/StacksTable.tsx used by both the local and per-agent host sections. Removes the duplicate inline definition that was left behind by the half-finished extraction (which broke the build: redeclared StacksTable + dangling imports). No behaviour change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f65ec5f268
commit
ffb4ee39a0
@@ -62,7 +62,7 @@ def _map_docker(exc: DockerError):
|
|||||||
raise HTTPException(status_code=code, detail=exc.detail or exc.error)
|
raise HTTPException(status_code=code, detail=exc.detail or exc.error)
|
||||||
raise exc # falls through to the global 502 DockerError handler
|
raise exc # falls through to the global 502 DockerError handler
|
||||||
|
|
||||||
AGENT_VERSION = "0.21.1"
|
AGENT_VERSION = "0.21.3"
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
|
|||||||
+1
-1
@@ -55,7 +55,7 @@ async def lifespan(app: FastAPI):
|
|||||||
schedule_task.cancel()
|
schedule_task.cancel()
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(title="StackPilot", version="0.21.2", lifespan=lifespan)
|
app = FastAPI(title="StackPilot", version="0.21.3", lifespan=lifespan)
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "stackpilot-frontend",
|
"name": "stackpilot-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.21.1",
|
"version": "0.21.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { Play, Square, RotateCw } from "lucide-react";
|
||||||
|
import { Card, Spinner, StatusDot, Badge } from "@/components/ui";
|
||||||
|
import { formatBytes } from "@/lib/utils";
|
||||||
|
import type { StackStats, StackSummary } from "@/types";
|
||||||
|
|
||||||
|
/** Stack list rendered as a table with live CPU/memory usage meters and inline
|
||||||
|
* start/stop/restart, shared by the Dashboard and the Stacks page. */
|
||||||
|
export function StacksTable({
|
||||||
|
stacks,
|
||||||
|
stats,
|
||||||
|
hostCpus,
|
||||||
|
hostMem,
|
||||||
|
isAdmin,
|
||||||
|
busyId,
|
||||||
|
loading,
|
||||||
|
linkBase = "/stacks",
|
||||||
|
onStart,
|
||||||
|
onStop,
|
||||||
|
onRestart,
|
||||||
|
emptyText,
|
||||||
|
}: {
|
||||||
|
stacks: StackSummary[] | undefined;
|
||||||
|
stats: Record<string, StackStats> | undefined;
|
||||||
|
hostCpus: number;
|
||||||
|
hostMem: number;
|
||||||
|
isAdmin: boolean;
|
||||||
|
busyId: string | null;
|
||||||
|
loading: boolean;
|
||||||
|
linkBase?: string;
|
||||||
|
onStart: (id: string) => void;
|
||||||
|
onStop: (id: string) => void;
|
||||||
|
onRestart: (id: string) => void;
|
||||||
|
emptyText: string;
|
||||||
|
}) {
|
||||||
|
if (loading) return <Spinner />;
|
||||||
|
if (!stacks || stacks.length === 0) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<p className="text-sm text-slate-500">{emptyText}</p>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Card className="overflow-x-auto p-0">
|
||||||
|
<table className="w-full text-left text-sm">
|
||||||
|
<thead className="border-b border-slate-200 text-xs uppercase text-slate-500 dark:border-slate-700">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-2">Stack</th>
|
||||||
|
<th className="px-4 py-2 w-48">CPU</th>
|
||||||
|
<th className="px-4 py-2 w-48">Memory</th>
|
||||||
|
{isAdmin && <th className="px-4 py-2 w-px"></th>}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-slate-100 dark:divide-slate-700">
|
||||||
|
{stacks.map((s) => (
|
||||||
|
<StackRow
|
||||||
|
key={s.id}
|
||||||
|
stack={s}
|
||||||
|
stats={stats?.[s.id]}
|
||||||
|
hostCpus={hostCpus}
|
||||||
|
hostMem={hostMem}
|
||||||
|
isAdmin={isAdmin}
|
||||||
|
busy={busyId === s.id}
|
||||||
|
linkBase={linkBase}
|
||||||
|
onStart={onStart}
|
||||||
|
onStop={onStop}
|
||||||
|
onRestart={onRestart}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StackRow({
|
||||||
|
stack,
|
||||||
|
stats,
|
||||||
|
hostCpus,
|
||||||
|
hostMem,
|
||||||
|
isAdmin,
|
||||||
|
busy,
|
||||||
|
linkBase,
|
||||||
|
onStart,
|
||||||
|
onStop,
|
||||||
|
onRestart,
|
||||||
|
}: {
|
||||||
|
stack: StackSummary;
|
||||||
|
stats?: StackStats;
|
||||||
|
hostCpus: number;
|
||||||
|
hostMem: number;
|
||||||
|
isAdmin: boolean;
|
||||||
|
busy: boolean;
|
||||||
|
linkBase: string;
|
||||||
|
onStart: (id: string) => void;
|
||||||
|
onStop: (id: string) => void;
|
||||||
|
onRestart: (id: string) => void;
|
||||||
|
}) {
|
||||||
|
const running = stack.running_count > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr className="hover:bg-slate-50 dark:hover:bg-slate-800/50">
|
||||||
|
<td className="px-4 py-2.5">
|
||||||
|
<Link to={`${linkBase}/${stack.id}`} className="flex items-center gap-2">
|
||||||
|
<StatusDot status={stack.status} />
|
||||||
|
<span className="font-medium">{stack.name}</span>
|
||||||
|
<Badge status={stack.status}>{stack.status}</Badge>
|
||||||
|
<span className="text-xs text-slate-400">
|
||||||
|
{stack.running_count}/{stack.service_count} svc
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2.5">
|
||||||
|
{running && stats ? (
|
||||||
|
<Meter
|
||||||
|
used={stats.cpu_used}
|
||||||
|
limit={stats.cpu_limit}
|
||||||
|
hostMax={hostCpus}
|
||||||
|
label={
|
||||||
|
stats.cpu_limit != null
|
||||||
|
? `${stats.cpu_used.toFixed(2)} / ${stats.cpu_limit} cores`
|
||||||
|
: `${stats.cpu_used.toFixed(2)} cores`
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="text-slate-400">—</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2.5">
|
||||||
|
{running && stats ? (
|
||||||
|
<Meter
|
||||||
|
used={stats.mem_used}
|
||||||
|
limit={stats.mem_limit}
|
||||||
|
hostMax={hostMem}
|
||||||
|
label={
|
||||||
|
stats.mem_limit != null
|
||||||
|
? `${formatBytes(stats.mem_used)} / ${formatBytes(stats.mem_limit)}`
|
||||||
|
: formatBytes(stats.mem_used)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="text-slate-400">—</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
{isAdmin && (
|
||||||
|
<td className="px-4 py-2.5">
|
||||||
|
<div className="flex items-center justify-end gap-1">
|
||||||
|
{running ? (
|
||||||
|
<>
|
||||||
|
<IconBtn title="Restart" onClick={() => onRestart(stack.id)} disabled={busy}>
|
||||||
|
<RotateCw className="h-4 w-4" />
|
||||||
|
</IconBtn>
|
||||||
|
<IconBtn title="Stop" onClick={() => onStop(stack.id)} disabled={busy}>
|
||||||
|
<Square className="h-4 w-4 text-red-500" />
|
||||||
|
</IconBtn>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<IconBtn title="Start" onClick={() => onStart(stack.id)} disabled={busy}>
|
||||||
|
<Play className="h-4 w-4 text-green-500" />
|
||||||
|
</IconBtn>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function IconBtn({
|
||||||
|
title,
|
||||||
|
onClick,
|
||||||
|
disabled,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
onClick: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
title={title}
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={disabled}
|
||||||
|
className="rounded-lg p-1.5 text-slate-500 hover:bg-slate-100 disabled:opacity-40 dark:hover:bg-slate-700"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A compact usage bar. When a limit is set the bar fills toward the limit;
|
||||||
|
* otherwise it fills toward the host total as a faint reference. */
|
||||||
|
function Meter({
|
||||||
|
used,
|
||||||
|
limit,
|
||||||
|
hostMax,
|
||||||
|
label,
|
||||||
|
}: {
|
||||||
|
used: number;
|
||||||
|
limit: number | null;
|
||||||
|
hostMax: number;
|
||||||
|
label: string;
|
||||||
|
}) {
|
||||||
|
const denom = limit ?? (hostMax || 0);
|
||||||
|
const pct = denom > 0 ? Math.min((used / denom) * 100, 100) : 0;
|
||||||
|
const over = limit != null && used > limit * 1.001;
|
||||||
|
const bar =
|
||||||
|
over || pct >= 90
|
||||||
|
? "bg-red-500"
|
||||||
|
: pct >= 75
|
||||||
|
? "bg-amber-500"
|
||||||
|
: limit != null
|
||||||
|
? "bg-sky-500"
|
||||||
|
: "bg-slate-400";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<span className="text-xs text-slate-600 dark:text-slate-300">{label}</span>
|
||||||
|
{denom > 0 && (
|
||||||
|
<span className="text-[10px] tabular-nums text-slate-400">{Math.round(pct)}%</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="h-1.5 w-full overflow-hidden rounded-full bg-slate-200 dark:bg-slate-700">
|
||||||
|
<div className={`h-full rounded-full ${bar}`} style={{ width: `${pct}%` }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,15 +8,13 @@ import {
|
|||||||
Container,
|
Container,
|
||||||
Clock,
|
Clock,
|
||||||
ArrowUpCircle,
|
ArrowUpCircle,
|
||||||
Play,
|
|
||||||
Square,
|
|
||||||
RotateCw,
|
|
||||||
Server,
|
Server,
|
||||||
Database,
|
Database,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Card, Spinner, StatusDot, Badge } from "@/components/ui";
|
import { Card } from "@/components/ui";
|
||||||
import { HostHeader } from "@/components/hosts/HostHeader";
|
import { HostHeader } from "@/components/hosts/HostHeader";
|
||||||
|
import { StacksTable } from "@/components/stacks/StacksTable";
|
||||||
import { stacksApi } from "@/api/stacks";
|
import { stacksApi } from "@/api/stacks";
|
||||||
import { systemApi } from "@/api/system";
|
import { systemApi } from "@/api/system";
|
||||||
import { imagesApi } from "@/api/images";
|
import { imagesApi } from "@/api/images";
|
||||||
@@ -26,7 +24,7 @@ import { apiErrorMessage } from "@/api/client";
|
|||||||
import { formatBytes, relativeTime } from "@/lib/utils";
|
import { formatBytes, relativeTime } from "@/lib/utils";
|
||||||
import { useAuthStore } from "@/store/auth";
|
import { useAuthStore } from "@/store/auth";
|
||||||
import { useStackActions } from "@/hooks/useStackActions";
|
import { useStackActions } from "@/hooks/useStackActions";
|
||||||
import type { Agent, StackStats, StackSummary } from "@/types";
|
import type { Agent } from "@/types";
|
||||||
|
|
||||||
const sumSizes = (m: Record<string, number | null>) =>
|
const sumSizes = (m: Record<string, number | null>) =>
|
||||||
Object.values(m).reduce<number>((a, b) => a + (b ?? 0), 0);
|
Object.values(m).reduce<number>((a, b) => a + (b ?? 0), 0);
|
||||||
@@ -216,230 +214,6 @@ function AgentDashboardSection({ agent, isAdmin }: { agent: Agent; isAdmin: bool
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function StacksTable({
|
|
||||||
stacks,
|
|
||||||
stats,
|
|
||||||
hostCpus,
|
|
||||||
hostMem,
|
|
||||||
isAdmin,
|
|
||||||
busyId,
|
|
||||||
loading,
|
|
||||||
linkBase = "/stacks",
|
|
||||||
onStart,
|
|
||||||
onStop,
|
|
||||||
onRestart,
|
|
||||||
emptyText,
|
|
||||||
}: {
|
|
||||||
stacks: StackSummary[] | undefined;
|
|
||||||
stats: Record<string, StackStats> | undefined;
|
|
||||||
hostCpus: number;
|
|
||||||
hostMem: number;
|
|
||||||
isAdmin: boolean;
|
|
||||||
busyId: string | null;
|
|
||||||
loading: boolean;
|
|
||||||
linkBase?: string;
|
|
||||||
onStart: (id: string) => void;
|
|
||||||
onStop: (id: string) => void;
|
|
||||||
onRestart: (id: string) => void;
|
|
||||||
emptyText: string;
|
|
||||||
}) {
|
|
||||||
if (loading) return <Spinner />;
|
|
||||||
if (!stacks || stacks.length === 0) {
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<p className="text-sm text-slate-500">{emptyText}</p>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Card className="overflow-x-auto p-0">
|
|
||||||
<table className="w-full text-left text-sm">
|
|
||||||
<thead className="border-b border-slate-200 text-xs uppercase text-slate-500 dark:border-slate-700">
|
|
||||||
<tr>
|
|
||||||
<th className="px-4 py-2">Stack</th>
|
|
||||||
<th className="px-4 py-2 w-48">CPU</th>
|
|
||||||
<th className="px-4 py-2 w-48">Memory</th>
|
|
||||||
{isAdmin && <th className="px-4 py-2 w-px"></th>}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-slate-100 dark:divide-slate-700">
|
|
||||||
{stacks.map((s) => (
|
|
||||||
<StackRow
|
|
||||||
key={s.id}
|
|
||||||
stack={s}
|
|
||||||
stats={stats?.[s.id]}
|
|
||||||
hostCpus={hostCpus}
|
|
||||||
hostMem={hostMem}
|
|
||||||
isAdmin={isAdmin}
|
|
||||||
busy={busyId === s.id}
|
|
||||||
linkBase={linkBase}
|
|
||||||
onStart={onStart}
|
|
||||||
onStop={onStop}
|
|
||||||
onRestart={onRestart}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function StackRow({
|
|
||||||
stack,
|
|
||||||
stats,
|
|
||||||
hostCpus,
|
|
||||||
hostMem,
|
|
||||||
isAdmin,
|
|
||||||
busy,
|
|
||||||
linkBase,
|
|
||||||
onStart,
|
|
||||||
onStop,
|
|
||||||
onRestart,
|
|
||||||
}: {
|
|
||||||
stack: StackSummary;
|
|
||||||
stats?: StackStats;
|
|
||||||
hostCpus: number;
|
|
||||||
hostMem: number;
|
|
||||||
isAdmin: boolean;
|
|
||||||
busy: boolean;
|
|
||||||
linkBase: string;
|
|
||||||
onStart: (id: string) => void;
|
|
||||||
onStop: (id: string) => void;
|
|
||||||
onRestart: (id: string) => void;
|
|
||||||
}) {
|
|
||||||
const running = stack.running_count > 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr className="hover:bg-slate-50 dark:hover:bg-slate-800/50">
|
|
||||||
<td className="px-4 py-2.5">
|
|
||||||
<Link to={`${linkBase}/${stack.id}`} className="flex items-center gap-2">
|
|
||||||
<StatusDot status={stack.status} />
|
|
||||||
<span className="font-medium">{stack.name}</span>
|
|
||||||
<Badge status={stack.status}>{stack.status}</Badge>
|
|
||||||
<span className="text-xs text-slate-400">
|
|
||||||
{stack.running_count}/{stack.service_count} svc
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-2.5">
|
|
||||||
{running && stats ? (
|
|
||||||
<Meter
|
|
||||||
used={stats.cpu_used}
|
|
||||||
limit={stats.cpu_limit}
|
|
||||||
hostMax={hostCpus}
|
|
||||||
label={
|
|
||||||
stats.cpu_limit != null
|
|
||||||
? `${stats.cpu_used.toFixed(2)} / ${stats.cpu_limit} cores`
|
|
||||||
: `${stats.cpu_used.toFixed(2)} cores`
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<span className="text-slate-400">—</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-2.5">
|
|
||||||
{running && stats ? (
|
|
||||||
<Meter
|
|
||||||
used={stats.mem_used}
|
|
||||||
limit={stats.mem_limit}
|
|
||||||
hostMax={hostMem}
|
|
||||||
label={
|
|
||||||
stats.mem_limit != null
|
|
||||||
? `${formatBytes(stats.mem_used)} / ${formatBytes(stats.mem_limit)}`
|
|
||||||
: formatBytes(stats.mem_used)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<span className="text-slate-400">—</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
{isAdmin && (
|
|
||||||
<td className="px-4 py-2.5">
|
|
||||||
<div className="flex items-center justify-end gap-1">
|
|
||||||
{running ? (
|
|
||||||
<>
|
|
||||||
<IconBtn title="Restart" onClick={() => onRestart(stack.id)} disabled={busy}>
|
|
||||||
<RotateCw className="h-4 w-4" />
|
|
||||||
</IconBtn>
|
|
||||||
<IconBtn title="Stop" onClick={() => onStop(stack.id)} disabled={busy}>
|
|
||||||
<Square className="h-4 w-4 text-red-500" />
|
|
||||||
</IconBtn>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<IconBtn title="Start" onClick={() => onStart(stack.id)} disabled={busy}>
|
|
||||||
<Play className="h-4 w-4 text-green-500" />
|
|
||||||
</IconBtn>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function IconBtn({
|
|
||||||
title,
|
|
||||||
onClick,
|
|
||||||
disabled,
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
title: string;
|
|
||||||
onClick: () => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
title={title}
|
|
||||||
onClick={onClick}
|
|
||||||
disabled={disabled}
|
|
||||||
className="rounded-lg p-1.5 text-slate-500 hover:bg-slate-100 disabled:opacity-40 dark:hover:bg-slate-700"
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A compact usage bar. When a limit is set the bar fills toward the limit;
|
|
||||||
* otherwise it fills toward the host total as a faint reference. */
|
|
||||||
function Meter({
|
|
||||||
used,
|
|
||||||
limit,
|
|
||||||
hostMax,
|
|
||||||
label,
|
|
||||||
}: {
|
|
||||||
used: number;
|
|
||||||
limit: number | null;
|
|
||||||
hostMax: number;
|
|
||||||
label: string;
|
|
||||||
}) {
|
|
||||||
const denom = limit ?? (hostMax || 0);
|
|
||||||
const pct = denom > 0 ? Math.min((used / denom) * 100, 100) : 0;
|
|
||||||
const over = limit != null && used > limit * 1.001;
|
|
||||||
const bar =
|
|
||||||
over || pct >= 90
|
|
||||||
? "bg-red-500"
|
|
||||||
: pct >= 75
|
|
||||||
? "bg-amber-500"
|
|
||||||
: limit != null
|
|
||||||
? "bg-sky-500"
|
|
||||||
: "bg-slate-400";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="flex items-center justify-between gap-2">
|
|
||||||
<span className="text-xs text-slate-600 dark:text-slate-300">{label}</span>
|
|
||||||
{denom > 0 && (
|
|
||||||
<span className="text-[10px] tabular-nums text-slate-400">{Math.round(pct)}%</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="h-1.5 w-full overflow-hidden rounded-full bg-slate-200 dark:bg-slate-700">
|
|
||||||
<div className={`h-full rounded-full ${bar}`} style={{ width: `${pct}%` }} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ResourceBar({
|
function ResourceBar({
|
||||||
cpuCores,
|
cpuCores,
|
||||||
memUsed,
|
memUsed,
|
||||||
|
|||||||
Reference in New Issue
Block a user