fix: terminal.astro must be SSR, not prerendered
Page reads from astro:content collections (machines, sessions, users) that are populated at request time from headscale. Pre-rendering at build time hits empty/undefined data and throws 'Cannot read properties of undefined (reading length)'.
This commit is contained in:
parent
72f6277b6f
commit
6e2679ac3a
553
src/pages/terminal.astro
Normal file
553
src/pages/terminal.astro
Normal file
@ -0,0 +1,553 @@
|
|||||||
|
---
|
||||||
|
// Heady Terminal Page - Alpine.js/Astro Remote Access 🤠
|
||||||
|
// SSR-only: the page reads from `astro:content` collections that are
|
||||||
|
// populated at request time from headscale, so pre-rendering at build
|
||||||
|
// time hits empty/undefined data and throws.
|
||||||
|
export const prerender = false;
|
||||||
|
|
||||||
|
import Layout from '../layouts/Layout.astro';
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
import GuacamoleLiteClient from '../components/GuacamoleLiteClient.astro';
|
||||||
|
import NodeGrid from '../components/NodeGrid.astro';
|
||||||
|
import SessionManager from '../components/SessionManager.astro';
|
||||||
|
|
||||||
|
// Fetch live data for terminal page with error handling
|
||||||
|
let allMachines = [];
|
||||||
|
let activeSessions = [];
|
||||||
|
let currentUser = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
allMachines = await getCollection('machines');
|
||||||
|
activeSessions = await getCollection('sessions', ({ data }) => data.status === 'active');
|
||||||
|
const users = await getCollection('users');
|
||||||
|
currentUser = users[0] || null; // In real app, get from auth
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading terminal page data:', error);
|
||||||
|
// Use fallback data for development
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter online machines for terminal access
|
||||||
|
const onlineMachines = allMachines.filter(machine => machine.data?.online);
|
||||||
|
|
||||||
|
// User permissions for protocol access
|
||||||
|
const userPermissions = {
|
||||||
|
ssh: true,
|
||||||
|
rdp: currentUser?.data.role === 'owner' || currentUser?.data.role === 'admin' || currentUser?.data.role === 'it_admin',
|
||||||
|
vnc: currentUser?.data.role === 'owner' || currentUser?.data.role === 'admin',
|
||||||
|
telnet: currentUser?.data.role !== 'member' && currentUser?.data.role !== 'auditor',
|
||||||
|
kubernetes: currentUser?.data.role === 'owner' || currentUser?.data.role === 'admin' || currentUser?.data.role === 'network_admin',
|
||||||
|
file_transfer: currentUser?.data.role !== 'member' && currentUser?.data.role !== 'auditor',
|
||||||
|
session_recording: currentUser?.data.role !== 'owner'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Available protocols based on permissions
|
||||||
|
const availableProtocols = Object.entries(userPermissions)
|
||||||
|
.filter(([protocol, allowed]) => allowed && ['ssh', 'rdp', 'vnc', 'telnet', 'kubernetes'].includes(protocol))
|
||||||
|
.map(([protocol]) => protocol);
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title="Remote Access Terminal">
|
||||||
|
<div
|
||||||
|
class="min-h-screen bg-gray-900"
|
||||||
|
x-data="terminalPage()"
|
||||||
|
x-init="init()"
|
||||||
|
>
|
||||||
|
<!-- Terminal Header -->
|
||||||
|
<div class="bg-gray-800 border-b border-gray-700">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
|
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between">
|
||||||
|
<div class="mb-4 lg:mb-0">
|
||||||
|
<h1 class="text-2xl font-bold text-white flex items-center">
|
||||||
|
<span class="mr-3">💻</span>
|
||||||
|
Heady Remote Access
|
||||||
|
</h1>
|
||||||
|
<p class="mt-1 text-sm text-gray-400">
|
||||||
|
Secure multi-protocol terminal access to your VPN infrastructure
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Connection -->
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<span class="text-sm text-gray-400">Role:</span>
|
||||||
|
<span
|
||||||
|
class="px-2 py-1 text-xs rounded font-medium"
|
||||||
|
:class="`heady-role-${currentUser?.role?.replace('_', '-')}`"
|
||||||
|
x-text="currentUser?.role"
|
||||||
|
>
|
||||||
|
{currentUser?.data.role}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="showQuickConnect = true"
|
||||||
|
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors flex items-center"
|
||||||
|
>
|
||||||
|
🚀 Quick Connect
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search and Filters -->
|
||||||
|
<div class="bg-gray-800 border-b border-gray-700">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||||
|
<div class="flex flex-col md:flex-row gap-4 items-center justify-between">
|
||||||
|
<!-- Search -->
|
||||||
|
<div class="flex-1 max-w-md">
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<svg class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
x-model="searchTerm"
|
||||||
|
type="text"
|
||||||
|
placeholder="Search machines..."
|
||||||
|
class="w-full pl-10 pr-4 py-2 bg-gray-900 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:border-blue-500 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Protocol Filter -->
|
||||||
|
<div class="flex gap-2 flex-wrap">
|
||||||
|
{availableProtocols.map(protocol => (
|
||||||
|
<button
|
||||||
|
@click={`selectedProtocol = '${protocol}'`}
|
||||||
|
:class={`selectedProtocol === '${protocol}' ? 'bg-blue-500 text-white' : 'bg-gray-700 text-gray-300 hover:bg-gray-600'`}
|
||||||
|
class="px-3 py-2 text-sm rounded font-semibold transition-colors"
|
||||||
|
>
|
||||||
|
{protocol.toUpperCase()}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- View Toggle -->
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button
|
||||||
|
@click="viewMode = 'grid'"
|
||||||
|
:class="viewMode === 'grid' ? 'bg-blue-500 text-white' : 'bg-gray-700 text-gray-300 hover:bg-gray-600'"
|
||||||
|
class="px-3 py-2 text-sm rounded transition-colors"
|
||||||
|
>
|
||||||
|
🔲 Grid
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="viewMode = 'list'"
|
||||||
|
:class="viewMode === 'list' ? 'bg-blue-500 text-white' : 'bg-gray-700 text-gray-300 hover:bg-gray-600'"
|
||||||
|
class="px-3 py-2 text-sm rounded transition-colors"
|
||||||
|
>
|
||||||
|
📋 List
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
<!-- Active Sessions Section -->
|
||||||
|
<div x-show="activeSessions && activeSessions.length > 0" x-transition class="mb-8">
|
||||||
|
<SessionManager
|
||||||
|
sessions={activeSessions}
|
||||||
|
userId={currentUser?.data.id}
|
||||||
|
compact={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Machines Grid/List -->
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h2 class="text-lg font-semibold text-white flex items-center">
|
||||||
|
<span class="mr-2">🖥️</span>
|
||||||
|
Available Machines
|
||||||
|
<span class="ml-2 text-sm text-gray-400" x-text="`(${filteredMachines ? filteredMachines.length : 0})`"></span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<!-- Sort Options -->
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<span class="text-sm text-gray-400">Sort by:</span>
|
||||||
|
<select
|
||||||
|
x-model="sortBy"
|
||||||
|
@change="sortMachines()"
|
||||||
|
class="bg-gray-800 border border-gray-600 text-white text-sm rounded px-3 py-1"
|
||||||
|
>
|
||||||
|
<option value="name">Name</option>
|
||||||
|
<option value="status">Status</option>
|
||||||
|
<option value="last_seen">Last Seen</option>
|
||||||
|
<option value="os">OS</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Grid View -->
|
||||||
|
<div x-show="viewMode === 'grid'" x-transition>
|
||||||
|
<NodeGrid
|
||||||
|
nodes={onlineMachines}
|
||||||
|
userId={currentUser?.data.id}
|
||||||
|
showOffline={false}
|
||||||
|
gridCols={4}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- List View -->
|
||||||
|
<div x-show="viewMode === 'list'" x-transition>
|
||||||
|
<div class="bg-gray-800 rounded-lg border border-gray-700 overflow-hidden">
|
||||||
|
<div class="px-6 py-4 bg-gray-800 border-b border-gray-700">
|
||||||
|
<h3 class="text-lg font-medium text-white">Machine List</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divide-y divide-gray-700">
|
||||||
|
<template x-for="machine in filteredMachines" :key="machine.id">
|
||||||
|
<div
|
||||||
|
class="px-6 py-4 hover:bg-gray-700 cursor-pointer transition-colors"
|
||||||
|
@click="connectToMachine(machine)"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<!-- Machine Info -->
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<div
|
||||||
|
class="w-3 h-3 rounded-full"
|
||||||
|
:class="machine.online ? 'bg-green-500 animate-pulse' : 'bg-gray-500'"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-medium text-white" x-text="machine.name"></div>
|
||||||
|
<div class="text-xs text-gray-400" x-text="machine.ip_address"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- OS Icon -->
|
||||||
|
<div class="text-lg">
|
||||||
|
<span x-show="machine.os === 'linux'">🐧</span>
|
||||||
|
<span x-show="machine.os === 'windows'">🪟</span>
|
||||||
|
<span x-show="machine.os === 'macos'">🍎</span>
|
||||||
|
<span x-show="!machine.os || machine.os === 'unknown'">❓</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Protocols and Actions -->
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<!-- Available Protocols -->
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<template x-for="protocol in getAvailableProtocols(machine)" :key="protocol">
|
||||||
|
<span
|
||||||
|
class="heady-protocol-badge text-xs"
|
||||||
|
:class="`protocol-${protocol}`"
|
||||||
|
x-text="protocol"
|
||||||
|
></span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button
|
||||||
|
@click.stop="quickConnect(machine, 'ssh')"
|
||||||
|
class="bg-green-600 hover:bg-green-700 text-white px-2 py-1 text-xs rounded transition-colors"
|
||||||
|
>
|
||||||
|
SSH
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<template x-if="machine.os === 'windows' && hasPermission('rdp')">
|
||||||
|
<button
|
||||||
|
@click.stop="quickConnect(machine, 'rdp')"
|
||||||
|
class="bg-blue-600 hover:bg-blue-700 text-white px-2 py-1 text-xs rounded transition-colors"
|
||||||
|
>
|
||||||
|
RDP
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div x-show="filteredMachines && filteredMachines.length === 0" x-transition class="text-center py-12">
|
||||||
|
<span class="text-6xl mb-4 block">🤠</span>
|
||||||
|
<h3 class="text-xl font-semibold mb-2 text-white">No machines available</h3>
|
||||||
|
<p class="text-gray-400">
|
||||||
|
<span x-show="searchTerm">No machines match your search criteria.</span>
|
||||||
|
<span x-show="!searchTerm">No online machines support the selected protocol.</span>
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
@click="clearFilters()"
|
||||||
|
class="mt-4 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors"
|
||||||
|
>
|
||||||
|
Clear Filters
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Terminal Modal -->
|
||||||
|
<div
|
||||||
|
x-show="isTerminalOpen"
|
||||||
|
x-transition
|
||||||
|
class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-75"
|
||||||
|
@click.self="closeTerminal()"
|
||||||
|
>
|
||||||
|
<div class="w-full h-full md:w-5/6 md:h-5/6 md:max-w-6xl">
|
||||||
|
<div class="bg-gray-900 rounded-lg overflow-hidden h-full flex flex-col">
|
||||||
|
<!-- Terminal Header -->
|
||||||
|
<div class="bg-gray-800 border-b border-gray-600 px-4 py-3 flex items-center justify-between">
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<span
|
||||||
|
class="heady-protocol-badge"
|
||||||
|
:class="`protocol-${currentConnection?.protocol}`"
|
||||||
|
x-text="currentConnection?.protocol?.toUpperCase()"
|
||||||
|
></span>
|
||||||
|
<span class="font-mono text-white" x-text="currentConnection?.node"></span>
|
||||||
|
|
||||||
|
<!-- Connection Status -->
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<div
|
||||||
|
class="w-2 h-2 rounded-full"
|
||||||
|
:class="connectionStatus === 'connected' ? 'bg-green-500 animate-pulse' :
|
||||||
|
connectionStatus === 'connecting' ? 'bg-yellow-500' :
|
||||||
|
'bg-red-500'"
|
||||||
|
></div>
|
||||||
|
<span class="text-xs text-gray-300" x-text="connectionStatus"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<span class="text-xs text-gray-300" x-text="`${currentUser?.email} • ${currentUser?.role}`"></span>
|
||||||
|
<button
|
||||||
|
@click="closeTerminal()"
|
||||||
|
class="text-red-400 hover:text-red-300 font-bold text-lg"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Guacamole Client Container -->
|
||||||
|
<div class="flex-1" x-show="currentConnection">
|
||||||
|
<GuacamoleLiteClient
|
||||||
|
x-bind:nodeId="currentConnection?.node"
|
||||||
|
x-bind:protocol="currentConnection?.protocol"
|
||||||
|
autoConnect={true}
|
||||||
|
showToolbar={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Connect Modal -->
|
||||||
|
<div
|
||||||
|
x-show="showQuickConnect"
|
||||||
|
x-transition
|
||||||
|
class="fixed inset-0 z-40 flex items-center justify-center bg-black bg-opacity-50"
|
||||||
|
@click.self="showQuickConnect = false"
|
||||||
|
>
|
||||||
|
<div class="bg-gray-800 rounded-lg p-6 w-full max-w-md mx-4">
|
||||||
|
<h3 class="text-lg font-semibold text-white mb-4">Quick Connect</h3>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<!-- Machine Selection -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-300 mb-2">Machine</label>
|
||||||
|
<select
|
||||||
|
x-model="quickConnectMachine"
|
||||||
|
class="w-full bg-gray-900 border border-gray-600 text-white rounded px-3 py-2"
|
||||||
|
>
|
||||||
|
<option value="">Select a machine...</option>
|
||||||
|
<template x-for="machine in onlineMachines" :key="machine.id">
|
||||||
|
<option :value="machine.name" x-text="`${machine.name} (${machine.ip_address})`"></option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Protocol Selection -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-300 mb-2">Protocol</label>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
{availableProtocols.map(protocol => (
|
||||||
|
<button
|
||||||
|
@click={`quickConnectProtocol = '${protocol}'`}
|
||||||
|
:class={`quickConnectProtocol === '${protocol}' ? 'bg-blue-500 text-white' : 'bg-gray-700 text-gray-300 hover:bg-gray-600'`}
|
||||||
|
class="px-3 py-2 text-sm rounded font-medium transition-colors"
|
||||||
|
>
|
||||||
|
{protocol.toUpperCase()}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="flex gap-3 pt-4">
|
||||||
|
<button
|
||||||
|
@click="executeQuickConnect()"
|
||||||
|
:disabled="!quickConnectMachine || !quickConnectProtocol"
|
||||||
|
class="flex-1 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white px-4 py-2 rounded font-medium transition-colors"
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="showQuickConnect = false"
|
||||||
|
class="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded font-medium transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script define:vars={{ onlineMachines, activeSessions, userPermissions, currentUser }}>
|
||||||
|
function terminalPage() {
|
||||||
|
return {
|
||||||
|
// Current user and permissions
|
||||||
|
currentUser: currentUser?.data || null,
|
||||||
|
permissions: userPermissions,
|
||||||
|
|
||||||
|
// UI State
|
||||||
|
searchTerm: '',
|
||||||
|
selectedProtocol: 'ssh',
|
||||||
|
viewMode: 'grid',
|
||||||
|
sortBy: 'name',
|
||||||
|
|
||||||
|
// Machine data
|
||||||
|
machines: onlineMachines.map(m => m.data),
|
||||||
|
activeSessions: activeSessions.map(s => s.data),
|
||||||
|
|
||||||
|
// Terminal state
|
||||||
|
isTerminalOpen: false,
|
||||||
|
currentConnection: null,
|
||||||
|
connectionStatus: 'disconnected',
|
||||||
|
|
||||||
|
// Quick connect
|
||||||
|
showQuickConnect: false,
|
||||||
|
quickConnectMachine: '',
|
||||||
|
quickConnectProtocol: 'ssh',
|
||||||
|
|
||||||
|
get filteredMachines() {
|
||||||
|
let filtered = this.machines.filter(machine => {
|
||||||
|
const matchesSearch = machine.name.toLowerCase().includes(this.searchTerm.toLowerCase()) ||
|
||||||
|
machine.ip_address.includes(this.searchTerm);
|
||||||
|
const hasProtocol = this.getAvailableProtocols(machine).includes(this.selectedProtocol);
|
||||||
|
return matchesSearch && hasProtocol;
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.sortMachines(filtered);
|
||||||
|
},
|
||||||
|
|
||||||
|
get onlineMachines() {
|
||||||
|
return this.machines.filter(m => m.online);
|
||||||
|
},
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// Parse JSON data
|
||||||
|
this.currentUser = JSON.parse(this.currentUser || 'null');
|
||||||
|
this.permissions = JSON.parse(this.permissions);
|
||||||
|
this.machines = JSON.parse(this.machines);
|
||||||
|
this.activeSessions = JSON.parse(this.activeSessions);
|
||||||
|
|
||||||
|
// Set up periodic refresh
|
||||||
|
setInterval(() => this.refreshData(), 30000);
|
||||||
|
|
||||||
|
// Listen for terminal events
|
||||||
|
this.$el.addEventListener('open-terminal', (e) => {
|
||||||
|
this.connectToNode(e.detail.node, e.detail.protocol);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getAvailableProtocols(machine) {
|
||||||
|
const protocols = [];
|
||||||
|
|
||||||
|
if (machine.ssh_enabled && this.permissions.ssh) protocols.push('ssh');
|
||||||
|
if (machine.rdp_enabled && this.permissions.rdp) protocols.push('rdp');
|
||||||
|
if (machine.vnc_enabled && this.permissions.vnc) protocols.push('vnc');
|
||||||
|
if (machine.telnet_enabled && this.permissions.telnet) protocols.push('telnet');
|
||||||
|
if (machine.kubernetes_enabled && this.permissions.kubernetes) protocols.push('kubernetes');
|
||||||
|
|
||||||
|
return protocols;
|
||||||
|
},
|
||||||
|
|
||||||
|
hasPermission(protocol) {
|
||||||
|
return this.permissions[protocol] || false;
|
||||||
|
},
|
||||||
|
|
||||||
|
sortMachines(machines = this.machines) {
|
||||||
|
return machines.sort((a, b) => {
|
||||||
|
switch (this.sortBy) {
|
||||||
|
case 'name':
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
case 'status':
|
||||||
|
return b.online - a.online;
|
||||||
|
case 'last_seen':
|
||||||
|
return new Date(b.last_seen || 0) - new Date(a.last_seen || 0);
|
||||||
|
case 'os':
|
||||||
|
return (a.os || 'unknown').localeCompare(b.os || 'unknown');
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
connectToMachine(machine) {
|
||||||
|
const protocols = this.getAvailableProtocols(machine);
|
||||||
|
const protocol = protocols.includes(this.selectedProtocol) ? this.selectedProtocol : protocols[0];
|
||||||
|
this.connectToNode(machine.name, protocol);
|
||||||
|
},
|
||||||
|
|
||||||
|
connectToNode(nodeName, protocol) {
|
||||||
|
this.currentConnection = { node: nodeName, protocol };
|
||||||
|
this.connectionStatus = 'connecting';
|
||||||
|
this.isTerminalOpen = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
quickConnect(machine, protocol) {
|
||||||
|
this.connectToNode(machine.name, protocol);
|
||||||
|
},
|
||||||
|
|
||||||
|
executeQuickConnect() {
|
||||||
|
if (this.quickConnectMachine && this.quickConnectProtocol) {
|
||||||
|
this.connectToNode(this.quickConnectMachine, this.quickConnectProtocol);
|
||||||
|
this.showQuickConnect = false;
|
||||||
|
this.quickConnectMachine = '';
|
||||||
|
this.quickConnectProtocol = 'ssh';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
closeTerminal() {
|
||||||
|
this.isTerminalOpen = false;
|
||||||
|
this.currentConnection = null;
|
||||||
|
this.connectionStatus = 'disconnected';
|
||||||
|
},
|
||||||
|
|
||||||
|
clearFilters() {
|
||||||
|
this.searchTerm = '';
|
||||||
|
this.selectedProtocol = 'ssh';
|
||||||
|
},
|
||||||
|
|
||||||
|
async refreshData() {
|
||||||
|
try {
|
||||||
|
const [machinesResponse, sessionsResponse] = await Promise.all([
|
||||||
|
fetch('/api/machines'),
|
||||||
|
fetch('/api/sessions?status=active')
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (machinesResponse.ok) {
|
||||||
|
this.machines = await machinesResponse.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sessionsResponse.ok) {
|
||||||
|
this.activeSessions = await sessionsResponse.json();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to refresh terminal data:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make globally available
|
||||||
|
window.terminalPage = terminalPage;
|
||||||
|
</script>
|
||||||
|
</Layout>
|
||||||
Loading…
x
Reference in New Issue
Block a user