Compare commits

...
Author SHA1 Message Date
ji ruo 524f8bc632 fix: 隐藏筛选与请求标签滚动条 2026-10-30 09:11:05 +08:00
ji ruo 255e11aca6 feat(web): show agent source in approval records 2026-10-30 09:01:57 +08:00
ji ruo 6ad055d24f fix(web): enable repeater request undo 2026-10-30 02:12:36 +08:00
RuoJi6 571b6116e0 fix: place resource status on dashboard header
* fix: place resource status on dashboard header

* fix: show resource metrics as progress bars

* fix: preview unsupported resource layout

* fix: disable non-Linux resource preview
2026-10-29 10:59:32 -07:00
RuoJi6 df9d38af0d fix: support non-Linux resource status builds 2026-10-29 10:41:04 -07:00
11 changed files with 182 additions and 137 deletions

No files matched your search

+2 -73
View File
@@ -1,17 +1,8 @@
package server
// Container-level resource metrics. These deliberately read cgroups directly:
// generic host libraries often report the host's capacity from inside Docker.
import (
"encoding/json"
"os"
"net/http"
"path/filepath"
"runtime"
"strconv"
"strings"
"syscall"
"time"
)
@@ -23,6 +14,7 @@ type metricValue struct {
type systemMetricsResponse struct {
Scope string `json:"scope"`
Supported bool `json:"supported"`
Timestamp time.Time `json:"timestamp"`
CPU metricValue `json:"cpu"`
Memory metricValue `json:"memory"`
@@ -32,71 +24,8 @@ type systemMetricsResponse struct {
}
func (s *Server) systemMetrics(w http.ResponseWriter, r *http.Request) {
cpuUse, cpuLimit, ok := cgroupCPU()
cpuPct := 0.0
if ok {
now := time.Now()
s.metricsMu.Lock()
if !s.lastCPUAt.IsZero() && cpuUse >= s.lastCPUUse {
elapsed := now.Sub(s.lastCPUAt).Seconds()
if elapsed > 0 {
cpuPct = float64(cpuUse-s.lastCPUUse) / 1e6 / elapsed / cpuLimit * 100
}
}
s.lastCPUUse, s.lastCPUAt = cpuUse, now
s.metricsMu.Unlock()
}
memUsed, memTotal, memOK := cgroupMemory()
root := diskMetric("/")
data := diskMetric(s.dataDir)
if data.TotalBytes == 0 { data = root }
containerMetrics := ok || memOK
scope := "process"
if containerMetrics {
scope = "container"
}
out := systemMetricsResponse{Scope: scope, Timestamp: time.Now().UTC(), Container: containerMetrics,
CPU: metricValue{Percent: clampPercent(cpuPct)},
Memory: metricValue{UsedBytes: memUsed, TotalBytes: memTotal, Percent: percent(memUsed, memTotal)}, Disk: root, DataDisk: data}
out := systemMetricsSnapshot(s)
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(out)
}
func cgroupCPU() (uint64, float64, bool) {
for _, dir := range []string{"/sys/fs/cgroup", "/sys/fs/cgroup/cpu"} {
b, err := os.ReadFile(filepath.Join(dir, "cpu.stat")); if err != nil { continue }
var use uint64
for _, line := range strings.Split(string(b), "\n") { f := strings.Fields(line); if len(f) == 2 && f[0] == "usage_usec" { use, _ = strconv.ParseUint(f[1], 10, 64) } }
limit := float64(runtime.NumCPU())
if q, err := os.ReadFile(filepath.Join(dir, "cpu.max")); err == nil { f := strings.Fields(string(q)); if len(f) == 2 && f[0] != "max" { quota, _ := strconv.ParseFloat(f[0], 64); period, _ := strconv.ParseFloat(f[1], 64); if period > 0 { limit = quota / period } } }
if use > 0 { return use, limit, true }
}
return 0, 0, false
}
func cgroupMemory() (uint64, uint64, bool) {
for _, dir := range []string{"/sys/fs/cgroup", "/sys/fs/cgroup/memory"} {
cur, e1 := readUint(filepath.Join(dir, "memory.current")); max, e2 := readUint(filepath.Join(dir, "memory.max"))
if e1 == nil && e2 == nil { return cur, max, max > 0 }
if e1 == nil {
if b, err := os.ReadFile(filepath.Join(dir, "memory.max")); err == nil && strings.TrimSpace(string(b)) == "max" {
return cur, hostMemoryTotal(), true
}
}
cur, e1 = readUint(filepath.Join(dir, "memory.usage_in_bytes")); max, e2 = readUint(filepath.Join(dir, "memory.limit_in_bytes")); if e1 == nil && e2 == nil && max < ^uint64(0)/2 { return cur, max, true }
}
return 0, 0, false
}
func hostMemoryTotal() uint64 {
b, err := os.ReadFile("/proc/meminfo"); if err != nil { return 0 }
for _, line := range strings.Split(string(b), "\n") { f := strings.Fields(line); if len(f) >= 2 && f[0] == "MemTotal:" { n, _ := strconv.ParseUint(f[1], 10, 64); return n * 1024 } }
return 0
}
func readUint(path string) (uint64, error) { b, err := os.ReadFile(path); if err != nil { return 0, err }; return strconv.ParseUint(strings.TrimSpace(string(b)), 10, 64) }
func diskMetric(path string) metricValue { var st syscall.Statfs_t; if path == "" || syscall.Statfs(path, &st) != nil { return metricValue{} }; total := st.Blocks * uint64(st.Bsize); free := st.Bavail * uint64(st.Bsize); used := total - free; return metricValue{UsedBytes: used, TotalBytes: total, Percent: percent(used, total)} }
func percent(used, total uint64) float64 { if total == 0 { return 0 }; return clampPercent(float64(used) / float64(total) * 100) }
func clampPercent(v float64) float64 { if v < 0 { return 0 }; if v > 100 { return 100 }; return v }
+62
View File
@@ -0,0 +1,62 @@
//go:build linux
package server
import (
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"syscall"
"time"
)
func systemMetricsSnapshot(s *Server) systemMetricsResponse {
use, limit, cpuOK := cgroupCPU()
cpuPct := 0.0
if cpuOK {
now := time.Now()
s.metricsMu.Lock()
if !s.lastCPUAt.IsZero() && use >= s.lastCPUUse && limit > 0 {
elapsed := now.Sub(s.lastCPUAt).Seconds()
if elapsed > 0 { cpuPct = float64(use-s.lastCPUUse) / 1e6 / elapsed / limit * 100 }
}
s.lastCPUUse, s.lastCPUAt = use, now
s.metricsMu.Unlock()
}
memUsed, memTotal, memOK := cgroupMemory()
root, data := diskMetric("/"), diskMetric(s.dataDir)
if data.TotalBytes == 0 { data = root }
return systemMetricsResponse{Scope: "container", Supported: true, Timestamp: time.Now().UTC(), Container: cpuOK || memOK,
CPU: metricValue{Percent: clampPercent(cpuPct)}, Memory: metricValue{UsedBytes: memUsed, TotalBytes: memTotal, Percent: percent(memUsed, memTotal)}, Disk: root, DataDisk: data}
}
func cgroupCPU() (uint64, float64, bool) {
for _, dir := range []string{"/sys/fs/cgroup", "/sys/fs/cgroup/cpu"} {
b, err := os.ReadFile(filepath.Join(dir, "cpu.stat")); if err != nil { continue }
var use uint64
for _, line := range strings.Split(string(b), "\n") { f := strings.Fields(line); if len(f) == 2 && f[0] == "usage_usec" { use, _ = strconv.ParseUint(f[1], 10, 64) } }
limit := float64(runtime.NumCPU())
if q, err := os.ReadFile(filepath.Join(dir, "cpu.max")); err == nil { f := strings.Fields(string(q)); if len(f) == 2 && f[0] != "max" { quota, _ := strconv.ParseFloat(f[0], 64); period, _ := strconv.ParseFloat(f[1], 64); if period > 0 { limit = quota / period } } }
if use > 0 { return use, limit, true }
}
return 0, 0, false
}
func cgroupMemory() (uint64, uint64, bool) {
for _, dir := range []string{"/sys/fs/cgroup", "/sys/fs/cgroup/memory"} {
cur, ce := readUint(filepath.Join(dir, "memory.current")); max, me := readUint(filepath.Join(dir, "memory.max"))
if ce == nil && me == nil { return cur, max, max > 0 }
if ce == nil { if b, err := os.ReadFile(filepath.Join(dir, "memory.max")); err == nil && strings.TrimSpace(string(b)) == "max" { return cur, hostMemoryTotal(), true } }
cur, ce = readUint(filepath.Join(dir, "memory.usage_in_bytes")); max, me = readUint(filepath.Join(dir, "memory.limit_in_bytes"))
if ce == nil && me == nil && max < ^uint64(0)/2 { return cur, max, true }
}
return 0, 0, false
}
func hostMemoryTotal() uint64 { b, err := os.ReadFile("/proc/meminfo"); if err != nil { return 0 }; for _, line := range strings.Split(string(b), "\n") { f := strings.Fields(line); if len(f) >= 2 && f[0] == "MemTotal:" { n, _ := strconv.ParseUint(f[1], 10, 64); return n * 1024 } }; return 0 }
func readUint(path string) (uint64, error) { b, err := os.ReadFile(path); if err != nil { return 0, err }; return strconv.ParseUint(strings.TrimSpace(string(b)), 10, 64) }
func diskMetric(path string) metricValue { var st syscall.Statfs_t; if path == "" || syscall.Statfs(path, &st) != nil { return metricValue{} }; total, free := st.Blocks*uint64(st.Bsize), st.Bavail*uint64(st.Bsize); used := total-free; return metricValue{UsedBytes: used, TotalBytes: total, Percent: percent(used, total)} }
func percent(used, total uint64) float64 { if total == 0 { return 0 }; return clampPercent(float64(used)/float64(total)*100) }
func clampPercent(v float64) float64 { if v < 0 { return 0 }; if v > 100 { return 100 }; return v }
+7
View File
@@ -3,6 +3,7 @@ package server
import (
"encoding/json"
"net/http/httptest"
"runtime"
"testing"
)
@@ -21,6 +22,12 @@ func TestSystemMetricsReturnsContainerAndDiskValues(t *testing.T) {
if got.Scope == "" || got.Timestamp.IsZero() {
t.Fatalf("unexpected response metadata: %+v", got)
}
if runtime.GOOS != "linux" {
if got.Supported || got.Scope != "unsupported" {
t.Fatalf("unsupported platform returned metrics: %+v", got)
}
return
}
if got.Disk.TotalBytes == 0 || got.DataDisk.TotalBytes == 0 {
t.Fatalf("disk metrics missing: %+v", got)
}
+9
View File
@@ -0,0 +1,9 @@
//go:build !linux
package server
import "time"
func systemMetricsSnapshot(_ *Server) systemMetricsResponse {
return systemMetricsResponse{Scope: "unsupported", Supported: false, Timestamp: time.Now().UTC()}
}
+43 -37
View File
@@ -9,13 +9,13 @@ import {
ArrowUpRightIcon,
BugIcon,
ClockIcon,
CpuIcon,
HardDriveIcon,
MemoryStickIcon,
NetworkIcon,
ShieldCheckIcon,
TargetIcon,
ZapIcon,
CpuIcon,
HardDriveIcon,
MemoryStickIcon,
} from "lucide-react";
import { Bar, BarChart, CartesianGrid, Tooltip as RechartsTooltip, XAxis, YAxis } from "recharts";
@@ -73,15 +73,6 @@ function fmtTokens(n: number): string {
return String(n);
}
function fmtBytes(n: number): string {
if (!n) return "—";
const units = ["B", "KB", "MB", "GB", "TB"];
let value = n;
let i = 0;
while (value >= 1024 && i < units.length - 1) { value /= 1024; i++; }
return `${value.toFixed(value >= 10 || i === 0 ? 0 : 1)} ${units[i]}`;
}
const ASSET_TYPE_LABELS: Record<string, string> = {
root_domain: "根域名",
ip: "IP",
@@ -114,6 +105,35 @@ function statusBg(code: number): string {
return "bg-red-400";
}
function SystemResourceStatus({ metrics }: { metrics: SystemMetrics | null }) {
if (!metrics) return null;
if (!metrics.supported) {
return <div className="ml-auto text-xs text-muted-foreground" role="status">资源状态:暂不支持</div>;
}
const items = [
[CpuIcon, "CPU", metrics.cpu.percent],
[MemoryStickIcon, "内存", metrics.memory.percent],
[HardDriveIcon, "磁盘", metrics.data_disk.percent],
] as const;
return (
<div className="ml-auto flex min-w-0 items-center justify-end gap-3 text-xs text-muted-foreground" role="status" aria-live="polite">
<span className="shrink-0 font-medium">容器资源</span>
{items.map(([Icon, label, value]) => (
<div key={label} className="w-24 min-w-0" title={`${label} ${value.toFixed(1)}%`}>
<div className="mb-1 flex items-center justify-between gap-1 text-[10px]">
<span className="inline-flex items-center gap-1"><Icon className="size-3" aria-hidden="true" />{label}</span>
<span className="tabular-nums">{value.toFixed(1)}%</span>
</div>
<Progress
value={value}
className="h-2 bg-muted [&_[data-slot=progress-indicator]]:bg-slate-500 dark:[&_[data-slot=progress-indicator]]:bg-slate-400"
/>
</div>
))}
</div>
);
}
// ── sub-components ────────────────────────────────────────────────────────────
function LiveDot({ className }: { className?: string }) {
@@ -164,7 +184,14 @@ export default function DashboardPage() {
React.useEffect(() => {
let alive = true;
const load = async () => { try { const m = await api.systemMetrics(); if (alive) setSystemMetrics(m); } catch { /* retry */ } };
const load = async () => {
try {
const next = await api.systemMetrics();
if (alive) setSystemMetrics(next);
} catch {
/* retry on the next interval */
}
};
void load();
const timer = setInterval(load, 5000);
return () => { alive = false; clearInterval(timer); };
@@ -517,10 +544,13 @@ export default function DashboardPage() {
<div className="flex flex-1 flex-col gap-4 pb-6">
{/* ── Header ── */}
<div>
<div className="flex items-center gap-6">
<div>
<h1 className="text-lg font-semibold tracking-tight">总览</h1>
<p className="text-xs text-muted-foreground">系统全局状态 · 实时刷新</p>
</div>
<SystemResourceStatus metrics={systemMetrics} />
</div>
</div>
{/* ── Row 1: 5 stat cards ── */}
@@ -1151,30 +1181,6 @@ export default function DashboardPage() {
<Card className="p-4">
<SectionTitle icon={ShieldCheckIcon}>系统状态</SectionTitle>
{systemMetrics && (
<div className="mb-3 rounded-lg border bg-muted/20 p-3">
<div className="mb-2 flex items-center justify-between">
<span className="text-[10px] font-medium">容器资源</span>
<span className="text-[9px] text-muted-foreground">每 5 秒刷新</span>
</div>
<div className="grid grid-cols-3 gap-2">
{([
[CpuIcon, "CPU", systemMetrics.cpu],
[MemoryStickIcon, "内存", systemMetrics.memory],
[HardDriveIcon, "磁盘", systemMetrics.data_disk],
] as const).map(([Icon, label, metric]) => (
<div key={label} className="min-w-0">
<div className="mb-1 flex items-center gap-1 text-[9px] text-muted-foreground"><Icon className="size-3" />{label}</div>
<Progress value={metric.percent} className="h-1.5" />
<div className="mt-1 truncate text-[9px] tabular-nums">{metric.percent.toFixed(1)}%</div>
{label !== "CPU" && <div className="truncate text-[9px] text-muted-foreground">{fmtBytes(metric.used_bytes)} / {fmtBytes(metric.total_bytes)}</div>}
</div>
))}
</div>
<div className="mt-2 text-[9px] text-muted-foreground">监控范围:{systemMetrics.scope === "container" ? "ARTEX 容器" : "当前进程(非容器运行)"}</div>
</div>
)}
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between rounded-lg border bg-muted/20 px-3 py-2">
<div className="text-[10px] text-muted-foreground">LLM 配置</div>
@@ -923,7 +923,7 @@ export default function FindingsPage() {
})}
</div>
<div className="flex min-w-0 flex-nowrap items-center gap-2 overflow-x-auto pb-1">
<div className="scrollbar-none flex min-w-0 flex-nowrap items-center gap-2 overflow-x-auto pb-1">
<InputGroup className="w-72 shrink-0">
<InputGroupInput
type="search"
+25 -3
View File
@@ -65,6 +65,28 @@ function originLabel(row: InterceptApprovalRow) {
return "—";
}
// Background task runs carry a pool label such as "work#2 · #2770", while
// conversation-backed runs carry the agent key (for example "reporter").
// Present a stable, human-readable agent label in both cases.
function agentLabel(row: InterceptApprovalRow) {
const key = (row.conv_agent_key || "").trim().toLowerCase();
const labels: Record<string, string> = {
auto: "自动 Agent",
mainagent: "主 Agent",
planner: "规划 Agent",
worker: "执行 Worker",
reporter: "报告编写",
retester: "复测 Agent",
pentest: "渗透测试 Agent",
};
if (key && labels[key]) return labels[key];
const raw = (row.agent_name || "").trim();
if (/^work#\d+(?:\s|$)/i.test(raw)) return "执行 Worker";
const rawKey = raw.toLowerCase();
if (labels[rawKey]) return labels[rawKey];
return raw || "未知 Agent";
}
function ApprovalOrigin({ row, detail = false }: { row: InterceptApprovalRow; detail?: boolean }) {
const [locating, setLocating] = React.useState(false);
const label = detail && row.task_id ? `任务 ${row.task_id}` : originLabel(row);
@@ -454,7 +476,7 @@ export function ApprovalDetail({
<SideQuestionButton side={side} />
</div>
<CodeBlock
label={`${current.agent_name || current.conv_agent_key || "Agent"} · 工具请求`}
label={`${agentLabel(current)} · 工具请求`}
text={JSON.stringify(row.tool_input ?? {}, null, 2)}
/>
{command ? (
@@ -726,8 +748,8 @@ function ApprovalTable({
<span className="truncate" title={originLabel(row)}>
<ApprovalOrigin row={row} />
</span>
<span className="truncate text-muted-foreground text-xs">
{row.agent_name || row.conv_agent_key}
<span className="truncate text-muted-foreground text-xs" title={agentLabel(row)}>
Agent:{agentLabel(row)}
</span>
</div>
</TableCell>
+29 -20
View File
@@ -2,14 +2,8 @@
import * as React from "react";
import { EditorState } from "@codemirror/state";
import {
drawSelection,
EditorView,
highlightActiveLine,
keymap,
lineNumbers,
} from "@codemirror/view";
import { defaultKeymap, indentWithTab } from "@codemirror/commands";
import { drawSelection, EditorView, highlightActiveLine, keymap, lineNumbers } from "@codemirror/view";
import { defaultKeymap, history, historyKeymap, indentWithTab } from "@codemirror/commands";
import { defaultHighlightStyle, HighlightStyle, StreamLanguage, syntaxHighlighting } from "@codemirror/language";
import { tags } from "@lezer/highlight";
@@ -37,17 +31,31 @@ const httpLanguage = StreamLanguage.define({
},
});
const packetTheme = EditorView.theme({
"&": { height: "100%", backgroundColor: "transparent", color: "var(--foreground)" },
".cm-scroller": { overflow: "auto", fontFamily: "var(--font-mono)", fontSize: "0.75rem", fontWeight: "500", lineHeight: "1.65rem" },
".cm-content": { minHeight: "100%", padding: "0.5rem 0.5rem" },
".cm-gutters": { backgroundColor: "transparent", color: "var(--muted-foreground)", border: "0", borderRight: "1px solid var(--border)" },
".cm-lineNumbers .cm-gutterElement": { minWidth: "2rem", padding: "0 0.5rem" },
".cm-activeLine": { backgroundColor: "transparent" },
".cm-activeLineGutter": { backgroundColor: "transparent" },
".cm-selectionBackground, ::selection": { backgroundColor: "#c9d2e3" },
".cm-cursor": { borderLeftColor: "#222" },
}, { dark: false });
const packetTheme = EditorView.theme(
{
"&": { height: "100%", backgroundColor: "transparent", color: "var(--foreground)" },
".cm-scroller": {
overflow: "auto",
fontFamily: "var(--font-mono)",
fontSize: "0.75rem",
fontWeight: "500",
lineHeight: "1.65rem",
},
".cm-content": { minHeight: "100%", padding: "0.5rem 0.5rem" },
".cm-gutters": {
backgroundColor: "transparent",
color: "var(--muted-foreground)",
border: "0",
borderRight: "1px solid var(--border)",
},
".cm-lineNumbers .cm-gutterElement": { minWidth: "2rem", padding: "0 0.5rem" },
".cm-activeLine": { backgroundColor: "transparent" },
".cm-activeLineGutter": { backgroundColor: "transparent" },
".cm-selectionBackground, ::selection": { backgroundColor: "#c9d2e3" },
".cm-cursor": { borderLeftColor: "#222" },
},
{ dark: false },
);
export function PacketCodeEditor({
value,
@@ -76,7 +84,8 @@ export function PacketCodeEditor({
lineNumbers(),
drawSelection(),
highlightActiveLine(),
keymap.of([...defaultKeymap, indentWithTab]),
history(),
keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab]),
httpLanguage,
syntaxHighlighting(defaultHighlightStyle),
syntaxHighlighting(packetHighlight),
@@ -112,7 +112,7 @@ export function RepeaterWorkspace() {
</div>
</div>
<div className="flex min-h-10 min-w-0 items-center rounded-md border bg-muted/20 p-1">
<div className="flex min-w-0 flex-1 items-center gap-1 overflow-x-auto">
<div className="scrollbar-none flex min-w-0 flex-1 items-center gap-1 overflow-x-auto">
{workspace.tabs.map((tab) => {
const selected = tab.id === workspace.activeTabId;
return (
+1 -1
View File
@@ -14,7 +14,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
data-slot="input-group"
role="group"
className={cn(
"group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
"group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-1 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/30 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
className
)}
{...props}
+2 -1
View File
@@ -1338,7 +1338,8 @@ export interface SystemMetricValue {
}
export interface SystemMetrics {
scope: "container" | string;
scope: "container" | "unsupported" | string;
supported: boolean;
timestamp: string;
container_metrics: boolean;
cpu: SystemMetricValue;