Commit 7c6cdb148c3c1b54ee31b477667144675bb45840

Authored by Igor Kulikov
Committed by GitHub
1 parent 31b24892

TB-39: UI: Add Gateway flag. (#44)

* TB-39: UI: Add Gateway flag.

* TB-39: UI: Update package version.
1 { 1 {
2 "name": "thingsboard", 2 "name": "thingsboard",
3 "private": true, 3 "private": true,
4 - "version": "1.0.1", 4 + "version": "1.1.0",
5 "description": "Thingsboard UI", 5 "description": "Thingsboard UI",
6 "licenses": [ 6 "licenses": [
7 { 7 {
@@ -52,6 +52,11 @@ const apiProxy = httpProxy.createProxyServer({ @@ -52,6 +52,11 @@ const apiProxy = httpProxy.createProxyServer({
52 } 52 }
53 }); 53 });
54 54
  55 +apiProxy.on('error', function (err, req, res) {
  56 + console.warn('API proxy error: ' + err);
  57 + res.end('Error.');
  58 +});
  59 +
55 console.info(`Forwarding API requests to http://${forwardHost}:${forwardPort}`); 60 console.info(`Forwarding API requests to http://${forwardHost}:${forwardPort}`);
56 61
57 app.all('/api/*', (req, res) => { 62 app.all('/api/*', (req, res) => {
@@ -256,6 +256,9 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -256,6 +256,9 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
256 type: types.dataKeyType.timeseries, 256 type: types.dataKeyType.timeseries,
257 onData: function (data) { 257 onData: function (data) {
258 onData(data, types.dataKeyType.timeseries); 258 onData(data, types.dataKeyType.timeseries);
  259 + },
  260 + onReconnected: function() {
  261 + onReconnected();
259 } 262 }
260 }; 263 };
261 264
@@ -278,6 +281,9 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -278,6 +281,9 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
278 type: types.dataKeyType.timeseries, 281 type: types.dataKeyType.timeseries,
279 onData: function (data) { 282 onData: function (data) {
280 onData(data, types.dataKeyType.timeseries); 283 onData(data, types.dataKeyType.timeseries);
  284 + },
  285 + onReconnected: function() {
  286 + onReconnected();
281 } 287 }
282 }; 288 };
283 289
@@ -299,6 +305,9 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -299,6 +305,9 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
299 type: types.dataKeyType.attribute, 305 type: types.dataKeyType.attribute,
300 onData: function (data) { 306 onData: function (data) {
301 onData(data, types.dataKeyType.attribute); 307 onData(data, types.dataKeyType.attribute);
  308 + },
  309 + onReconnected: function() {
  310 + onReconnected();
302 } 311 }
303 }; 312 };
304 313
@@ -428,6 +437,25 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -428,6 +437,25 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
428 } 437 }
429 } 438 }
430 439
  440 + function onReconnected() {
  441 + if (datasourceType === types.datasourceType.device) {
  442 + for (var key in dataKeys) {
  443 + var dataKeysList = dataKeys[key];
  444 + for (var i = 0; i < dataKeysList.length; i++) {
  445 + var dataKey = dataKeysList[i];
  446 + var datasourceKey = key + '_' + i;
  447 + datasourceData[datasourceKey] = [];
  448 + for (var l in listeners) {
  449 + var listener = listeners[l];
  450 + listener.dataUpdated(datasourceData[datasourceKey],
  451 + listener.datasourceIndex,
  452 + dataKey.index);
  453 + }
  454 + }
  455 + }
  456 + }
  457 + }
  458 +
431 function onData(sourceData, type) { 459 function onData(sourceData, type) {
432 for (var keyName in sourceData) { 460 for (var keyName in sourceData) {
433 var keyData = sourceData[keyName]; 461 var keyData = sourceData[keyName];
@@ -307,12 +307,12 @@ function DeviceService($http, $q, $filter, telemetryWebsocketService, types) { @@ -307,12 +307,12 @@ function DeviceService($http, $q, $filter, telemetryWebsocketService, types) {
307 onSubscriptionData(data, subscriptionId); 307 onSubscriptionData(data, subscriptionId);
308 } 308 }
309 }; 309 };
310 - telemetryWebsocketService.subscribe(subscriber);  
311 deviceAttributesSubscription = { 310 deviceAttributesSubscription = {
312 subscriber: subscriber, 311 subscriber: subscriber,
313 attributes: null 312 attributes: null
314 } 313 }
315 deviceAttributesSubscriptionMap[subscriptionId] = deviceAttributesSubscription; 314 deviceAttributesSubscriptionMap[subscriptionId] = deviceAttributesSubscription;
  315 + telemetryWebsocketService.subscribe(subscriber);
316 } 316 }
317 return subscriptionId; 317 return subscriptionId;
318 } 318 }
@@ -20,11 +20,17 @@ export default angular.module('thingsboard.api.telemetryWebsocket', [thingsboard @@ -20,11 +20,17 @@ export default angular.module('thingsboard.api.telemetryWebsocket', [thingsboard
20 .factory('telemetryWebsocketService', TelemetryWebsocketService) 20 .factory('telemetryWebsocketService', TelemetryWebsocketService)
21 .name; 21 .name;
22 22
  23 +const RECONNECT_INTERVAL = 5000;
  24 +const WS_IDLE_TIMEOUT = 90000;
  25 +
23 /*@ngInject*/ 26 /*@ngInject*/
24 -function TelemetryWebsocketService($websocket, $timeout, $window, types, userService) { 27 +function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, types, userService) {
25 28
26 var isOpening = false, 29 var isOpening = false,
27 isOpened = false, 30 isOpened = false,
  31 + isActive = false,
  32 + isReconnect = false,
  33 + reconnectSubscribers = [],
28 lastCmdId = 0, 34 lastCmdId = 0,
29 subscribers = {}, 35 subscribers = {},
30 subscribersCount = 0, 36 subscribersCount = 0,
@@ -36,7 +42,8 @@ function TelemetryWebsocketService($websocket, $timeout, $window, types, userSer @@ -36,7 +42,8 @@ function TelemetryWebsocketService($websocket, $timeout, $window, types, userSer
36 telemetryUri, 42 telemetryUri,
37 dataStream, 43 dataStream,
38 location = $window.location, 44 location = $window.location,
39 - socketCloseTimer; 45 + socketCloseTimer,
  46 + reconnectTimer;
40 47
41 if (location.protocol === "https:") { 48 if (location.protocol === "https:") {
42 telemetryUri = "wss:"; 49 telemetryUri = "wss:";
@@ -46,11 +53,18 @@ function TelemetryWebsocketService($websocket, $timeout, $window, types, userSer @@ -46,11 +53,18 @@ function TelemetryWebsocketService($websocket, $timeout, $window, types, userSer
46 telemetryUri += "//" + location.hostname + ":" + location.port; 53 telemetryUri += "//" + location.hostname + ":" + location.port;
47 telemetryUri += "/api/ws/plugins/telemetry"; 54 telemetryUri += "/api/ws/plugins/telemetry";
48 55
  56 +
49 var service = { 57 var service = {
50 subscribe: subscribe, 58 subscribe: subscribe,
51 unsubscribe: unsubscribe 59 unsubscribe: unsubscribe
52 } 60 }
53 61
  62 + $rootScope.telemetryWsLogoutHandle = $rootScope.$on('unauthenticated', function (event, doLogout) {
  63 + if (doLogout) {
  64 + reset(true);
  65 + }
  66 + });
  67 +
54 return service; 68 return service;
55 69
56 function publishCommands () { 70 function publishCommands () {
@@ -74,12 +88,42 @@ function TelemetryWebsocketService($websocket, $timeout, $window, types, userSer @@ -74,12 +88,42 @@ function TelemetryWebsocketService($websocket, $timeout, $window, types, userSer
74 function onOpen () { 88 function onOpen () {
75 isOpening = false; 89 isOpening = false;
76 isOpened = true; 90 isOpened = true;
77 - publishCommands(); 91 + if (reconnectTimer) {
  92 + $timeout.cancel(reconnectTimer);
  93 + reconnectTimer = null;
  94 + }
  95 + if (isReconnect) {
  96 + isReconnect = false;
  97 + for (var r in reconnectSubscribers) {
  98 + var reconnectSubscriber = reconnectSubscribers[r];
  99 + if (reconnectSubscriber.onReconnected) {
  100 + reconnectSubscriber.onReconnected();
  101 + }
  102 + subscribe(reconnectSubscriber);
  103 + }
  104 + reconnectSubscribers = [];
  105 + } else {
  106 + publishCommands();
  107 + }
78 } 108 }
79 109
80 function onClose () { 110 function onClose () {
81 isOpening = false; 111 isOpening = false;
82 isOpened = false; 112 isOpened = false;
  113 + if (isActive) {
  114 + if (!isReconnect) {
  115 + reconnectSubscribers = [];
  116 + for (var id in subscribers) {
  117 + reconnectSubscribers.push(subscribers[id]);
  118 + }
  119 + reset(false);
  120 + isReconnect = true;
  121 + }
  122 + if (reconnectTimer) {
  123 + $timeout.cancel(reconnectTimer);
  124 + }
  125 + reconnectTimer = $timeout(tryOpenSocket, RECONNECT_INTERVAL, false);
  126 + }
83 } 127 }
84 128
85 function onMessage (message) { 129 function onMessage (message) {
@@ -137,28 +181,60 @@ function TelemetryWebsocketService($websocket, $timeout, $window, types, userSer @@ -137,28 +181,60 @@ function TelemetryWebsocketService($websocket, $timeout, $window, types, userSer
137 function checkToClose () { 181 function checkToClose () {
138 if (subscribersCount === 0 && isOpened) { 182 if (subscribersCount === 0 && isOpened) {
139 if (!socketCloseTimer) { 183 if (!socketCloseTimer) {
140 - socketCloseTimer = $timeout(closeSocket, 90000, false); 184 + socketCloseTimer = $timeout(closeSocket, WS_IDLE_TIMEOUT, false);
141 } 185 }
142 } 186 }
143 } 187 }
144 188
145 function tryOpenSocket () { 189 function tryOpenSocket () {
  190 + isActive = true;
146 if (!isOpened && !isOpening) { 191 if (!isOpened && !isOpening) {
147 isOpening = true; 192 isOpening = true;
148 - dataStream = $websocket(telemetryUri + '?token=' + userService.getJwtToken());  
149 - dataStream.onError(onError);  
150 - dataStream.onOpen(onOpen);  
151 - dataStream.onClose(onClose);  
152 - dataStream.onMessage(onMessage); 193 + if (userService.isJwtTokenValid()) {
  194 + openSocket(userService.getJwtToken());
  195 + } else {
  196 + userService.refreshJwtToken().then(function success() {
  197 + openSocket(userService.getJwtToken());
  198 + }, function fail() {
  199 + isOpening = false;
  200 + $rootScope.$broadcast('unauthenticated');
  201 + });
  202 + }
153 } 203 }
154 if (socketCloseTimer) { 204 if (socketCloseTimer) {
155 $timeout.cancel(socketCloseTimer); 205 $timeout.cancel(socketCloseTimer);
  206 + socketCloseTimer = null;
156 } 207 }
157 } 208 }
158 209
  210 + function openSocket(token) {
  211 + dataStream = $websocket(telemetryUri + '?token=' + token);
  212 + dataStream.onError(onError);
  213 + dataStream.onOpen(onOpen);
  214 + dataStream.onClose(onClose);
  215 + dataStream.onMessage(onMessage);
  216 + }
  217 +
159 function closeSocket() { 218 function closeSocket() {
  219 + isActive = false;
160 if (isOpened) { 220 if (isOpened) {
161 dataStream.close(); 221 dataStream.close();
162 } 222 }
163 } 223 }
  224 +
  225 + function reset(closeSocket) {
  226 + if (socketCloseTimer) {
  227 + $timeout.cancel(socketCloseTimer);
  228 + socketCloseTimer = null;
  229 + }
  230 + lastCmdId = 0;
  231 + subscribers = {};
  232 + subscribersCount = 0;
  233 + cmdsWrapper.tsSubCmds = [];
  234 + cmdsWrapper.historyCmds = [];
  235 + cmdsWrapper.attrSubCmds = [];
  236 + if (closeSocket) {
  237 + closeSocket();
  238 + }
  239 + }
164 } 240 }
@@ -147,6 +147,12 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS @@ -147,6 +147,12 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
147 scope.subscriptionId = newSubscriptionId; 147 scope.subscriptionId = newSubscriptionId;
148 } 148 }
149 149
  150 + scope.$on('$destroy', function() {
  151 + if (scope.subscriptionId) {
  152 + deviceService.unsubscribeForDeviceAttributes(scope.subscriptionId);
  153 + }
  154 + });
  155 +
150 scope.editAttribute = function($event, attribute) { 156 scope.editAttribute = function($event, attribute) {
151 if (!scope.attributeScope.clientSide) { 157 if (!scope.attributeScope.clientSide) {
152 $event.stopPropagation(); 158 $event.stopPropagation();
@@ -60,6 +60,11 @@ @@ -60,6 +60,11 @@
60 </div> 60 </div>
61 </md-input-container> 61 </md-input-container>
62 <md-input-container class="md-block"> 62 <md-input-container class="md-block">
  63 + <md-checkbox ng-disabled="loading || !isEdit" flex aria-label="{{ 'device.is-gateway' | translate }}"
  64 + ng-model="device.additionalInfo.gateway">{{ 'device.is-gateway' | translate }}
  65 + </md-checkbox>
  66 + </md-input-container>
  67 + <md-input-container class="md-block">
63 <label translate>device.description</label> 68 <label translate>device.description</label>
64 <textarea ng-model="device.additionalInfo.description" rows="2"></textarea> 69 <textarea ng-model="device.additionalInfo.description" rows="2"></textarea>
65 </md-input-container> 70 </md-input-container>
@@ -32,11 +32,13 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl @@ -32,11 +32,13 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl
32 32
33 scope.$watch('device', function(newVal) { 33 scope.$watch('device', function(newVal) {
34 if (newVal) { 34 if (newVal) {
35 - deviceService.getDeviceCredentials(scope.device.id.id).then(  
36 - function success(credentials) {  
37 - scope.deviceCredentials = credentials;  
38 - }  
39 - ); 35 + if (scope.device.id) {
  36 + deviceService.getDeviceCredentials(scope.device.id.id).then(
  37 + function success(credentials) {
  38 + scope.deviceCredentials = credentials;
  39 + }
  40 + );
  41 + }
40 if (scope.device.customerId && scope.device.customerId.id !== types.id.nullUid) { 42 if (scope.device.customerId && scope.device.customerId.id !== types.id.nullUid) {
41 scope.isAssignedToCustomer = true; 43 scope.isAssignedToCustomer = true;
42 customerService.getCustomer(scope.device.customerId.id).then( 44 customerService.getCustomer(scope.device.customerId.id).then(
@@ -323,7 +323,8 @@ @@ -323,7 +323,8 @@
323 "accessTokenCopiedMessage": "Device access token has been copied to clipboard", 323 "accessTokenCopiedMessage": "Device access token has been copied to clipboard",
324 "assignedToCustomer": "Assigned to customer", 324 "assignedToCustomer": "Assigned to customer",
325 "unable-delete-device-alias-title": "Unable to delete device alias", 325 "unable-delete-device-alias-title": "Unable to delete device alias",
326 - "unable-delete-device-alias-text": "Device alias '{{deviceAlias}}' can't be deleted as it used by the following widget(s):<br/>{{widgetsList}}" 326 + "unable-delete-device-alias-text": "Device alias '{{deviceAlias}}' can't be deleted as it used by the following widget(s):<br/>{{widgetsList}}",
  327 + "is-gateway": "Is gateway"
327 }, 328 },
328 "dialog": { 329 "dialog": {
329 "close": "Close dialog" 330 "close": "Close dialog"