feat: open console in new tab instead of iframe panel
- Remove ConsolePanel and /api/vnc route (iframe session auth won't work cross-origin) - VMList/LXCList console buttons now open Proxmox web UI console in new tab - Use web UI console URL format so browser session cookies handle auth automatically - Add resize=scale and xtermjs=1 for better terminal UX Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,7 @@ export async function GET(_req) {
|
|||||||
...lxcs.map((lxc) => ({
|
...lxcs.map((lxc) => ({
|
||||||
...lxc,
|
...lxc,
|
||||||
node: node.name,
|
node: node.name,
|
||||||
consoleUrl: `${PROXMOX_WEB_URL}/?console=lxc&novnc=1&node=${encodeURIComponent(node.name)}&vmid=${lxc.vmid}&token=${encodeURIComponent(PROXMOX_CONSOLE_TOKEN)}`,
|
consoleUrl: `${PROXMOX_WEB_URL}/?console=lxc&vmid=${lxc.vmid}&node=${encodeURIComponent(node.name)}&resize=scale&xtermjs=1`,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
} catch { /* skip node */ }
|
} catch { /* skip node */ }
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export async function GET(_req) {
|
|||||||
...vms.map((vm) => ({
|
...vms.map((vm) => ({
|
||||||
...vm,
|
...vm,
|
||||||
node: node.name,
|
node: node.name,
|
||||||
consoleUrl: `${PROXMOX_WEB_URL}/?console=qemu&novnc=1&node=${encodeURIComponent(node.name)}&vmid=${vm.vmid}&token=${encodeURIComponent(PROXMOX_CONSOLE_TOKEN)}`,
|
consoleUrl: `${PROXMOX_WEB_URL}/?console=qemu&vmid=${vm.vmid}&node=${encodeURIComponent(node.name)}&resize=scale&novnc=1`,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
} catch { /* skip node */ }
|
} catch { /* skip node */ }
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
import { NextResponse } from 'next/server';
|
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
const PROXMOX_API_URL = process.env.PROXMOX_API_URL;
|
|
||||||
const PROXMOX_WEB_URL = process.env.PROXMOX_WEB_URL || PROXMOX_API_URL?.replace('/api2/json', '');
|
|
||||||
|
|
||||||
/** @param {import('next').Request} req */
|
|
||||||
export async function GET(req) {
|
|
||||||
const url = new URL(req.url);
|
|
||||||
const node = url.searchParams.get('node');
|
|
||||||
const vmid = url.searchParams.get('vmid');
|
|
||||||
const type = url.searchParams.get('type');
|
|
||||||
|
|
||||||
if (!node || !vmid || !type) {
|
|
||||||
return NextResponse.json({ error: 'Missing node, vmid, or type' }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const ticketRes = await axios({
|
|
||||||
url: `${PROXMOX_API_URL}/nodes/${node}/${type}/${vmid}/vncproxy`,
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
Authorization: `PVEAPIToken=${process.env.PROXMOX_TOKEN_ID}=${process.env.PROXMOX_TOKEN_SECRET}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
data: JSON.stringify({ websocket: 0 }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const ticket = ticketRes.data?.ticket;
|
|
||||||
|
|
||||||
if (!ticket) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'No VNC ticket returned from Proxmox API' },
|
|
||||||
{ status: 502 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const consoleUrl = `${PROXMOX_WEB_URL}/?vncticket=${encodeURIComponent(ticket)}&novnc=1&node=${encodeURIComponent(node)}&vmid=${vmid}`;
|
|
||||||
|
|
||||||
return NextResponse.json({ consoleUrl });
|
|
||||||
} catch (err) {
|
|
||||||
const msg = err.response?.data?.message || err.response?.data || err.message;
|
|
||||||
return NextResponse.json({ error: msg }, { status: 502 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {{ vmid: number; name: string; node: string; type: string; consoleUrl: string }} vm
|
|
||||||
* @param {() => void} onClose
|
|
||||||
* @returns {import('react').ReactNode}
|
|
||||||
*/
|
|
||||||
export default function ConsolePanel({ vm, onClose }) {
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed bottom-0 left-0 right-0 z-50 flex h-[60vh] flex-col rounded-t-xl border border-border-card bg-bg-card shadow-2xl sm:right-4">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between border-b border-border-card px-4 py-2">
|
|
||||||
<div className="flex items-center gap-2 text-sm font-medium text-text-primary">
|
|
||||||
<span>Console</span>
|
|
||||||
<span className="text-[10px] text-text-secondary">
|
|
||||||
({vm.type.toUpperCase()} {vm.vmid} on {vm.node})
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{loading && <span className="text-xs text-text-secondary">Loading…</span>}
|
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
className="rounded-md border border-border-card bg-bg-card px-2 py-1 text-xs text-text-secondary hover:text-text-primary"
|
|
||||||
>
|
|
||||||
Close
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Frame */}
|
|
||||||
<iframe
|
|
||||||
src={vm.consoleUrl}
|
|
||||||
title={`${vm.name} console`}
|
|
||||||
className="h-full w-full border-0"
|
|
||||||
onLoad={() => setLoading(false)}
|
|
||||||
sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@ import NodeList from './NodeList';
|
|||||||
import VMList from './VMList';
|
import VMList from './VMList';
|
||||||
import LXCList from './LXCList';
|
import LXCList from './LXCList';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import ConsolePanel from './ConsolePanel';
|
|
||||||
|
|
||||||
const POLL_INTERVAL = Number(process.env.NEXT_PUBLIC_API_POLL_INTERVAL || 30000);
|
const POLL_INTERVAL = Number(process.env.NEXT_PUBLIC_API_POLL_INTERVAL || 30000);
|
||||||
|
|
||||||
@@ -29,7 +28,6 @@ async function fetchData(url) {
|
|||||||
/** @returns {import('react').ReactNode} */
|
/** @returns {import('react').ReactNode} */
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const [activeTab, setActiveTab] = useState('nodes');
|
const [activeTab, setActiveTab] = useState('nodes');
|
||||||
const [activeConsole, setActiveConsole] = useState(null);
|
|
||||||
const [powerActionStatus, setPowerActionStatus] = useState(null);
|
const [powerActionStatus, setPowerActionStatus] = useState(null);
|
||||||
|
|
||||||
const handlePowerAction = async (node, vmid, type, action) => {
|
const handlePowerAction = async (node, vmid, type, action) => {
|
||||||
@@ -90,9 +88,12 @@ export default function Dashboard() {
|
|||||||
{ id: 'nodes', label: 'Nodes' },
|
{ id: 'nodes', label: 'Nodes' },
|
||||||
{ id: 'vms', label: 'VMs' },
|
{ id: 'vms', label: 'VMs' },
|
||||||
{ id: 'lxc', label: 'LXCs' },
|
{ id: 'lxc', label: 'LXCs' },
|
||||||
...(activeConsole ? [{ id: 'console', label: `Console: ${activeConsole.name}` }] : []),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const handleOpenConsole = (vm) => {
|
||||||
|
window.open(vm.consoleUrl, '_blank');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen p-4 sm:p-6 lg:p-8">
|
<div className="min-h-screen p-4 sm:p-6 lg:p-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -167,11 +168,8 @@ export default function Dashboard() {
|
|||||||
{/* Tab content */}
|
{/* Tab content */}
|
||||||
<div>
|
<div>
|
||||||
{activeTab === 'nodes' && <NodeList nodes={nodes} />}
|
{activeTab === 'nodes' && <NodeList nodes={nodes} />}
|
||||||
{activeTab === 'vms' && <VMList vms={vms} onPowerAction={handlePowerAction} onOpenConsole={(vm) => { setActiveConsole(vm); setActiveTab('console'); }} />}
|
{activeTab === 'vms' && <VMList vms={vms} onPowerAction={handlePowerAction} onOpenConsole={handleOpenConsole} />}
|
||||||
{activeTab === 'lxc' && <LXCList lxc={lxc} onPowerAction={handlePowerAction} onOpenConsole={(lxc) => { setActiveConsole(lxc); setActiveTab('console'); }} />}
|
{activeTab === 'lxc' && <LXCList lxc={lxc} onPowerAction={handlePowerAction} onOpenConsole={handleOpenConsole} />}
|
||||||
{activeTab === 'console' && activeConsole && (
|
|
||||||
<ConsolePanel vm={activeConsole} onClose={() => setActiveConsole(null)} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
|
|||||||
Reference in New Issue
Block a user