160 lines
5.9 KiB
Vue
Raw Normal View History

<template>
<div class="flow-builder">
<h3>{{ editing ? 'Edit Flow' : 'Create Flow' }}</h3>
<form @submit.prevent="submit">
<div class="form-row">
<label>Name</label>
<input v-model="form.name" placeholder="My Flow" />
</div>
<div class="form-row">
<label>Destination IP *</label>
<input v-model="form.dst_ip" placeholder="10.100.0.100" required />
</div>
<div class="form-row">
<label>Source IP</label>
<input v-model="form.src_ip" placeholder="auto (from interface)" />
</div>
<div class="form-row">
<label>Dst MAC</label>
<input v-model="form.dst_mac" placeholder="auto" />
</div>
<div class="form-row">
<label>Protocol</label>
<select v-model="form.protocol">
<option value="udp">UDP</option>
<option value="tcp">TCP</option>
<option value="icmp">ICMP</option>
</select>
</div>
<div v-if="form.protocol !== 'icmp'" class="form-row-pair">
<div class="form-row">
<label>Src Port</label>
<input v-model.number="form.src_port" type="number" min="1" max="65535" />
</div>
<div class="form-row">
<label>Dst Port</label>
<input v-model.number="form.dst_port" type="number" min="1" max="65535" />
</div>
</div>
<div class="form-row-pair">
<div class="form-row">
<label>Frame Size (bytes)</label>
<input v-model.number="form.frame_size" type="number" min="64" max="9000" />
</div>
<div class="form-row">
<label>Rate</label>
<input v-model.number="form.rate_val" type="number" min="1" step="any" />
<select v-model="form.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-pair">
<div class="form-row">
<label>Duration (sec)</label>
<input v-model.number="form.duration" type="number" min="0" :disabled="form.continuous" />
<label class="checkbox-inline">
<input type="checkbox" v-model="form.continuous" @change="onContinuousChange" />
Continuous
</label>
</div>
<div class="form-row">
<label>DSCP</label>
<input v-model.number="form.dscp" type="number" min="0" max="63" />
</div>
</div>
<div class="form-row">
<label>Responder URL (optional)</label>
<input v-model="form.responder_url" placeholder="http://host:5053" />
</div>
<div class="form-actions">
<button type="submit" class="btn btn-accent" :disabled="!form.dst_ip">
{{ editing ? 'Update' : 'Create Flow' }}
</button>
<button v-if="editing" type="button" class="btn btn-muted" @click="$emit('cancel')">Cancel</button>
</div>
</form>
</div>
</template>
<script setup>
import { reactive, computed } from 'vue'
import { api } from '../api.js'
const props = defineProps({ editFlow: Object })
const emit = defineEmits(['created', 'updated', 'cancel'])
const editing = computed(() => !!props.editFlow)
const defaults = {
name: '', dst_ip: '', src_ip: '', dst_mac: '',
protocol: 'udp', src_port: 50000, dst_port: 5001,
frame_size: 512, rate_val: 1000, rate_unit: 'pps', duration: 30,
dscp: 0, responder_url: '', continuous: false,
}
function ppsToDisplay(pps, frameSize) {
// Convert stored PPS to a friendlier unit if it was originally set that way
return { rate_val: pps, rate_unit: 'pps' }
}
const initData = props.editFlow
? { ...props.editFlow, continuous: props.editFlow.duration === 0, ...ppsToDisplay(props.editFlow.rate_pps, props.editFlow.frame_size) }
: {}
const form = reactive({ ...defaults, ...initData })
function onContinuousChange() { if (form.continuous) form.duration = 0 }
function computePps(val, unit, frameSize) {
if (unit === 'kbps') return Math.max(1, Math.round((val * 1000) / (frameSize * 8)))
if (unit === 'mbps') return Math.max(1, Math.round((val * 1_000_000) / (frameSize * 8)))
return Math.round(val)
}
async function submit() {
try {
const payload = { ...form }
payload.rate_pps = computePps(form.rate_val, form.rate_unit, form.frame_size)
delete payload.rate_val
delete payload.rate_unit
delete payload.continuous
if (form.continuous) payload.duration = 0
if (!payload.src_ip) delete payload.src_ip
if (!payload.dst_mac) delete payload.dst_mac
if (!payload.responder_url) delete payload.responder_url
if (!payload.name) payload.name = `${payload.protocol.toUpperCase()} -> ${payload.dst_ip}`
if (editing.value) {
await api.updateFlow(props.editFlow.id, payload)
emit('updated')
} else {
await api.createFlow(payload)
Object.assign(form, defaults)
emit('created')
}
} catch (e) {
alert('Error: ' + e.message)
}
}
</script>
<style scoped>
.flow-builder { padding: 0; }
h3 { font-size: 15px; margin-bottom: 12px; color: var(--accent); }
.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; }
.form-actions { display: flex; gap: 8px; margin-top: 14px; }
.btn { padding: 8px 16px; font-weight: 600; font-size: 13px; }
.btn-accent { background: var(--accent); color: #fff; }
.btn-accent:hover { opacity: 0.9; }
.btn-accent:disabled { opacity: 0.4; }
.btn-muted { background: var(--border); color: var(--text); }
.rate-unit-standalone { width: 100%; margin-top: 4px; }
.checkbox-inline { display: inline-flex !important; align-items: center; gap: 4px; margin-top: 4px; font-size: 12px; cursor: pointer; }
.checkbox-inline input { width: auto; }
</style>