Adds sender/responder mode switching via API, QuickPing component, echo-mode responder with dedicated container, improved flow state sync, and RFC2544 test runner enhancements. Includes UI improvements across all traffic-gen components. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
167 lines
5.5 KiB
Vue
167 lines
5.5 KiB
Vue
<template>
|
|
<div class="test-builder">
|
|
<h3>RFC 2544 Test</h3>
|
|
|
|
<div class="form-row">
|
|
<label>Test Type</label>
|
|
<select v-model="form.type">
|
|
<option value="throughput">Throughput (binary search for max rate)</option>
|
|
<option value="latency">Latency (measure RTT)</option>
|
|
<option value="frame_loss">Frame Loss (loss vs rate curve)</option>
|
|
<option value="back_to_back">Back-to-Back (max burst)</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<label>Destination IP</label>
|
|
<input v-model="form.dst_ip" placeholder="10.100.0.1" />
|
|
</div>
|
|
|
|
<div class="form-row-pair">
|
|
<div class="form-row">
|
|
<label>Protocol</label>
|
|
<select v-model="form.protocol">
|
|
<option value="udp">UDP</option>
|
|
<option value="icmp">ICMP</option>
|
|
<option value="tcp">TCP</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-row">
|
|
<label>Source IP</label>
|
|
<input v-model="form.src_ip" placeholder="auto" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<label>Frame Sizes</label>
|
|
<div class="frame-sizes">
|
|
<label v-for="s in standardSizes" :key="s" class="checkbox-label">
|
|
<input type="checkbox" :value="s" v-model="form.frame_sizes" /> {{ s }}
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row-pair">
|
|
<div class="form-row">
|
|
<label>Trial Duration (sec)</label>
|
|
<input v-model.number="form.trial_duration" type="number" min="5" max="300" />
|
|
</div>
|
|
<div class="form-row">
|
|
<label>Max Rate</label>
|
|
<input v-model.number="form.max_rate_val" type="number" min="1" step="any" />
|
|
<select v-model="form.max_rate_unit" class="rate-unit-standalone">
|
|
<option value="pps">pps</option>
|
|
<option value="kbps">Kbps</option>
|
|
<option value="mbps">Mbps</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<label>Acceptable Loss %</label>
|
|
<input v-model.number="form.acceptable_loss_pct" type="number" min="0" max="100" step="0.1" />
|
|
</div>
|
|
|
|
<button class="btn btn-accent" @click="create" :disabled="!form.dst_ip">
|
|
Create & Run Test
|
|
</button>
|
|
|
|
<div class="presets-section">
|
|
<h4>Quick Presets</h4>
|
|
<div class="preset-list">
|
|
<button v-for="(p, name) in presets" :key="name" class="btn-preset" @click="loadPreset(name)">
|
|
<strong>{{ name }}</strong>
|
|
<span>{{ p.description }}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { reactive, ref, onMounted } from 'vue'
|
|
import { api } from '../api.js'
|
|
|
|
const emit = defineEmits(['created', 'refresh'])
|
|
|
|
const standardSizes = [64, 128, 256, 512, 1024, 1280, 1518, 2048, 4096, 9000]
|
|
const presets = ref({})
|
|
|
|
const form = reactive({
|
|
type: 'throughput',
|
|
dst_ip: '',
|
|
src_ip: '',
|
|
protocol: 'udp',
|
|
frame_sizes: [64, 512, 1518],
|
|
trial_duration: 30,
|
|
max_rate_val: 10,
|
|
max_rate_unit: 'mbps',
|
|
acceptable_loss_pct: 0.0,
|
|
})
|
|
|
|
function computePps(val, unit) {
|
|
if (unit === 'kbps') return Math.max(1, Math.round((val * 1000) / (512 * 8)))
|
|
if (unit === 'mbps') return Math.max(1, Math.round((val * 1_000_000) / (512 * 8)))
|
|
return Math.round(val)
|
|
}
|
|
|
|
onMounted(async () => {
|
|
try { const r = await api.presets(); presets.value = r.presets || r } catch (_) {}
|
|
})
|
|
|
|
async function create() {
|
|
try {
|
|
const payload = {
|
|
type: form.type,
|
|
flow_config: {
|
|
dst_ip: form.dst_ip,
|
|
src_ip: form.src_ip || 'auto',
|
|
protocol: form.protocol,
|
|
src_port: 50000,
|
|
dst_port: 5001,
|
|
},
|
|
frame_sizes: form.frame_sizes,
|
|
trial_duration: form.trial_duration,
|
|
max_rate_pps: computePps(form.max_rate_val, form.max_rate_unit),
|
|
acceptable_loss_pct: form.acceptable_loss_pct,
|
|
}
|
|
const test = await api.createTest(payload)
|
|
await api.startTest(test.id)
|
|
emit('created')
|
|
} catch (e) { alert(e.message) }
|
|
}
|
|
|
|
async function loadPreset(name) {
|
|
const dstIp = prompt('Destination IP for this preset:', '10.100.0.100')
|
|
if (!dstIp) return
|
|
try {
|
|
await api.loadPreset(name, { dst_ip: dstIp })
|
|
emit('refresh')
|
|
} catch (e) { alert(e.message) }
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
h3 { font-size: 15px; margin-bottom: 12px; color: var(--accent); }
|
|
h4 { font-size: 13px; margin: 16px 0 8px; color: var(--muted); }
|
|
.form-row { margin-bottom: 10px; }
|
|
.form-row label { display: block; font-size: 11px; color: var(--muted); margin-bottom: 3px; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
.form-row input, .form-row select { width: 100%; }
|
|
.form-row-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
|
.rate-unit-standalone { width: 100%; margin-top: 4px; }
|
|
.frame-sizes { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
.checkbox-label { font-size: 12px; display: flex; align-items: center; gap: 4px; color: var(--text); cursor: pointer; }
|
|
.btn { padding: 8px 16px; font-weight: 600; font-size: 13px; width: 100%; margin-top: 8px; }
|
|
.btn-accent { background: var(--accent); color: #fff; }
|
|
.btn-accent:disabled { opacity: 0.4; }
|
|
.preset-list { display: flex; flex-direction: column; gap: 6px; }
|
|
.btn-preset {
|
|
display: flex; flex-direction: column; align-items: flex-start;
|
|
padding: 8px 12px; background: var(--card-bg); border: 1px solid var(--border);
|
|
border-radius: var(--radius); text-align: left;
|
|
}
|
|
.btn-preset:hover { border-color: var(--accent); }
|
|
.btn-preset strong { font-size: 12px; color: var(--accent); }
|
|
.btn-preset span { font-size: 11px; color: var(--muted); }
|
|
</style>
|