- gNMI integration: NETCONF script to enable gRPC on all 9 routers, Telegraf container with gnmi input plugin, InfluxDB for time-series storage, 3 Grafana telemetry dashboards (utilization, errors, combined) - Traffic generator: Scapy-based dual-mode container (sender/responder) with Flask API, RFC 2544 test suite (throughput, latency, frame-loss, back-to-back), Vue 3 web UI with flow builder, test runner, real-time stats monitor, and results export - docker-compose.yml updated with influxdb, telegraf, traffic-gen, traffic-gen-ui services - Full documentation in DOCS.md sections 15-16 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
93 lines
3.4 KiB
Vue
93 lines
3.4 KiB
Vue
<template>
|
|
<div class="flow-table">
|
|
<div v-if="!flows.length" class="empty">No flows created yet. Use the builder to create one.</div>
|
|
<table v-else>
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Dst IP</th>
|
|
<th>Proto</th>
|
|
<th>Size</th>
|
|
<th>Rate</th>
|
|
<th>State</th>
|
|
<th>TX pps</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="f in flows" :key="f.id" :class="{ running: f.state === 'running' }">
|
|
<td>{{ f.name || '-' }}</td>
|
|
<td class="mono">{{ f.dst_ip }}</td>
|
|
<td>{{ f.protocol.toUpperCase() }}</td>
|
|
<td>{{ f.frame_size }}B</td>
|
|
<td>{{ f.rate_pps }} pps</td>
|
|
<td>
|
|
<span class="state-badge" :class="'state-' + f.state">{{ f.state }}</span>
|
|
</td>
|
|
<td class="mono">{{ stats[f.id]?.tx_pps || 0 }}</td>
|
|
<td class="actions">
|
|
<button v-if="f.state !== 'running'" class="btn-sm btn-go" @click="start(f.id)">Start</button>
|
|
<button v-else class="btn-sm btn-stop" @click="stop(f.id)">Stop</button>
|
|
<button class="btn-sm btn-del" @click="del(f.id)" :disabled="f.state === 'running'">Del</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
import { api } from '../api.js'
|
|
|
|
const props = defineProps({ flows: Array })
|
|
const emit = defineEmits(['refresh'])
|
|
const stats = ref({})
|
|
|
|
let statsTimer = null
|
|
|
|
async function fetchStats() {
|
|
for (const f of (props.flows || [])) {
|
|
if (f.state === 'running') {
|
|
try {
|
|
const s = await api.flowStats(f.id)
|
|
stats.value[f.id] = s
|
|
} catch (_) {}
|
|
}
|
|
}
|
|
}
|
|
|
|
onMounted(() => { statsTimer = setInterval(fetchStats, 1000) })
|
|
onUnmounted(() => { clearInterval(statsTimer) })
|
|
|
|
async function start(id) {
|
|
try { await api.startFlow(id); emit('refresh') } catch (e) { alert(e.message) }
|
|
}
|
|
async function stop(id) {
|
|
try { await api.stopFlow(id); emit('refresh') } catch (e) { alert(e.message) }
|
|
}
|
|
async function del(id) {
|
|
try { await api.deleteFlow(id); emit('refresh') } catch (e) { alert(e.message) }
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.flow-table { overflow-x: auto; }
|
|
.empty { color: var(--muted); padding: 20px; text-align: center; }
|
|
table { width: 100%; border-collapse: collapse; }
|
|
th { text-align: left; font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; padding: 6px 8px; border-bottom: 1px solid var(--border); }
|
|
td { padding: 8px; border-bottom: 1px solid rgba(45,55,72,0.5); font-size: 13px; }
|
|
tr.running { background: rgba(79,156,249,0.05); }
|
|
.mono { font-family: 'Cascadia Code', 'Fira Code', monospace; font-size: 12px; }
|
|
.state-badge { font-size: 11px; padding: 2px 8px; border-radius: 10px; font-weight: 600; }
|
|
.state-idle { background: rgba(113,128,150,0.2); color: var(--muted); }
|
|
.state-running { background: rgba(72,187,120,0.15); color: var(--success); }
|
|
.state-stopped { background: rgba(246,173,85,0.15); color: var(--warning); }
|
|
.actions { display: flex; gap: 4px; }
|
|
.btn-sm { padding: 3px 10px; font-size: 11px; font-weight: 600; border-radius: 6px; }
|
|
.btn-go { background: var(--success); color: #fff; }
|
|
.btn-stop { background: var(--warning); color: #000; }
|
|
.btn-del { background: rgba(252,129,129,0.15); color: var(--danger); }
|
|
.btn-del:disabled { opacity: 0.3; }
|
|
</style>
|