127 lines
4.4 KiB
Vue
127 lines
4.4 KiB
Vue
|
|
<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 (pps)</label>
|
||
|
|
<input v-model.number="form.rate_pps" type="number" min="1" max="100000" />
|
||
|
|
</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" />
|
||
|
|
</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, watch } from 'vue'
|
||
|
|
import { api } from '../api.js'
|
||
|
|
|
||
|
|
const props = defineProps({ editFlow: Object })
|
||
|
|
const emit = defineEmits(['created', 'updated', 'cancel'])
|
||
|
|
|
||
|
|
const editing = !!props.editFlow
|
||
|
|
|
||
|
|
const defaults = {
|
||
|
|
name: '', dst_ip: '', src_ip: '', dst_mac: '',
|
||
|
|
protocol: 'udp', src_port: 50000, dst_port: 5001,
|
||
|
|
frame_size: 512, rate_pps: 1000, duration: 30,
|
||
|
|
dscp: 0, responder_url: '',
|
||
|
|
}
|
||
|
|
|
||
|
|
const form = reactive({ ...defaults, ...(props.editFlow || {}) })
|
||
|
|
|
||
|
|
async function submit() {
|
||
|
|
try {
|
||
|
|
const payload = { ...form }
|
||
|
|
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) {
|
||
|
|
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); }
|
||
|
|
</style>
|