diff --git a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java index a4a28935e3..05534970ca 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java @@ -273,6 +273,9 @@ public class EdgeController extends BaseController { Customer publicCustomer = customerService.findOrCreatePublicCustomer(edge.getTenantId()); Edge savedEdge = checkNotNull(edgeService.assignEdgeToCustomer(getCurrentUser().getTenantId(), edgeId, publicCustomer.getId())); + tbClusterService.onEntityStateChange(getTenantId(), edgeId, + ComponentLifecycleEvent.UPDATED); + logEntityAction(edgeId, savedEdge, savedEdge.getCustomerId(), ActionType.ASSIGNED_TO_CUSTOMER, null, strEdgeId, publicCustomer.getId().toString(), publicCustomer.getName()); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceProcessor.java index 9e233e828b..713fcdb158 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceProcessor.java @@ -27,6 +27,7 @@ import org.apache.commons.lang.StringUtils; import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.stereotype.Component; import org.thingsboard.rule.engine.api.RpcError; +import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.edge.Edge; @@ -45,6 +46,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.util.mapping.JacksonUtil; import org.thingsboard.server.gen.edge.DeviceCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.DeviceRpcCallMsg; @@ -184,7 +186,8 @@ public class DeviceProcessor extends BaseProcessor { log.debug("[{}] Creating device entity [{}] from edge [{}]", tenantId, deviceUpdateMsg, edge.getName()); device = new Device(); device.setTenantId(edge.getTenantId()); - device.setCustomerId(edge.getCustomerId()); + // make device private, if edge is public + device.setCustomerId(getCustomerId(edge)); device.setName(deviceName); device.setType(deviceUpdateMsg.getType()); device.setLabel(deviceUpdateMsg.getLabel()); @@ -204,6 +207,18 @@ public class DeviceProcessor extends BaseProcessor { return device; } + private CustomerId getCustomerId(Edge edge) { + if (edge.getCustomerId() == null || edge.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { + return edge.getCustomerId(); + } + Customer publicCustomer = customerService.findOrCreatePublicCustomer(edge.getTenantId()); + if (publicCustomer.getId().equals(edge.getCustomerId())) { + return null; + } else { + return edge.getCustomerId(); + } + } + private void createRelationFromEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId) { EntityRelation relation = new EntityRelation(); relation.setFrom(edgeId); diff --git a/ui/src/app/edge/downlinks/downlink.scss b/ui/src/app/edge/downlinks/downlink.scss new file mode 100644 index 0000000000..9f975fc1e1 --- /dev/null +++ b/ui/src/app/edge/downlinks/downlink.scss @@ -0,0 +1,93 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +md-list.tb-edge-downlinks-table { + padding: 0; + + md-list-item { + padding: 0; + } + + .tb-row { + height: 48px; + padding: 0; + overflow: hidden; + + .tb-cell { + text-overflow: ellipsis; + + &.tb-scroll { + overflow-x: auto; + overflow-y: hidden; + white-space: nowrap; + } + + &.tb-nowrap { + white-space: nowrap; + } + } + } + + .tb-row:hover { + background-color: #eee; + } + + .tb-header:hover { + background: none; + } + + .tb-header { + .tb-cell { + font-size: 12px; + font-weight: 700; + color: rgba(0, 0, 0, .54); + white-space: nowrap; + background: none; + } + } + + .tb-cell { + &:first-child { + padding-left: 14px; + } + + &:last-child { + padding-right: 14px; + } + padding: 0 6px; + margin: auto 0; + overflow: hidden; + font-size: 13px; + color: rgba(0, 0, 0, .87); + text-align: left; + vertical-align: middle; + + .md-button { + padding: 0; + margin: 0; + } + } + + .tb-cell.tb-number { + text-align: right; + } +} + +#tb-edge-downlinks-content { + width: 100%; + min-width: 400px; + height: 100%; + min-height: 50px; +} diff --git a/ui/src/app/edge/downlinks/edge-downlinks-content-dialog.controller.js b/ui/src/app/edge/downlinks/edge-downlinks-content-dialog.controller.js new file mode 100644 index 0000000000..432bdf4d50 --- /dev/null +++ b/ui/src/app/edge/downlinks/edge-downlinks-content-dialog.controller.js @@ -0,0 +1,92 @@ +/* + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import $ from 'jquery'; +import 'brace/ext/language_tools'; +import 'brace/ext/searchbox'; +import 'brace/mode/java'; +import 'brace/theme/github'; +import beautify from 'js-beautify'; + +/* eslint-disable angular/angularelement */ + +const js_beautify = beautify.js; + +/*@ngInject*/ +export default function EdgeDownlinksContentDialogController($mdDialog, types, content, contentType, title, showingCallback) { + + var vm = this; + + showingCallback.onShowing = function(scope, element) { + updateEditorSize(element); + } + + vm.content = content; + vm.title = title; + + var mode; + if (contentType) { + mode = types.contentType[contentType].code; + if (contentType == types.contentType.JSON.value && vm.content) { + vm.content = js_beautify(vm.content, {indent_size: 4}); + } + } else { + mode = 'java'; + } + + vm.contentOptions = { + useWrapMode: false, + mode: mode, + showGutter: false, + showPrintMargin: false, + theme: 'github', + advanced: { + enableSnippets: false, + enableBasicAutocompletion: false, + enableLiveAutocompletion: false + }, + onLoad: function (_ace) { + vm.editor = _ace; + } + }; + + function updateEditorSize(element) { + var newHeight = 400; + var newWidth = 600; + if (vm.content && vm.content.length > 0) { + var lines = vm.content.split('\n'); + newHeight = 16 * lines.length + 16; + var maxLineLength = 0; + for (var i = 0; i < lines.length; i++) { + var line = lines[i].replace(/\t/g, ' ').replace(/\n/g, ''); + var lineLength = line.length; + maxLineLength = Math.max(maxLineLength, lineLength); + } + newWidth = 8 * maxLineLength + 16; + } + $('#tb-edge-downlinks-content', element).height(newHeight.toString() + "px") + .width(newWidth.toString() + "px"); + vm.editor.resize(); + } + + vm.close = close; + + function close () { + $mdDialog.hide(); + } + +} + +/* eslint-enable angular/angularelement */ diff --git a/ui/src/app/edge/downlinks/edge-downlinks-content-dialog.tpl.html b/ui/src/app/edge/downlinks/edge-downlinks-content-dialog.tpl.html new file mode 100644 index 0000000000..798990ec07 --- /dev/null +++ b/ui/src/app/edge/downlinks/edge-downlinks-content-dialog.tpl.html @@ -0,0 +1,42 @@ + + + + + {{ vm.title }} + + + + + + + + + + + + + + + {{ 'action.close' | + translate }} + + + diff --git a/ui/src/app/edge/downlinks/edge-downlinks-header.directive.js b/ui/src/app/edge/downlinks/edge-downlinks-header.directive.js new file mode 100644 index 0000000000..3af243ca48 --- /dev/null +++ b/ui/src/app/edge/downlinks/edge-downlinks-header.directive.js @@ -0,0 +1,39 @@ +/* + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* eslint-disable import/no-unresolved, import/default */ + +import edgeDownlinksHeaderTemplate from './edge-downlinks-header.tpl.html' + +/* eslint-enable import/no-unresolved, import/default */ + +/*@ngInject*/ +export default function EdgeDownlinksHeaderDirective($compile, $templateCache) { + + var linker = function (scope, element) { + + var template = edgeDownlinksHeaderTemplate; + + element.html($templateCache.get(template)); + $compile(element.contents())(scope); + } + + return { + restrict: "A", + replace: false, + link: linker, + scope: false + }; +} diff --git a/ui/src/app/edge/downlinks/edge-downlinks-row.directive.js b/ui/src/app/edge/downlinks/edge-downlinks-row.directive.js new file mode 100644 index 0000000000..16015d12db --- /dev/null +++ b/ui/src/app/edge/downlinks/edge-downlinks-row.directive.js @@ -0,0 +1,124 @@ +/* + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* eslint-disable import/no-unresolved, import/default */ + +import edgeDownlinksContentTemplate from './edge-downlinks-content-dialog.tpl.html'; +import edgeDownlinlsRowTemplate from './edge-downlinks-row.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +/*@ngInject*/ +export default function EdgeDownlinksRowDirective($compile, $templateCache, $mdDialog, $document, $translate, + types, utils, toast, entityService, ruleChainService) { + + var linker = function (scope, element, attrs) { + + var template = edgeDownlinlsRowTemplate; + + element.html($templateCache.get(template)); + $compile(element.contents())(scope); + + scope.types = types; + scope.downlink = attrs.downlink; + + scope.showEdgeEntityContent = function($event, title, contentType) { + var onShowingCallback = { + onShowing: function(){} + } + if (!contentType) { + contentType = null; + } + var content = ''; + switch(scope.downlink.type) { + case types.edgeEventType.relation: + content = angular.toJson(scope.downlink.body); + showDialog(); + break; + case types.edgeEventType.ruleChainMetaData: + content = ruleChainService.getRuleChainMetaData(scope.downlink.entityId, {ignoreErrors: true}).then( + function success(info) { + showDialog(); + return angular.toJson(info); + }, function fail() { + showError(); + }); + break; + default: + content = entityService.getEntity(scope.downlink.type, scope.downlink.entityId, {ignoreErrors: true}).then( + function success(info) { + showDialog(); + return angular.toJson(info); + }, function fail() { + showError(); + }); + break; + } + function showDialog() { + $mdDialog.show({ + controller: 'EdgeDownlinksContentDialogController', + controllerAs: 'vm', + templateUrl: edgeDownlinksContentTemplate, + locals: {content: content, title: title, contentType: contentType, showingCallback: onShowingCallback}, + parent: angular.element($document[0].body), + fullscreen: true, + targetEvent: $event, + multiple: true, + onShowing: function(scope, element) { + onShowingCallback.onShowing(scope, element); + } + }); + } + function showError() { + toast.showError($translate.instant('edge.load-entity-error')); + } + } + + scope.checkEdgeDownlinksType = function (type) { + return !(type === types.edgeEventType.widgetType || + type === types.edgeEventType.adminSettings || + type === types.edgeEventType.widgetsBundle ); + } + + scope.checkTooltip = function($event) { + var el = $event.target; + var $el = angular.element(el); + if(el.offsetWidth < el.scrollWidth && !$el.attr('title')){ + $el.attr('title', $el.text()); + } + } + + $compile(element.contents())(scope); + + scope.updateStatus = function(downlinkCreatedTime) { + var status; + if (downlinkCreatedTime < scope.queueStartTs) { + status = $translate.instant(types.edgeEventStatus.DEPLOYED.name); + scope.statusColor = types.edgeEventStatus.DEPLOYED.color; + } else { + status = $translate.instant(types.edgeEventStatus.PENDING.name); + scope.statusColor = types.edgeEventStatus.PENDING.color; + } + return status; + } + } + + return { + restrict: "A", + replace: false, + link: linker, + scope: false + }; +} diff --git a/ui/src/app/edge/downlinks/edge-downlinks-table.directive.js b/ui/src/app/edge/downlinks/edge-downlinks-table.directive.js new file mode 100644 index 0000000000..98bd63abb0 --- /dev/null +++ b/ui/src/app/edge/downlinks/edge-downlinks-table.directive.js @@ -0,0 +1,230 @@ +/* + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import './downlink.scss'; + +/* eslint-disable import/no-unresolved, import/default */ + +import edgeDownlinksTableTemplate from './edge-downlinks-table.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +/*@ngInject*/ +export default function EdgeDownlinksDirective($compile, $templateCache, $rootScope, $translate, types, edgeService, attributeService) { + + var linker = function (scope, element) { + + var template = $templateCache.get(edgeDownlinksTableTemplate); + element.html(template); + + var pageSize = 20; + var startTime = 0; + var endTime = 0; + + scope.timewindow = { + history: { + timewindowMs: 24 * 60 * 60 * 1000 // 1 day + } + } + + scope.topIndex = 0; + + scope.theDownlinks = { + getItemAtIndex: function (index) { + if (index > scope.downlinks.data.length) { + scope.theDownlinks.fetchMoreItems_(index); + return null; + } + var item = scope.downlinks.data[index]; + if (item) { + item.indexNumber = index + 1; + } + return item; + }, + + getLength: function () { + if (scope.downlinks.hasNext) { + return scope.downlinks.data.length + scope.downlinks.nextPageLink.limit; + } else { + return scope.downlinks.data.length; + } + }, + + fetchMoreItems_: function () { + if (scope.downlinks.hasNext && !scope.downlinks.pending) { + if (scope.entityType && scope.entityId && scope.tenantId) { + scope.loadEdgeInfo(); + scope.downlinks.pending = true; + edgeService.getEdgeEvents(scope.entityId, scope.downlinks.nextPageLink).then( + function success(downlinks) { + scope.downlinks.data = scope.downlinks.data.concat(prepareEdgeDownlinksData(downlinks.data)); + scope.downlinks.nextPageLink = downlinks.nextPageLink; + scope.downlinks.hasNext = downlinks.hasNext; + if (scope.downlinks.hasNext) { + scope.downlinks.nextPageLink.limit = pageSize; + } + scope.downlinks.pending = false; + }, + function fail() { + scope.downlinks.hasNext = false; + scope.downlinks.pending = false; + }); + } else { + scope.downlinks.hasNext = false; + } + } + } + }; + + scope.$watch("entityId", function(newVal, prevVal) { + if (newVal && !angular.equals(newVal, prevVal)) { + scope.resetFilter(); + scope.reload(); + } + }); + + scope.$watch("timewindow", function(newVal, prevVal) { + if (newVal && !angular.equals(newVal, prevVal)) { + scope.reload(); + } + }, true); + + scope.resetFilter = function() { + scope.timewindow = { + history: { + timewindowMs: 24 * 60 * 60 * 1000 // 1 day + } + }; + } + + scope.updateTimeWindowRange = function() { + if (scope.timewindow.history.timewindowMs) { + var currentTime = (new Date).getTime(); + startTime = currentTime - scope.timewindow.history.timewindowMs; + endTime = currentTime; + } else { + startTime = scope.timewindow.history.fixedTimewindow.startTimeMs; + endTime = scope.timewindow.history.fixedTimewindow.endTimeMs; + } + } + + scope.reload = function() { + scope.topIndex = 0; + scope.selected = []; + scope.updateTimeWindowRange(); + scope.downlinks = { + data: [], + nextPageLink: { + limit: pageSize, + startTime: startTime, + endTime: endTime + }, + hasNext: true, + pending: false + }; + scope.theDownlinks.getItemAtIndex(pageSize); + } + + scope.noData = function() { + return scope.downlinks.data.length == 0 && !scope.downlinks.hasNext; + } + + scope.hasData = function() { + return scope.downlinks.data.length > 0; + } + + scope.loading = function() { + return $rootScope.loading; + } + + scope.hasScroll = function() { + var repeatContainer = scope.repeatContainer[0]; + if (repeatContainer) { + var scrollElement = repeatContainer.children[0]; + if (scrollElement) { + return scrollElement.scrollHeight > scrollElement.clientHeight; + } + } + return false; + } + + scope.subscriptionId = null; + + scope.loadEdgeInfo = function() { + attributeService.getEntityAttributesValues( + scope.entityType, + scope.entityId, + types.attributesScope.server.value, + types.edgeAttributeKeys.queueStartTs, + null).then( + function success(attributes) { + attributes.length > 0 ? scope.onEdgeAttributesUpdate(attributes) : scope.queueStartTs = 0; + }); + scope.checkSubscription(); + } + + scope.onEdgeAttributesUpdate = function(attributes) { + let edgeAttributes = attributes.reduce(function (map, attribute) { + map[attribute.key] = attribute; + return map; + }, {}); + if (edgeAttributes.queueStartTs) { + scope.queueStartTs = edgeAttributes.queueStartTs.lastUpdateTs; + } + } + + scope.checkSubscription = function() { + var newSubscriptionId = null; + if (scope.entityId && scope.entityType && types.attributesScope.server.value) { + newSubscriptionId = + attributeService.subscribeForEntityAttributes(scope.entityType, scope.entityId, types.attributesScope.server.value); + } + if (scope.subscriptionId && scope.subscriptionId != newSubscriptionId) { + attributeService.unsubscribeForEntityAttributes(scope.subscriptionId); + } + scope.subscriptionId = newSubscriptionId; + } + + scope.$on('$destroy', function () { + if (scope.subscriptionId) { + attributeService.unsubscribeForEntityAttributes(scope.subscriptionId); + } + }); + + scope.reload(); + + $compile(element.contents())(scope); + } + function prepareEdgeDownlinksData(data) { + + data.forEach( + edgeDownlink => { + edgeDownlink.edgeEventActionText = $translate.instant(types.edgeEventActionTypeTranslations[edgeDownlink.action].name); + edgeDownlink.edgeEventTypeText = $translate.instant(types.edgeEventTypeTranslations[edgeDownlink.type].name); + } + ); + return data; + } + + return { + restrict: "E", + link: linker, + scope: { + entityType: '=', + entityId: '=', + tenantId: '=' + } + }; +} diff --git a/ui/src/app/edge/downlinks/edge-downlinks-table.tpl.html b/ui/src/app/edge/downlinks/edge-downlinks-table.tpl.html new file mode 100644 index 0000000000..0e66b2b685 --- /dev/null +++ b/ui/src/app/edge/downlinks/edge-downlinks-table.tpl.html @@ -0,0 +1,46 @@ + + + + + + refresh + + {{ 'action.refresh' | translate }} + + + + + + + + + edge.no-downlinks-prompt + + + + + + + + +