diff --git a/ax-mm-hubconnector/src/main/java/com/adamos/hubconnector/controller/EventRulesApi.java b/ax-mm-hubconnector/src/main/java/com/adamos/hubconnector/controller/EventRulesApi.java index 1f8ca49..5555375 100644 --- a/ax-mm-hubconnector/src/main/java/com/adamos/hubconnector/controller/EventRulesApi.java +++ b/ax-mm-hubconnector/src/main/java/com/adamos/hubconnector/controller/EventRulesApi.java @@ -9,31 +9,49 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; -import com.adamos.hubconnector.model.events.EventDirection; +import com.adamos.hubconnector.model.events.EventMapping; import com.adamos.hubconnector.model.events.EventRules; import com.adamos.hubconnector.services.EventRulesService; @RestController public class EventRulesApi { - + @Autowired EventRulesService eventService; @PreAuthorize("hasRole('ROLE_ADAMOS_HUB_READ')") @RequestMapping(value = "/eventRules", method = RequestMethod.GET) public ResponseEntity getEventRulesFromHub() { - EventRules list = eventService.getEventRules(EventDirection.FROM_HUB); - if (list != null) { + EventRules list = eventService.getEventRules(); + if (list != null) { return new ResponseEntity(list, HttpStatus.OK); } - + return new ResponseEntity<>(HttpStatus.NOT_FOUND); } - + @PreAuthorize("hasRole('ROLE_ADAMOS_HUB_UPDATE')") @RequestMapping(value = "/eventRules", method = RequestMethod.PUT) public ResponseEntity updateEventRulesFromHub(@RequestBody EventRules eventRules) { eventService.storeEventRules(eventRules); return new ResponseEntity(eventRules, HttpStatus.OK); } + + @PreAuthorize("hasRole('ROLE_ADAMOS_HUB_READ')") + @RequestMapping(value = "/eventMapping", method = RequestMethod.GET) + public ResponseEntity getEventMappingFromHub() { + EventMapping[] list = eventService.getEventMapping(); + if (list != null) { + return new ResponseEntity(list, HttpStatus.OK); + } + + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + @PreAuthorize("hasRole('ROLE_ADAMOS_HUB_UPDATE')") + @RequestMapping(value = "/eventMapping", method = RequestMethod.PUT) + public ResponseEntity updateEventMappingFromHub(@RequestBody EventMapping[] mappings) { + eventService.updateEventMapping(mappings); + return new ResponseEntity(mappings, HttpStatus.OK); + } } diff --git a/ax-mm-hubconnector/src/main/java/com/adamos/hubconnector/model/events/EventMapping.java b/ax-mm-hubconnector/src/main/java/com/adamos/hubconnector/model/events/EventMapping.java new file mode 100644 index 0000000..ef08692 --- /dev/null +++ b/ax-mm-hubconnector/src/main/java/com/adamos/hubconnector/model/events/EventMapping.java @@ -0,0 +1,34 @@ +package com.adamos.hubconnector.model.events; + +import java.util.ArrayList; +import java.util.UUID; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class EventMapping { + + private String c8yEventType; + private ArrayList c8yDevices; + private String adamosEventType; + private ArrayList c8yFragments; + + private String id; + private String name; + private boolean enabled; + + public EventMapping() { + this.id = UUID.randomUUID().toString(); + this.name = ""; + this.enabled = false; + + this.c8yEventType = ""; + this.c8yDevices = new ArrayList<>(); + this.adamosEventType = ""; + this.c8yFragments = new ArrayList<>(); + } + +} diff --git a/ax-mm-hubconnector/src/main/java/com/adamos/hubconnector/services/EventRulesService.java b/ax-mm-hubconnector/src/main/java/com/adamos/hubconnector/services/EventRulesService.java index a05e8a4..e1f463b 100644 --- a/ax-mm-hubconnector/src/main/java/com/adamos/hubconnector/services/EventRulesService.java +++ b/ax-mm-hubconnector/src/main/java/com/adamos/hubconnector/services/EventRulesService.java @@ -3,7 +3,14 @@ import java.io.IOException; import java.net.URI; import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -13,17 +20,21 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.web.util.UriComponentsBuilder; +import org.svenson.util.InvalidPropertyPathException; +import org.svenson.util.JSONPathUtil; +import org.svenson.util.PropertyPathAccessException; import com.adamos.hubconnector.CustomProperties; import com.adamos.hubconnector.model.HubConnectorResponse; +import com.adamos.hubconnector.model.events.AdamosEventAttribute; import com.adamos.hubconnector.model.events.AdamosEventData; import com.adamos.hubconnector.model.events.AdamosEventProcessor; import com.adamos.hubconnector.model.events.EventDirection; +import com.adamos.hubconnector.model.events.EventMapping; import com.adamos.hubconnector.model.events.EventRule; import com.adamos.hubconnector.model.events.EventRules; import com.adamos.hubconnector.model.hub.EquipmentDTO; import com.cumulocity.microservice.subscription.service.MicroserviceSubscriptionsService; -import com.cumulocity.model.idtype.GId; import com.cumulocity.rest.representation.event.EventRepresentation; import com.cumulocity.rest.representation.inventory.ManagedObjectRepresentation; import com.cumulocity.sdk.client.Param; @@ -35,13 +46,13 @@ import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.joda.JodaModule; -import com.google.common.base.Strings; import com.jayway.jsonpath.DocumentContext; @Service public class EventRulesService { private static final ObjectMapper mapper = new ObjectMapper().registerModule(new JodaModule()); private static final Logger appLogger = LoggerFactory.getLogger(EventRulesService.class); + private static final JSONPathUtil util = new JSONPathUtil(); QueryParam revertParam = new QueryParam(new Param() { @Override @@ -65,15 +76,17 @@ public String getName() { @Autowired private HubService hubService; - - @Autowired - private MicroserviceSubscriptionsService service; - + @Autowired + private MicroserviceSubscriptionsService service; + @Value("${C8Y.tenant}") private String tenant; - public EventRulesService() {} - + private Map lastUpdateDatesCache = new HashMap<>(); + + public EventRulesService() { + } + /** * Inbound event processing (Hub -> C8Y) * @@ -87,7 +100,7 @@ public EventRulesService() {} public boolean consumeHubMessage(String message, DocumentContext jsonContext) throws JsonParseException, JsonMappingException, IOException { appLogger.debug("Consuming message: " + message); - EventRules rulesFromHub = getEventRules(EventDirection.FROM_HUB); + EventRules rulesFromHub = getEventRules(); for (EventRule rule : rulesFromHub.getRules()) { if (rule.doesMatch(message) && rule.isEnabled()) { AdamosEventProcessor adamosEventProcessor = (AdamosEventProcessor) rule.getEventProcessor(); @@ -99,76 +112,186 @@ public boolean consumeHubMessage(String message, DocumentContext jsonContext) return false; } - /** - * Outbound event processing - * TODO replace with Notification API 2.0 once available - */ @Scheduled(fixedRate = 60000) - public void consumeC8YEvent() { + public void pollEventMappings() { service.runForEachTenant(() -> { appLogger.info("Scheduled Hub event processing for tenant " + service.getTenant()); - Iterable events = eventApi.getEventsByFilter( - new EventFilter().byType("AdamosHubEvent").byFromDate(Date.from(Instant.now().minusSeconds(60)))) - .get(2000, revertParam).allPages(); - for (EventRepresentation e : events) { - GId source = e.getSource().getId(); - ManagedObjectRepresentation mo = inventoryApi.get(source); - if (mo.hasProperty(CustomProperties.HUB_DATA)) { - EquipmentDTO hubData = new HubConnectorResponse(mo, CustomProperties.HUB_DATA, - EquipmentDTO.class).getData(); - AdamosEventData eventData = e.get(AdamosEventData.class); - eventData.setTimestampCreated(e.getDateTime()); - eventData.setReferenceObjectType("adamos:masterdata:type:machine:1"); - eventData.setReferenceObjectId(hubData.getUuid()); - URI uriService = UriComponentsBuilder - .fromUriString(hubConnectorService.getGlobalSettings().getAdamosEventServiceEndpoint()) - .path("event").build().toUri(); - appLogger.info("Posting to " + uriService + ": " + eventData); - hubService.restToHub(uriService, HttpMethod.POST, eventData, AdamosEventData.class); - } else { - appLogger.warn( - "Cannot send event to Adamos Hub. Device " + source + " is not synchronized to Adamos Hub"); + + EventMapping[] mappings = getEventMapping(); + if (mappings == null || mappings.length == 0) { + return; + } + + final List hubDevices = cumulocityService + .getManagedObjectsByFragmentType("adamos_hub_data"); + + Stream mappingStream = Arrays.asList(mappings).stream(); + // remove deleted mapping entries from cache + for (String cachedId : lastUpdateDatesCache.keySet()) { + if (mappingStream.filter(m -> m.getId().equals(cachedId)).findFirst().orElse(null) == null) { + lastUpdateDatesCache.remove(cachedId); + } + } + + for (EventMapping mapping : mappings) { + if (mapping.isEnabled()) { + ArrayList relevantHubDeviceIds = mapping.getC8yDevices(); + // filter for c8y devices which were configured in the mapping via c8yDevices + List devicesFromMapping = (relevantHubDeviceIds == null + || relevantHubDeviceIds.isEmpty()) ? hubDevices + : hubDevices.stream() + .filter(d -> relevantHubDeviceIds.contains(d.getId().getValue())) + .collect(Collectors.toList()); + mapEvents(mapping, devicesFromMapping); } } }); } - - private String getTypeByDirection(EventDirection direction) { - switch (direction) { - case FROM_HUB: - return CustomProperties.HUB_EVENTRULES_FROM_HUB_OBJECT_TYPE; - case TO_HUB: - return CustomProperties.HUB_EVENTRULES_TO_HUB_OBJECT_TYPE; - default: - return ""; + + public void mapEvents(EventMapping mapping, List selectedDevices) { + appLogger.info("Starting Hub event mapping " + mapping.getName()); + Instant fromDate = lastUpdateDatesCache.containsKey(mapping.getId()) + ? lastUpdateDatesCache.get(mapping.getId()) + : Instant.now().minusSeconds(60); + + ArrayList allEvents = new ArrayList<>(); + for (ManagedObjectRepresentation device : selectedDevices) { + Iterable events = eventApi.getEventsByFilter( + new EventFilter().bySource(device.getId()).byType(mapping.getC8yEventType()) + .byFromCreationDate(Date.from(fromDate))) + .get(2000, revertParam).allPages(); + events.forEach(allEvents::add); + } + + List mappedEvents = allEvents.stream().map(e -> mapToAdamosEvent(e, mapping, selectedDevices)) + .collect(Collectors.toList()); + if (!mappedEvents.isEmpty()) { + // update cache with latest date of all events we fetched + // need to use time attribute from event as this is also used when compared to teh from date - otherwise we refetch the same events again + Date latestDate = allEvents.stream().map(e -> e.getCreationDateTime().toDate()).max(Date::compareTo).get(); + // add 1 ms to latest date to prevent latest event to be fetched twice + Instant nextFromDate = latestDate.toInstant().plusMillis(1); + lastUpdateDatesCache.put(mapping.getId(), nextFromDate); + + mappedEvents.forEach(e -> this.createAdamosEvent(e)); + appLogger.info("Hub event mapping " + mapping.getName() + " finished. Converted " + mappedEvents.size() + + " events."); + } else { + appLogger.info("Hub event mapping " + mapping.getName() + " finished with nothing to do"); + } + } + + private void createAdamosEvent(AdamosEventData eventData) { + URI uriService = UriComponentsBuilder + .fromUriString(hubConnectorService.getGlobalSettings().getAdamosEventServiceEndpoint()) + .path("event").build().toUri(); + appLogger.info("Posting to " + uriService + ": " + eventData); + hubService.restToHub(uriService, HttpMethod.POST, eventData, AdamosEventData.class); + } + + private AdamosEventData mapToAdamosEvent(EventRepresentation event, EventMapping mapping, + List selectedDevices) { + + // get the hub uuid from the source device of the event + ManagedObjectRepresentation device = selectedDevices.stream() + .filter(d -> d.getId().getValue().equals(event.getSource().getId().getValue())).findFirst().get(); + EquipmentDTO hubData = new HubConnectorResponse(device, CustomProperties.HUB_DATA, + EquipmentDTO.class).getData(); + String hubUuid = hubData.getUuid(); + + AdamosEventData eventData = new AdamosEventData(); + eventData.setTimestampCreated(event.getCreationDateTime()); + eventData.setEventCode(mapping.getAdamosEventType()); + eventData.setReferenceObjectType("adamos:masterdata:type:machine:1"); + eventData.setReferenceObjectId(hubUuid); + + if (!mapping.getC8yFragments().isEmpty()) { + List attributes = mapping.getC8yFragments().stream() + .map(fragment -> mapToAdamosEventAttribute(fragment, event)).flatMap(attrs -> attrs.stream()) + .collect(Collectors.toList()); + eventData.setAttributes(attributes.toArray(new AdamosEventAttribute[attributes.size()])); } + + return eventData; } - public EventRules getEventRules(EventDirection direction) { - String directionType = getTypeByDirection(direction); + private List mapToAdamosEventAttribute(String c8yPropertyPath, EventRepresentation event) { + List result = new ArrayList<>(); + try { + Object value = util.getPropertyPath(event, c8yPropertyPath); + if (value instanceof List) { + AdamosEventAttribute[] attributes = mapper.convertValue(value, AdamosEventAttribute[].class); + result.addAll(Arrays.asList(attributes)); - if (!Strings.isNullOrEmpty(directionType)) { - ManagedObjectRepresentation obj = cumulocityService.getManagedObjectByFragmentType(directionType); - if (obj != null && obj.hasProperty(directionType)) { - return mapper.convertValue(obj.getProperty(directionType), EventRules.class); + } else { + AdamosEventAttribute attribute = mapper.convertValue(value, AdamosEventAttribute.class); + if (attribute.getAttributeTypeId() == null || attribute.getValue() == null) { + throw new IllegalArgumentException("Expected attributeId and value to be nonnull."); + } + result.add(attribute); } + } catch (InvalidPropertyPathException ippe) { + appLogger.warn("Invalid path " + c8yPropertyPath + ". Skipping this attribute for event " + + event.getId().getValue()); + appLogger.debug("Invalid path error: " + ippe.getMessage()); + + } catch (PropertyPathAccessException ppae) { + appLogger.warn("Invalid value in path " + c8yPropertyPath + ". Skipping this attribute for event " + + event.getId().getValue()); + appLogger.debug("Invalid value in path error: " + ppae.getMessage()); + + } catch (IllegalArgumentException iae) { + appLogger.warn("Parsing failed for path " + c8yPropertyPath + ". Skipping this attribute for event " + + event.getId().getValue()); + appLogger.debug("Parsing error " + iae.getMessage()); + } + return result; + } + + public EventRules getEventRules() { + ManagedObjectRepresentation obj = cumulocityService + .getManagedObjectByFragmentType(CustomProperties.HUB_EVENTRULES_FROM_HUB_OBJECT_TYPE); + if (obj != null) { + return mapper.convertValue(obj.getProperty(CustomProperties.HUB_EVENTRULES_FROM_HUB_OBJECT_TYPE), + EventRules.class); + } + + return null; + } + public EventMapping[] getEventMapping() { + ManagedObjectRepresentation obj = cumulocityService + .getManagedObjectByFragmentType(CustomProperties.HUB_EVENTRULES_TO_HUB_OBJECT_TYPE); + if (obj != null) { + return mapper.convertValue(obj.getProperty(CustomProperties.HUB_EVENTRULES_TO_HUB_OBJECT_TYPE), + EventMapping[].class); + } return null; } public void storeEventRules(EventRules eventRules) { - String directionType = getTypeByDirection(eventRules.getDirection()); - - if (!Strings.isNullOrEmpty(directionType)) { - ManagedObjectRepresentation obj = cumulocityService.getManagedObjectByFragmentType(directionType); - if (obj != null && obj.hasProperty(directionType)) { - obj.setProperty(directionType, eventRules); - obj.setLastUpdatedDateTime(null); - service.runForTenant(tenant, () -> { - inventoryApi.update(obj); - }); - } + ManagedObjectRepresentation obj = cumulocityService + .getManagedObjectByFragmentType(CustomProperties.HUB_EVENTRULES_FROM_HUB_OBJECT_TYPE); + if (obj != null) { + obj.setProperty(CustomProperties.HUB_EVENTRULES_FROM_HUB_OBJECT_TYPE, eventRules); + obj.setLastUpdatedDateTime(null); + service.runForTenant(tenant, () -> { + inventoryApi.update(obj); + }); + } + + } + + public void updateEventMapping(EventMapping[] eventMapping) { + ManagedObjectRepresentation obj = cumulocityService + .getManagedObjectByFragmentType(CustomProperties.HUB_EVENTRULES_TO_HUB_OBJECT_TYPE); + if (obj != null) { + obj.setProperty(CustomProperties.HUB_EVENTRULES_TO_HUB_OBJECT_TYPE, eventMapping); + obj.setLastUpdatedDateTime(null); + service.runForTenant(tenant, () -> { + inventoryApi.update(obj); + }); } } @@ -177,7 +300,7 @@ public void initMappingRules() { cumulocityService.createManagedObjectIfNotExists(CustomProperties.HUB_EVENTRULES_FROM_HUB_OBJECT_TYPE, new EventRules(EventDirection.FROM_HUB)); cumulocityService.createManagedObjectIfNotExists(CustomProperties.HUB_EVENTRULES_TO_HUB_OBJECT_TYPE, - new EventRules(EventDirection.TO_HUB)); + new EventMapping[0]); } } diff --git a/ax-mm-hubconnector/src/main/java/com/adamos/hubconnector/services/MigrationService.java b/ax-mm-hubconnector/src/main/java/com/adamos/hubconnector/services/MigrationService.java index f12441f..1802d71 100644 --- a/ax-mm-hubconnector/src/main/java/com/adamos/hubconnector/services/MigrationService.java +++ b/ax-mm-hubconnector/src/main/java/com/adamos/hubconnector/services/MigrationService.java @@ -6,6 +6,7 @@ import com.adamos.hubconnector.CustomProperties; import com.adamos.hubconnector.model.HubConnectorGlobalSettings; +import com.adamos.hubconnector.model.events.EventMapping; import com.adamos.hubconnector.model.hub.EquipmentDTO; import com.adamos.hubconnector.model.hub.hierarchy.AreaDTO; import com.adamos.hubconnector.model.hub.hierarchy.SiteDTO; @@ -188,6 +189,15 @@ private void migrateToVersion_1_4_0() { options.add(optionEnvironment); cumulocityService.updateTenantOptions(options); + ManagedObjectRepresentation mo = cumulocityService + .getManagedObjectByFragmentType(CustomProperties.HUB_EVENTRULES_TO_HUB_OBJECT_TYPE); + if (mo != null) { + mo.setProperty(CustomProperties.HUB_EVENTRULES_TO_HUB_OBJECT_TYPE, new EventMapping[0]); + mo.setLastUpdatedDateTime(null); + cumulocityService.updateManagedObject(mo); + } + + MigrationService.migrationRunning = false; LOGGER.info("Migration to version 1.4.0 finished..."); } diff --git a/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/eventRules/event-rules-to-hub.component.html b/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/eventRules/event-rules-to-hub.component.html new file mode 100644 index 0000000..a16cb11 --- /dev/null +++ b/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/eventRules/event-rules-to-hub.component.html @@ -0,0 +1,130 @@ +{{ "Events to ADAMOS-Hub" | translate }} + + + + {{ "Add new mapping" | translate }} + + + + + + + Reload + + + +
+
+
+

+

No mappings found.

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
{{ "Name" | translate }}{{ "Cumulocity Event type" | translate }}{{ "Cumulocity devices" | translate }}{{ "ADAMOS Hub event code" | translate }}{{ "Cumulocity event fragments" | translate }}{{ "Move" | translate }}{{ "Action" | translate }}
+ + {{ + rule.name + }} + - + + {{ rule.c8yEventType }}{{ rule.c8yDevices.toString() }}{{ rule.adamosEventType }}{{ rule.c8yFragments.toString() }} + + + +
+
+
+ + +
+ + + + diff --git a/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/eventRules/event-rules-to-hub.component.ts b/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/eventRules/event-rules-to-hub.component.ts new file mode 100644 index 0000000..fa0899e --- /dev/null +++ b/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/eventRules/event-rules-to-hub.component.ts @@ -0,0 +1,154 @@ +import { Component, ViewChild } from "@angular/core"; +import { NewAdamosHubService } from "../shared/new-adamos-hub.service"; +import { cloneDeep } from "lodash-es"; +import { BsModalRef, BsModalService } from "ngx-bootstrap/modal"; +import { v4 as uuid } from "uuid"; + +export interface IEventMapping { + /** The Cumulocity event type that should be the source of the mapping */ + c8yEventType: string; + + /* Optionally select one or multiple devices for which the event should be mapped. If no device is selected, rule should apply to all devices */ + c8yDevices: string[]; + + /* The ADAMOS Hub event code (=type) */ + adamosEventType: string; + + /* list of fragments from the C8Y event that should be mapped 1:1 into the "attributes" array of the ADMOS Hub event */ + c8yFragments: string[]; + + name: string; + + id: string; + + enabled: boolean; +} + +export class EventMapping implements IEventMapping { + c8yEventType: string; + c8yDevices: string[] = []; + adamosEventType: string; + c8yFragments: string[] = [""]; + enabled = false; + name = ""; + id = uuid(); +} + +@Component({ + templateUrl: "./event-rules-to-hub.component.html", +}) +export class EventRulesToHubComponent { + response: IEventMapping[]; + rules: IEventMapping[]; + + selectedMapping: IEventMapping = null; + + @ViewChild("mappingModal") mappingModal: any; + modalRef: BsModalRef; + + isLoading = false; + hasChanges = false; + + constructor( + private bsModalService: BsModalService, + private adamosService: NewAdamosHubService + ) { + this.refresh(); + } + + async fetchRules() { + const response = await this.adamosService.getMappingRules(); + this.response = response; + this.rules = cloneDeep(response); + } + + onClickMoveUp(rule: IEventMapping) { + this.hasChanges = true; + this.array_move( + this.rules, + this.rules.indexOf(rule), + this.rules.indexOf(rule) - 1 + ); + } + + onClickMoveDown(rule: IEventMapping) { + this.hasChanges = true; + this.array_move( + this.rules, + this.rules.indexOf(rule), + this.rules.indexOf(rule) + 1 + ); + } + + onClickDelete(rule: IEventMapping) { + this.rules.splice(this.rules.indexOf(rule), 1); + this.hasChanges = true; + } + + onClickDuplicate(rule: IEventMapping) { + const duplicate = cloneDeep(rule); + duplicate.id = uuid(); + duplicate.name = `${duplicate.name} (Copy)`; + + this.rules.splice(this.rules.indexOf(rule) + 1, 0, duplicate); + this.hasChanges = true; + } + + private array_move(arr: unknown[], old_index: number, new_index: number) { + if (new_index >= arr.length) { + let k = new_index - arr.length + 1; + while (k--) { + arr.push(undefined); + } + } + arr.splice(new_index, 0, arr.splice(old_index, 1)[0]); + return arr; + } + + async refresh() { + this.isLoading = true; + this.fetchRules() + .then(() => (this.hasChanges = false)) + .finally(() => (this.isLoading = false)); + } + + onClickAdd(): void { + this.selectedMapping = new EventMapping(); + this.modalRef = this.bsModalService.show(this.mappingModal); + } + + onClickEdit(rule: IEventMapping): void { + this.selectedMapping = rule; + this.modalRef = this.bsModalService.show(this.mappingModal); + } + + onClickUndo(): void { + this.rules = cloneDeep(this.response); + this.hasChanges = false; + } + + onModalCancel() { + this.modalRef.hide(); + } + + onModalSave(mapping: IEventMapping) { + const oldRule = this.rules.find((r) => r.id === mapping.id); + if (oldRule) { + // if user saved in edit mode + this.rules[this.rules.indexOf(oldRule)] = mapping; + } else { + // if user saved in create mode + this.rules.push(mapping); + } + + this.modalRef.hide(); + this.hasChanges = true; + } + + async onSave() { + this.response = cloneDeep(this.rules); + await this.adamosService.updateMappingRules(this.response); + await this.fetchRules(); + this.hasChanges = false; + } +} diff --git a/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/eventRules/eventRules-list.component.html b/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/eventRules/eventRules-list.component.html index 852d50b..c03a783 100644 --- a/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/eventRules/eventRules-list.component.html +++ b/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/eventRules/eventRules-list.component.html @@ -1,4 +1,4 @@ -{{ title | translate}} +{{ 'Events from ADAMOS-Hub' | translate}} diff --git a/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/eventRules/eventRules-list.component.ts b/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/eventRules/eventRules-list.component.ts index 34d4fa6..ea5a89d 100644 --- a/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/eventRules/eventRules-list.component.ts +++ b/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/eventRules/eventRules-list.component.ts @@ -1,7 +1,6 @@ import { _ } from '@c8y/ngx-components'; import { Component, ViewChild } from '@angular/core'; import { AdamosHubService } from '../shared/adamosHub.service'; -import { ActivatedRoute } from '@angular/router'; import { BehaviorSubject, combineLatest } from 'rxjs'; import { IHubResponse } from '../shared/model/IHubResponse'; import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal'; @@ -30,21 +29,8 @@ export class EventRulesListComponent { selectedRule: IEventRule; private _tempCopyRule: IEventRule; - private getTitleByDirection(direction: String) { - switch (direction) { - case "fromAdamosHub": - return _("from ADAMOS-Hub"); - case "toAdamosHub": - return _("to ADAMOS-Hub"); - default: - return _("unknown"); - } - } - - constructor(private hubService: AdamosHubService, private route: ActivatedRoute, private bsModalService: BsModalService) { - this.direction = this.route.snapshot.paramMap.get('direction'); - this.title += " " + this.getTitleByDirection(this.direction); - + constructor(private hubService: AdamosHubService, private bsModalService: BsModalService) { + // _ annotation to mark this string as translatable string. this.informationText = _('Ooops! It seems that there is no device to display.'); // this.loadMappings(); diff --git a/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/eventRules/eventRules.module.ts b/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/eventRules/eventRules.module.ts index bb1ff2e..6151595 100644 --- a/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/eventRules/eventRules.module.ts +++ b/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/eventRules/eventRules.module.ts @@ -5,13 +5,15 @@ import { routes } from "./routes"; import { SharedModule } from "../shared/shared.module"; import { AdamosHubService } from "../shared/adamosHub.service"; import { EventRulesListComponent } from "./eventRules-list.component"; +import { EventRulesToHubComponent } from "./event-rules-to-hub.component"; +import { MappingModalComponent } from "./mapping-modal.component"; /* * Defines a module for the device-management. */ @NgModule({ imports: [CoreModule, FormsModule, SharedModule], - declarations: [EventRulesListComponent], + declarations: [EventRulesListComponent, EventRulesToHubComponent, MappingModalComponent], exports: [EventRulesListComponent], providers: [ AdamosHubService, diff --git a/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/eventRules/mapping-modal.component.html b/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/eventRules/mapping-modal.component.html new file mode 100644 index 0000000..8cd9d0f --- /dev/null +++ b/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/eventRules/mapping-modal.component.html @@ -0,0 +1,131 @@ + + + diff --git a/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/eventRules/mapping-modal.component.ts b/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/eventRules/mapping-modal.component.ts new file mode 100644 index 0000000..04e05b1 --- /dev/null +++ b/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/eventRules/mapping-modal.component.ts @@ -0,0 +1,122 @@ +import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { IManagedObject, InventoryService, IResultList } from "@c8y/client"; +import { isEmpty } from "lodash-es"; +import { Observable, pipe, UnaryFunction } from "rxjs"; +import { map } from "rxjs/operators"; +import { EventMapping } from "./event-rules-to-hub.component"; + +type UIMapping = { + c8yEventType: string; + c8yFragments: { value: string }[]; + c8yDevices: string[]; + enabled: boolean; + name: string; + adamosEventType: string; + id: string; +}; + +@Component({ + templateUrl: "./mapping-modal.component.html", + selector: "mapping-modal", +}) +export class MappingModalComponent { + ui: UIMapping; + @Input() set selectedMapping(value: EventMapping) { + this.ui = { + c8yEventType: value.c8yEventType, + c8yFragments: value.c8yFragments.map((f) => ({ value: f })), + c8yDevices: value.c8yDevices, + adamosEventType: value.adamosEventType, + enabled: value.enabled, + name: value.name, + id: value.id, + }; + } + + @Output() save = new EventEmitter(); + @Output() cancel = new EventEmitter(); + + devices: IResultList; + + filterPipe: UnaryFunction, Observable>; + pattern = ""; + + constructor(private inventory: InventoryService) { + this.inventory + .list({ + query: "$filter=has('c8y_IsDevice') and has('adamos_hub_isDevice')", + pageSize: 100, + withTotalPages: true, + }) + .then((result) => this.devices = result); + } + + setPipe(filterStr: string) { + this.pattern = filterStr; + this.filterPipe = pipe( + map((data: []) => + data.filter( + (mo: IManagedObject) => + mo.name.toLowerCase().indexOf(filterStr.toLowerCase()) > -1 || + mo.id.indexOf(filterStr) > -1 + ) + ) + ); + } + + onChangeCheckbox(device: IManagedObject, checked: boolean): void { + const devices = this.ui.c8yDevices; + if (checked) { + devices.push(device.id); + } else { + this.ui.c8yDevices = devices.filter((d) => d !== device.id); + } + } + + onEditOKClick(): void { + const { + c8yEventType, + c8yFragments, + c8yDevices, + enabled, + name, + id, + adamosEventType, + } = this.ui; + this.save.emit({ + c8yEventType, + c8yFragments: c8yFragments.map((v) => v.value), + c8yDevices, + enabled, + name, + id, + adamosEventType, + }); + } + + onEditCancelClick(): void { + this.cancel.emit(); + } + + isChecked(device: IManagedObject): boolean { + return this.ui.c8yDevices.includes(device.id); + } + + removeFragment(fragment: { value: string }) { + const fragments = this.ui.c8yFragments; + this.ui.c8yFragments = fragments.filter((f) => f !== fragment); + } + + addFragment(): void { + this.ui.c8yFragments.push({ value: "" }); + } + + mandatoryFieldsFilled(): boolean { + const m = this.ui; + return ( + !isEmpty(m.name) && + !isEmpty(m.c8yEventType) && + !isEmpty(m.adamosEventType) + ); + } +} diff --git a/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/eventRules/routes.ts b/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/eventRules/routes.ts index db8bfcd..b992f2d 100644 --- a/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/eventRules/routes.ts +++ b/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/eventRules/routes.ts @@ -1,3 +1,4 @@ +import { EventRulesToHubComponent } from "./event-rules-to-hub.component"; import { EventRulesListComponent } from "./eventRules-list.component"; /* @@ -6,12 +7,11 @@ import { EventRulesListComponent } from "./eventRules-list.component"; export const routes = [ { - path: 'hub/eventRules', - redirectTo: 'hub/eventRules/fromAdamosHub', - pathMatch: 'full' + path: 'hub/event-rules/from-hub', + component: EventRulesListComponent }, { - path: 'hub/eventRules/:direction', - component: EventRulesListComponent + path: 'hub/event-rules/to-hub', + component: EventRulesToHubComponent } ]; \ No newline at end of file diff --git a/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/factories/Navigation.ts b/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/factories/Navigation.ts index 967c4d4..3b8f1cc 100644 --- a/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/factories/Navigation.ts +++ b/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/factories/Navigation.ts @@ -32,16 +32,24 @@ export class HubConnectorNavigationFactory implements NavigatorNodeFactory { new NavigatorNode({ label: _("Events from Hub"), icon: "c8y-icon c8y-icon-events", - path: "/hub/eventRules", + path: "/hub/event-rules/from-hub", routerLinkExact: true, priority: 99, }), + new NavigatorNode({ + label: _("Events to Hub"), + icon: "c8y-icon c8y-icon-events", + path: "/hub/event-rules/to-hub", + routerLinkExact: true, + priority: 98, + }), + new NavigatorNode({ label: _("Settings"), icon: "c8y-icon c8y-icon-administration", path: "/hub/settings", - priority: 98, + priority: 97, }), ]; diff --git a/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/shared/new-adamos-hub.service.ts b/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/shared/new-adamos-hub.service.ts index 47535c7..bcc7123 100644 --- a/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/shared/new-adamos-hub.service.ts +++ b/ax-ui-hubconnector-microfrontend/ax-ui-hubconnector/shared/new-adamos-hub.service.ts @@ -1,5 +1,8 @@ import { Injectable } from "@angular/core"; import { FetchClient, IManagedObject } from "@c8y/client"; +import { + IEventMapping, +} from "ax-ui-hubconnector/eventRules/event-rules-to-hub.component"; import { isArray } from "lodash-es"; import { AdamosHubDevice } from "./model/AdamosDevice"; @@ -104,4 +107,41 @@ export class NewAdamosHubService { const data = await response.json(); return data; } + + async getMappingRules(): Promise { + const url = `${this.hubUrl}/eventMapping`; + + const response = await this.fc.fetch(url, { + method: "GET", + headers: this.headers + }); + + if (response.status !== 200) { + const error = new Error(); + error.message = `Status not ok (${response.status})`; + throw error; + } + + const data = await response.json(); + if (isArray(data)) { + return data as IEventMapping[]; + } else { + return []; + } + } + + async updateMappingRules(mapping: IEventMapping[]): Promise { + const url = `${this.hubUrl}/eventMapping`; + const response = await this.fc.fetch(url, { + method: "POST", + headers: this.headers, + body: JSON.stringify(mapping) + }); + + if (response.status !== 200) { + const error = new Error(); + error.message = `Status not ok (${response.status})`; + throw error; + } + } }