feat: quick connect, search, recent connections
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
19183ee546
commit
8546824b97
46
frontend/components/connections/QuickConnect.vue
Normal file
46
frontend/components/connections/QuickConnect.vue
Normal file
@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
const input = ref('')
|
||||
const protocol = ref<'ssh' | 'rdp'>('ssh')
|
||||
|
||||
const emit = defineEmits<{
|
||||
connect: [{ hostname: string; port: number; username: string; protocol: 'ssh' | 'rdp' }]
|
||||
}>()
|
||||
|
||||
function handleConnect() {
|
||||
const raw = input.value.trim()
|
||||
if (!raw) return
|
||||
|
||||
// Parse user@hostname:port format
|
||||
let username = ''
|
||||
let hostname = raw
|
||||
let port = protocol.value === 'rdp' ? 3389 : 22
|
||||
|
||||
if (hostname.includes('@')) {
|
||||
[username, hostname] = hostname.split('@')
|
||||
}
|
||||
if (hostname.includes(':')) {
|
||||
const parts = hostname.split(':')
|
||||
hostname = parts[0]
|
||||
port = parseInt(parts[1], 10)
|
||||
}
|
||||
|
||||
emit('connect', { hostname, port, username, protocol: protocol.value })
|
||||
input.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center gap-2 px-3 py-2 bg-gray-800 border-b border-gray-700">
|
||||
<select v-model="protocol" class="bg-gray-700 text-gray-300 text-xs rounded px-2 py-1.5 border-none">
|
||||
<option value="ssh">SSH</option>
|
||||
<option value="rdp">RDP</option>
|
||||
</select>
|
||||
<input
|
||||
v-model="input"
|
||||
@keydown.enter="handleConnect"
|
||||
:placeholder="`user@hostname:${protocol === 'rdp' ? '3389' : '22'}`"
|
||||
class="flex-1 bg-gray-900 text-white px-3 py-1.5 rounded text-sm border border-gray-700 focus:border-wraith-500 focus:outline-none"
|
||||
/>
|
||||
<button @click="handleConnect" class="text-sm text-wraith-400 hover:text-wraith-300 px-2">Connect</button>
|
||||
</div>
|
||||
</template>
|
||||
@ -2,9 +2,13 @@
|
||||
import { useTerminal } from '~/composables/useTerminal'
|
||||
|
||||
const connections = useConnectionStore()
|
||||
const sessions = useSessionStore()
|
||||
const showHostDialog = ref(false)
|
||||
const editingHost = ref<any>(null)
|
||||
const showGroupDialog = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const showSavePrompt = ref(false)
|
||||
const pendingQuickConnect = ref<any>(null)
|
||||
|
||||
// Terminal composable for connect-on-click
|
||||
const { createTerminal, connectToHost } = useTerminal()
|
||||
@ -25,15 +29,9 @@ function openEditHost(host: any) {
|
||||
|
||||
function connectHost(host: any) {
|
||||
if (host.protocol !== 'ssh') {
|
||||
// RDP support in Phase 3
|
||||
// RDP handled via quick connect for now
|
||||
return
|
||||
}
|
||||
// We connect via useTerminal — TerminalInstance will handle the actual mount
|
||||
// Here we just trigger SessionContainer to render a new TerminalInstance
|
||||
// The terminal composable is invoked inside TerminalInstance on mount
|
||||
// We signal the session store directly to open a pending session slot
|
||||
// TerminalInstance picks up the hostId prop and opens the WS connection
|
||||
const sessions = useSessionStore()
|
||||
const pendingId = `pending-${Date.now()}`
|
||||
sessions.addSession({
|
||||
id: pendingId,
|
||||
@ -44,38 +42,155 @@ function connectHost(host: any) {
|
||||
active: true,
|
||||
})
|
||||
}
|
||||
|
||||
// Quick connect: launch a temporary session without saving
|
||||
function handleQuickConnect(params: { hostname: string; port: number; username: string; protocol: 'ssh' | 'rdp' }) {
|
||||
const sessionId = `quick-${Date.now()}`
|
||||
const displayName = params.username
|
||||
? `${params.username}@${params.hostname}`
|
||||
: params.hostname
|
||||
|
||||
sessions.addSession({
|
||||
id: sessionId,
|
||||
hostId: null,
|
||||
hostName: displayName,
|
||||
hostname: params.hostname,
|
||||
port: params.port,
|
||||
username: params.username,
|
||||
protocol: params.protocol,
|
||||
color: null,
|
||||
active: true,
|
||||
isTemporary: true,
|
||||
})
|
||||
|
||||
pendingQuickConnect.value = params
|
||||
showSavePrompt.value = true
|
||||
}
|
||||
|
||||
// Save a quick-connect as a permanent host
|
||||
function saveQuickConnectHost() {
|
||||
if (!pendingQuickConnect.value) return
|
||||
const p = pendingQuickConnect.value
|
||||
editingHost.value = {
|
||||
hostname: p.hostname,
|
||||
port: p.port,
|
||||
protocol: p.protocol,
|
||||
name: p.hostname,
|
||||
}
|
||||
showSavePrompt.value = false
|
||||
pendingQuickConnect.value = null
|
||||
showHostDialog.value = true
|
||||
}
|
||||
|
||||
function dismissSavePrompt() {
|
||||
showSavePrompt.value = false
|
||||
pendingQuickConnect.value = null
|
||||
}
|
||||
|
||||
// Client-side search filtering
|
||||
const filteredHosts = computed(() => {
|
||||
if (!searchQuery.value.trim()) return connections.hosts
|
||||
const q = searchQuery.value.toLowerCase()
|
||||
return connections.hosts.filter((h: any) =>
|
||||
h.name?.toLowerCase().includes(q) ||
|
||||
h.hostname?.toLowerCase().includes(q) ||
|
||||
(h.tags || []).some((t: string) => t.toLowerCase().includes(q))
|
||||
)
|
||||
})
|
||||
|
||||
// Recent connections — hosts sorted by lastConnectedAt, non-null only, top 5
|
||||
const recentHosts = computed(() => {
|
||||
return [...connections.hosts]
|
||||
.filter((h: any) => h.lastConnectedAt)
|
||||
.sort((a: any, b: any) =>
|
||||
new Date(b.lastConnectedAt).getTime() - new Date(a.lastConnectedAt).getTime()
|
||||
)
|
||||
.slice(0, 5)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full">
|
||||
<!-- Sidebar: Group tree -->
|
||||
<aside class="w-64 bg-gray-900 border-r border-gray-800 flex flex-col overflow-y-auto shrink-0">
|
||||
<div class="p-3 flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-400">Connections</span>
|
||||
<div class="flex gap-1">
|
||||
<button @click="showGroupDialog = true" class="text-xs text-gray-500 hover:text-wraith-400" title="New Group">+ Group</button>
|
||||
<button @click="openNewHost()" class="text-xs text-gray-500 hover:text-wraith-400" title="New Host">+ Host</button>
|
||||
</div>
|
||||
</div>
|
||||
<HostTree :groups="connections.groups" @select-host="openEditHost" @new-host="openNewHost" />
|
||||
</aside>
|
||||
<div class="flex w-full flex-col">
|
||||
<!-- Quick Connect bar -->
|
||||
<QuickConnect @connect="handleQuickConnect" />
|
||||
|
||||
<!-- Main: host list -->
|
||||
<main class="flex-1 p-4 overflow-y-auto">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
<HostCard
|
||||
v-for="host in connections.hosts"
|
||||
:key="host.id"
|
||||
:host="host"
|
||||
@connect="connectHost(host)"
|
||||
@edit="openEditHost(host)"
|
||||
@delete="connections.deleteHost(host.id)"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="!connections.hosts.length && !connections.loading" class="text-gray-600 text-center mt-12">
|
||||
No hosts yet. Click "+ Host" to add your first connection.
|
||||
</p>
|
||||
</main>
|
||||
<!-- Save prompt toast -->
|
||||
<div v-if="showSavePrompt"
|
||||
class="fixed bottom-4 right-4 z-40 bg-gray-800 border border-gray-700 rounded-lg shadow-xl p-4 flex items-center gap-4 text-sm">
|
||||
<span class="text-gray-300">Save this connection for later?</span>
|
||||
<button @click="saveQuickConnectHost" class="text-wraith-400 hover:text-wraith-300 font-medium">Save</button>
|
||||
<button @click="dismissSavePrompt" class="text-gray-500 hover:text-gray-300">Dismiss</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-1 overflow-hidden">
|
||||
<!-- Sidebar: Group tree -->
|
||||
<aside class="w-64 bg-gray-900 border-r border-gray-800 flex flex-col overflow-y-auto shrink-0">
|
||||
<div class="p-3 flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-400">Connections</span>
|
||||
<div class="flex gap-1">
|
||||
<button @click="showGroupDialog = true" class="text-xs text-gray-500 hover:text-wraith-400" title="New Group">+ Group</button>
|
||||
<button @click="openNewHost()" class="text-xs text-gray-500 hover:text-wraith-400" title="New Host">+ Host</button>
|
||||
</div>
|
||||
</div>
|
||||
<HostTree :groups="connections.groups" @select-host="openEditHost" @new-host="openNewHost" />
|
||||
</aside>
|
||||
|
||||
<!-- Main: host grid with search + recent -->
|
||||
<main class="flex-1 p-4 overflow-y-auto">
|
||||
<!-- Search bar -->
|
||||
<div class="mb-4">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="Search hosts by name, hostname, or tag..."
|
||||
class="w-full bg-gray-800 text-white px-3 py-2 rounded border border-gray-700 focus:border-wraith-500 focus:outline-none text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Recent connections (only when not searching) -->
|
||||
<div v-if="!searchQuery && recentHosts.length > 0" class="mb-6">
|
||||
<h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">Recent</h3>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-2">
|
||||
<button
|
||||
v-for="host in recentHosts"
|
||||
:key="host.id"
|
||||
@click="connectHost(host)"
|
||||
class="flex items-center gap-2 p-2 bg-gray-800/70 hover:bg-gray-700 rounded border border-gray-700/50 text-left"
|
||||
>
|
||||
<span class="w-2 h-2 rounded-full shrink-0 bg-wraith-500" />
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm text-white truncate">{{ host.name }}</p>
|
||||
<p class="text-xs text-gray-500 truncate">{{ host.hostname }}</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- All hosts grid -->
|
||||
<div>
|
||||
<h3 v-if="!searchQuery && recentHosts.length > 0"
|
||||
class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">
|
||||
All Hosts
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
<HostCard
|
||||
v-for="host in filteredHosts"
|
||||
:key="host.id"
|
||||
:host="host"
|
||||
@connect="connectHost(host)"
|
||||
@edit="openEditHost(host)"
|
||||
@delete="connections.deleteHost(host.id)"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="filteredHosts.length === 0 && searchQuery" class="text-gray-600 text-center mt-12">
|
||||
No hosts match "{{ searchQuery }}"
|
||||
</p>
|
||||
<p v-if="!connections.hosts.length && !connections.loading && !searchQuery" class="text-gray-600 text-center mt-12">
|
||||
No hosts yet. Click "+ Host" to add your first connection.
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Dialogs -->
|
||||
<HostEditDialog v-model:visible="showHostDialog" :host="editingHost" @saved="connections.fetchHosts()" />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user