mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-10-30 18:13:50 +08:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
524f8bc632 | ||
|
|
255e11aca6 | ||
|
|
6ad055d24f | ||
|
|
571b6116e0 | ||
|
|
df9d38af0d |
No files matched your search
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user