feat: add D3.js network map visualization to Federation page
- Install d3@7 and @types/d3@7 - NetworkMap.vue: force-directed graph with draggable nodes, trust-level coloring (green/amber/red), online/offline opacity, dashed links - Federation.vue: List/Map tab switcher with localStorage persistence - Wire map to real federation data (self node centered, peers as satellites) - Default to map view when 3+ nodes federated Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,25 @@
|
||||
<p class="text-sm text-white/60 mt-2">{{ nodes.length }} federated node{{ nodes.length !== 1 ? 's' : '' }}</p>
|
||||
</div>
|
||||
|
||||
<!-- View Tabs -->
|
||||
<div v-if="nodes.length > 0" class="flex gap-1 mb-6 p-1 bg-black/20 rounded-lg w-fit">
|
||||
<button
|
||||
v-for="tab in viewTabs"
|
||||
:key="tab.id"
|
||||
class="px-4 py-2 rounded text-sm font-medium transition-colors"
|
||||
:class="activeView === tab.id ? 'bg-white/10 text-white border-b-2 border-orange-400' : 'text-white/50 hover:text-white/70'"
|
||||
@click="setView(tab.id)"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Network Map View -->
|
||||
<div v-if="activeView === 'map' && nodes.length > 0" class="mb-6">
|
||||
<NetworkMap :nodes="mapNodes" :links="mapLinks" />
|
||||
</div>
|
||||
|
||||
<template v-if="activeView === 'list'">
|
||||
<!-- Quick Actions -->
|
||||
<div class="glass-card p-6 mb-6">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
@@ -164,6 +183,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<!-- Node Detail Modal -->
|
||||
<div v-if="selectedNode" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm" @click.self="selectedNode = null; confirmRemove = false">
|
||||
<div class="glass-card p-6 w-full max-w-lg max-h-[80vh] overflow-y-auto">
|
||||
@@ -337,6 +358,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import NetworkMap from '@/components/federation/NetworkMap.vue'
|
||||
|
||||
interface AppStatus {
|
||||
id: string
|
||||
@@ -391,6 +413,56 @@ const deployAppId = ref('')
|
||||
const deploying = ref(false)
|
||||
const deployResult = ref('')
|
||||
|
||||
const viewTabs = [
|
||||
{ id: 'list', label: 'List View' },
|
||||
{ id: 'map', label: 'Network Map' },
|
||||
] as const
|
||||
|
||||
type ViewId = typeof viewTabs[number]['id']
|
||||
const activeView = ref<ViewId>(
|
||||
(localStorage.getItem('federation-view') as ViewId) || (nodes.value.length >= 3 ? 'map' : 'list')
|
||||
)
|
||||
|
||||
function setView(id: ViewId) {
|
||||
activeView.value = id
|
||||
localStorage.setItem('federation-view', id)
|
||||
}
|
||||
|
||||
const selfDid = ref('')
|
||||
|
||||
const mapNodes = computed(() => {
|
||||
const result = []
|
||||
if (selfDid.value) {
|
||||
result.push({
|
||||
did: selfDid.value,
|
||||
label: 'This Node',
|
||||
trust_level: 'trusted' as const,
|
||||
online: true,
|
||||
app_count: 0,
|
||||
is_self: true,
|
||||
})
|
||||
}
|
||||
for (const node of nodes.value) {
|
||||
result.push({
|
||||
did: node.did,
|
||||
label: node.name || shortDid(node.did),
|
||||
trust_level: node.trust_level as 'trusted' | 'observer' | 'untrusted',
|
||||
online: isOnline(node),
|
||||
app_count: node.last_state?.apps?.length ?? 0,
|
||||
is_self: false,
|
||||
})
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
const mapLinks = computed(() => {
|
||||
if (!selfDid.value) return []
|
||||
return nodes.value.map(n => ({
|
||||
source: selfDid.value,
|
||||
target: n.did,
|
||||
}))
|
||||
})
|
||||
|
||||
interface DwnStatus {
|
||||
sync_status: string
|
||||
last_sync: string | null
|
||||
@@ -605,8 +677,14 @@ function trustBadgeClass(level: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
loadNodes()
|
||||
loadDwnStatus()
|
||||
try {
|
||||
const result = await rpcClient.getNodeDid()
|
||||
selfDid.value = result.did
|
||||
} catch {
|
||||
// Self DID not available
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user