feat: add console link next to VM/LXC names
Clicking the name opens the Proxmox web console in a new tab. URLs are constructed in the API routes using the configured PROXMOX_API_URL. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { getNodes, getLXC } from '@/lib/proxmox';
|
import { getNodes, getLXC } from '@/lib/proxmox';
|
||||||
|
|
||||||
|
const PROXMOX_API_URL = process.env.PROXMOX_API_URL || 'https://proxmox:8006/api2/json';
|
||||||
|
const PROXMOX_WEBUI = PROXMOX_API_URL.replace('/api2/json', '');
|
||||||
|
|
||||||
/** @param {import('next').Request} _req */
|
/** @param {import('next').Request} _req */
|
||||||
export async function GET(_req) {
|
export async function GET(_req) {
|
||||||
try {
|
try {
|
||||||
@@ -10,7 +13,13 @@ export async function GET(_req) {
|
|||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
try {
|
try {
|
||||||
const lxcs = await getLXC(node.name);
|
const lxcs = await getLXC(node.name);
|
||||||
allLXC.push(...lxcs.map((lxc) => ({ ...lxc, node: node.name })));
|
allLXC.push(
|
||||||
|
...lxcs.map((lxc) => ({
|
||||||
|
...lxc,
|
||||||
|
node: node.name,
|
||||||
|
consoleUrl: `${PROXMOX_WEBUI}/pve/console?node=${encodeURIComponent(node.name)}&vmid=${lxc.vmid}`,
|
||||||
|
})),
|
||||||
|
);
|
||||||
} catch { /* skip node */ }
|
} catch { /* skip node */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { getNodes, getVMs } from '@/lib/proxmox';
|
import { getNodes, getVMs } from '@/lib/proxmox';
|
||||||
|
|
||||||
|
const PROXMOX_API_URL = process.env.PROXMOX_API_URL || 'https://proxmox:8006/api2/json';
|
||||||
|
const PROXMOX_WEBUI = PROXMOX_API_URL.replace('/api2/json', '');
|
||||||
|
|
||||||
/** @param {import('next').Request} _req */
|
/** @param {import('next').Request} _req */
|
||||||
export async function GET(_req) {
|
export async function GET(_req) {
|
||||||
try {
|
try {
|
||||||
@@ -10,7 +13,13 @@ export async function GET(_req) {
|
|||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
try {
|
try {
|
||||||
const vms = await getVMs(node.name);
|
const vms = await getVMs(node.name);
|
||||||
allVMs.push(...vms.map((vm) => ({ ...vm, node: node.name })));
|
allVMs.push(
|
||||||
|
...vms.map((vm) => ({
|
||||||
|
...vm,
|
||||||
|
node: node.name,
|
||||||
|
consoleUrl: `${PROXMOX_WEBUI}/pve/console?node=${encodeURIComponent(node.name)}&vmid=${vm.vmid}`,
|
||||||
|
})),
|
||||||
|
);
|
||||||
} catch { /* skip node */ }
|
} catch { /* skip node */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,21 @@ export default function LXCList({ lxc, onPowerAction }) {
|
|||||||
{running.map((item) => (
|
{running.map((item) => (
|
||||||
<tr key={item.vmid} className="border-b border-border-card last:border-0">
|
<tr key={item.vmid} className="border-b border-border-card last:border-0">
|
||||||
<td className="px-4 py-2 text-text-secondary">{item.vmid}</td>
|
<td className="px-4 py-2 text-text-secondary">{item.vmid}</td>
|
||||||
<td className="px-4 py-2 font-medium text-text-primary">{item.name}</td>
|
<td className="px-4 py-2">
|
||||||
|
{item.consoleUrl ? (
|
||||||
|
<a
|
||||||
|
href={item.consoleUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="font-medium text-text-primary hover:text-accent"
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
<span className="ml-1 text-[10px] text-text-secondary">[⟶]</span>
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span className="font-medium text-text-primary">{item.name}</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
<td className="px-4 py-2 text-text-secondary">{item.node}</td>
|
<td className="px-4 py-2 text-text-secondary">{item.node}</td>
|
||||||
<td className="px-4 py-2 text-text-secondary">{(item.cpu * 100).toFixed(1)}%</td>
|
<td className="px-4 py-2 text-text-secondary">{(item.cpu * 100).toFixed(1)}%</td>
|
||||||
<td className="px-4 py-2 text-text-secondary">{formatBytes(item.memory?.used || 0)} / {formatBytes(item.memory?.total || 0)}</td>
|
<td className="px-4 py-2 text-text-secondary">{formatBytes(item.memory?.used || 0)} / {formatBytes(item.memory?.total || 0)}</td>
|
||||||
@@ -82,7 +96,21 @@ export default function LXCList({ lxc, onPowerAction }) {
|
|||||||
{stopped.map((item) => (
|
{stopped.map((item) => (
|
||||||
<tr key={item.vmid} className="border-b border-border-card last:border-0">
|
<tr key={item.vmid} className="border-b border-border-card last:border-0">
|
||||||
<td className="px-4 py-2 text-text-secondary">{item.vmid}</td>
|
<td className="px-4 py-2 text-text-secondary">{item.vmid}</td>
|
||||||
<td className="px-4 py-2 font-medium text-text-primary">{item.name}</td>
|
<td className="px-4 py-2">
|
||||||
|
{item.consoleUrl ? (
|
||||||
|
<a
|
||||||
|
href={item.consoleUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="font-medium text-text-primary hover:text-accent"
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
<span className="ml-1 text-[10px] text-text-secondary">[⟶]</span>
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span className="font-medium text-text-primary">{item.name}</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
<td className="px-4 py-2 text-text-secondary">{item.node}</td>
|
<td className="px-4 py-2 text-text-secondary">{item.node}</td>
|
||||||
<td className="px-4 py-2 text-text-secondary">{formatBytes(item.disk?.used || 0)}</td>
|
<td className="px-4 py-2 text-text-secondary">{formatBytes(item.disk?.used || 0)}</td>
|
||||||
<td className="px-4 py-2"><StatusBadge status={item.status} /></td>
|
<td className="px-4 py-2"><StatusBadge status={item.status} /></td>
|
||||||
|
|||||||
@@ -37,7 +37,21 @@ export default function VMList({ vms, onPowerAction }) {
|
|||||||
{running.map((vm) => (
|
{running.map((vm) => (
|
||||||
<tr key={vm.vmid} className="border-b border-border-card last:border-0">
|
<tr key={vm.vmid} className="border-b border-border-card last:border-0">
|
||||||
<td className="px-4 py-2 text-text-secondary">{vm.vmid}</td>
|
<td className="px-4 py-2 text-text-secondary">{vm.vmid}</td>
|
||||||
<td className="px-4 py-2 font-medium text-text-primary">{vm.name}</td>
|
<td className="px-4 py-2">
|
||||||
|
{vm.consoleUrl ? (
|
||||||
|
<a
|
||||||
|
href={vm.consoleUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="font-medium text-text-primary hover:text-accent"
|
||||||
|
>
|
||||||
|
{vm.name}
|
||||||
|
<span className="ml-1 text-[10px] text-text-secondary">[⟶]</span>
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span className="font-medium text-text-primary">{vm.name}</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
<td className="px-4 py-2 text-text-secondary">{vm.node}</td>
|
<td className="px-4 py-2 text-text-secondary">{vm.node}</td>
|
||||||
<td className="px-4 py-2 text-text-secondary">{(vm.cpu * 100).toFixed(1)}%</td>
|
<td className="px-4 py-2 text-text-secondary">{(vm.cpu * 100).toFixed(1)}%</td>
|
||||||
<td className="px-4 py-2 text-text-secondary">{formatBytes(vm.memory?.used || 0)} / {formatBytes(vm.memory?.total || 0)}</td>
|
<td className="px-4 py-2 text-text-secondary">{formatBytes(vm.memory?.used || 0)} / {formatBytes(vm.memory?.total || 0)}</td>
|
||||||
@@ -83,7 +97,21 @@ export default function VMList({ vms, onPowerAction }) {
|
|||||||
{stopped.map((vm) => (
|
{stopped.map((vm) => (
|
||||||
<tr key={vm.vmid} className="border-b border-border-card last:border-0">
|
<tr key={vm.vmid} className="border-b border-border-card last:border-0">
|
||||||
<td className="px-4 py-2 text-text-secondary">{vm.vmid}</td>
|
<td className="px-4 py-2 text-text-secondary">{vm.vmid}</td>
|
||||||
<td className="px-4 py-2 font-medium text-text-primary">{vm.name}</td>
|
<td className="px-4 py-2">
|
||||||
|
{vm.consoleUrl ? (
|
||||||
|
<a
|
||||||
|
href={vm.consoleUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="font-medium text-text-primary hover:text-accent"
|
||||||
|
>
|
||||||
|
{vm.name}
|
||||||
|
<span className="ml-1 text-[10px] text-text-secondary">[⟶]</span>
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span className="font-medium text-text-primary">{vm.name}</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
<td className="px-4 py-2 text-text-secondary">{vm.node}</td>
|
<td className="px-4 py-2 text-text-secondary">{vm.node}</td>
|
||||||
<td className="px-4 py-2 text-text-secondary">{formatBytes(vm.disk?.used || 0)}</td>
|
<td className="px-4 py-2 text-text-secondary">{formatBytes(vm.disk?.used || 0)}</td>
|
||||||
<td className="px-4 py-2"><StatusBadge status={vm.status} /></td>
|
<td className="px-4 py-2"><StatusBadge status={vm.status} /></td>
|
||||||
|
|||||||
Reference in New Issue
Block a user