diff --git a/acs-admin/src/components/EdgeManager/Devices/ISA95HierarchyPanel.vue b/acs-admin/src/components/EdgeManager/Devices/ISA95HierarchyPanel.vue new file mode 100644 index 000000000..1633be8f7 --- /dev/null +++ b/acs-admin/src/components/EdgeManager/Devices/ISA95HierarchyPanel.vue @@ -0,0 +1,164 @@ + + + + + diff --git a/acs-admin/src/components/EdgeManager/Devices/OriginMapEditor/OriginMapEditor.vue b/acs-admin/src/components/EdgeManager/Devices/OriginMapEditor/OriginMapEditor.vue index 0587116c4..713ed1763 100644 --- a/acs-admin/src/components/EdgeManager/Devices/OriginMapEditor/OriginMapEditor.vue +++ b/acs-admin/src/components/EdgeManager/Devices/OriginMapEditor/OriginMapEditor.vue @@ -158,6 +158,7 @@ import { useConnectionStore } from '@store/useConnectionStore.js' import _ from 'lodash' import NewObjectOverlayForm from './NewObjectOverlayForm.vue' import { updateEdgeAgentConfig } from '@/utils/edgeAgentConfigUpdater' +import { ISA95_LEVELS } from '@/store/useISA95Store.js' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { generateCsv, downloadCsv, parseCsv, applyCsvToModel } from '@/composables/useOriginMapCsv.js' import Papa from 'papaparse' @@ -275,11 +276,14 @@ export default { immediate: true, handler(newOriginMap) { console.debug('Origin map changed:', newOriginMap) - // If the origin map is null or undefined, reset our model if (newOriginMap === null || newOriginMap === undefined) { console.debug('Resetting origin map model') this.resetModel() - // Trigger a re-render of the schema group + this.groupRerenderTrigger = +new Date() + } else if (!this.isDirty) { + // Accept external updates (e.g. from the ISA-95 sidebar panel) + // when there are no unsaved local changes in the editor. + this.model = newOriginMap this.groupRerenderTrigger = +new Date() } } @@ -888,6 +892,46 @@ export default { this.isDirty = true }, + /* Called by Device.vue when the ISA-95 sidebar panel selects a value. + * Applies the mutation directly to this.model (the object that gets + * saved) rather than relying on device.deviceInformation.originMap, + * which may be a different reference after a Pinia store refresh. + * Also clears all lower hierarchy levels and triggers a tree re-render. */ + applyISA95Selection ({ level, value, level_index }) { + if (!this.model.Device_Information) this.model.Device_Information = {} + if (!this.model.Device_Information.ISA95_Hierarchy) this.model.Device_Information.ISA95_Hierarchy = {} + + const hierarchy = this.model.Device_Information.ISA95_Hierarchy + + if (!hierarchy[level]) hierarchy[level] = {} + hierarchy[level].Value = value + if (!hierarchy[level].Sparkplug_Type) + hierarchy[level].Sparkplug_Type = 'String' + + /* Clear all levels below the one just set. + * Use null rather than delete: the save uses a JSON merge patch, where + * absent keys are preserved (not deleted). Setting Value to null is the + * RFC 7396 signal that tells ConfigDB to delete that key. */ + for (const lower of ISA95_LEVELS.slice(level_index + 1)) { + if (hierarchy[lower] !== undefined) { + if (!hierarchy[lower]) hierarchy[lower] = {} + hierarchy[lower].Value = null + } + } + + /* For fresh devices that have no originMap yet, wire device.deviceInformation.originMap + * to this.model so the ISA-95 panel's display (which reads from the device prop) + * stays in sync with what we will actually save. Once the user saves, Pinia + * refreshes the store with the real saved reference, and the two stay aligned + * via the existing originMap watcher. */ + if (this.device?.deviceInformation && !this.device.deviceInformation.originMap) { + this.device.deviceInformation.originMap = this.model + } + + this.isDirty = true + this.groupRerenderTrigger = +new Date() + }, + resetModel() { // Reset the model to an empty object this.model = {} @@ -1066,7 +1110,7 @@ export default { }, applyParsedCsv () { - if (!this.csvParsedData) return { applied: 0, skipped: 0 } + if (!this.csvParsedData) return { applied: 0, skipped: 0, ignored: 0 } // Pass the schema to applyCsvToModel so missing metrics can be created const result = applyCsvToModel(this.csvParsedData, this.model, this.schema, this.set) @@ -1089,7 +1133,12 @@ export default { }) } - toast.warning("Test message", { pauseOnHover: true }) + if (result.ignored > 0) { + toast.warning(`${result.ignored} ISA-95 row${result.ignored === 1 ? '' : 's'} ignored`, { + description: 'Set the hierarchy in the ISA-95 Hierarchy panel.', + pauseOnHover: true, + }) + } this.markDirty() this.groupRerenderTrigger = +new Date() diff --git a/acs-admin/src/components/EdgeManager/Devices/OriginMapEditor/SchemaGroup.vue b/acs-admin/src/components/EdgeManager/Devices/OriginMapEditor/SchemaGroup.vue index 3d869d166..ef71273fe 100644 --- a/acs-admin/src/components/EdgeManager/Devices/OriginMapEditor/SchemaGroup.vue +++ b/acs-admin/src/components/EdgeManager/Devices/OriginMapEditor/SchemaGroup.vue @@ -116,7 +116,6 @@ import { SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar' import { ChevronRight } from 'lucide-vue-next' import { Button } from '@/components/ui/button' import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs' - export default { name: 'SchemaGroup', @@ -903,9 +902,11 @@ export default { computed: { filteredKeys() { // Get all keys that aren't reserved words in the order they appear in the schema - // We use Object.keys to get the keys in the order they were defined in the schema + // We use Object.keys to get the keys in the order they were defined in the schema. + // ISA95_Hierarchy is always hidden from the tree — it is managed exclusively via + // the dedicated ISA-95 Hierarchy panel in the device sidebar. const allKeys = Object.keys(this.schema.properties).filter(e => - !['patternProperties', '$meta', 'Schema_UUID', 'Instance_UUID', 'required'].includes(e) + !['patternProperties', '$meta', 'Schema_UUID', 'Instance_UUID', 'required', 'ISA95_Hierarchy'].includes(e) ); // If not filtering, return all keys in their original order diff --git a/acs-admin/src/components/EdgeManager/Devices/OriginMapEditor/SparkplugMetric.vue b/acs-admin/src/components/EdgeManager/Devices/OriginMapEditor/SparkplugMetric.vue index 2eecc9e07..4bd752ef8 100644 --- a/acs-admin/src/components/EdgeManager/Devices/OriginMapEditor/SparkplugMetric.vue +++ b/acs-admin/src/components/EdgeManager/Devices/OriginMapEditor/SparkplugMetric.vue @@ -24,7 +24,34 @@ - + + + + + + + + + No results + + {{ opt.name }} + + + +