Files
adam-acs/inertia/pages/configurations/ConfigurationEdit.vue
T
kevin.lhereec 8cddf98093
Build & Deploy / build (push) Successful in 30s
Build & Deploy / docker (push) Successful in 1m6s
Build & Deploy / deploy (push) Failing after 33s
fix: CSRF token sur les requetes mutantes /internal + IPv6 Docker + tests Connection Request
- 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)
2026-07-17 10:16:29 +02:00

206 lines
9.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>