From 3394abd5eca82bbb8b52a159539d5263f6c2f700 Mon Sep 17 00:00:00 2001 From: Kevin L'Hereec Date: Mon, 1 Jun 2026 11:32:26 +0200 Subject: [PATCH] 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> --- inertia/components/AutocompleteInput.vue | 79 ++++++ .../configuration/ComplianceBadge.vue | 11 +- .../configuration/CriteriaEditor.vue | 165 ++++++++----- inertia/locales/en.json | 73 +++++- inertia/locales/fr.json | 73 +++++- inertia/pages/DeviceDetail.vue | 187 ++++++++++---- inertia/pages/Home.vue | 31 ++- .../configurations/ConfigurationDetail.vue | 229 +++++++++++++++++- .../configurations/ConfigurationEdit.vue | 42 +++- .../configurations/ConfigurationsList.vue | 89 ++++--- 10 files changed, 797 insertions(+), 182 deletions(-) create mode 100644 inertia/components/AutocompleteInput.vue diff --git a/inertia/components/AutocompleteInput.vue b/inertia/components/AutocompleteInput.vue new file mode 100644 index 0000000..7e1bfb5 --- /dev/null +++ b/inertia/components/AutocompleteInput.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/inertia/components/configuration/ComplianceBadge.vue b/inertia/components/configuration/ComplianceBadge.vue index e7e6836..74bad2c 100644 --- a/inertia/components/configuration/ComplianceBadge.vue +++ b/inertia/components/configuration/ComplianceBadge.vue @@ -9,15 +9,16 @@ import { computed } from 'vue' interface Props { - status: 'compliant' | 'in_progress' | 'failed' + status: 'pending' | 'compliant' | 'in_progress' | 'failed' } const props = withDefaults(defineProps(), { - status: 'in_progress', + status: 'pending', }) const statusLabel = computed(() => { const labels: Record = { + pending: 'Pending', compliant: 'Compliant', in_progress: 'In progress', failed: 'Failed', @@ -27,6 +28,7 @@ const statusLabel = computed(() => { const iconName = computed(() => { const icons: Record = { + pending: 'clock', compliant: 'check-circle', in_progress: 'loader', failed: 'alert-circle', @@ -47,6 +49,11 @@ const iconName = computed(() => { white-space: nowrap; } +.status-pending { + background-color: #e9ecef; + color: #495057; +} + .status-compliant { background-color: #e6f4ea; color: #0d652d; diff --git a/inertia/components/configuration/CriteriaEditor.vue b/inertia/components/configuration/CriteriaEditor.vue index 5732b4f..fd3ffff 100644 --- a/inertia/components/configuration/CriteriaEditor.vue +++ b/inertia/components/configuration/CriteriaEditor.vue @@ -1,5 +1,7 @@ - diff --git a/inertia/locales/en.json b/inertia/locales/en.json index a4f0b08..d6ed08b 100644 --- a/inertia/locales/en.json +++ b/inertia/locales/en.json @@ -77,7 +77,8 @@ "firmwareUrl": "Firmware URL", "checksum": "Checksum", "filterByProductClass": "Filter by product class…", - "saveChanges": "Save changes" + "saveChanges": "Save changes", + "hardwareVersion": "Hardware version" }, "button": { "refresh": "Refresh", @@ -121,5 +122,73 @@ "language": { "fr": "Français", "en": "English" + }, + "criteriaEditor": { + "deviceId": "Device Identity", + "standardParams": "Standard Parameters", + "customCriteria": "Custom Conditions", + "customCriteriaHelp": "Match devices where known parameter values equal the expected values", + "addCondition": "+ Add condition", + "noConditions": "No parameter conditions — matches all devices passing DeviceId criteria above.", + "paramCriteriaWarning": "If a parameter is not yet known on the device, this configuration will not match. A lower-priority configuration without this condition should run first to discover the parameter via its inventory. Avoid using the same path in both criteria and desired parameters — this risks a deadlock.", + "knownValue": "known value", + "knownValues": "known values", + "fields": { + "manufacturer": { + "label": "Manufacturer", + "placeholder": "e.g. Sagemcom or Sagemcom*", + "help": "Exact match or glob pattern (* ?)" + }, + "oui": { + "label": "OUI", + "placeholder": "e.g. 00E0FC or 00*", + "help": "Organizationally Unique Identifier" + }, + "productClass": { + "label": "Product Class", + "placeholder": "e.g. BM632w or BM*", + "help": "Supports glob patterns" + }, + "serialNumber": { + "label": "Serial Number", + "placeholder": "e.g. ABC12345 or ABC*", + "help": "Supports glob patterns" + } + }, + "presets": { + "modelName": "Model Name", + "hardwareVersion": "Hardware Version", + "softwareVersion": "Software Version", + "modelNamePlaceholder": "e.g. BM632w", + "hardwareVersionPlaceholder": "e.g. V3R1", + "softwareVersionPlaceholder": "e.g. 1.2.3" + } + }, + "configDetail": { + "tabGeneral": "General", + "tabCriteria": "Criteria", + "tabInventory": "Inventory", + "tabParameters": "Parameters", + "tabCompliance": "Compliance", + "tabDryRun": "Dry Run", + "tabHistory": "History", + "totalDevices": "Matching devices", + "compliant": "Compliant", + "nonCompliant": "Non-compliant", + "noMatchingDevices": "No devices match this configuration criteria.", + "loadCompliance": "Load compliance report", + "complianceError": "Error loading compliance report.", + "diffs": "Diffs", + "dryRunDesc": "Simulate applying this configuration to a device without making any real changes.", + "dryRunPlaceholder": "e.g. AABBCC-Device-123", + "runDryRun": "Run", + "dryRunError": "Error during dry run.", + "firmwareSkipped": "firmware outside schedule window", + "paramDiffs": "Parameter differences", + "currentValue": "Current value", + "targetValue": "Target value", + "notSet": "(not set)", + "noParamDiffs": "No parameter differences.", + "firmwarePath": "Firmware migration path" } -} +} \ No newline at end of file diff --git a/inertia/locales/fr.json b/inertia/locales/fr.json index 4909aea..4a169a2 100644 --- a/inertia/locales/fr.json +++ b/inertia/locales/fr.json @@ -77,7 +77,8 @@ "firmwareUrl": "URL du firmware", "checksum": "Checksum", "filterByProductClass": "Filtrer par classe produit…", - "saveChanges": "Enregistrer" + "saveChanges": "Enregistrer", + "hardwareVersion": "Version matérielle" }, "button": { "refresh": "Rafraîchir", @@ -121,5 +122,73 @@ "language": { "fr": "Français", "en": "English" + }, + "criteriaEditor": { + "deviceId": "Identifiant équipement", + "standardParams": "Paramètres standards", + "customCriteria": "Conditions personnalisées", + "customCriteriaHelp": "Correspond aux équipements dont les valeurs de paramètres connus sont égales aux valeurs attendues", + "addCondition": "+ Ajouter une condition", + "noConditions": "Aucune condition — correspond à tous les équipements passant les critères d'identifiant ci-dessus.", + "paramCriteriaWarning": "Si un paramètre n'est pas encore connu sur l'équipement, cette configuration ne correspondra pas. Une configuration de priorité inférieure sans cette condition doit s'exécuter en premier pour découvrir le paramètre via son inventaire. Évitez d'utiliser le même chemin dans les critères et les paramètres souhaités — cela risque un blocage.", + "knownValue": "valeur connue", + "knownValues": "valeurs connues", + "fields": { + "manufacturer": { + "label": "Fabricant", + "placeholder": "ex. Sagemcom ou Sagemcom*", + "help": "Correspondance exacte ou motif glob (* ?)" + }, + "oui": { + "label": "OUI", + "placeholder": "ex. 00E0FC ou 00*", + "help": "Identifiant Unique Organisationnel" + }, + "productClass": { + "label": "Classe produit", + "placeholder": "ex. BM632w ou BM*", + "help": "Supporte les motifs glob" + }, + "serialNumber": { + "label": "Numéro de série", + "placeholder": "ex. ABC12345 ou ABC*", + "help": "Supporte les motifs glob" + } + }, + "presets": { + "modelName": "Nom du modèle", + "hardwareVersion": "Version matérielle", + "softwareVersion": "Version logicielle", + "modelNamePlaceholder": "ex. BM632w", + "hardwareVersionPlaceholder": "ex. V3R1", + "softwareVersionPlaceholder": "ex. 1.2.3" + } + }, + "configDetail": { + "tabGeneral": "Général", + "tabCriteria": "Critères", + "tabInventory": "Inventaire", + "tabParameters": "Paramètres", + "tabCompliance": "Conformité", + "tabDryRun": "Dry Run", + "tabHistory": "Historique", + "totalDevices": "Équipements correspondants", + "compliant": "Conforme", + "nonCompliant": "Non conforme", + "noMatchingDevices": "Aucun équipement ne correspond aux critères.", + "loadCompliance": "Charger le rapport de conformité", + "complianceError": "Erreur lors du chargement de la conformité.", + "diffs": "Différences", + "dryRunDesc": "Simulez l'application de cette configuration sur un équipement sans effectuer de modification réelle.", + "dryRunPlaceholder": "ex: AABBCC-Device-123", + "runDryRun": "Lancer", + "dryRunError": "Erreur lors du dry run.", + "firmwareSkipped": "firmware hors plage horaire", + "paramDiffs": "Différences de paramètres", + "currentValue": "Valeur actuelle", + "targetValue": "Valeur cible", + "notSet": "(non défini)", + "noParamDiffs": "Aucune différence de paramètres.", + "firmwarePath": "Chemin de migration firmware" } -} +} \ No newline at end of file diff --git a/inertia/pages/DeviceDetail.vue b/inertia/pages/DeviceDetail.vue index 3d36053..813624a 100644 --- a/inertia/pages/DeviceDetail.vue +++ b/inertia/pages/DeviceDetail.vue @@ -13,12 +13,19 @@ const { device, modelPaths, commands } = defineProps<{ }>() const isLoading = ref(false) +const showDeleteConfirm = ref(false) + +function deleteDevice() { + router.delete(`/devices/${encodeURIComponent(device.identifier)}`, { + onSuccess: () => router.visit('/devices'), + }) +} const showGetModelPopup = ref(false) const getModelInput = ref('') const activeSection = ref('identifiers') // Collapsible sections -const openSections = ref>(new Set(['identifiers', 'properties', 'parameters', 'commands'])) +const openSections = ref>(new Set(['identifiers', 'commands'])) function toggleSection(section: string) { if (openSections.value.has(section)) { @@ -82,7 +89,7 @@ function formatName(name: unknown): string { function getPropertyValue(name: string): string { const prop = device.properties?.find((p: any) => p.name === name) - return prop?.value ?? '—' + return prop?.value ?? '' } @@ -100,7 +107,7 @@ function getPropertyValue(name: string): string {