- Ajout de fetchWithCsrf() helper qui lit le cookie XSRF-TOKEN et injecte X-XSRF-TOKEN sur toutes les requetes POST/PUT/PATCH/DELETE vers /internal/* - Remplacement de fetch() par fetchWithCsrf() dans tous les composants Vue concernes (Tr069Trace, DeviceDetail, SequenceViewer, Home, configurations, firmware_catalog) - Activation IPv6 sur les reseaux Docker (enable_ipv6: true, subnet fd00:adam::/64) dans docker-compose.yml et docker-compose.prod.yml - Ajout tests unitaires DeviceService.sendWakeOnWlan : IPv4, IPv6, ENETUNREACH, credentials fallback chain (11 tests)
206 lines
9.6 KiB
Vue
206 lines
9.6 KiB
Vue
<script setup lang="ts">
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import { router } from '@inertiajs/vue3'
|
||
import { useI18n } from 'vue-i18n'
|
||
import TitleBar from '~/components/TitleBar.vue'
|
||
import TagsInput from '~/components/configuration/TagsInput.vue'
|
||
import CriteriaEditor from '~/components/configuration/CriteriaEditor.vue'
|
||
import { fetchWithCsrf } from '~/helpers/fetch_with_csrf'
|
||
|
||
const { t } = useI18n()
|
||
|
||
const props = defineProps<{ id: string }>()
|
||
|
||
const configuration = ref<any>(null)
|
||
const errorMsg = ref('')
|
||
const form = ref<any>({
|
||
name: '', bumpType: 'patch', priority: 1, policy: 'maintain',
|
||
enabled: true, tags: [], criteria: {}, inventory: [], desiredParameters: [],
|
||
})
|
||
|
||
onMounted(loadConfiguration)
|
||
|
||
async function loadConfiguration() {
|
||
try {
|
||
const res = await fetch(`/internal/configurations/${props.id}`)
|
||
const data = await res.json()
|
||
const d = data.data ?? data
|
||
configuration.value = d
|
||
form.value = {
|
||
name: d.name, bumpType: 'patch', priority: d.priority, policy: d.policy,
|
||
enabled: d.enabled, tags: d.tags || [], criteria: d.criteria || {},
|
||
inventory: d.inventory || [], desiredParameters: d.desiredParameters || [],
|
||
}
|
||
} catch {
|
||
router.get('/configurations')
|
||
}
|
||
}
|
||
|
||
function addInventoryPath() { form.value.inventory.push({ path: '' }) }
|
||
|
||
const nextVersion = computed(() => {
|
||
if (!configuration.value?.version) return ''
|
||
const [maj, min, pat] = configuration.value.version.split('.').map(Number)
|
||
if (form.value.bumpType === 'major') return `→ ${maj + 1}.0.0`
|
||
if (form.value.bumpType === 'minor') return `→ ${maj}.${min + 1}.0`
|
||
return `→ ${maj}.${min}.${pat + 1}`
|
||
})
|
||
function removeInventoryPath(i: number) { form.value.inventory.splice(i, 1) }
|
||
function addParameter() { form.value.desiredParameters.push({ name: '', value: '', type: 'string' }) }
|
||
function removeParameter(i: number) { form.value.desiredParameters.splice(i, 1) }
|
||
|
||
async function submitForm() {
|
||
errorMsg.value = ''
|
||
try {
|
||
const res = await fetchWithCsrf(`/internal/configurations/${props.id}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(form.value),
|
||
})
|
||
if (res.ok) router.get('/configurations')
|
||
else { const e = await res.json(); errorMsg.value = e.message || 'Error' }
|
||
} catch { errorMsg.value = 'Failed to update configuration' }
|
||
}
|
||
|
||
function goBack() { router.get('/configurations') }
|
||
</script>
|
||
|
||
<template>
|
||
<TitleBar section="configurations" :subtitle="configuration ? `Edit: ${configuration.name}` : 'Edit'" />
|
||
|
||
<div class="container-xxl py-4">
|
||
<div class="row">
|
||
<div class="col-12">
|
||
|
||
<div v-if="errorMsg" class="alert alert-danger mb-3">{{ errorMsg }}</div>
|
||
|
||
<form v-if="configuration" @submit.prevent="submitForm">
|
||
|
||
<!-- General Info -->
|
||
<div class="card border-0 shadow-sm mb-4">
|
||
<div class="card-header"><h2 class="h6 mb-0">{{ t('labels.general') }}</h2></div>
|
||
<div class="card-body">
|
||
<div class="row g-3">
|
||
<div class="col-md-6">
|
||
<label class="form-label">{{ t('labels.identifier') }}</label>
|
||
<input :value="configuration.identifier" type="text" class="form-control font-monospace" disabled />
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label">{{ t('labels.name') }} <span class="text-danger">*</span></label>
|
||
<input v-model="form.name" type="text" class="form-control" required />
|
||
</div>
|
||
<div class="col-md-3">
|
||
<label class="form-label">{{ t('labels.version') }}</label>
|
||
<input :value="configuration.version" type="text" class="form-control font-monospace" disabled />
|
||
</div>
|
||
<div class="col-md-3">
|
||
<label class="form-label">Type de changement <span class="text-danger">*</span></label>
|
||
<div class="d-flex gap-2 flex-wrap mt-1">
|
||
<div class="form-check">
|
||
<input v-model="form.bumpType" class="form-check-input" type="radio" value="patch" id="bump-patch" />
|
||
<label class="form-check-label small" for="bump-patch">
|
||
<span class="badge bg-secondary">patch</span> fix
|
||
</label>
|
||
</div>
|
||
<div class="form-check">
|
||
<input v-model="form.bumpType" class="form-check-input" type="radio" value="minor" id="bump-minor" />
|
||
<label class="form-check-label small" for="bump-minor">
|
||
<span class="badge bg-info text-dark">minor</span> feature
|
||
</label>
|
||
</div>
|
||
<div class="form-check">
|
||
<input v-model="form.bumpType" class="form-check-input" type="radio" value="major" id="bump-major" />
|
||
<label class="form-check-label small" for="bump-major">
|
||
<span class="badge bg-warning text-dark">major</span> breaking
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div class="form-text">{{ nextVersion }}</div>
|
||
</div>
|
||
<div class="col-md-3">
|
||
<label class="form-label">{{ t('labels.priority') }} <span class="text-danger">*</span></label>
|
||
<input v-model.number="form.priority" type="number" class="form-control" min="1" required />
|
||
</div>
|
||
<div class="col-md-3">
|
||
<label class="form-label">{{ t('labels.policy') }} <span class="text-danger">*</span></label>
|
||
<select v-model="form.policy" class="form-select" required>
|
||
<option value="maintain">{{ t('labels.maintain') }}</option>
|
||
<option value="once">{{ t('labels.once') }}</option>
|
||
</select>
|
||
</div>
|
||
<div class="col-md-3 d-flex align-items-end">
|
||
<div class="form-check form-switch mb-2">
|
||
<input v-model="form.enabled" type="checkbox" class="form-check-input" id="enabled" role="switch" />
|
||
<label class="form-check-label" for="enabled">{{ t('labels.enabled') }}</label>
|
||
</div>
|
||
</div>
|
||
<div class="col-12">
|
||
<label class="form-label">{{ t('labels.tags') }}</label>
|
||
<TagsInput v-model="form.tags" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Criteria -->
|
||
<div class="card border-0 shadow-sm mb-4">
|
||
<div class="card-header"><h2 class="h6 mb-0">{{ t('labels.matchingCriteria') }}</h2></div>
|
||
<div class="card-body">
|
||
<CriteriaEditor v-model="form.criteria" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Inventory Paths -->
|
||
<div class="card border-0 shadow-sm mb-4">
|
||
<div class="card-header"><h2 class="h6 mb-0">{{ t('labels.inventoryPaths') }}</h2></div>
|
||
<div class="card-body">
|
||
<div v-for="(path, i) in form.inventory" :key="i" class="input-group mb-2">
|
||
<input v-model="path.path" type="text" class="form-control font-monospace" placeholder="InternetGatewayDevice.DeviceInfo." />
|
||
<button type="button" class="btn btn-outline-danger" @click="removeInventoryPath(i)">×</button>
|
||
</div>
|
||
<button type="button" class="btn btn-outline-secondary btn-sm mt-1" @click="addInventoryPath">+ {{ t('button.addModelPath') }}</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Desired Parameters -->
|
||
<div class="card border-0 shadow-sm mb-4">
|
||
<div class="card-header"><h2 class="h6 mb-0">{{ t('labels.desiredParameters') }}</h2></div>
|
||
<div class="card-body">
|
||
<div v-for="(param, i) in form.desiredParameters" :key="i" class="row g-2 mb-2 align-items-center">
|
||
<div class="col-md-5">
|
||
<input v-model="param.name" type="text" class="form-control form-control-sm font-monospace" placeholder="Parameter name" />
|
||
</div>
|
||
<div class="col-md-4">
|
||
<input v-model="param.value" type="text" class="form-control form-control-sm" placeholder="Value" />
|
||
</div>
|
||
<div class="col-md-2">
|
||
<select v-model="param.type" class="form-select form-select-sm">
|
||
<option value="string">string</option>
|
||
<option value="int">int</option>
|
||
<option value="unsignedInt">unsignedInt</option>
|
||
<option value="boolean">boolean</option>
|
||
<option value="dateTime">dateTime</option>
|
||
</select>
|
||
</div>
|
||
<div class="col-md-1 text-end">
|
||
<button type="button" class="btn btn-outline-danger btn-sm" @click="removeParameter(i)">×</button>
|
||
</div>
|
||
</div>
|
||
<button type="button" class="btn btn-outline-secondary btn-sm mt-1" @click="addParameter">+ {{ t('button.addParameter') }}</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Actions -->
|
||
<div class="d-flex gap-2 justify-content-end">
|
||
<button type="button" class="btn btn-outline-secondary" @click="goBack">{{ t('button.cancel') }}</button>
|
||
<button type="submit" class="btn btn-primary">{{ t('button.confirm') }}</button>
|
||
</div>
|
||
|
||
</form>
|
||
<div v-else class="text-center py-5 text-muted">Loading…</div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|