mirror of
https://github.com/MengMengCode/CLICD.git
synced 2026-08-07 22:24:42 +08:00
release: v1.1.22
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "clicd-frontend",
|
||||
"private": true,
|
||||
"version": "1.1.21",
|
||||
"version": "1.1.22",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -98,6 +98,13 @@ export default function CreateContainerModal({ isOpen, onClose, onSuccess, exist
|
||||
const manualIPv4s = form.public_ipv4s || []
|
||||
const maxVCPU = hostInfo?.cpu.cores || 64
|
||||
const maxRAMMB = hostInfo?.ram.total_mb ? Number(hostInfo.ram.total_mb) : undefined
|
||||
const kvmAvailable = !!hostInfo?.runtime?.kvm_available
|
||||
|
||||
useEffect(() => {
|
||||
if (hostInfo && !kvmAvailable && form.virtualization === 'kvm') {
|
||||
setForm((prev) => applyTemplateDefaults({ ...prev, virtualization: 'lxc', template_id: '' }))
|
||||
}
|
||||
}, [hostInfo, kvmAvailable, form.virtualization])
|
||||
const maxDiskGB = hostInfo?.disk.total_gb ? Math.max(1, Math.floor(hostInfo.disk.total_gb)) : undefined
|
||||
const resourceErrors = validateResourceInputs(form, maxVCPU, maxRAMMB, maxDiskGB)
|
||||
const natEnabled = form.assign_nat !== false
|
||||
@@ -241,8 +248,14 @@ export default function CreateContainerModal({ isOpen, onClose, onSuccess, exist
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setForm((prev) => applyTemplateDefaults({ ...prev, virtualization: 'kvm', template_id: '' }))}
|
||||
className={`rounded-md border px-3 py-2 text-sm font-medium transition-colors ${form.virtualization === 'kvm' ? 'border-black bg-black text-white' : 'border-gray-300 text-gray-700 hover:bg-gray-50'}`}
|
||||
disabled={!kvmAvailable}
|
||||
title={kvmAvailable ? '' : '当前宿主机不支持 KVM'}
|
||||
onClick={() => {
|
||||
if (kvmAvailable) {
|
||||
setForm((prev) => applyTemplateDefaults({ ...prev, virtualization: 'kvm', template_id: '' }))
|
||||
}
|
||||
}}
|
||||
className={`rounded-md border px-3 py-2 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-400 ${form.virtualization === 'kvm' ? 'border-black bg-black text-white' : 'border-gray-300 text-gray-700 hover:bg-gray-50'}`}
|
||||
>
|
||||
KVM 虚拟机
|
||||
</button>
|
||||
@@ -408,7 +421,7 @@ export default function CreateContainerModal({ isOpen, onClose, onSuccess, exist
|
||||
<span className="min-w-0">
|
||||
<span className="block font-medium text-gray-800">{networkText.publicIPv6}</span>
|
||||
<span className="block text-xs text-gray-500 truncate">
|
||||
{ipv6Available ? `${networkText.use} ${ipv6Prefix}` : (ipv6Status?.reason || networkText.checkingIPv6Prefix)}
|
||||
{ipv6Available ? `${networkText.use} ${ipv6Prefix}` : formatIPv6StatusReason(ipv6Status?.reason, language, networkText.checkingIPv6Prefix)}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
@@ -762,7 +775,7 @@ const createNetworkText = {
|
||||
zh: {
|
||||
publicIPv4: '公网 IPv4',
|
||||
noAllocatableIPv4: '未检测到可分配公网 IPv4',
|
||||
publicIPv6: '公网 IPv6',
|
||||
publicIPv6: '可分配 IPv6 前缀',
|
||||
use: '使用',
|
||||
checkingIPv6Prefix: '正在检测 IPv6 前缀...',
|
||||
publicNAT: '公网 NAT',
|
||||
@@ -771,7 +784,7 @@ const createNetworkText = {
|
||||
en: {
|
||||
publicIPv4: 'Public IPv4',
|
||||
noAllocatableIPv4: 'No allocatable public IPv4 detected',
|
||||
publicIPv6: 'Public IPv6',
|
||||
publicIPv6: 'Allocatable IPv6 Prefix',
|
||||
use: 'Use',
|
||||
checkingIPv6Prefix: 'Checking IPv6 prefix...',
|
||||
publicNAT: 'Public NAT',
|
||||
@@ -779,6 +792,21 @@ const createNetworkText = {
|
||||
},
|
||||
} as const
|
||||
|
||||
function formatIPv6StatusReason(reason: string | undefined, language: Language, fallback: string) {
|
||||
if (!reason) return fallback
|
||||
if (reason.includes('/128 single-address IPv6 is not assignable')) {
|
||||
return language === 'en'
|
||||
? 'No allocatable IPv6 prefix. The host only has a /128 single IPv6 address.'
|
||||
: '未检测到可分配 IPv6 前缀;宿主机只有 /128 单个 IPv6 地址,不能分配给容器。'
|
||||
}
|
||||
if (reason.includes('outbound IPv6 connectivity test failed')) {
|
||||
return language === 'en'
|
||||
? reason
|
||||
: '宿主机检测到 IPv6 前缀,但 IPv6 出站连通性测试失败。'
|
||||
}
|
||||
return reason
|
||||
}
|
||||
|
||||
function formatAllocatableIPv4Count(count: number, language: Language) {
|
||||
return language === 'en'
|
||||
? `${count} allocatable address${count === 1 ? '' : 'es'} detected`
|
||||
|
||||
@@ -395,10 +395,8 @@ export default function Containers() {
|
||||
const isPlaceholder = !!container.isPlaceholder
|
||||
const isPolicyBlocked = !!container.policy_blocked
|
||||
const usage = usageByName[container.name]
|
||||
const isKVM = (container.virtualization || 'lxc') === 'kvm'
|
||||
|
||||
const cpuPct = isRunning
|
||||
? clamp((usage?.cpu_usage_pct || 0) / (isKVM ? (container.vcpu || 1) : 1))
|
||||
? clamp((usage?.cpu_usage_pct || 0) / (container.vcpu || 1))
|
||||
: 0
|
||||
const ramTotalBytes = usage?.memory_total_bytes && usage.memory_total_bytes > 0
|
||||
? usage.memory_total_bytes
|
||||
|
||||
@@ -205,7 +205,7 @@ const hostReportText = {
|
||||
ipv4Address: 'IPv4 地址',
|
||||
ipv4Prefix: 'IPv4 段',
|
||||
ipv6Address: 'IPv6 地址',
|
||||
ipv6Prefix: 'IPv6 段',
|
||||
ipv6Prefix: '可分配 IPv6 前缀',
|
||||
gateway: '网关',
|
||||
memoryModules: '内存条',
|
||||
noMemoryModules: '未检测到内存条明细,可能缺少 dmidecode 或权限受限',
|
||||
@@ -277,7 +277,7 @@ const hostReportText = {
|
||||
ipv4Address: 'IPv4 Addresses',
|
||||
ipv4Prefix: 'IPv4 Prefixes',
|
||||
ipv6Address: 'IPv6 Addresses',
|
||||
ipv6Prefix: 'IPv6 Prefixes',
|
||||
ipv6Prefix: 'Allocatable IPv6 Prefixes',
|
||||
gateway: 'Gateway',
|
||||
memoryModules: 'Memory Modules',
|
||||
noMemoryModules: 'No memory module details detected. dmidecode may be missing or permissions may be limited.',
|
||||
@@ -511,6 +511,7 @@ function diskTypeLabel(d: { type?: string; rotational?: boolean; virtual?: boole
|
||||
}
|
||||
|
||||
function gpuTypeLabel(value: string, language: Language) {
|
||||
if (value === 'virtual') return language === 'en' ? 'Virtual' : '虚拟'
|
||||
if (value === 'integrated') return language === 'en' ? 'Integrated' : '核显'
|
||||
if (value === 'discrete') return language === 'en' ? 'Discrete' : '独显'
|
||||
return value || '-'
|
||||
|
||||
@@ -148,17 +148,19 @@ export default function ImageManagement() {
|
||||
onToggle={handleToggle}
|
||||
/>
|
||||
|
||||
<ImageTable
|
||||
title="KVM 虚拟机镜像"
|
||||
images={kvmImages}
|
||||
actionLoading={actionLoading}
|
||||
downloadedCount={kvmImages.filter((img) => img.downloaded).length}
|
||||
totalCount={kvmImages.length}
|
||||
onDownload={handleDownload}
|
||||
onCancelDownload={handleCancelDownload}
|
||||
onDelete={handleDelete}
|
||||
onToggle={handleToggle}
|
||||
/>
|
||||
{kvmImages.length > 0 && (
|
||||
<ImageTable
|
||||
title="KVM 虚拟机镜像"
|
||||
images={kvmImages}
|
||||
actionLoading={actionLoading}
|
||||
downloadedCount={kvmImages.filter((img) => img.downloaded).length}
|
||||
totalCount={kvmImages.length}
|
||||
onDownload={handleDownload}
|
||||
onCancelDownload={handleCancelDownload}
|
||||
onDelete={handleDelete}
|
||||
onToggle={handleToggle}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ export default function Login() {
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p className="text-center text-xs text-gray-400 mt-6">CLICD v1.1.21</p>
|
||||
<p className="text-center text-xs text-gray-400 mt-6">CLICD v1.1.22</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -235,6 +235,14 @@ export interface HostInfo {
|
||||
}
|
||||
disk_io: { read_bytes: number; write_bytes: number; read_bps: number; write_bps: number }
|
||||
load: { load1: number; load5: number; load15: number }
|
||||
runtime?: {
|
||||
lxc_available: boolean
|
||||
kvm_available: boolean
|
||||
dev_kvm: boolean
|
||||
nested_virtualization: boolean
|
||||
nested_detail: string
|
||||
support_mode: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface HostProbeReport {
|
||||
|
||||
Reference in New Issue
Block a user