Commit 7c6cdb148c3c1b54ee31b477667144675bb45840
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.
Showing
9 changed files
with
140 additions
and
17 deletions
@@ -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" |