chore: pre-existing WIP (overview/realtime/format/types/use-instances)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { Cpu, MemoryStick, HardDrive, Clock, Wifi, Globe, Server, ArrowDown, ArrowUp } from 'lucide-react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { useVMLiveMetrics, useVMConfig, useVMInterfaces } from '@/hooks/use-instances';
|
||||
import { formatBytes, formatUptime, formatPercent } from '@/lib/format';
|
||||
import { formatBytes, formatUptime, formatPercent, vmVcpuCount } from '@/lib/format';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { DiskInfo } from '@/types';
|
||||
|
||||
@@ -165,7 +165,7 @@ export default function OverviewTab({ serverId, vmid, type, node }: Props) {
|
||||
<MetricCard
|
||||
label={t('common.cpu')}
|
||||
value={formatPercent(cpuPercent)}
|
||||
subtitle={config?.cores ? `${config.cores} ${t('common.cores')}` : undefined}
|
||||
subtitle={vmVcpuCount(config) ? `${vmVcpuCount(config)} ${t('common.cores')}` : undefined}
|
||||
percent={cpuPercent}
|
||||
icon={Cpu}
|
||||
/>
|
||||
@@ -258,7 +258,7 @@ export default function OverviewTab({ serverId, vmid, type, node }: Props) {
|
||||
<InfoRow label={t('common.vmid')} value={vmid} />
|
||||
<InfoRow label={t('common.node')} value={node} />
|
||||
<InfoRow label={t('common.type')} value={type === 'qemu' ? t('common.qemu_virtual_machine') : t('common.lxc_container')} />
|
||||
<InfoRow label={t('common.vcpu')} value={config?.cores} />
|
||||
<InfoRow label={t('common.vcpu')} value={vmVcpuCount(config)} />
|
||||
<InfoRow label={t('common.memory')} value={config?.memory ? `${config.memory} MB` : undefined} />
|
||||
<InfoRow label={t('common.os_type')} value={config?.ostype as string} />
|
||||
<InfoRow label={t('common.boot_order')} value={config?.boot as string} />
|
||||
|
||||
@@ -178,7 +178,17 @@ export function useVMConfig(serverId: number, vmid: number, type: string, node:
|
||||
});
|
||||
}
|
||||
|
||||
export function useVMRrddata(serverId: number, vmid: number, type: string, node: string, timeframe = 'hour') {
|
||||
// RRD refresh cadence per timeframe — matches Proxmox's native RRA resolution,
|
||||
// so the chart advances as new samples land instead of staying frozen.
|
||||
const RRD_REFETCH_MS: Record<string, number> = {
|
||||
hour: 60_000, // 1-min resolution
|
||||
day: 300_000, // ~5-min
|
||||
week: 1_800_000, // ~30-min
|
||||
month: 3_600_000, // ~hourly
|
||||
year: 3_600_000,
|
||||
};
|
||||
|
||||
export function useVMRrddata(serverId: number, vmid: number, type: string, node: string, timeframe = 'hour', enabled = true) {
|
||||
const prefix = type === 'lxc' ? 'container' : 'vm';
|
||||
return useQuery({
|
||||
queryKey: vmKeys.rrddata(serverId, vmid, timeframe),
|
||||
@@ -186,6 +196,8 @@ export function useVMRrddata(serverId: number, vmid: number, type: string, node:
|
||||
const res = await apiClient.get<{ data: unknown[]; timeframe: string }>(`/proxmox/api/${serverId}/${prefix}/${vmid}/rrddata?node=${node}&timeframe=${timeframe}`);
|
||||
return res.data ?? [];
|
||||
},
|
||||
enabled,
|
||||
refetchInterval: enabled ? (RRD_REFETCH_MS[timeframe] ?? 60_000) : false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -274,7 +286,11 @@ export function useSetVMOwner(serverId: number, vmid: number) {
|
||||
return useMutation({
|
||||
mutationFn: (user_id: number | null) =>
|
||||
apiClient.put(`/proxmox/api/${serverId}/vm/${vmid}/owner`, { user_id }),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['vm-owner', serverId, vmid] }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['vm-owner', serverId, vmid] });
|
||||
// Refresh the instances list so the owner column updates without a manual reload.
|
||||
qc.invalidateQueries({ queryKey: vmKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,24 @@ export function useGlobalRealtimeSync() {
|
||||
qc.invalidateQueries({ queryKey: vmKeys.resourcesAll });
|
||||
});
|
||||
|
||||
const unsubVmOwnerChanged = ws.onType('vm_owner_changed', (data: unknown) => {
|
||||
const msg = data as {
|
||||
vmid: number;
|
||||
server_id: number;
|
||||
owner_id: number | null;
|
||||
owner_user: { username: string; email?: string; full_name?: string } | null;
|
||||
};
|
||||
qc.setQueryData<Array<Record<string, unknown>>>(vmKeys.all, (old) =>
|
||||
old?.map((vm) =>
|
||||
vm.server_id === msg.server_id && vm.vmid === msg.vmid
|
||||
? { ...vm, owner_id: msg.owner_id, owner_user: msg.owner_user }
|
||||
: vm
|
||||
)
|
||||
);
|
||||
// Keep the admin owner panel (settings tab) in sync as well.
|
||||
qc.invalidateQueries({ queryKey: ['vm-owner', msg.server_id, msg.vmid] });
|
||||
});
|
||||
|
||||
// ── Server events ──────────────────────────────────────────────────────
|
||||
const invalidateServerCaches = () => {
|
||||
qc.invalidateQueries({ queryKey: nodeKeys.servers });
|
||||
@@ -74,6 +92,7 @@ export function useGlobalRealtimeSync() {
|
||||
unsubVmStatus();
|
||||
unsubVmDeleted();
|
||||
unsubVmCreated();
|
||||
unsubVmOwnerChanged();
|
||||
unsubServerAdded();
|
||||
unsubServerUpdated();
|
||||
unsubServerDeleted();
|
||||
|
||||
@@ -28,19 +28,32 @@ export function vmTypeLabel(type: string): string {
|
||||
return type === 'qemu' ? 'VM' : type === 'lxc' ? 'LXC' : type.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Total vCPU count from a live Proxmox config.
|
||||
* QEMU reports `cores` *per socket*, so the real total is `sockets * cores`
|
||||
* (LXC has no sockets and reports the total in `cores`).
|
||||
*/
|
||||
export function vmVcpuCount(config?: { cores?: number; sockets?: number } | null): number | undefined {
|
||||
if (!config?.cores) return undefined;
|
||||
return config.cores * (config.sockets ?? 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a VM/LXC hardware configuration as "N vCPU / X GB RAM / Y GB".
|
||||
* `memory` is expected in MB; disk in GB (falls back to `maxdisk` in bytes).
|
||||
* `memory` and `maxdisk` come from the cached instances list in bytes;
|
||||
* `disk_size` (when present) is already in GB.
|
||||
*/
|
||||
export function formatVmConfig(vm: {
|
||||
cores?: number;
|
||||
memory?: number;
|
||||
maxmem?: number;
|
||||
disk_size?: number;
|
||||
maxdisk?: number;
|
||||
}): string {
|
||||
const parts: string[] = [];
|
||||
if (vm.cores) parts.push(`${vm.cores} vCPU`);
|
||||
if (vm.memory) parts.push(`${Math.round(vm.memory / 1024)} GB RAM`);
|
||||
const memBytes = vm.memory || vm.maxmem;
|
||||
if (memBytes) parts.push(`${formatBytes(memBytes, 0)} RAM`);
|
||||
const diskGb = vm.disk_size ?? (vm.maxdisk ? Math.round(vm.maxdisk / 1024 ** 3) : 0);
|
||||
if (diskGb) parts.push(`${diskGb} GB`);
|
||||
return parts.join(' / ');
|
||||
|
||||
@@ -120,6 +120,10 @@ export interface VMInstance {
|
||||
|
||||
export interface VMConfig {
|
||||
cores?: number;
|
||||
/** QEMU socket count; total vCPU = sockets * cores (defaults to 1) */
|
||||
sockets?: number;
|
||||
/** QEMU hotplugged vCPU count, when fewer than sockets * cores are online */
|
||||
vcpus?: number;
|
||||
memory?: number;
|
||||
disk?: string;
|
||||
name?: string;
|
||||
|
||||
Reference in New Issue
Block a user