wraith/frontend/components/connections/HostTree.vue
Vantz Stockwell 3b14a7c1d1 feat: Termius-inspired UI — right sidebar, host counts, terminal themes
Left sidebar:
- Groups now show recursive host count badges
- Hosts in tree show up to 3 tags inline

Right sidebar (Host Details panel):
- Click any host card to open details panel on the right
- Shows address, port, protocol, group, credential, tags, color, notes
- Connect, Edit, Delete action buttons at bottom
- Selected card gets ring highlight

Terminal themes (10 prebuilt):
- Wraith (default), Dracula, Nord, Solarized Dark, Monokai, One Dark,
  Gruvbox Dark, Tokyo Night, Catppuccin Mocha, Cyberpunk
- Visual theme picker in Settings with color preview + sample text
- Persisted to /api/settings and localStorage for immediate use
- useTerminal reads theme on terminal creation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 10:19:57 -04:00

119 lines
3.7 KiB
Vue

<script setup lang="ts">
interface Host {
id: number
name: string
hostname: string
port: number
protocol: 'ssh' | 'rdp'
color: string | null
tags?: string[]
}
interface HostGroup {
id: number
name: string
parentId: number | null
children: HostGroup[]
hosts: Host[]
}
const props = defineProps<{
groups: HostGroup[]
}>()
const emit = defineEmits<{
(e: 'select-host', host: Host): void
(e: 'new-host', groupId?: number): void
}>()
const expanded = ref<Set<number>>(new Set())
function toggleGroup(id: number) {
if (expanded.value.has(id)) {
expanded.value.delete(id)
} else {
expanded.value.add(id)
}
}
function isExpanded(id: number) {
return expanded.value.has(id)
}
// Recursively count all hosts in a group and its children
function countHosts(group: HostGroup): number {
let count = group.hosts?.length || 0
for (const child of group.children || []) {
count += countHosts(child)
}
return count
}
</script>
<template>
<div class="flex-1 overflow-y-auto text-sm">
<!-- Groups -->
<template v-for="group in groups" :key="group.id">
<div>
<!-- Group header -->
<div
class="flex items-center gap-1 px-3 py-1.5 cursor-pointer hover:bg-gray-800 text-gray-400 hover:text-gray-200 select-none"
@click="toggleGroup(group.id)"
>
<span class="text-xs w-3">{{ isExpanded(group.id) ? '▾' : '▸' }}</span>
<span class="font-medium truncate flex-1">{{ group.name }}</span>
<span class="text-xs text-gray-600 tabular-nums mr-1">{{ countHosts(group) }}</span>
<button
@click.stop="emit('new-host', group.id)"
class="text-xs text-gray-600 hover:text-wraith-400 px-1"
title="Add host to group"
>+</button>
</div>
<!-- Group children (hosts + sub-groups) -->
<div v-if="isExpanded(group.id)" class="pl-3">
<!-- Sub-groups recursively -->
<HostTree
v-if="group.children?.length"
:groups="group.children"
@select-host="(h) => emit('select-host', h)"
@new-host="(gid) => emit('new-host', gid)"
/>
<!-- Hosts in this group -->
<div
v-for="host in group.hosts"
:key="host.id"
class="flex items-center gap-2 px-3 py-1.5 cursor-pointer hover:bg-gray-800 text-gray-300 hover:text-white group/host"
@click="emit('select-host', host)"
>
<span
class="w-2 h-2 rounded-full shrink-0"
:style="host.color ? { backgroundColor: host.color } : {}"
:class="!host.color ? 'bg-gray-600' : ''"
/>
<div class="min-w-0 flex-1">
<span class="truncate block">{{ host.name }}</span>
<div v-if="host.tags?.length" class="flex gap-1 mt-0.5 flex-wrap">
<span
v-for="tag in host.tags.slice(0, 3)"
:key="tag"
class="text-[10px] leading-tight px-1 py-px rounded bg-gray-800 text-gray-500"
>{{ tag }}</span>
<span v-if="host.tags.length > 3" class="text-[10px] text-gray-600">+{{ host.tags.length - 3 }}</span>
</div>
</div>
<span
class="text-xs px-1 rounded shrink-0"
:class="host.protocol === 'rdp' ? 'text-purple-400 bg-purple-900/30' : 'text-wraith-400 bg-wraith-900/30'"
>{{ host.protocol.toUpperCase() }}</span>
</div>
</div>
</div>
</template>
<!-- Ungrouped hosts (shown at root level when no groups) -->
<div v-if="!groups.length" class="px-3 py-2 text-gray-600 text-xs">No groups yet</div>
</div>
</template>