wraith/frontend/components/connections/HostTree.vue
Vantz Stockwell 74d3c0bd9a feat: add delete button for groups in sidebar
Shows × on hover next to the + button. Confirms before deleting.
Hosts in deleted groups become ungrouped.

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

126 lines
4.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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
(e: 'delete-group', 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 group/grp"
@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('delete-group', group.id)"
class="text-xs text-gray-600 hover:text-red-400 px-0.5 opacity-0 group-hover/grp:opacity-100 transition-opacity"
title="Delete group"
>×</button>
<button
@click.stop="emit('new-host', group.id)"
class="text-xs text-gray-600 hover:text-wraith-400 px-0.5 opacity-0 group-hover/grp:opacity-100 transition-opacity"
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)"
@delete-group="(gid) => emit('delete-group', 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>