diff --git a/ui/package.json b/ui/package.json index 9a12bb596a..fd69493161 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard", "private": true, - "version": "1.0.1", + "version": "1.1.0", "description": "Thingsboard UI", "licenses": [ { diff --git a/ui/server.js b/ui/server.js index 4987bd2167..1f1d82b138 100644 --- a/ui/server.js +++ b/ui/server.js @@ -52,6 +52,11 @@ const apiProxy = httpProxy.createProxyServer({ } }); +apiProxy.on('error', function (err, req, res) { + console.warn('API proxy error: ' + err); + res.end('Error.'); +}); + console.info(`Forwarding API requests to http://${forwardHost}:${forwardPort}`); app.all('/api/*', (req, res) => { diff --git a/ui/src/app/api/datasource.service.js b/ui/src/app/api/datasource.service.js index 6a8ae64d76..a519d8bac2 100644 --- a/ui/src/app/api/datasource.service.js +++ b/ui/src/app/api/datasource.service.js @@ -256,6 +256,9 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic type: types.dataKeyType.timeseries, onData: function (data) { onData(data, types.dataKeyType.timeseries); + }, + onReconnected: function() { + onReconnected(); } }; @@ -278,6 +281,9 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic type: types.dataKeyType.timeseries, onData: function (data) { onData(data, types.dataKeyType.timeseries); + }, + onReconnected: function() { + onReconnected(); } }; @@ -299,6 +305,9 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic type: types.dataKeyType.attribute, onData: function (data) { onData(data, types.dataKeyType.attribute); + }, + onReconnected: function() { + onReconnected(); } }; @@ -428,6 +437,25 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic } } + function onReconnected() { + if (datasourceType === types.datasourceType.device) { + for (var key in dataKeys) { + var dataKeysList = dataKeys[key]; + for (var i = 0; i < dataKeysList.length; i++) { + var dataKey = dataKeysList[i]; + var datasourceKey = key + '_' + i; + datasourceData[datasourceKey] = []; + for (var l in listeners) { + var listener = listeners[l]; + listener.dataUpdated(datasourceData[datasourceKey], + listener.datasourceIndex, + dataKey.index); + } + } + } + } + } + function onData(sourceData, type) { for (var keyName in sourceData) { var keyData = sourceData[keyName]; diff --git a/ui/src/app/api/device.service.js b/ui/src/app/api/device.service.js index 2edaeb1554..46c64da261 100644 --- a/ui/src/app/api/device.service.js +++ b/ui/src/app/api/device.service.js @@ -307,12 +307,12 @@ function DeviceService($http, $q, $filter, telemetryWebsocketService, types) { onSubscriptionData(data, subscriptionId); } }; - telemetryWebsocketService.subscribe(subscriber); deviceAttributesSubscription = { subscriber: subscriber, attributes: null } deviceAttributesSubscriptionMap[subscriptionId] = deviceAttributesSubscription; + telemetryWebsocketService.subscribe(subscriber); } return subscriptionId; } diff --git a/ui/src/app/api/telemetry-websocket.service.js b/ui/src/app/api/telemetry-websocket.service.js index dde4c627e0..99a7c80d38 100644 --- a/ui/src/app/api/telemetry-websocket.service.js +++ b/ui/src/app/api/telemetry-websocket.service.js @@ -20,11 +20,17 @@ export default angular.module('thingsboard.api.telemetryWebsocket', [thingsboard .factory('telemetryWebsocketService', TelemetryWebsocketService) .name; +const RECONNECT_INTERVAL = 5000; +const WS_IDLE_TIMEOUT = 90000; + /*@ngInject*/ -function TelemetryWebsocketService($websocket, $timeout, $window, types, userService) { +function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, types, userService) { var isOpening = false, isOpened = false, + isActive = false, + isReconnect = false, + reconnectSubscribers = [], lastCmdId = 0, subscribers = {}, subscribersCount = 0, @@ -36,7 +42,8 @@ function TelemetryWebsocketService($websocket, $timeout, $window, types, userSer telemetryUri, dataStream, location = $window.location, - socketCloseTimer; + socketCloseTimer, + reconnectTimer; if (location.protocol === "https:") { telemetryUri = "wss:"; @@ -46,11 +53,18 @@ function TelemetryWebsocketService($websocket, $timeout, $window, types, userSer telemetryUri += "//" + location.hostname + ":" + location.port; telemetryUri += "/api/ws/plugins/telemetry"; + var service = { subscribe: subscribe, unsubscribe: unsubscribe } + $rootScope.telemetryWsLogoutHandle = $rootScope.$on('unauthenticated', function (event, doLogout) { + if (doLogout) { + reset(true); + } + }); + return service; function publishCommands () { @@ -74,12 +88,42 @@ function TelemetryWebsocketService($websocket, $timeout, $window, types, userSer function onOpen () { isOpening = false; isOpened = true; - publishCommands(); + if (reconnectTimer) { + $timeout.cancel(reconnectTimer); + reconnectTimer = null; + } + if (isReconnect) { + isReconnect = false; + for (var r in reconnectSubscribers) { + var reconnectSubscriber = reconnectSubscribers[r]; + if (reconnectSubscriber.onReconnected) { + reconnectSubscriber.onReconnected(); + } + subscribe(reconnectSubscriber); + } + reconnectSubscribers = []; + } else { + publishCommands(); + } } function onClose () { isOpening = false; isOpened = false; + if (isActive) { + if (!isReconnect) { + reconnectSubscribers = []; + for (var id in subscribers) { + reconnectSubscribers.push(subscribers[id]); + } + reset(false); + isReconnect = true; + } + if (reconnectTimer) { + $timeout.cancel(reconnectTimer); + } + reconnectTimer = $timeout(tryOpenSocket, RECONNECT_INTERVAL, false); + } } function onMessage (message) { @@ -137,28 +181,60 @@ function TelemetryWebsocketService($websocket, $timeout, $window, types, userSer function checkToClose () { if (subscribersCount === 0 && isOpened) { if (!socketCloseTimer) { - socketCloseTimer = $timeout(closeSocket, 90000, false); + socketCloseTimer = $timeout(closeSocket, WS_IDLE_TIMEOUT, false); } } } function tryOpenSocket () { + isActive = true; if (!isOpened && !isOpening) { isOpening = true; - dataStream = $websocket(telemetryUri + '?token=' + userService.getJwtToken()); - dataStream.onError(onError); - dataStream.onOpen(onOpen); - dataStream.onClose(onClose); - dataStream.onMessage(onMessage); + if (userService.isJwtTokenValid()) { + openSocket(userService.getJwtToken()); + } else { + userService.refreshJwtToken().then(function success() { + openSocket(userService.getJwtToken()); + }, function fail() { + isOpening = false; + $rootScope.$broadcast('unauthenticated'); + }); + } } if (socketCloseTimer) { $timeout.cancel(socketCloseTimer); + socketCloseTimer = null; } } + function openSocket(token) { + dataStream = $websocket(telemetryUri + '?token=' + token); + dataStream.onError(onError); + dataStream.onOpen(onOpen); + dataStream.onClose(onClose); + dataStream.onMessage(onMessage); + } + function closeSocket() { + isActive = false; if (isOpened) { dataStream.close(); } } + + function reset(closeSocket) { + if (socketCloseTimer) { + $timeout.cancel(socketCloseTimer); + socketCloseTimer = null; + } + lastCmdId = 0; + subscribers = {}; + subscribersCount = 0; + cmdsWrapper.tsSubCmds = []; + cmdsWrapper.historyCmds = []; + cmdsWrapper.attrSubCmds = []; + if (closeSocket) { + closeSocket(); + } + } } diff --git a/ui/src/app/device/attribute/attribute-table.directive.js b/ui/src/app/device/attribute/attribute-table.directive.js index bafe2b2ccb..a3a43902b2 100644 --- a/ui/src/app/device/attribute/attribute-table.directive.js +++ b/ui/src/app/device/attribute/attribute-table.directive.js @@ -147,6 +147,12 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS scope.subscriptionId = newSubscriptionId; } + scope.$on('$destroy', function() { + if (scope.subscriptionId) { + deviceService.unsubscribeForDeviceAttributes(scope.subscriptionId); + } + }); + scope.editAttribute = function($event, attribute) { if (!scope.attributeScope.clientSide) { $event.stopPropagation(); diff --git a/ui/src/app/device/device-fieldset.tpl.html b/ui/src/app/device/device-fieldset.tpl.html index 3a7ab9afec..99cedbdada 100644 --- a/ui/src/app/device/device-fieldset.tpl.html +++ b/ui/src/app/device/device-fieldset.tpl.html @@ -59,6 +59,11 @@
device.name-required
+ + {{ 'device.is-gateway' | translate }} + + diff --git a/ui/src/app/device/device.directive.js b/ui/src/app/device/device.directive.js index 5dda0156c8..9c927aebae 100644 --- a/ui/src/app/device/device.directive.js +++ b/ui/src/app/device/device.directive.js @@ -32,11 +32,13 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl scope.$watch('device', function(newVal) { if (newVal) { - deviceService.getDeviceCredentials(scope.device.id.id).then( - function success(credentials) { - scope.deviceCredentials = credentials; - } - ); + if (scope.device.id) { + deviceService.getDeviceCredentials(scope.device.id.id).then( + function success(credentials) { + scope.deviceCredentials = credentials; + } + ); + } if (scope.device.customerId && scope.device.customerId.id !== types.id.nullUid) { scope.isAssignedToCustomer = true; customerService.getCustomer(scope.device.customerId.id).then( diff --git a/ui/src/locale/en_US.json b/ui/src/locale/en_US.json index f042993493..cf1a97bd68 100644 --- a/ui/src/locale/en_US.json +++ b/ui/src/locale/en_US.json @@ -323,7 +323,8 @@ "accessTokenCopiedMessage": "Device access token has been copied to clipboard", "assignedToCustomer": "Assigned to customer", "unable-delete-device-alias-title": "Unable to delete device alias", - "unable-delete-device-alias-text": "Device alias '{{deviceAlias}}' can't be deleted as it used by the following widget(s):
{{widgetsList}}" + "unable-delete-device-alias-text": "Device alias '{{deviceAlias}}' can't be deleted as it used by the following widget(s):
{{widgetsList}}", + "is-gateway": "Is gateway" }, "dialog": { "close": "Close dialog"