Commit 0a5796c9fded2939c54290fd48e964b176f67605

Authored by Artem Babak
1 parent b85b51f5

Edge Downlinks as tab

... ... @@ -35,7 +35,7 @@ function EdgeService($http, $q, customerService) {
35 35 unassignEdgeFromCustomer: unassignEdgeFromCustomer,
36 36 makeEdgePublic: makeEdgePublic,
37 37 setRootRuleChain: setRootRuleChain,
38   - getEdgeEvents: getEdgeEvents,
  38 + getEdgeDownlinks: getEdgeDownlinks,
39 39 syncEdge: syncEdge,
40 40 findMissingToRelatedRuleChains: findMissingToRelatedRuleChains
41 41 };
... ... @@ -276,7 +276,7 @@ function EdgeService($http, $q, customerService) {
276 276 return deferred.promise;
277 277 }
278 278
279   - function getEdgeEvents(edgeId, pageLink) {
  279 + function getEdgeDownlinks(edgeId, pageLink) {
280 280 var deferred = $q.defer();
281 281 var url = '/api/edge/' + edgeId + '/events' + '?limit=' + pageLink.limit;
282 282 if (angular.isDefined(pageLink.startTime) && pageLink.startTime != null) {
... ...
... ... @@ -661,10 +661,6 @@ export default angular.module('thingsboard.types', [])
661 661 stats: {
662 662 value: "STATS",
663 663 name: "event.type-stats"
664   - },
665   - edgeEvent: {
666   - value: "EDGE_EVENT",
667   - name: "event.type-edge-event"
668 664 }
669 665 },
670 666 debugEventType: {
... ... @@ -1197,6 +1193,14 @@ export default angular.module('thingsboard.types', [])
1197 1193 "ADMIN_SETTINGS": {
1198 1194 name: "permission.resource.display-type.ADMIN_SETTINGS"
1199 1195 }
  1196 + },
  1197 + edgeEvent: {
  1198 + value: "EDGE_EVENT",
  1199 + name: "edge.downlink"
  1200 + },
  1201 + edgeDownlinks: {
  1202 + value: "EDGE_DOWNLINKS",
  1203 + name: "edge.downlinks"
1200 1204 }
1201 1205 }
1202 1206 ).name;
... ...
... ... @@ -65,6 +65,13 @@
65 65 default-event-type="{{vm.types.eventType.error.value}}">
66 66 </tb-event-table>
67 67 </md-tab>
  68 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'edge.downlinks' | translate }}">
  69 + <tb-edge-downlinks-table flex entity-type="vm.types.entityType.edge"
  70 + entity-id="vm.grid.operatingItem().id.id"
  71 + tenant-id="vm.grid.operatingItem().tenantId.id"
  72 + default-event-type="{{vm.types.edgeDownlinks.value}}">
  73 + </tb-edge-downlinks-table>
  74 + </md-tab>
68 75 <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'relation.relations' | translate }}">
69 76 <tb-relation-table flex
70 77 readonly="!('edge' | hasGenericPermission:'write')"
... ...
  1 +/*
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +import './event.scss';
  17 +
  18 +/* eslint-disable import/no-unresolved, import/default */
  19 +
  20 +import edgeDownlinksTableTemplate from './edge-downlinks-table.tpl.html';
  21 +
  22 +/* eslint-enable import/no-unresolved, import/default */
  23 +
  24 +/*@ngInject*/
  25 +export default function EdgeDownlinksDirective($compile, $templateCache, $rootScope, $translate, types,
  26 + eventService, edgeService, attributeService) {
  27 +
  28 + var linker = function (scope, element, attrs) {
  29 +
  30 + var template = $templateCache.get(edgeDownlinksTableTemplate);
  31 +
  32 + element.html(template);
  33 +
  34 + if (attrs.disabledEventTypes) {
  35 + var disabledEventTypes = attrs.disabledEventTypes.split(',');
  36 + scope.eventTypes = {};
  37 + for (var type in types.eventType) {
  38 + var eventType = types.eventType[type];
  39 + var enabled = true;
  40 + for (var i=0;i<disabledEventTypes.length;i++) {
  41 + if (eventType.value === disabledEventTypes[i]) {
  42 + enabled = false;
  43 + break;
  44 + }
  45 + }
  46 + if (enabled) {
  47 + scope.eventTypes[type] = eventType;
  48 + }
  49 + }
  50 + } else {
  51 + scope.eventTypes = angular.copy(types.eventType);
  52 + }
  53 +
  54 + if (attrs.debugEventTypes) {
  55 + var debugEventTypes = attrs.debugEventTypes.split(',');
  56 + for (i=0;i<debugEventTypes.length;i++) {
  57 + for (type in types.debugEventType) {
  58 + eventType = types.debugEventType[type];
  59 + if (eventType.value === debugEventTypes[i]) {
  60 + scope.eventTypes[type] = eventType;
  61 + }
  62 + }
  63 + }
  64 + }
  65 +
  66 + scope.eventType = attrs.defaultEventType;
  67 +
  68 + var pageSize = 20;
  69 + var startTime = 0;
  70 + var endTime = 0;
  71 +
  72 + scope.timewindow = {
  73 + history: {
  74 + timewindowMs: 24 * 60 * 60 * 1000 // 1 day
  75 + }
  76 + }
  77 +
  78 + scope.topIndex = 0;
  79 +
  80 + scope.theEvents = {
  81 + getItemAtIndex: function (index) {
  82 + if (index > scope.events.data.length) {
  83 + scope.theEvents.fetchMoreItems_(index);
  84 + return null;
  85 + }
  86 + var item = scope.events.data[index];
  87 + if (item) {
  88 + item.indexNumber = index + 1;
  89 + }
  90 + return item;
  91 + },
  92 +
  93 + getLength: function () {
  94 + if (scope.events.hasNext) {
  95 + return scope.events.data.length + scope.events.nextPageLink.limit;
  96 + } else {
  97 + return scope.events.data.length;
  98 + }
  99 + },
  100 +
  101 + fetchMoreItems_: function () {
  102 + if (scope.events.hasNext && !scope.events.pending) {
  103 + if (scope.entityType && scope.entityId && scope.eventType && scope.tenantId) {
  104 + scope.loadEdgeInfo();
  105 + scope.events.pending = true;
  106 + edgeService.getEdgeDownlinks(scope.entityId, scope.events.nextPageLink).then(
  107 + function success(events) {
  108 + scope.events.data = scope.events.data.concat(prepareEdgeEventData(events.data));
  109 + scope.events.nextPageLink = events.nextPageLink;
  110 + scope.events.hasNext = events.hasNext;
  111 + if (scope.events.hasNext) {
  112 + scope.events.nextPageLink.limit = pageSize;
  113 + }
  114 + scope.events.pending = false;
  115 + },
  116 + function fail() {
  117 + scope.events.hasNext = false;
  118 + scope.events.pending = false;
  119 + });
  120 + } else {
  121 + scope.events.hasNext = false;
  122 + }
  123 + }
  124 + }
  125 + };
  126 +
  127 + scope.$watch("entityId", function(newVal, prevVal) {
  128 + if (newVal && !angular.equals(newVal, prevVal)) {
  129 + scope.resetFilter();
  130 + scope.reload();
  131 + }
  132 + });
  133 +
  134 + scope.$watch("eventType", function(newVal, prevVal) {
  135 + if (newVal && !angular.equals(newVal, prevVal)) {
  136 + scope.reload();
  137 + }
  138 + });
  139 +
  140 + scope.$watch("timewindow", function(newVal, prevVal) {
  141 + if (newVal && !angular.equals(newVal, prevVal)) {
  142 + scope.reload();
  143 + }
  144 + }, true);
  145 +
  146 + scope.resetFilter = function() {
  147 + scope.timewindow = {
  148 + history: {
  149 + timewindowMs: 24 * 60 * 60 * 1000 // 1 day
  150 + }
  151 + };
  152 + }
  153 +
  154 + scope.updateTimeWindowRange = function() {
  155 + if (scope.timewindow.history.timewindowMs) {
  156 + var currentTime = (new Date).getTime();
  157 + startTime = currentTime - scope.timewindow.history.timewindowMs;
  158 + endTime = currentTime;
  159 + } else {
  160 + startTime = scope.timewindow.history.fixedTimewindow.startTimeMs;
  161 + endTime = scope.timewindow.history.fixedTimewindow.endTimeMs;
  162 + }
  163 + }
  164 +
  165 + scope.reload = function() {
  166 + scope.topIndex = 0;
  167 + scope.selected = [];
  168 + scope.updateTimeWindowRange();
  169 + scope.events = {
  170 + data: [],
  171 + nextPageLink: {
  172 + limit: pageSize,
  173 + startTime: startTime,
  174 + endTime: endTime
  175 + },
  176 + hasNext: true,
  177 + pending: false
  178 + };
  179 + scope.theEvents.getItemAtIndex(pageSize);
  180 + }
  181 +
  182 + scope.noData = function() {
  183 + return scope.events.data.length == 0 && !scope.events.hasNext;
  184 + }
  185 +
  186 + scope.hasData = function() {
  187 + return scope.events.data.length > 0;
  188 + }
  189 +
  190 + scope.loading = function() {
  191 + return $rootScope.loading;
  192 + }
  193 +
  194 + scope.hasScroll = function() {
  195 + var repeatContainer = scope.repeatContainer[0];
  196 + if (repeatContainer) {
  197 + var scrollElement = repeatContainer.children[0];
  198 + if (scrollElement) {
  199 + return scrollElement.scrollHeight > scrollElement.clientHeight;
  200 + }
  201 + }
  202 + return false;
  203 + }
  204 +
  205 + scope.subscriptionId = null;
  206 +
  207 + scope.loadEdgeInfo = function() {
  208 + attributeService.getEntityAttributesValues(
  209 + scope.entityType,
  210 + scope.entityId,
  211 + types.attributesScope.server.value,
  212 + types.edgeAttributeKeys.queueStartTs,
  213 + null).then(
  214 + function success(attributes) {
  215 + attributes.length > 0 ? scope.onEdgeAttributesUpdate(attributes) : scope.queueStartTs = 0;
  216 + });
  217 + scope.checkSubscription();
  218 + }
  219 +
  220 + scope.onEdgeAttributesUpdate = function(attributes) {
  221 + let edgeAttributes = attributes.reduce(function (map, attribute) {
  222 + map[attribute.key] = attribute;
  223 + return map;
  224 + }, {});
  225 + if (edgeAttributes.queueStartTs) {
  226 + scope.queueStartTs = edgeAttributes.queueStartTs.lastUpdateTs;
  227 + }
  228 + }
  229 +
  230 + scope.checkSubscription = function() {
  231 + var newSubscriptionId = null;
  232 + if (scope.entityId && scope.entityType && types.attributesScope.server.value) {
  233 + newSubscriptionId =
  234 + attributeService.subscribeForEntityAttributes(scope.entityType, scope.entityId, types.attributesScope.server.value);
  235 + }
  236 + if (scope.subscriptionId && scope.subscriptionId != newSubscriptionId) {
  237 + attributeService.unsubscribeForEntityAttributes(scope.subscriptionId);
  238 + }
  239 + scope.subscriptionId = newSubscriptionId;
  240 + }
  241 +
  242 + scope.$on('$destroy', function () {
  243 + if (scope.subscriptionId) {
  244 + attributeService.unsubscribeForEntityAttributes(scope.subscriptionId);
  245 + }
  246 + });
  247 +
  248 + scope.reload();
  249 +
  250 + $compile(element.contents())(scope);
  251 + }
  252 + function prepareEdgeEventData(data) {
  253 +
  254 + data.forEach(
  255 + edgeEvent => {
  256 + edgeEvent.edgeEventActionText = $translate.instant(types.edgeEventActionType[edgeEvent.action].name);
  257 + edgeEvent.edgeEventTypeText = $translate.instant(types.edgeEventTypeTranslations[edgeEvent.edgeId.entityType].name);
  258 + }
  259 + );
  260 + return data;
  261 + }
  262 +
  263 + return {
  264 + restrict: "E",
  265 + link: linker,
  266 + scope: {
  267 + entityType: '=',
  268 + entityId: '=',
  269 + tenantId: '='
  270 + }
  271 + };
  272 +}
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2020 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<md-content flex class="md-padding tb-absolute-fill" layout="column">
  19 + <section layout="row">
  20 + <tb-timewindow flex ng-model="timewindow" history-only as-button="true"></tb-timewindow>
  21 + <md-button ng-disabled="$root.loading"
  22 + class="md-icon-button" ng-click="reload()">
  23 + <md-icon>refresh</md-icon>
  24 + <md-tooltip md-direction="top">
  25 + {{ 'action.refresh' | translate }}
  26 + </md-tooltip>
  27 + </md-button>
  28 + </section>
  29 + <md-list flex layout="column" class="md-whiteframe-z1 tb-edge-downlinks-table">
  30 + <md-list class="tb-row tb-header" layout="row" layout-align="start center" tb-event-header event-type="{{eventType}}">
  31 + </md-list>
  32 + <md-progress-linear style="max-height: 0px;" md-mode="indeterminate" ng-disabled="!$root.loading"
  33 + ng-show="$root.loading"></md-progress-linear>
  34 + <md-divider></md-divider>
  35 + <span translate layout-align="center center"
  36 + style="margin-top: 25px;"
  37 + class="tb-prompt" ng-show="noData()">event.no-events-prompt</span>
  38 + <md-virtual-repeat-container ng-show="hasData()" flex md-top-index="topIndex" tb-scope-element="repeatContainer">
  39 + <md-list-item md-virtual-repeat="event in theEvents" md-on-demand flex ng-style="hasScroll() ? {'margin-right':'-15px'} : {}">
  40 + <md-list class="tb-row" flex layout="row" layout-align="start center" tb-event-row event-type="{{eventType}}" event="{{event}}">
  41 + </md-list>
  42 + <md-divider flex></md-divider>
  43 + </md-list-item>
  44 + </md-virtual-repeat-container>
  45 + </md-list>
  46 +</md-content>
... ...
... ... @@ -46,7 +46,7 @@ export default function EventHeaderDirective($compile, $templateCache, types) {
46 46 case types.debugEventType.debugRuleChain.value:
47 47 template = eventHeaderDebugRuleNodeTemplate;
48 48 break;
49   - case types.eventType.edgeEvent.value:
  49 + case types.edgeDownlinks.value:
50 50 template = eventHeaderEdgeEventTemplate;
51 51 break;
52 52 }
... ...
... ... @@ -49,7 +49,7 @@ export default function EventRowDirective($compile, $templateCache, $mdDialog, $
49 49 case types.debugEventType.debugRuleChain.value:
50 50 template = eventRowDebugRuleNodeTemplate;
51 51 break;
52   - case types.eventType.edgeEvent.value:
  52 + case types.edgeDownlinks.value:
53 53 template = eventRowEdgeEventTemplate;
54 54 break;
55 55 }
... ...
... ... @@ -22,8 +22,7 @@ import eventTableTemplate from './event-table.tpl.html';
22 22 /* eslint-enable import/no-unresolved, import/default */
23 23
24 24 /*@ngInject*/
25   -export default function EventTableDirective($compile, $templateCache, $rootScope, $translate, types,
26   - eventService, edgeService, attributeService) {
  25 +export default function EventTableDirective($compile, $templateCache, $rootScope, types, eventService) {
27 26
28 27 var linker = function (scope, element, attrs) {
29 28
... ... @@ -31,16 +30,11 @@ export default function EventTableDirective($compile, $templateCache, $rootScope
31 30
32 31 element.html(template);
33 32
34   - scope.eventTypeScope = angular.copy(types.eventType);
35   - if (scope.entityType !== types.entityType.edge) {
36   - delete scope.eventTypeScope.edgeEvent;
37   - }
38   -
39 33 if (attrs.disabledEventTypes) {
40 34 var disabledEventTypes = attrs.disabledEventTypes.split(',');
41 35 scope.eventTypes = {};
42   - for (var type in scope.eventTypeScope) {
43   - var eventType = scope.eventTypeScope[type];
  36 + for (var type in types.eventType) {
  37 + var eventType = types.eventType[type];
44 38 var enabled = true;
45 39 for (var i=0;i<disabledEventTypes.length;i++) {
46 40 if (eventType.value === disabledEventTypes[i]) {
... ... @@ -53,7 +47,7 @@ export default function EventTableDirective($compile, $templateCache, $rootScope
53 47 }
54 48 }
55 49 } else {
56   - scope.eventTypes = angular.copy(scope.eventTypeScope);
  50 + scope.eventTypes = angular.copy(types.eventType);
57 51 }
58 52
59 53 if (attrs.debugEventTypes) {
... ... @@ -106,23 +100,13 @@ export default function EventTableDirective($compile, $templateCache, $rootScope
106 100 fetchMoreItems_: function () {
107 101 if (scope.events.hasNext && !scope.events.pending) {
108 102 if (scope.entityType && scope.entityId && scope.eventType && scope.tenantId) {
109   - var promise = '';
110   - if (scope.eventType !== types.eventType.edgeEvent.value) {
111   - promise = eventService.getEvents(scope.entityType, scope.entityId,
112   - scope.eventType, scope.tenantId, scope.events.nextPageLink);
113   - } else {
114   - promise = edgeService.getEdgeEvents(scope.entityId, scope.events.nextPageLink);
115   - scope.loadEdgeInfo();
116   - }
  103 + var promise = eventService.getEvents(scope.entityType, scope.entityId,
  104 + scope.eventType, scope.tenantId, scope.events.nextPageLink);
117 105 if (promise) {
118 106 scope.events.pending = true;
119 107 promise.then(
120 108 function success(events) {
121   - if (scope.eventType === types.eventType.edgeEvent.value) {
122   - scope.events.data = scope.events.data.concat(prepareEdgeEventData(events.data));
123   - } else {
124   - scope.events.data = scope.events.data.concat(events.data);
125   - }
  109 + scope.events.data = scope.events.data.concat(events.data);
126 110 scope.events.nextPageLink = events.nextPageLink;
127 111 scope.events.hasNext = events.hasNext;
128 112 if (scope.events.hasNext) {
... ... @@ -223,64 +207,10 @@ export default function EventTableDirective($compile, $templateCache, $rootScope
223 207 return false;
224 208 }
225 209
226   - scope.subscriptionId = null;
227   -
228   - scope.loadEdgeInfo = function() {
229   - attributeService.getEntityAttributesValues(
230   - scope.entityType,
231   - scope.entityId,
232   - types.attributesScope.server.value,
233   - types.edgeAttributeKeys.queueStartTs,
234   - null).then(
235   - function success(attributes) {
236   - attributes.length > 0 ? scope.onEdgeAttributesUpdate(attributes) : scope.queueStartTs = 0;
237   - });
238   - scope.checkSubscription();
239   - }
240   -
241   - scope.onEdgeAttributesUpdate = function(attributes) {
242   - let edgeAttributes = attributes.reduce(function (map, attribute) {
243   - map[attribute.key] = attribute;
244   - return map;
245   - }, {});
246   - if (edgeAttributes.queueStartTs) {
247   - scope.queueStartTs = edgeAttributes.queueStartTs.lastUpdateTs;
248   - }
249   - }
250   -
251   - scope.checkSubscription = function() {
252   - var newSubscriptionId = null;
253   - if (scope.entityId && scope.entityType && types.attributesScope.server.value) {
254   - newSubscriptionId =
255   - attributeService.subscribeForEntityAttributes(scope.entityType, scope.entityId, types.attributesScope.server.value);
256   - }
257   - if (scope.subscriptionId && scope.subscriptionId != newSubscriptionId) {
258   - attributeService.unsubscribeForEntityAttributes(scope.subscriptionId);
259   - }
260   - scope.subscriptionId = newSubscriptionId;
261   - }
262   -
263   - scope.$on('$destroy', function () {
264   - if (scope.subscriptionId) {
265   - attributeService.unsubscribeForEntityAttributes(scope.subscriptionId);
266   - }
267   - });
268   -
269 210 scope.reload();
270 211
271 212 $compile(element.contents())(scope);
272 213 }
273   -
274   - function prepareEdgeEventData(data) {
275   - data.forEach(
276   - edgeEvent => {
277   - edgeEvent.edgeEventActionText = $translate.instant(types.edgeEventActionType[edgeEvent.action].name);
278   - edgeEvent.edgeEventTypeText = $translate.instant(types.edgeEventTypeTranslations[edgeEvent.edgeId.entityType].name);
279   - }
280   - );
281   - return data;
282   - }
283   -
284 214 return {
285 215 restrict: "E",
286 216 link: linker,
... ...
... ... @@ -85,6 +85,78 @@ md-list.tb-event-table {
85 85 }
86 86 }
87 87
  88 +md-list.tb-edge-downlinks-table {
  89 + padding: 0;
  90 +
  91 + md-list-item {
  92 + padding: 0;
  93 + }
  94 +
  95 + .tb-row {
  96 + height: 48px;
  97 + padding: 0;
  98 + overflow: hidden;
  99 +
  100 + .tb-cell {
  101 + text-overflow: ellipsis;
  102 +
  103 + &.tb-scroll {
  104 + overflow-x: auto;
  105 + overflow-y: hidden;
  106 + white-space: nowrap;
  107 + }
  108 +
  109 + &.tb-nowrap {
  110 + white-space: nowrap;
  111 + }
  112 + }
  113 + }
  114 +
  115 + .tb-row:hover {
  116 + background-color: #eee;
  117 + }
  118 +
  119 + .tb-header:hover {
  120 + background: none;
  121 + }
  122 +
  123 + .tb-header {
  124 + .tb-cell {
  125 + font-size: 12px;
  126 + font-weight: 700;
  127 + color: rgba(0, 0, 0, .54);
  128 + white-space: nowrap;
  129 + background: none;
  130 + }
  131 + }
  132 +
  133 + .tb-cell {
  134 + &:first-child {
  135 + padding-left: 14px;
  136 + }
  137 +
  138 + &:last-child {
  139 + padding-right: 14px;
  140 + }
  141 + padding: 0 6px;
  142 + margin: auto 0;
  143 + overflow: hidden;
  144 + font-size: 13px;
  145 + color: rgba(0, 0, 0, .87);
  146 + text-align: left;
  147 + vertical-align: middle;
  148 +
  149 + .md-button {
  150 + padding: 0;
  151 + margin: 0;
  152 + }
  153 + }
  154 +
  155 + .tb-cell.tb-number {
  156 + text-align: right;
  157 + }
  158 +}
  159 +
88 160 #tb-event-content {
89 161 width: 100%;
90 162 min-width: 400px;
... ...
... ... @@ -19,6 +19,7 @@ import EventContentDialogController from './event-content-dialog.controller';
19 19 import EventHeaderDirective from './event-header.directive';
20 20 import EventRowDirective from './event-row.directive';
21 21 import EventTableDirective from './event-table.directive';
  22 +import EdgeDownlinksDirective from "./edge-downlinks-table.directive";
22 23
23 24 export default angular.module('thingsboard.event', [
24 25 thingsboardApiEvent
... ... @@ -27,4 +28,5 @@ export default angular.module('thingsboard.event', [
27 28 .directive('tbEventHeader', EventHeaderDirective)
28 29 .directive('tbEventRow', EventRowDirective)
29 30 .directive('tbEventTable', EventTableDirective)
  31 + .directive('tbEdgeDownlinksTable', EdgeDownlinksDirective)
30 32 .name;
... ...
... ... @@ -856,7 +856,9 @@
856 856 "license-key-hint": "To obtain your license please navigate to the <a href='https://thingsboard.io/pricing/?active=thingsboard-edge' target='_blank'>pricing page</a> and select the best license option for your case.",
857 857 "cloud-endpoint-hint": "Edge requires HTTP(s) access to Cloud (ThingsBoard CE/PE) to verify the license key. Please specify Cloud URL that Edge is able to connect to.",
858 858 "missing-related-rule-chains-title": "Edge has missing related rule chain(s)",
859   - "missing-related-rule-chains-text": "Assigned to edge rule chain(s) use rule nodes that forward message(s) to rule chain(s) that are not assigned to this edge. <br><br> List of missing rule chain(s): <br> {{missingRuleChains}}"
  859 + "missing-related-rule-chains-text": "Assigned to edge rule chain(s) use rule nodes that forward message(s) to rule chain(s) that are not assigned to this edge. <br><br> List of missing rule chain(s): <br> {{missingRuleChains}}",
  860 + "downlinks": "Downlinks",
  861 + "no-downlinks-prompt": "No downlinks found"
860 862 },
861 863 "error": {
862 864 "unable-to-connect": "Unable to connect to the server! Please check your internet connection.",
... ... @@ -1097,7 +1099,6 @@
1097 1099 "type-debug-rule-chain": "Debug",
1098 1100 "no-events-prompt": "No events found",
1099 1101 "error": "Error",
1100   - "type-edge-event": "Downlink",
1101 1102 "alarm": "Alarm",
1102 1103 "event-time": "Event time",
1103 1104 "server": "Server",
... ...