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 { Cpu, MemoryStick, HardDrive, Clock, Wifi, Globe, Server, ArrowDown, ArrowUp } from 'lucide-react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { useVMLiveMetrics, useVMConfig, useVMInterfaces } from '@/hooks/use-instances';
|
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 { useTranslation } from 'react-i18next';
|
||||||
import type { DiskInfo } from '@/types';
|
import type { DiskInfo } from '@/types';
|
||||||
|
|
||||||
@@ -165,7 +165,7 @@ export default function OverviewTab({ serverId, vmid, type, node }: Props) {
|
|||||||
<MetricCard
|
<MetricCard
|
||||||
label={t('common.cpu')}
|
label={t('common.cpu')}
|
||||||
value={formatPercent(cpuPercent)}
|
value={formatPercent(cpuPercent)}
|
||||||
subtitle={config?.cores ? `${config.cores} ${t('common.cores')}` : undefined}
|
subtitle={vmVcpuCount(config) ? `${vmVcpuCount(config)} ${t('common.cores')}` : undefined}
|
||||||
percent={cpuPercent}
|
percent={cpuPercent}
|
||||||
icon={Cpu}
|
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.vmid')} value={vmid} />
|
||||||
<InfoRow label={t('common.node')} value={node} />
|
<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.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.memory')} value={config?.memory ? `${config.memory} MB` : undefined} />
|
||||||
<InfoRow label={t('common.os_type')} value={config?.ostype as string} />
|
<InfoRow label={t('common.os_type')} value={config?.ostype as string} />
|
||||||
<InfoRow label={t('common.boot_order')} value={config?.boot 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';
|
const prefix = type === 'lxc' ? 'container' : 'vm';
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: vmKeys.rrddata(serverId, vmid, timeframe),
|
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}`);
|
const res = await apiClient.get<{ data: unknown[]; timeframe: string }>(`/proxmox/api/${serverId}/${prefix}/${vmid}/rrddata?node=${node}&timeframe=${timeframe}`);
|
||||||
return res.data ?? [];
|
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({
|
return useMutation({
|
||||||
mutationFn: (user_id: number | null) =>
|
mutationFn: (user_id: number | null) =>
|
||||||
apiClient.put(`/proxmox/api/${serverId}/vm/${vmid}/owner`, { user_id }),
|
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 });
|
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 ──────────────────────────────────────────────────────
|
// ── Server events ──────────────────────────────────────────────────────
|
||||||
const invalidateServerCaches = () => {
|
const invalidateServerCaches = () => {
|
||||||
qc.invalidateQueries({ queryKey: nodeKeys.servers });
|
qc.invalidateQueries({ queryKey: nodeKeys.servers });
|
||||||
@@ -74,6 +92,7 @@ export function useGlobalRealtimeSync() {
|
|||||||
unsubVmStatus();
|
unsubVmStatus();
|
||||||
unsubVmDeleted();
|
unsubVmDeleted();
|
||||||
unsubVmCreated();
|
unsubVmCreated();
|
||||||
|
unsubVmOwnerChanged();
|
||||||
unsubServerAdded();
|
unsubServerAdded();
|
||||||
unsubServerUpdated();
|
unsubServerUpdated();
|
||||||
unsubServerDeleted();
|
unsubServerDeleted();
|
||||||
|
|||||||
@@ -28,19 +28,32 @@ export function vmTypeLabel(type: string): string {
|
|||||||
return type === 'qemu' ? 'VM' : type === 'lxc' ? 'LXC' : type.toUpperCase();
|
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".
|
* 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: {
|
export function formatVmConfig(vm: {
|
||||||
cores?: number;
|
cores?: number;
|
||||||
memory?: number;
|
memory?: number;
|
||||||
|
maxmem?: number;
|
||||||
disk_size?: number;
|
disk_size?: number;
|
||||||
maxdisk?: number;
|
maxdisk?: number;
|
||||||
}): string {
|
}): string {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
if (vm.cores) parts.push(`${vm.cores} vCPU`);
|
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);
|
const diskGb = vm.disk_size ?? (vm.maxdisk ? Math.round(vm.maxdisk / 1024 ** 3) : 0);
|
||||||
if (diskGb) parts.push(`${diskGb} GB`);
|
if (diskGb) parts.push(`${diskGb} GB`);
|
||||||
return parts.join(' / ');
|
return parts.join(' / ');
|
||||||
|
|||||||
@@ -120,6 +120,10 @@ export interface VMInstance {
|
|||||||
|
|
||||||
export interface VMConfig {
|
export interface VMConfig {
|
||||||
cores?: number;
|
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;
|
memory?: number;
|
||||||
disk?: string;
|
disk?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user