mirror of
https://github.com/MengMengCode/CLICD.git
synced 2026-08-08 06:24:44 +08:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c99f3f6d55 | |||
| 2df92be501 | |||
| f8d16ca792 |
@@ -64,3 +64,4 @@ backend/tmp/
|
|||||||
Thumbs.db
|
Thumbs.db
|
||||||
linux.txt
|
linux.txt
|
||||||
push-release.ps1
|
push-release.ps1
|
||||||
|
deploy.ps1
|
||||||
|
|||||||
@@ -11,9 +11,10 @@
|
|||||||
<img alt="Vite" src="https://img.shields.io/badge/Vite-5-646CFF?style=flat-square&logo=vite&logoColor=white">
|
<img alt="Vite" src="https://img.shields.io/badge/Vite-5-646CFF?style=flat-square&logo=vite&logoColor=white">
|
||||||
<img alt="Tailwind CSS" src="https://img.shields.io/badge/Tailwind_CSS-3-06B6D4?style=flat-square&logo=tailwindcss&logoColor=white">
|
<img alt="Tailwind CSS" src="https://img.shields.io/badge/Tailwind_CSS-3-06B6D4?style=flat-square&logo=tailwindcss&logoColor=white">
|
||||||
<img alt="LXC" src="https://img.shields.io/badge/LXC-container-111111?style=flat-square">
|
<img alt="LXC" src="https://img.shields.io/badge/LXC-container-111111?style=flat-square">
|
||||||
|
<img alt="KVM" src="https://img.shields.io/badge/KVM-virtualization-EE0000?style=flat-square&logo=linux&logoColor=white">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
CLICD 是一个面向 LXC 的轻量容器管理面板,提供 Web 控制台、CLI、批量任务、镜像管理、NAT 端口、IPv6 分配、WebSSH、资源限制、流量限制和安全告警能力。它适合用来管理小型 VPS 上的 LXC 容器,也适合需要批量创建和分发子用户管理链接的场景。
|
CLICD 是一个面向 LXC/KVM 的轻量虚拟化管理面板,提供 Web 控制台、CLI、批量任务、镜像管理、NAT 端口、IPv6 分配、WebSSH、VNC、资源限制、流量限制和安全告警能力。它适合用来管理小型 VPS 上的 LXC 容器和 KVM 虚拟机,也适合需要批量创建和分发子用户管理链接的场景。
|
||||||
|
|
||||||
## 功能介绍
|
## 功能介绍
|
||||||
|
|
||||||
@@ -29,9 +30,9 @@ CLICD 是一个面向 LXC 的轻量容器管理面板,提供 Web 控制台、C
|
|||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
- Backend: Go, net/http, LXC, cgroup v2, iptables, conntrack
|
- Backend: Go, net/http, LXC, KVM/libvirt, cgroup v2, iptables, conntrack
|
||||||
- Frontend: React, TypeScript, Vite, Tailwind CSS, lucide-react, xterm.js
|
- Frontend: React, TypeScript, Vite, Tailwind CSS, lucide-react, xterm.js
|
||||||
- Runtime: Linux, systemd, LXC
|
- Runtime: Linux, systemd, LXC, KVM/QEMU
|
||||||
- Build: GitHub Actions, Node.js 20, Go 1.22
|
- Build: GitHub Actions, Node.js 20, Go 1.22
|
||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|||||||
@@ -298,7 +298,11 @@ func updateResourceLimit(w http.ResponseWriter, r *http.Request, id int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Message: "Resource limits updated"})
|
msg := "Resource limits updated"
|
||||||
|
if c.IsKVM() && c.Status == "running" {
|
||||||
|
msg = "资源已保存,请关机重启虚拟机后生效"
|
||||||
|
}
|
||||||
|
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Message: msg})
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRandomPort(w http.ResponseWriter, r *http.Request, id int) {
|
func getRandomPort(w http.ResponseWriter, r *http.Request, id int) {
|
||||||
|
|||||||
@@ -716,14 +716,29 @@ func UpdateVNC(containers []Container) {
|
|||||||
SaveConfig()
|
SaveConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
// AllocateSSHPort allocates a new SSH port
|
// AllocateSSHPort allocates a new SSH port, skipping ports already used by any container
|
||||||
func AllocateSSHPort() int {
|
func AllocateSSHPort() int {
|
||||||
|
used := collectAllHostPorts()
|
||||||
port := AppConfig.NextSSHPort
|
port := AppConfig.NextSSHPort
|
||||||
AppConfig.NextSSHPort++
|
for used[port] {
|
||||||
|
port++
|
||||||
|
}
|
||||||
|
AppConfig.NextSSHPort = port + 1
|
||||||
SaveConfig()
|
SaveConfig()
|
||||||
return port
|
return port
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// collectAllHostPorts collects all host ports used by any container (LXC + KVM)
|
||||||
|
func collectAllHostPorts() map[int]bool {
|
||||||
|
used := map[int]bool{}
|
||||||
|
for _, c := range AppConfig.Containers {
|
||||||
|
for _, pm := range c.PortMappings {
|
||||||
|
used[pm.HostPort] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return used
|
||||||
|
}
|
||||||
|
|
||||||
// IsValidContainerName checks if container name is valid (no duplicate check needed, ID is primary key)
|
// IsValidContainerName checks if container name is valid (no duplicate check needed, ID is primary key)
|
||||||
func IsValidContainerName(name string) bool {
|
func IsValidContainerName(name string) bool {
|
||||||
return IsValidContainerNameSyntax(name)
|
return IsValidContainerNameSyntax(name)
|
||||||
|
|||||||
@@ -503,7 +503,8 @@ func (m *Manager) ApplyContainerLimits(c *config.Container) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if c.Status == "running" {
|
if c.Status == "running" {
|
||||||
return fmt.Errorf("KVM resource changes require shutdown and start")
|
// Config already saved; domain definition will be refreshed on next start
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
if c.DiskImage == "" || c.MACAddress == "" {
|
if c.DiskImage == "" || c.MACAddress == "" {
|
||||||
return nil
|
return nil
|
||||||
@@ -901,6 +902,9 @@ func (m *Manager) GetResourceUsage(id int) (map[string]interface{}, error) {
|
|||||||
"disk_write_bytes": writeBytes,
|
"disk_write_bytes": writeBytes,
|
||||||
"disk_read_bps": 0.0,
|
"disk_read_bps": 0.0,
|
||||||
"disk_write_bps": 0.0,
|
"disk_write_bps": 0.0,
|
||||||
|
"load1": 0.0,
|
||||||
|
"load5": 0.0,
|
||||||
|
"load15": 0.0,
|
||||||
}
|
}
|
||||||
if c.DiskImage != "" {
|
if c.DiskImage != "" {
|
||||||
if info, err := os.Stat(c.DiskImage); err == nil {
|
if info, err := os.Stat(c.DiskImage); err == nil {
|
||||||
@@ -2264,10 +2268,20 @@ func allocateDefaultEqualPorts(c *config.Container, count int) []int {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
used := map[int]bool{}
|
used := map[int]bool{}
|
||||||
|
// Mark current container's ports
|
||||||
for _, pm := range c.PortMappings {
|
for _, pm := range c.PortMappings {
|
||||||
used[pm.HostPort] = true
|
used[pm.HostPort] = true
|
||||||
used[pm.ContainerPort] = true
|
used[pm.ContainerPort] = true
|
||||||
}
|
}
|
||||||
|
// Also mark all other containers' host ports (LXC + KVM)
|
||||||
|
for _, oc := range config.AppConfig.Containers {
|
||||||
|
if oc.ID == c.ID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, pm := range oc.PortMappings {
|
||||||
|
used[pm.HostPort] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
ports := make([]int, 0, count)
|
ports := make([]int, 0, count)
|
||||||
for next := 20000; next <= 65535 && len(ports) < count; next++ {
|
for next := 20000; next <= 65535 && len(ports) < count; next++ {
|
||||||
if !used[next] {
|
if !used[next] {
|
||||||
|
|||||||
@@ -2272,6 +2272,11 @@ func (m *Manager) GetResourceUsage(id int) (map[string]interface{}, error) {
|
|||||||
usage := make(map[string]interface{})
|
usage := make(map[string]interface{})
|
||||||
|
|
||||||
// Read raw values
|
// Read raw values
|
||||||
|
load1, load5, load15 := m.getContainerLoadAvg(lxcName)
|
||||||
|
usage["load1"] = load1
|
||||||
|
usage["load5"] = load5
|
||||||
|
usage["load15"] = load15
|
||||||
|
|
||||||
memUsage := readIntCommand(fmt.Sprintf(
|
memUsage := readIntCommand(fmt.Sprintf(
|
||||||
"cat /sys/fs/cgroup/lxc/%[1]s/memory.current 2>/dev/null || "+
|
"cat /sys/fs/cgroup/lxc/%[1]s/memory.current 2>/dev/null || "+
|
||||||
"cat /sys/fs/cgroup/lxc.payload.%[1]s/memory.current 2>/dev/null || "+
|
"cat /sys/fs/cgroup/lxc.payload.%[1]s/memory.current 2>/dev/null || "+
|
||||||
@@ -2319,6 +2324,25 @@ func (m *Manager) GetResourceUsage(id int) (map[string]interface{}, error) {
|
|||||||
return usage, nil
|
return usage, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) getContainerLoadAvg(lxcName string) (float64, float64, float64) {
|
||||||
|
pid := m.getContainerInitPID(lxcName)
|
||||||
|
if pid == "" {
|
||||||
|
return 0, 0, 0
|
||||||
|
}
|
||||||
|
out, err := exec.Command("nsenter", "-t", pid, "-m", "-p", "cat", "/proc/loadavg").Output()
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0
|
||||||
|
}
|
||||||
|
parts := strings.Fields(string(out))
|
||||||
|
if len(parts) < 3 {
|
||||||
|
return 0, 0, 0
|
||||||
|
}
|
||||||
|
load1, _ := strconv.ParseFloat(parts[0], 64)
|
||||||
|
load5, _ := strconv.ParseFloat(parts[1], 64)
|
||||||
|
load15, _ := strconv.ParseFloat(parts[2], 64)
|
||||||
|
return load1, load5, load15
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) getContainerNetworkBytes(lxcName string) (uint64, uint64) {
|
func (m *Manager) getContainerNetworkBytes(lxcName string) (uint64, uint64) {
|
||||||
pid := m.getContainerInitPID(lxcName)
|
pid := m.getContainerInitPID(lxcName)
|
||||||
if pid == "" {
|
if pid == "" {
|
||||||
|
|||||||
@@ -174,12 +174,24 @@ func normalizePortMapping(c *config.Container, skipIndex int, pm config.PortMapp
|
|||||||
if pm.HostPort <= 0 {
|
if pm.HostPort <= 0 {
|
||||||
pm.HostPort = pm.ContainerPort
|
pm.HostPort = pm.ContainerPort
|
||||||
}
|
}
|
||||||
|
// Check current container's own mappings
|
||||||
for i, existing := range c.PortMappings {
|
for i, existing := range c.PortMappings {
|
||||||
if i == skipIndex {
|
if i == skipIndex {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if existing.HostPort == pm.HostPort && existing.Protocol == pm.Protocol {
|
if existing.HostPort == pm.HostPort && existing.Protocol == pm.Protocol {
|
||||||
return pm, fmt.Errorf("host port %d/%s already mapped", pm.HostPort, pm.Protocol)
|
return pm, fmt.Errorf("host port %d/%s already mapped in this container", pm.HostPort, pm.Protocol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check all other containers (LXC + KVM) for port conflicts
|
||||||
|
for _, oc := range config.AppConfig.Containers {
|
||||||
|
if oc.ID == c.ID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, existing := range oc.PortMappings {
|
||||||
|
if existing.HostPort == pm.HostPort && existing.Protocol == pm.Protocol {
|
||||||
|
return pm, fmt.Errorf("host port %d/%s already used by container %s (ID: %d)", pm.HostPort, pm.Protocol, oc.Name, oc.ID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return pm, nil
|
return pm, nil
|
||||||
@@ -190,10 +202,20 @@ func allocateDefaultEqualPorts(c *config.Container, count int) []int {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
used := map[int]bool{}
|
used := map[int]bool{}
|
||||||
|
// Mark current container's ports
|
||||||
for _, pm := range c.PortMappings {
|
for _, pm := range c.PortMappings {
|
||||||
used[pm.HostPort] = true
|
used[pm.HostPort] = true
|
||||||
used[pm.ContainerPort] = true
|
used[pm.ContainerPort] = true
|
||||||
}
|
}
|
||||||
|
// Also mark all other containers' host ports (LXC + KVM)
|
||||||
|
for _, oc := range config.AppConfig.Containers {
|
||||||
|
if oc.ID == c.ID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, pm := range oc.PortMappings {
|
||||||
|
used[pm.HostPort] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
ports := make([]int, 0, count)
|
ports := make([]int, 0, count)
|
||||||
next := 20000
|
next := 20000
|
||||||
for len(ports) < count {
|
for len(ports) < count {
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package version
|
package version
|
||||||
|
|
||||||
var (
|
var (
|
||||||
Version = "1.0.11"
|
Version = "1.0.12"
|
||||||
Repo = "MengMengCode/CLICD"
|
Repo = "MengMengCode/CLICD"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,3 +17,4 @@ func Current() string {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ export function RingStat({ value, max = 100, label, subLabel, size = 120, stroke
|
|||||||
|
|
||||||
const radius = (size - strokeWidth) / 2
|
const radius = (size - strokeWidth) / 2
|
||||||
const circumference = radius * 2 * Math.PI
|
const circumference = radius * 2 * Math.PI
|
||||||
const percentage = Math.min(Math.max(value / max * 100, 0), 100)
|
const percentage = max === Infinity ? Math.max(value, 0) : Math.min(Math.max(value / max * 100, 0), 100)
|
||||||
const strokeDashoffset = circumference - (percentage / 100) * circumference
|
const strokeDashoffset = circumference - (Math.min(percentage, 100) / 100) * circumference
|
||||||
|
|
||||||
const bgStroke = isDark ? '#374151' : '#f3f4f6'
|
const bgStroke = isDark ? '#374151' : '#f3f4f6'
|
||||||
const progressStroke = isDark ? '#f9fafb' : '#000000'
|
const progressStroke = isDark ? '#f9fafb' : '#000000'
|
||||||
@@ -51,7 +51,7 @@ export function RingStat({ value, max = 100, label, subLabel, size = 120, stroke
|
|||||||
</svg>
|
</svg>
|
||||||
{/* Center value */}
|
{/* Center value */}
|
||||||
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
||||||
<span className="text-2xl font-bold text-black dark:text-white">{value.toFixed(percentage < 1 ? 2 : 1)}%</span>
|
<span className="text-2xl font-bold text-black dark:text-white">{percentage.toFixed(percentage < 1 ? 2 : 1)}%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 text-center">
|
<div className="mt-2 text-center">
|
||||||
@@ -73,7 +73,6 @@ interface RingStatsProps {
|
|||||||
swapUsed?: number
|
swapUsed?: number
|
||||||
swapTotal?: number
|
swapTotal?: number
|
||||||
loadPercent: number
|
loadPercent: number
|
||||||
loadStatus: string
|
|
||||||
diskPercent: number
|
diskPercent: number
|
||||||
diskUsed: number
|
diskUsed: number
|
||||||
diskTotal: number
|
diskTotal: number
|
||||||
@@ -90,7 +89,6 @@ export default function RingStats({
|
|||||||
swapUsed = 0,
|
swapUsed = 0,
|
||||||
swapTotal = 0,
|
swapTotal = 0,
|
||||||
loadPercent,
|
loadPercent,
|
||||||
loadStatus,
|
|
||||||
diskPercent,
|
diskPercent,
|
||||||
diskUsed,
|
diskUsed,
|
||||||
diskTotal,
|
diskTotal,
|
||||||
@@ -125,8 +123,8 @@ export default function RingStats({
|
|||||||
)}
|
)}
|
||||||
<RingStat
|
<RingStat
|
||||||
value={loadPercent}
|
value={loadPercent}
|
||||||
|
max={Infinity}
|
||||||
label="负载"
|
label="负载"
|
||||||
subLabel={loadStatus}
|
|
||||||
/>
|
/>
|
||||||
<RingStat
|
<RingStat
|
||||||
value={diskPercent}
|
value={diskPercent}
|
||||||
|
|||||||
@@ -654,6 +654,7 @@ export default function ContainerDetail() {
|
|||||||
const filtered = filterHistory(history, range)
|
const filtered = filterHistory(history, range)
|
||||||
const cpuPct = clamp(usage?.cpu_usage_pct || 0)
|
const cpuPct = clamp(usage?.cpu_usage_pct || 0)
|
||||||
const ramPct = container.ram_mb > 0 ? clamp(((usage?.memory_usage_bytes || 0) / (container.ram_mb * 1024 * 1024)) * 100) : 0
|
const ramPct = container.ram_mb > 0 ? clamp(((usage?.memory_usage_bytes || 0) / (container.ram_mb * 1024 * 1024)) * 100) : 0
|
||||||
|
const loadPct = container.vcpu > 0 ? ((usage?.load1 || 0) / container.vcpu) * 100 : 0
|
||||||
const diskPct = container.disk_gb > 0 ? clamp(((usage?.disk_usage_bytes || 0) / (container.disk_gb * 1024 * 1024 * 1024)) * 100) : 0
|
const diskPct = container.disk_gb > 0 ? clamp(((usage?.disk_usage_bytes || 0) / (container.disk_gb * 1024 * 1024 * 1024)) * 100) : 0
|
||||||
const networkBps = (usage?.network_rx_bps || 0) + (usage?.network_tx_bps || 0)
|
const networkBps = (usage?.network_rx_bps || 0) + (usage?.network_tx_bps || 0)
|
||||||
const rx = usage?.network_rx_bps || 0
|
const rx = usage?.network_rx_bps || 0
|
||||||
@@ -873,9 +874,9 @@ export default function ContainerDetail() {
|
|||||||
subLabel={`${formatMB(usage?.memory_usage_bytes || 0)} / ${formatMB(container.ram_mb * 1024 * 1024)}`}
|
subLabel={`${formatMB(usage?.memory_usage_bytes || 0)} / ${formatMB(container.ram_mb * 1024 * 1024)}`}
|
||||||
/>
|
/>
|
||||||
<RingStat
|
<RingStat
|
||||||
value={Math.min(cpuPct, 100)}
|
value={loadPct}
|
||||||
|
max={Infinity}
|
||||||
label="负载"
|
label="负载"
|
||||||
subLabel={cpuPct < 70 ? '正常' : cpuPct < 90 ? '中等' : '高'}
|
|
||||||
/>
|
/>
|
||||||
<RingStat
|
<RingStat
|
||||||
value={diskPct}
|
value={diskPct}
|
||||||
|
|||||||
@@ -119,8 +119,7 @@ export default function Dashboard() {
|
|||||||
ramPercent={host.ram.total_mb > 0 ? (host.ram.used_mb / host.ram.total_mb) * 100 : 0}
|
ramPercent={host.ram.total_mb > 0 ? (host.ram.used_mb / host.ram.total_mb) * 100 : 0}
|
||||||
ramUsed={host.ram.used_mb}
|
ramUsed={host.ram.used_mb}
|
||||||
ramTotal={host.ram.total_mb}
|
ramTotal={host.ram.total_mb}
|
||||||
loadPercent={Math.min((host.load.load1 / host.cpu.cores) * 100, 100)}
|
loadPercent={(host.load.load1 / Math.max(host.cpu.cores, 1)) * 100}
|
||||||
loadStatus={host.load.load1 < host.cpu.cores * 0.7 ? '正常' : host.load.load1 < host.cpu.cores * 1.0 ? '中等' : '高'}
|
|
||||||
diskPercent={host.disk.total_gb > 0 ? (host.disk.used_gb / host.disk.total_gb) * 100 : 0}
|
diskPercent={host.disk.total_gb > 0 ? (host.disk.used_gb / host.disk.total_gb) * 100 : 0}
|
||||||
diskUsed={host.disk.used_gb * 1024}
|
diskUsed={host.disk.used_gb * 1024}
|
||||||
diskTotal={host.disk.total_gb * 1024}
|
diskTotal={host.disk.total_gb * 1024}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { Network, RefreshCw, Route, Search, Server, X } from 'lucide-react'
|
import { RefreshCw, Search, Server, X } from 'lucide-react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { getRoutingInfo, RoutingInfo, NAT4Route, IPv6Route } from '../services/api'
|
import { getRoutingInfo, RoutingInfo, NAT4Route, IPv6Route } from '../services/api'
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ export default function Routing() {
|
|||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<CapacityCard
|
<CapacityCard
|
||||||
title="NAT4 端口"
|
title="NAT4 端口"
|
||||||
icon={<Route className="h-5 w-5 text-gray-600" />}
|
icon={<Nat4Icon />}
|
||||||
remaining={routing?.nat4.remaining || '0'}
|
remaining={routing?.nat4.remaining || '0'}
|
||||||
total={routing?.nat4.total || '0'}
|
total={routing?.nat4.total || '0'}
|
||||||
used={routing?.nat4.used || 0}
|
used={routing?.nat4.used || 0}
|
||||||
@@ -104,7 +104,7 @@ export default function Routing() {
|
|||||||
/>
|
/>
|
||||||
<CapacityCard
|
<CapacityCard
|
||||||
title="IPv6 地址"
|
title="IPv6 地址"
|
||||||
icon={<Network className="h-5 w-5 text-gray-600" />}
|
icon={<IPv6Icon />}
|
||||||
remaining={formatCapacity(routing?.ipv6.remaining || '0')}
|
remaining={formatCapacity(routing?.ipv6.remaining || '0')}
|
||||||
total={formatCapacity(routing?.ipv6.total || '0')}
|
total={formatCapacity(routing?.ipv6.total || '0')}
|
||||||
used={routing?.ipv6.used || 0}
|
used={routing?.ipv6.used || 0}
|
||||||
@@ -137,7 +137,7 @@ export default function Routing() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{nat4Mappings.length === 0 ? (
|
{nat4Mappings.length === 0 ? (
|
||||||
<EmptyState icon={<Route className="h-7 w-7 text-gray-400" />} text="暂无 NAT4 端口映射" />
|
<EmptyState icon={<Nat4Icon className="h-7 w-7" />} text="暂无 NAT4 端口映射" />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
@@ -214,7 +214,7 @@ export default function Routing() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{ipv6Assignments.length === 0 ? (
|
{ipv6Assignments.length === 0 ? (
|
||||||
<EmptyState icon={<Network className="h-7 w-7 text-gray-400" />} text="暂无 IPv6 地址分配" />
|
<EmptyState icon={<IPv6Icon className="h-7 w-7" />} text="暂无 IPv6 地址分配" />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
@@ -357,3 +357,21 @@ function formatCapacity(value: string): string {
|
|||||||
if (value === 'large') return '充足'
|
if (value === 'large') return '充足'
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Nat4Icon({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg className={className} viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
|
||||||
|
<path d="M797.866667 128c64 0 115.2 51.2 119.466666 110.933333v558.933334c0 64-51.2 115.2-110.933333 119.466666H243.2c-59.733333 0-110.933333-51.2-115.2-110.933333V247.466667C128 187.733333 174.933333 136.533333 234.666667 128h563.2z m38.4 473.6H204.8v196.266667c0 21.333333 17.066667 38.4 38.4 38.4h554.666667c21.333333 0 38.4-17.066667 38.4-38.4v-196.266667z m-315.733334 76.8c21.333333 0 38.4 17.066667 38.4 42.666667 0 17.066667-12.8 34.133333-34.133333 38.4H320c-21.333333 0-38.4-17.066667-38.4-42.666667 0-17.066667 12.8-34.133333 34.133333-38.4h204.8z m157.866667 0c21.333333 0 38.4 17.066667 38.4 42.666667 0 17.066667-12.8 34.133333-34.133333 38.4h-46.933334c-21.333333 0-38.4-17.066667-38.4-42.666667 0-17.066667 12.8-34.133333 34.133334-38.4h46.933333z m119.466667-473.6h-554.666667c-21.333333 0-38.4 17.066667-38.4 38.4v277.333333h631.466667V243.2c0-17.066667-17.066667-34.133333-38.4-38.4z" />
|
||||||
|
<path d="M277.333333 426.666667V243.2h34.133334V426.666667h-34.133334zM426.666667 358.4h-34.133334V426.666667h-34.133333V243.2h72.533333c38.4 0 59.733333 25.6 59.733334 55.466667s-25.6 59.733333-64 59.733333z m-4.266667-81.066667h-34.133333v51.2h34.133333c17.066667 0 25.6-8.533333 25.6-25.6s-8.533333-25.6-25.6-25.6zM571.733333 426.666667h-25.6l-51.2-132.266667h34.133334l25.6 81.066667 25.6-81.066667h34.133333l-42.666667 132.266667zM733.866667 401.066667v25.6h-34.133334v-25.6h-72.533333v-29.866667l64-123.733333h38.4l-64 123.733333h38.4v-34.133333h34.133333v34.133333h17.066667v29.866667h-21.333333z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function IPv6Icon({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg className={className} viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
|
||||||
|
<path d="M797.866667 128c64 0 115.2 51.2 119.466666 110.933333v558.933334c0 64-51.2 115.2-110.933333 119.466666H243.2c-59.733333 0-110.933333-51.2-115.2-110.933333V247.466667C128 187.733333 174.933333 136.533333 234.666667 128h563.2z m38.4 473.6H204.8v196.266667c0 21.333333 17.066667 38.4 38.4 38.4h554.666667c21.333333 0 38.4-17.066667 38.4-38.4v-196.266667z m-315.733334 76.8c21.333333 0 38.4 17.066667 38.4 42.666667 0 17.066667-12.8 34.133333-34.133333 38.4H320c-21.333333 0-38.4-17.066667-38.4-42.666667 0-17.066667 12.8-34.133333 34.133333-38.4h204.8z m157.866667 0c21.333333 0 38.4 17.066667 38.4 42.666667 0 17.066667-12.8 34.133333-34.133333 38.4h-46.933334c-21.333333 0-38.4-17.066667-38.4-42.666667 0-17.066667 12.8-34.133333 34.133334-38.4h46.933333z m119.466667-473.6h-554.666667c-21.333333 0-38.4 17.066667-38.4 38.4v277.333333h631.466667V243.2c0-17.066667-17.066667-34.133333-38.4-38.4z" />
|
||||||
|
<path d="M277.333333 426.666667V243.2h34.133334V426.666667h-34.133334zM426.666667 358.4h-34.133334V426.666667h-34.133333V243.2h72.533333c38.4 0 59.733333 25.6 59.733334 55.466667s-25.6 59.733333-64 59.733333z m-4.266667-81.066667h-34.133333v51.2h34.133333c17.066667 0 25.6-8.533333 25.6-25.6s-8.533333-25.6-25.6-25.6zM571.733333 426.666667h-25.6l-51.2-132.266667h34.133334l25.6 81.066667 25.6-81.066667h34.133333l-42.666667 132.266667zM691.2 426.666667c-34.133333 0-55.466667-21.333333-55.466667-55.466667 0-17.066667 8.533333-34.133333 17.066667-46.933333l38.4-76.8h38.4l-38.4 76.8c4.266667 0 8.533333-4.266667 12.8-4.266667 25.6 0 46.933333 21.333333 46.933333 55.466667-4.266667 29.866667-29.866667 51.2-59.733333 51.2z m0-81.066667c-12.8 0-25.6 8.533333-25.6 25.6 0 17.066667 8.533333 25.6 25.6 25.6s25.6-8.533333 25.6-25.6c-4.266667-17.066667-12.8-25.6-25.6-25.6z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -170,6 +170,9 @@ export interface ContainerUsage {
|
|||||||
disk_write_bytes: number
|
disk_write_bytes: number
|
||||||
disk_read_bps: number
|
disk_read_bps: number
|
||||||
disk_write_bps: number
|
disk_write_bps: number
|
||||||
|
load1: number
|
||||||
|
load5: number
|
||||||
|
load15: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface APIResponse<T = unknown> {
|
export interface APIResponse<T = unknown> {
|
||||||
|
|||||||
Reference in New Issue
Block a user