Files
adam-acs/inertia/components/configuration/CriteriaEditor.vue
T
Kevin L'HereecandCopilot 3394abd5ec feat(ui): compliance report, configuration list improvements
- DeviceDetail: display compliance status with badge + parameter diffs
- Home: show compliance badge in device list
- ConfigurationDetail: compliance report tab with device list + diffs
- ConfigurationEdit: bumpType selector (patch/minor/major), remove version field
- ConfigurationsList: tag filter, name search, compliance column
- ComplianceBadge: pending/compliant/in_progress/failed/non_compliant states
- CriteriaEditor: parameterCriteria editor with autocomplete on parameter names
- AutocompleteInput: new reusable autocomplete component
- i18n: compliance labels in fr/en

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-01 11:32:26 +02:00

209 lines
8.0 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, onMounted, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import AutocompleteInput from '~/components/AutocompleteInput.vue'
import type { ConfigurationCriteria, ParameterCriteria } from '#domain/configuration/configuration'
interface Props {
modelValue: ConfigurationCriteria
}
interface Emits {
(e: 'update:modelValue', value: ConfigurationCriteria): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const { t } = useI18n()
const allowedFields = ['manufacturer', 'oui', 'productClass', 'serialNumber'] as const
const suggestions = ref<Record<string, string[]>>({
manufacturer: [],
oui: [],
productClass: [],
serialNumber: [],
modelName: [],
hardwareVersion: [],
softwareVersion: [],
})
onMounted(async () => {
try {
const res = await fetch('/api/v1/devices/criteria-values')
if (res.ok) {
suggestions.value = await res.json()
}
} catch {
// silently fail
}
})
function updateCriteria(field: string, value: string) {
const updated = { ...props.modelValue }
if (value) {
updated[field as keyof ConfigurationCriteria] = value as any
} else {
delete updated[field as keyof ConfigurationCriteria]
}
emit('update:modelValue', updated)
}
// ── Parameter Criteria ──────────────────────────────────────────────────────
const presetParams: { labelKey: string; tr181: string; tr098: string; placeholderKey: string; suggestionsKey: string }[] = [
{ labelKey: 'criteriaEditor.presets.modelName', tr181: 'Device.DeviceInfo.ModelName', tr098: 'InternetGatewayDevice.DeviceInfo.ModelName', placeholderKey: 'criteriaEditor.presets.modelNamePlaceholder', suggestionsKey: 'modelName' },
{ labelKey: 'criteriaEditor.presets.hardwareVersion', tr181: 'Device.DeviceInfo.HardwareVersion', tr098: 'InternetGatewayDevice.DeviceInfo.HardwareVersion', placeholderKey: 'criteriaEditor.presets.hardwareVersionPlaceholder', suggestionsKey: 'hardwareVersion' },
{ labelKey: 'criteriaEditor.presets.softwareVersion', tr181: 'Device.DeviceInfo.SoftwareVersion', tr098: 'InternetGatewayDevice.DeviceInfo.SoftwareVersion', placeholderKey: 'criteriaEditor.presets.softwareVersionPlaceholder', suggestionsKey: 'softwareVersion' },
]
// Paths already covered by preset fields — excluded from custom conditions suggestions
const presetPaths = new Set(presetParams.flatMap((p) => [p.tr181, p.tr098]))
const customSuggestions = computed(() =>
(suggestions.value.parameterNames || []).filter((name) => !presetPaths.has(name))
)
function getPresetValue(preset: typeof presetParams[0]): string {
const list = props.modelValue.parameterCriteria || []
const entry = list.find((p) => p.name === preset.tr181 || p.name === preset.tr098)
return entry ? String(entry.value) : ''
}
function updatePresetParam(preset: typeof presetParams[0], value: string) {
const updated = { ...props.modelValue }
const list = [...(updated.parameterCriteria || [])]
const idx = list.findIndex((p) => p.name === preset.tr181 || p.name === preset.tr098)
if (value) {
if (idx >= 0) {
list[idx] = { name: preset.tr181, value }
} else {
list.push({ name: preset.tr181, value })
}
} else {
if (idx >= 0) list.splice(idx, 1)
}
updated.parameterCriteria = list.length > 0 ? list : undefined
emit('update:modelValue', updated)
}
const paramCriteria = computed<ParameterCriteria[]>(() => props.modelValue.parameterCriteria || [])
function addParamCriteria() {
const updated = { ...props.modelValue }
updated.parameterCriteria = [...(updated.parameterCriteria || []), { name: '', value: '' }]
emit('update:modelValue', updated)
}
function removeParamCriteria(index: number) {
const updated = { ...props.modelValue }
const list = [...(updated.parameterCriteria || [])]
list.splice(index, 1)
updated.parameterCriteria = list.length > 0 ? list : undefined
emit('update:modelValue', updated)
}
function updateParamCriteria(index: number, field: 'name' | 'value', value: string) {
const updated = { ...props.modelValue }
const list = [...(updated.parameterCriteria || [])]
list[index] = { ...list[index], [field]: value }
updated.parameterCriteria = list
emit('update:modelValue', updated)
}
function knownValuesLabel(count: number): string {
return count > 1 ? t('criteriaEditor.knownValues') : t('criteriaEditor.knownValue')
}
</script>
<template>
<!-- DeviceId criteria -->
<p class="fw-semibold small mb-2">{{ t('criteriaEditor.deviceId') }}</p>
<div class="row g-3">
<div v-for="field in allowedFields" :key="field" class="col-md-6">
<label :for="`criteria-${field}`" class="form-label">{{ t(`criteriaEditor.fields.${field}.label`) }}</label>
<AutocompleteInput
:id="`criteria-${field}`"
:model-value="modelValue[field] || ''"
:suggestions="suggestions[field]"
:placeholder="t(`criteriaEditor.fields.${field}.placeholder`)"
monospace
@update:model-value="updateCriteria(field, $event)"
/>
<div class="form-text">
{{ t(`criteriaEditor.fields.${field}.help`) }}
<span v-if="suggestions[field]?.length" class="text-muted">
· {{ suggestions[field].length }} {{ knownValuesLabel(suggestions[field].length) }}
</span>
</div>
</div>
</div>
<!-- Parameter Criteria — Preset fields -->
<div class="mt-4">
<p class="fw-semibold small mb-2">{{ t('criteriaEditor.standardParams') }}</p>
<div class="row g-3 mb-3">
<div v-for="preset in presetParams" :key="preset.tr181" class="col-md-6">
<label class="form-label">{{ t(preset.labelKey) }}</label>
<AutocompleteInput
:model-value="getPresetValue(preset)"
:suggestions="suggestions[preset.suggestionsKey]"
:placeholder="t(preset.placeholderKey)"
monospace
@update:model-value="updatePresetParam(preset, $event)"
/>
<div class="form-text text-muted" style="font-size:0.7rem">
{{ preset.tr181 }}
<span v-if="suggestions[preset.suggestionsKey]?.length" class="ms-1">
· {{ suggestions[preset.suggestionsKey].length }} {{ knownValuesLabel(suggestions[preset.suggestionsKey].length) }}
</span>
</div>
</div>
</div>
</div>
<!-- Parameter Criteria — conditions libres -->
<div class="mt-4">
<div class="d-flex align-items-center justify-content-between mb-2">
<div>
<span class="fw-semibold small">{{ t('criteriaEditor.customCriteria') }}</span>
<span class="form-text ms-2">{{ t('criteriaEditor.customCriteriaHelp') }}</span>
</div>
<button type="button" class="btn btn-outline-secondary btn-sm" @click="addParamCriteria">
{{ t('criteriaEditor.addCondition') }}
</button>
</div>
<div v-if="paramCriteria.length === 0" class="text-muted small fst-italic">
{{ t('criteriaEditor.noConditions') }}
</div>
<div v-for="(pc, idx) in paramCriteria" :key="idx" class="d-flex align-items-center gap-2 mb-2">
<div class="flex-grow-1">
<AutocompleteInput
:model-value="pc.name"
:suggestions="customSuggestions"
placeholder="e.g. Device.DeviceInfo.Manufacturer"
monospace
@update:model-value="updateParamCriteria(idx, 'name', $event)"
/>
</div>
<span class="text-muted small">=</span>
<input
type="text"
class="form-control form-control-sm font-monospace"
style="max-width: 160px"
:value="String(pc.value)"
placeholder="e.g. true"
@input="updateParamCriteria(idx, 'value', ($event.target as HTMLInputElement).value)"
/>
<button type="button" class="btn btn-outline-danger btn-sm" :title="t('button.delete')" @click="removeParamCriteria(idx)">
✕
</button>
</div>
<div v-if="paramCriteria.length > 0" class="form-text mt-1">
ℹ️ {{ t('criteriaEditor.paramCriteriaWarning') }}
</div>
</div>
</template>