committed by
GitHub
69 changed files with 3290 additions and 1054 deletions
@ -0,0 +1,34 @@ |
|||
/** |
|||
* Copyright © 2016-2017 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. |
|||
*/ |
|||
package org.thingsboard.server.service.security.auth.rest; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonCreator; |
|||
import com.fasterxml.jackson.annotation.JsonProperty; |
|||
|
|||
public class PublicLoginRequest { |
|||
|
|||
private String publicId; |
|||
|
|||
@JsonCreator |
|||
public PublicLoginRequest(@JsonProperty("publicId") String publicId) { |
|||
this.publicId = publicId; |
|||
} |
|||
|
|||
public String getPublicId() { |
|||
return publicId; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,96 @@ |
|||
/** |
|||
* Copyright © 2016-2017 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. |
|||
*/ |
|||
package org.thingsboard.server.service.security.auth.rest; |
|||
|
|||
import com.fasterxml.jackson.databind.ObjectMapper; |
|||
import org.apache.commons.lang3.StringUtils; |
|||
import org.slf4j.Logger; |
|||
import org.slf4j.LoggerFactory; |
|||
import org.springframework.http.HttpMethod; |
|||
import org.springframework.security.authentication.AuthenticationServiceException; |
|||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; |
|||
import org.springframework.security.core.Authentication; |
|||
import org.springframework.security.core.AuthenticationException; |
|||
import org.springframework.security.core.context.SecurityContextHolder; |
|||
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; |
|||
import org.springframework.security.web.authentication.AuthenticationFailureHandler; |
|||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler; |
|||
import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException; |
|||
import org.thingsboard.server.service.security.model.UserPrincipal; |
|||
|
|||
import javax.servlet.FilterChain; |
|||
import javax.servlet.ServletException; |
|||
import javax.servlet.http.HttpServletRequest; |
|||
import javax.servlet.http.HttpServletResponse; |
|||
import java.io.IOException; |
|||
|
|||
public class RestPublicLoginProcessingFilter extends AbstractAuthenticationProcessingFilter { |
|||
private static Logger logger = LoggerFactory.getLogger(RestPublicLoginProcessingFilter.class); |
|||
|
|||
private final AuthenticationSuccessHandler successHandler; |
|||
private final AuthenticationFailureHandler failureHandler; |
|||
|
|||
private final ObjectMapper objectMapper; |
|||
|
|||
public RestPublicLoginProcessingFilter(String defaultProcessUrl, AuthenticationSuccessHandler successHandler, |
|||
AuthenticationFailureHandler failureHandler, ObjectMapper mapper) { |
|||
super(defaultProcessUrl); |
|||
this.successHandler = successHandler; |
|||
this.failureHandler = failureHandler; |
|||
this.objectMapper = mapper; |
|||
} |
|||
|
|||
@Override |
|||
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) |
|||
throws AuthenticationException, IOException, ServletException { |
|||
if (!HttpMethod.POST.name().equals(request.getMethod())) { |
|||
if(logger.isDebugEnabled()) { |
|||
logger.debug("Authentication method not supported. Request method: " + request.getMethod()); |
|||
} |
|||
throw new AuthMethodNotSupportedException("Authentication method not supported"); |
|||
} |
|||
|
|||
PublicLoginRequest loginRequest; |
|||
try { |
|||
loginRequest = objectMapper.readValue(request.getReader(), PublicLoginRequest.class); |
|||
} catch (Exception e) { |
|||
throw new AuthenticationServiceException("Invalid public login request payload"); |
|||
} |
|||
|
|||
if (StringUtils.isBlank(loginRequest.getPublicId())) { |
|||
throw new AuthenticationServiceException("Public Id is not provided"); |
|||
} |
|||
|
|||
UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.PUBLIC_ID, loginRequest.getPublicId()); |
|||
|
|||
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, ""); |
|||
|
|||
return this.getAuthenticationManager().authenticate(token); |
|||
} |
|||
|
|||
@Override |
|||
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, |
|||
Authentication authResult) throws IOException, ServletException { |
|||
successHandler.onAuthenticationSuccess(request, response, authResult); |
|||
} |
|||
|
|||
@Override |
|||
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, |
|||
AuthenticationException failed) throws IOException, ServletException { |
|||
SecurityContextHolder.clearContext(); |
|||
failureHandler.onAuthenticationFailure(request, response, failed); |
|||
} |
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
/** |
|||
* Copyright © 2016-2017 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. |
|||
*/ |
|||
|
|||
package org.thingsboard.server.service.security.model; |
|||
|
|||
public class UserPrincipal { |
|||
|
|||
private final Type type; |
|||
private final String value; |
|||
|
|||
public UserPrincipal(Type type, String value) { |
|||
this.type = type; |
|||
this.value = value; |
|||
} |
|||
|
|||
public Type getType() { |
|||
return type; |
|||
} |
|||
|
|||
public String getValue() { |
|||
return value; |
|||
} |
|||
|
|||
public enum Type { |
|||
USER_NAME, |
|||
PUBLIC_ID |
|||
} |
|||
|
|||
} |
|||
File diff suppressed because one or more lines are too long
@ -0,0 +1,647 @@ |
|||
/* |
|||
* Copyright © 2016-2017 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. |
|||
*/ |
|||
|
|||
/* |
|||
options = { |
|||
type, |
|||
targetDeviceAliasIds, // RPC
|
|||
targetDeviceIds, // RPC
|
|||
datasources, |
|||
timeWindowConfig, |
|||
useDashboardTimewindow, |
|||
legendConfig, |
|||
decimals, |
|||
units, |
|||
callbacks |
|||
} |
|||
*/ |
|||
|
|||
export default class Subscription { |
|||
constructor(subscriptionContext, options) { |
|||
|
|||
this.ctx = subscriptionContext; |
|||
this.type = options.type; |
|||
this.callbacks = options.callbacks; |
|||
this.id = this.ctx.utils.guid(); |
|||
this.cafs = {}; |
|||
this.registrations = []; |
|||
|
|||
if (this.type === this.ctx.types.widgetType.rpc.value) { |
|||
this.callbacks.rpcStateChanged = this.callbacks.rpcStateChanged || function(){}; |
|||
this.callbacks.onRpcSuccess = this.callbacks.onRpcSuccess || function(){}; |
|||
this.callbacks.onRpcFailed = this.callbacks.onRpcFailed || function(){}; |
|||
this.callbacks.onRpcErrorCleared = this.callbacks.onRpcErrorCleared || function(){}; |
|||
|
|||
this.targetDeviceAliasIds = options.targetDeviceAliasIds; |
|||
this.targetDeviceIds = options.targetDeviceIds; |
|||
|
|||
this.targetDeviceAliasId = null; |
|||
this.targetDeviceId = null; |
|||
|
|||
this.rpcRejection = null; |
|||
this.rpcErrorText = null; |
|||
this.rpcEnabled = false; |
|||
this.executingRpcRequest = false; |
|||
this.executingPromises = []; |
|||
this.initRpc(); |
|||
} else { |
|||
this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || function(){}; |
|||
this.callbacks.onDataUpdateError = this.callbacks.onDataUpdateError || function(){}; |
|||
this.callbacks.dataLoading = this.callbacks.dataLoading || function(){}; |
|||
this.callbacks.legendDataUpdated = this.callbacks.legendDataUpdated || function(){}; |
|||
this.callbacks.timeWindowUpdated = this.callbacks.timeWindowUpdated || function(){}; |
|||
|
|||
this.datasources = options.datasources; |
|||
this.datasourceListeners = []; |
|||
this.data = []; |
|||
this.hiddenData = []; |
|||
this.originalTimewindow = null; |
|||
this.timeWindow = { |
|||
stDiff: this.ctx.stDiff |
|||
} |
|||
this.useDashboardTimewindow = options.useDashboardTimewindow; |
|||
|
|||
if (this.useDashboardTimewindow) { |
|||
this.timeWindowConfig = angular.copy(options.dashboardTimewindow); |
|||
} else { |
|||
this.timeWindowConfig = angular.copy(options.timeWindowConfig); |
|||
} |
|||
|
|||
this.subscriptionTimewindow = null; |
|||
|
|||
this.units = options.units || ''; |
|||
this.decimals = angular.isDefined(options.decimals) ? options.decimals : 2; |
|||
|
|||
this.loadingData = false; |
|||
|
|||
if (options.legendConfig) { |
|||
this.legendConfig = options.legendConfig; |
|||
this.legendData = { |
|||
keys: [], |
|||
data: [] |
|||
}; |
|||
this.displayLegend = true; |
|||
} else { |
|||
this.displayLegend = false; |
|||
} |
|||
this.caulculateLegendData = this.displayLegend && |
|||
this.type === this.ctx.types.widgetType.timeseries.value && |
|||
(this.legendConfig.showMin === true || |
|||
this.legendConfig.showMax === true || |
|||
this.legendConfig.showAvg === true || |
|||
this.legendConfig.showTotal === true); |
|||
this.initDataSubscription(); |
|||
} |
|||
} |
|||
|
|||
initDataSubscription() { |
|||
var dataIndex = 0; |
|||
for (var i = 0; i < this.datasources.length; i++) { |
|||
var datasource = this.datasources[i]; |
|||
for (var a = 0; a < datasource.dataKeys.length; a++) { |
|||
var dataKey = datasource.dataKeys[a]; |
|||
dataKey.pattern = angular.copy(dataKey.label); |
|||
var datasourceData = { |
|||
datasource: datasource, |
|||
dataKey: dataKey, |
|||
data: [] |
|||
}; |
|||
this.data.push(datasourceData); |
|||
this.hiddenData.push({data: []}); |
|||
if (this.displayLegend) { |
|||
var legendKey = { |
|||
dataKey: dataKey, |
|||
dataIndex: dataIndex++ |
|||
}; |
|||
this.legendData.keys.push(legendKey); |
|||
var legendKeyData = { |
|||
min: null, |
|||
max: null, |
|||
avg: null, |
|||
total: null, |
|||
hidden: false |
|||
}; |
|||
this.legendData.data.push(legendKeyData); |
|||
} |
|||
} |
|||
} |
|||
|
|||
var subscription = this; |
|||
var registration; |
|||
|
|||
if (this.displayLegend) { |
|||
this.legendData.keys = this.ctx.$filter('orderBy')(this.legendData.keys, '+label'); |
|||
registration = this.ctx.$scope.$watch( |
|||
function() { |
|||
return subscription.legendData.data; |
|||
}, |
|||
function (newValue, oldValue) { |
|||
for(var i = 0; i < newValue.length; i++) { |
|||
if(newValue[i].hidden != oldValue[i].hidden) { |
|||
subscription.updateDataVisibility(i); |
|||
} |
|||
} |
|||
}, true); |
|||
this.registrations.push(registration); |
|||
} |
|||
|
|||
if (this.type === this.ctx.types.widgetType.timeseries.value) { |
|||
if (this.useDashboardTimewindow) { |
|||
registration = this.ctx.$scope.$on('dashboardTimewindowChanged', function (event, newDashboardTimewindow) { |
|||
if (!angular.equals(subscription.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) { |
|||
subscription.timeWindowConfig = angular.copy(newDashboardTimewindow); |
|||
subscription.unsubscribe(); |
|||
subscription.subscribe(); |
|||
} |
|||
}); |
|||
this.registrations.push(registration); |
|||
} else { |
|||
registration = this.ctx.$scope.$watch(function () { |
|||
return subscription.timeWindowConfig; |
|||
}, function (newTimewindow, prevTimewindow) { |
|||
if (!angular.equals(newTimewindow, prevTimewindow)) { |
|||
subscription.unsubscribe(); |
|||
subscription.subscribe(); |
|||
} |
|||
}); |
|||
this.registrations.push(registration); |
|||
} |
|||
} |
|||
|
|||
registration = this.ctx.$scope.$on('deviceAliasListChanged', function () { |
|||
subscription.checkSubscriptions(); |
|||
}); |
|||
|
|||
this.registrations.push(registration); |
|||
} |
|||
|
|||
initRpc() { |
|||
|
|||
if (this.targetDeviceAliasIds && this.targetDeviceAliasIds.length > 0) { |
|||
this.targetDeviceAliasId = this.targetDeviceAliasIds[0]; |
|||
if (this.ctx.aliasesInfo.deviceAliases[this.targetDeviceAliasId]) { |
|||
this.targetDeviceId = this.ctx.aliasesInfo.deviceAliases[this.targetDeviceAliasId].deviceId; |
|||
} |
|||
var subscription = this; |
|||
var registration = this.ctx.$scope.$on('deviceAliasListChanged', function () { |
|||
var deviceId = null; |
|||
if (subscription.ctx.aliasesInfo.deviceAliases[subscription.targetDeviceAliasId]) { |
|||
deviceId = subscription.ctx.aliasesInfo.deviceAliases[subscription.targetDeviceAliasId].deviceId; |
|||
} |
|||
if (!angular.equals(deviceId, subscription.targetDeviceId)) { |
|||
subscription.targetDeviceId = deviceId; |
|||
if (subscription.targetDeviceId) { |
|||
subscription.rpcEnabled = true; |
|||
} else { |
|||
subscription.rpcEnabled = subscription.ctx.$scope.widgetEditMode ? true : false; |
|||
} |
|||
subscription.callbacks.rpcStateChanged(subscription); |
|||
} |
|||
}); |
|||
this.registrations.push(registration); |
|||
} else if (this.targetDeviceIds && this.targetDeviceIds.length > 0) { |
|||
this.targetDeviceId = this.targetDeviceIds[0]; |
|||
} |
|||
|
|||
if (this.targetDeviceId) { |
|||
this.rpcEnabled = true; |
|||
} else { |
|||
this.rpcEnabled = this.ctx.$scope.widgetEditMode ? true : false; |
|||
} |
|||
this.callbacks.rpcStateChanged(this); |
|||
} |
|||
|
|||
clearRpcError() { |
|||
this.rpcRejection = null; |
|||
this.rpcErrorText = null; |
|||
this.callbacks.onRpcErrorCleared(this); |
|||
} |
|||
|
|||
sendOneWayCommand(method, params, timeout) { |
|||
return this.sendCommand(true, method, params, timeout); |
|||
} |
|||
|
|||
sendTwoWayCommand(method, params, timeout) { |
|||
return this.sendCommand(false, method, params, timeout); |
|||
} |
|||
|
|||
sendCommand(oneWayElseTwoWay, method, params, timeout) { |
|||
if (!this.rpcEnabled) { |
|||
return this.ctx.$q.reject(); |
|||
} |
|||
|
|||
if (this.rpcRejection && this.rpcRejection.status !== 408) { |
|||
this.rpcRejection = null; |
|||
this.rpcErrorText = null; |
|||
this.callbacks.onRpcErrorCleared(this); |
|||
} |
|||
|
|||
var subscription = this; |
|||
|
|||
var requestBody = { |
|||
method: method, |
|||
params: params |
|||
}; |
|||
|
|||
if (timeout && timeout > 0) { |
|||
requestBody.timeout = timeout; |
|||
} |
|||
|
|||
var deferred = this.ctx.$q.defer(); |
|||
this.executingRpcRequest = true; |
|||
this.callbacks.rpcStateChanged(this); |
|||
if (this.ctx.$scope.widgetEditMode) { |
|||
this.ctx.$timeout(function() { |
|||
subscription.executingRpcRequest = false; |
|||
subscription.callbacks.rpcStateChanged(subscription); |
|||
if (oneWayElseTwoWay) { |
|||
deferred.resolve(); |
|||
} else { |
|||
deferred.resolve(requestBody); |
|||
} |
|||
}, 500); |
|||
} else { |
|||
this.executingPromises.push(deferred.promise); |
|||
var targetSendFunction = oneWayElseTwoWay ? this.ctx.deviceService.sendOneWayRpcCommand : this.ctx.deviceService.sendTwoWayRpcCommand; |
|||
targetSendFunction(this.targetDeviceId, requestBody).then( |
|||
function success(responseBody) { |
|||
subscription.rpcRejection = null; |
|||
subscription.rpcErrorText = null; |
|||
var index = subscription.executingPromises.indexOf(deferred.promise); |
|||
if (index >= 0) { |
|||
subscription.executingPromises.splice( index, 1 ); |
|||
} |
|||
subscription.executingRpcRequest = subscription.executingPromises.length > 0; |
|||
subscription.callbacks.onRpcSuccess(subscription); |
|||
deferred.resolve(responseBody); |
|||
}, |
|||
function fail(rejection) { |
|||
var index = subscription.executingPromises.indexOf(deferred.promise); |
|||
if (index >= 0) { |
|||
subscription.executingPromises.splice( index, 1 ); |
|||
} |
|||
subscription.executingRpcRequest = subscription.executingPromises.length > 0; |
|||
subscription.callbacks.rpcStateChanged(subscription); |
|||
if (!subscription.executingRpcRequest || rejection.status === 408) { |
|||
subscription.rpcRejection = rejection; |
|||
if (rejection.status === 408) { |
|||
subscription.rpcErrorText = 'Device is offline.'; |
|||
} else { |
|||
subscription.rpcErrorText = 'Error : ' + rejection.status + ' - ' + rejection.statusText; |
|||
if (rejection.data && rejection.data.length > 0) { |
|||
subscription.rpcErrorText += '</br>'; |
|||
subscription.rpcErrorText += rejection.data; |
|||
} |
|||
} |
|||
subscription.callbacks.onRpcFailed(subscription); |
|||
} |
|||
deferred.reject(rejection); |
|||
} |
|||
); |
|||
} |
|||
return deferred.promise; |
|||
} |
|||
|
|||
updateDataVisibility(index) { |
|||
var hidden = this.legendData.data[index].hidden; |
|||
if (hidden) { |
|||
this.hiddenData[index].data = this.data[index].data; |
|||
this.data[index].data = []; |
|||
} else { |
|||
this.data[index].data = this.hiddenData[index].data; |
|||
this.hiddenData[index].data = []; |
|||
} |
|||
this.onDataUpdated(); |
|||
} |
|||
|
|||
onDataUpdated(apply) { |
|||
if (this.cafs['dataUpdated']) { |
|||
this.cafs['dataUpdated'](); |
|||
this.cafs['dataUpdated'] = null; |
|||
} |
|||
var subscription = this; |
|||
this.cafs['dataUpdated'] = this.ctx.tbRaf(function() { |
|||
try { |
|||
subscription.callbacks.onDataUpdated(this, apply); |
|||
} catch (e) { |
|||
subscription.callbacks.onDataUpdateError(this, e); |
|||
} |
|||
}); |
|||
if (apply) { |
|||
this.ctx.$scope.$digest(); |
|||
} |
|||
} |
|||
|
|||
updateTimewindowConfig(newTimewindow) { |
|||
this.timeWindowConfig = newTimewindow; |
|||
} |
|||
|
|||
onResetTimewindow() { |
|||
if (this.useDashboardTimewindow) { |
|||
this.ctx.dashboardTimewindowApi.onResetTimewindow(); |
|||
} else { |
|||
if (this.originalTimewindow) { |
|||
this.timeWindowConfig = angular.copy(this.originalTimewindow); |
|||
this.originalTimewindow = null; |
|||
this.callbacks.timeWindowUpdated(this, this.timeWindowConfig); |
|||
} |
|||
} |
|||
} |
|||
|
|||
onUpdateTimewindow(startTimeMs, endTimeMs) { |
|||
if (this.useDashboardTimewindow) { |
|||
this.ctx.dashboardTimewindowApi.onUpdateTimewindow(startTimeMs, endTimeMs); |
|||
} else { |
|||
if (!this.originalTimewindow) { |
|||
this.originalTimewindow = angular.copy(this.timeWindowConfig); |
|||
} |
|||
this.timeWindowConfig = this.ctx.timeService.toHistoryTimewindow(this.timeWindowConfig, startTimeMs, endTimeMs); |
|||
this.callbacks.timeWindowUpdated(this, this.timeWindowConfig); |
|||
} |
|||
} |
|||
|
|||
notifyDataLoading() { |
|||
this.loadingData = true; |
|||
this.callbacks.dataLoading(this); |
|||
} |
|||
|
|||
notifyDataLoaded() { |
|||
this.loadingData = false; |
|||
this.callbacks.dataLoading(this); |
|||
} |
|||
|
|||
updateTimewindow() { |
|||
this.timeWindow.interval = this.subscriptionTimewindow.aggregation.interval || 1000; |
|||
if (this.subscriptionTimewindow.realtimeWindowMs) { |
|||
this.timeWindow.maxTime = (new Date).getTime() + this.timeWindow.stDiff; |
|||
this.timeWindow.minTime = this.timeWindow.maxTime - this.subscriptionTimewindow.realtimeWindowMs; |
|||
} else if (this.subscriptionTimewindow.fixedWindow) { |
|||
this.timeWindow.maxTime = this.subscriptionTimewindow.fixedWindow.endTimeMs; |
|||
this.timeWindow.minTime = this.subscriptionTimewindow.fixedWindow.startTimeMs; |
|||
} |
|||
} |
|||
|
|||
updateRealtimeSubscription(subscriptionTimewindow) { |
|||
if (subscriptionTimewindow) { |
|||
this.subscriptionTimewindow = subscriptionTimewindow; |
|||
} else { |
|||
this.subscriptionTimewindow = |
|||
this.ctx.timeService.createSubscriptionTimewindow( |
|||
this.timeWindowConfig, |
|||
this.timeWindow.stDiff); |
|||
} |
|||
this.updateTimewindow(); |
|||
return this.subscriptionTimewindow; |
|||
} |
|||
|
|||
dataUpdated(sourceData, datasourceIndex, dataKeyIndex, apply) { |
|||
this.notifyDataLoaded(); |
|||
var update = true; |
|||
var currentData; |
|||
if (this.displayLegend && this.legendData.data[datasourceIndex + dataKeyIndex].hidden) { |
|||
currentData = this.hiddenData[datasourceIndex + dataKeyIndex]; |
|||
} else { |
|||
currentData = this.data[datasourceIndex + dataKeyIndex]; |
|||
} |
|||
if (this.type === this.ctx.types.widgetType.latest.value) { |
|||
var prevData = currentData.data; |
|||
if (prevData && prevData[0] && prevData[0].length > 1 && sourceData.data.length > 0) { |
|||
var prevValue = prevData[0][1]; |
|||
if (prevValue === sourceData.data[0][1]) { |
|||
update = false; |
|||
} |
|||
} |
|||
} |
|||
if (update) { |
|||
if (this.subscriptionTimewindow && this.subscriptionTimewindow.realtimeWindowMs) { |
|||
this.updateTimewindow(); |
|||
} |
|||
currentData.data = sourceData.data; |
|||
if (this.caulculateLegendData) { |
|||
this.updateLegend(datasourceIndex + dataKeyIndex, sourceData.data, apply); |
|||
} |
|||
this.onDataUpdated(apply); |
|||
} |
|||
} |
|||
|
|||
updateLegend(dataIndex, data, apply) { |
|||
var legendKeyData = this.legendData.data[dataIndex]; |
|||
if (this.legendConfig.showMin) { |
|||
legendKeyData.min = this.ctx.widgetUtils.formatValue(calculateMin(data), this.decimals, this.units); |
|||
} |
|||
if (this.legendConfig.showMax) { |
|||
legendKeyData.max = this.ctx.widgetUtils.formatValue(calculateMax(data), this.decimals, this.units); |
|||
} |
|||
if (this.legendConfig.showAvg) { |
|||
legendKeyData.avg = this.ctx.widgetUtils.formatValue(calculateAvg(data), this.decimals, this.units); |
|||
} |
|||
if (this.legendConfig.showTotal) { |
|||
legendKeyData.total = this.ctx.widgetUtils.formatValue(calculateTotal(data), this.decimals, this.units); |
|||
} |
|||
this.callbacks.legendDataUpdated(this, apply !== false); |
|||
} |
|||
|
|||
subscribe() { |
|||
if (this.type === this.ctx.types.widgetType.rpc.value) { |
|||
return; |
|||
} |
|||
this.notifyDataLoading(); |
|||
if (this.type === this.ctx.types.widgetType.timeseries.value && this.timeWindowConfig) { |
|||
this.updateRealtimeSubscription(); |
|||
if (this.subscriptionTimewindow.fixedWindow) { |
|||
this.onDataUpdated(); |
|||
} |
|||
} |
|||
var index = 0; |
|||
for (var i = 0; i < this.datasources.length; i++) { |
|||
var datasource = this.datasources[i]; |
|||
if (angular.isFunction(datasource)) |
|||
continue; |
|||
var deviceId = null; |
|||
if (datasource.type === this.ctx.types.datasourceType.device) { |
|||
var aliasName = null; |
|||
var deviceName = null; |
|||
if (datasource.deviceId) { |
|||
deviceId = datasource.deviceId; |
|||
datasource.name = datasource.deviceName; |
|||
aliasName = datasource.deviceName; |
|||
deviceName = datasource.deviceName; |
|||
} else if (datasource.deviceAliasId && this.ctx.aliasesInfo.deviceAliases[datasource.deviceAliasId]) { |
|||
deviceId = this.ctx.aliasesInfo.deviceAliases[datasource.deviceAliasId].deviceId; |
|||
datasource.name = this.ctx.aliasesInfo.deviceAliases[datasource.deviceAliasId].alias; |
|||
aliasName = this.ctx.aliasesInfo.deviceAliases[datasource.deviceAliasId].alias; |
|||
deviceName = ''; |
|||
var devicesInfo = this.ctx.aliasesInfo.deviceAliasesInfo[datasource.deviceAliasId]; |
|||
for (var d = 0; d < devicesInfo.length; d++) { |
|||
if (devicesInfo[d].id === deviceId) { |
|||
deviceName = devicesInfo[d].name; |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
} else { |
|||
datasource.name = datasource.name || this.ctx.types.datasourceType.function; |
|||
} |
|||
for (var dk = 0; dk < datasource.dataKeys.length; dk++) { |
|||
updateDataKeyLabel(datasource.dataKeys[dk], datasource.name, deviceName, aliasName); |
|||
} |
|||
|
|||
var subscription = this; |
|||
|
|||
var listener = { |
|||
subscriptionType: this.type, |
|||
subscriptionTimewindow: this.subscriptionTimewindow, |
|||
datasource: datasource, |
|||
deviceId: deviceId, |
|||
dataUpdated: function (data, datasourceIndex, dataKeyIndex, apply) { |
|||
subscription.dataUpdated(data, datasourceIndex, dataKeyIndex, apply); |
|||
}, |
|||
updateRealtimeSubscription: function () { |
|||
this.subscriptionTimewindow = subscription.updateRealtimeSubscription(); |
|||
return this.subscriptionTimewindow; |
|||
}, |
|||
setRealtimeSubscription: function (subscriptionTimewindow) { |
|||
subscription.updateRealtimeSubscription(angular.copy(subscriptionTimewindow)); |
|||
}, |
|||
datasourceIndex: index |
|||
}; |
|||
|
|||
for (var a = 0; a < datasource.dataKeys.length; a++) { |
|||
this.data[index + a].data = []; |
|||
} |
|||
|
|||
index += datasource.dataKeys.length; |
|||
|
|||
this.datasourceListeners.push(listener); |
|||
this.ctx.datasourceService.subscribeToDatasource(listener); |
|||
} |
|||
} |
|||
|
|||
unsubscribe() { |
|||
if (this.type !== this.ctx.types.widgetType.rpc.value) { |
|||
for (var i = 0; i < this.datasourceListeners.length; i++) { |
|||
var listener = this.datasourceListeners[i]; |
|||
this.ctx.datasourceService.unsubscribeFromDatasource(listener); |
|||
} |
|||
this.datasourceListeners = []; |
|||
} |
|||
} |
|||
|
|||
checkSubscriptions() { |
|||
var subscriptionsChanged = false; |
|||
for (var i = 0; i < this.datasourceListeners.length; i++) { |
|||
var listener = this.datasourceListeners[i]; |
|||
var deviceId = null; |
|||
var aliasName = null; |
|||
if (listener.datasource.type === this.ctx.types.datasourceType.device) { |
|||
if (listener.datasource.deviceAliasId && |
|||
this.ctx.aliasesInfo.deviceAliases[listener.datasource.deviceAliasId]) { |
|||
deviceId = this.ctx.aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].deviceId; |
|||
aliasName = this.ctx.aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].alias; |
|||
} |
|||
if (!angular.equals(deviceId, listener.deviceId) || |
|||
!angular.equals(aliasName, listener.datasource.name)) { |
|||
subscriptionsChanged = true; |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
if (subscriptionsChanged) { |
|||
this.unsubscribe(); |
|||
this.subscribe(); |
|||
} |
|||
} |
|||
|
|||
destroy() { |
|||
this.unsubscribe(); |
|||
for (var cafId in this.cafs) { |
|||
if (this.cafs[cafId]) { |
|||
this.cafs[cafId](); |
|||
this.cafs[cafId] = null; |
|||
} |
|||
} |
|||
this.registrations.forEach(function (registration) { |
|||
registration(); |
|||
}); |
|||
this.registrations = []; |
|||
} |
|||
|
|||
} |
|||
|
|||
const varsRegex = /\$\{([^\}]*)\}/g; |
|||
|
|||
function updateDataKeyLabel(dataKey, dsName, deviceName, aliasName) { |
|||
var pattern = dataKey.pattern; |
|||
var label = dataKey.pattern; |
|||
var match = varsRegex.exec(pattern); |
|||
while (match !== null) { |
|||
var variable = match[0]; |
|||
var variableName = match[1]; |
|||
if (variableName === 'dsName') { |
|||
label = label.split(variable).join(dsName); |
|||
} else if (variableName === 'deviceName') { |
|||
label = label.split(variable).join(deviceName); |
|||
} else if (variableName === 'aliasName') { |
|||
label = label.split(variable).join(aliasName); |
|||
} |
|||
match = varsRegex.exec(pattern); |
|||
} |
|||
dataKey.label = label; |
|||
} |
|||
|
|||
function calculateMin(data) { |
|||
if (data.length > 0) { |
|||
var result = Number(data[0][1]); |
|||
for (var i=1;i<data.length;i++) { |
|||
result = Math.min(result, Number(data[i][1])); |
|||
} |
|||
return result; |
|||
} else { |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
function calculateMax(data) { |
|||
if (data.length > 0) { |
|||
var result = Number(data[0][1]); |
|||
for (var i=1;i<data.length;i++) { |
|||
result = Math.max(result, Number(data[i][1])); |
|||
} |
|||
return result; |
|||
} else { |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
function calculateAvg(data) { |
|||
if (data.length > 0) { |
|||
return calculateTotal(data)/data.length; |
|||
} else { |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
function calculateTotal(data) { |
|||
if (data.length > 0) { |
|||
var result = 0; |
|||
for (var i = 0; i < data.length; i++) { |
|||
result += Number(data[i][1]); |
|||
} |
|||
return result; |
|||
} else { |
|||
return null; |
|||
} |
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2017 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-dialog aria-label="{{ 'dashboard.make-public' | translate }}" style="min-width: 400px;"> |
|||
<form> |
|||
<md-toolbar> |
|||
<div class="md-toolbar-tools"> |
|||
<h2 translate="dashboard.public-dashboard-title"></h2> |
|||
<span flex></span> |
|||
<md-button class="md-icon-button" ng-click="vm.close()"> |
|||
<ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon> |
|||
</md-button> |
|||
</div> |
|||
</md-toolbar> |
|||
<md-dialog-content> |
|||
<div id="make-dialog-public-content" class="md-dialog-content"> |
|||
<md-content class="md-padding" layout="column"> |
|||
<span translate="dashboard.public-dashboard-text" translate-values="{dashboardTitle: vm.dashboard.title, publicLink: vm.publicLink}"></span> |
|||
<div layout="row" layout-align="start center"> |
|||
<pre class="tb-highlight" flex><code>{{ vm.publicLink }}</code></pre> |
|||
<md-button class="md-icon-button" |
|||
ngclipboard |
|||
data-clipboard-text="{{ vm.publicLink }}" |
|||
ngclipboard-success="vm.onPublicLinkCopied(e)"> |
|||
<md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon> |
|||
<md-tooltip md-direction="top"> |
|||
{{ 'dashboard.copy-public-link' | translate }} |
|||
</md-tooltip> |
|||
</md-button> |
|||
</div> |
|||
<div class="tb-notice" translate>dashboard.public-dashboard-notice</div> |
|||
</md-content> |
|||
</div> |
|||
</md-dialog-content> |
|||
<md-dialog-actions layout="row"> |
|||
<span flex></span> |
|||
<md-button ng-click="vm.close()">{{ 'action.ok' | |
|||
translate }} |
|||
</md-button> |
|||
</md-dialog-actions> |
|||
</form> |
|||
</md-dialog> |
|||
@ -0,0 +1,325 @@ |
|||
/* |
|||
* Copyright © 2016-2017 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 './timeseries-table-widget.scss'; |
|||
|
|||
/* eslint-disable import/no-unresolved, import/default */ |
|||
|
|||
import timeseriesTableWidgetTemplate from './timeseries-table-widget.tpl.html'; |
|||
|
|||
/* eslint-enable import/no-unresolved, import/default */ |
|||
|
|||
import tinycolor from 'tinycolor2'; |
|||
import cssjs from '../../../vendor/css.js/css'; |
|||
|
|||
export default angular.module('thingsboard.widgets.timeseriesTableWidget', []) |
|||
.directive('tbTimeseriesTableWidget', TimeseriesTableWidget) |
|||
.name; |
|||
|
|||
/*@ngInject*/ |
|||
function TimeseriesTableWidget() { |
|||
return { |
|||
restrict: "E", |
|||
scope: true, |
|||
bindToController: { |
|||
tableId: '=', |
|||
config: '=', |
|||
datasources: '=', |
|||
data: '=' |
|||
}, |
|||
controller: TimeseriesTableWidgetController, |
|||
controllerAs: 'vm', |
|||
templateUrl: timeseriesTableWidgetTemplate |
|||
}; |
|||
} |
|||
|
|||
/*@ngInject*/ |
|||
function TimeseriesTableWidgetController($element, $scope, $filter) { |
|||
var vm = this; |
|||
|
|||
vm.sources = []; |
|||
vm.sourceIndex = 0; |
|||
|
|||
$scope.$watch('vm.config', function() { |
|||
if (vm.config) { |
|||
vm.settings = vm.config.settings; |
|||
vm.widgetConfig = vm.config.widgetConfig; |
|||
initialize(); |
|||
} |
|||
}); |
|||
|
|||
function initialize() { |
|||
vm.showTimestamp = vm.settings.showTimestamp !== false; |
|||
var origColor = vm.widgetConfig.color || 'rgba(0, 0, 0, 0.87)'; |
|||
var defaultColor = tinycolor(origColor); |
|||
var mdDark = defaultColor.setAlpha(0.87).toRgbString(); |
|||
var mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString(); |
|||
var mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString(); |
|||
//var mdDarkIcon = mdDarkSecondary;
|
|||
var mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString(); |
|||
|
|||
var cssString = 'table.md-table th.md-column {\n'+ |
|||
'color: ' + mdDarkSecondary + ';\n'+ |
|||
'}\n'+ |
|||
'table.md-table th.md-column md-icon.md-sort-icon {\n'+ |
|||
'color: ' + mdDarkDisabled + ';\n'+ |
|||
'}\n'+ |
|||
'table.md-table th.md-column.md-active, table.md-table th.md-column.md-active md-icon {\n'+ |
|||
'color: ' + mdDark + ';\n'+ |
|||
'}\n'+ |
|||
'table.md-table td.md-cell {\n'+ |
|||
'color: ' + mdDark + ';\n'+ |
|||
'border-top: 1px '+mdDarkDivider+' solid;\n'+ |
|||
'}\n'+ |
|||
'table.md-table td.md-cell.md-placeholder {\n'+ |
|||
'color: ' + mdDarkDisabled + ';\n'+ |
|||
'}\n'+ |
|||
'table.md-table td.md-cell md-select > .md-select-value > span.md-select-icon {\n'+ |
|||
'color: ' + mdDarkSecondary + ';\n'+ |
|||
'}\n'+ |
|||
'.md-table-pagination {\n'+ |
|||
'color: ' + mdDarkSecondary + ';\n'+ |
|||
'border-top: 1px '+mdDarkDivider+' solid;\n'+ |
|||
'}\n'+ |
|||
'.md-table-pagination .buttons md-icon {\n'+ |
|||
'color: ' + mdDarkSecondary + ';\n'+ |
|||
'}\n'+ |
|||
'.md-table-pagination md-select:not([disabled]):focus .md-select-value {\n'+ |
|||
'color: ' + mdDarkSecondary + ';\n'+ |
|||
'}'; |
|||
|
|||
var cssParser = new cssjs(); |
|||
cssParser.testMode = false; |
|||
var namespace = 'ts-table-' + hashCode(cssString); |
|||
cssParser.cssPreviewNamespace = namespace; |
|||
cssParser.createStyleElement(namespace, cssString); |
|||
$element.addClass(namespace); |
|||
|
|||
function hashCode(str) { |
|||
var hash = 0; |
|||
var i, char; |
|||
if (str.length === 0) return hash; |
|||
for (i = 0; i < str.length; i++) { |
|||
char = str.charCodeAt(i); |
|||
hash = ((hash << 5) - hash) + char; |
|||
hash = hash & hash; |
|||
} |
|||
return hash; |
|||
} |
|||
} |
|||
|
|||
$scope.$watch('vm.datasources', function() { |
|||
updateDatasources(); |
|||
}); |
|||
|
|||
$scope.$on('timeseries-table-data-updated', function(event, tableId) { |
|||
if (vm.tableId == tableId) { |
|||
dataUpdated(); |
|||
} |
|||
}); |
|||
|
|||
function dataUpdated() { |
|||
for (var s=0; s < vm.sources.length; s++) { |
|||
var source = vm.sources[s]; |
|||
source.rawData = vm.data.slice(source.keyStartIndex, source.keyEndIndex); |
|||
} |
|||
updateSourceData(vm.sources[vm.sourceIndex]); |
|||
$scope.$digest(); |
|||
} |
|||
|
|||
vm.onPaginate = function(source) { |
|||
updatePage(source); |
|||
} |
|||
|
|||
vm.onReorder = function(source) { |
|||
reorder(source); |
|||
updatePage(source); |
|||
} |
|||
|
|||
vm.cellStyle = function(source, index, value) { |
|||
var style = {}; |
|||
if (index > 0) { |
|||
var styleInfo = source.ts.stylesInfo[index-1]; |
|||
if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { |
|||
try { |
|||
style = styleInfo.cellStyleFunction(value); |
|||
} catch (e) { |
|||
style = {}; |
|||
} |
|||
} |
|||
} |
|||
return style; |
|||
} |
|||
|
|||
vm.cellContent = function(source, index, row, value) { |
|||
if (index === 0) { |
|||
return $filter('date')(value, 'yyyy-MM-dd HH:mm:ss'); |
|||
} else { |
|||
var strContent = ''; |
|||
if (angular.isDefined(value)) { |
|||
strContent = ''+value; |
|||
} |
|||
var content = strContent; |
|||
var contentInfo = source.ts.contentsInfo[index-1]; |
|||
if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { |
|||
try { |
|||
var rowData = source.ts.rowDataTemplate; |
|||
rowData['Timestamp'] = row[0]; |
|||
for (var h=0; h < source.ts.header.length; h++) { |
|||
var headerInfo = source.ts.header[h]; |
|||
rowData[headerInfo.dataKey.name] = row[headerInfo.index]; |
|||
} |
|||
content = contentInfo.cellContentFunction(value, rowData, $filter); |
|||
} catch (e) { |
|||
content = strContent; |
|||
} |
|||
} |
|||
return content; |
|||
} |
|||
} |
|||
|
|||
$scope.$watch('vm.sourceIndex', function(newIndex, oldIndex) { |
|||
if (newIndex != oldIndex) { |
|||
updateSourceData(vm.sources[vm.sourceIndex]); |
|||
} |
|||
}); |
|||
|
|||
function updateDatasources() { |
|||
vm.sources = []; |
|||
vm.sourceIndex = 0; |
|||
var keyOffset = 0; |
|||
if (vm.datasources) { |
|||
for (var ds = 0; ds < vm.datasources.length; ds++) { |
|||
var source = {}; |
|||
var datasource = vm.datasources[ds]; |
|||
source.keyStartIndex = keyOffset; |
|||
keyOffset += datasource.dataKeys.length; |
|||
source.keyEndIndex = keyOffset; |
|||
source.datasource = datasource; |
|||
source.data = []; |
|||
source.rawData = []; |
|||
source.query = { |
|||
limit: 5, |
|||
page: 1, |
|||
order: '-0' |
|||
} |
|||
source.ts = { |
|||
header: [], |
|||
count: 0, |
|||
data: [], |
|||
stylesInfo: [], |
|||
contentsInfo: [], |
|||
rowDataTemplate: {} |
|||
} |
|||
source.ts.rowDataTemplate['Timestamp'] = null; |
|||
for (var a = 0; a < datasource.dataKeys.length; a++ ) { |
|||
var dataKey = datasource.dataKeys[a]; |
|||
var keySettings = dataKey.settings; |
|||
source.ts.header.push({ |
|||
index: a+1, |
|||
dataKey: dataKey |
|||
}); |
|||
source.ts.rowDataTemplate[dataKey.label] = null; |
|||
|
|||
var cellStyleFunction = null; |
|||
var useCellStyleFunction = false; |
|||
|
|||
if (keySettings.useCellStyleFunction === true) { |
|||
if (angular.isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) { |
|||
try { |
|||
cellStyleFunction = new Function('value', keySettings.cellStyleFunction); |
|||
useCellStyleFunction = true; |
|||
} catch (e) { |
|||
cellStyleFunction = null; |
|||
useCellStyleFunction = false; |
|||
} |
|||
} |
|||
} |
|||
|
|||
source.ts.stylesInfo.push({ |
|||
useCellStyleFunction: useCellStyleFunction, |
|||
cellStyleFunction: cellStyleFunction |
|||
}); |
|||
|
|||
var cellContentFunction = null; |
|||
var useCellContentFunction = false; |
|||
|
|||
if (keySettings.useCellContentFunction === true) { |
|||
if (angular.isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) { |
|||
try { |
|||
cellContentFunction = new Function('value, rowData, filter', keySettings.cellContentFunction); |
|||
useCellContentFunction = true; |
|||
} catch (e) { |
|||
cellContentFunction = null; |
|||
useCellContentFunction = false; |
|||
} |
|||
} |
|||
} |
|||
|
|||
source.ts.contentsInfo.push({ |
|||
useCellContentFunction: useCellContentFunction, |
|||
cellContentFunction: cellContentFunction |
|||
}); |
|||
|
|||
} |
|||
vm.sources.push(source); |
|||
} |
|||
} |
|||
} |
|||
|
|||
function updatePage(source) { |
|||
var startIndex = source.query.limit * (source.query.page - 1); |
|||
source.ts.data = source.data.slice(startIndex, startIndex + source.query.limit); |
|||
} |
|||
|
|||
function reorder(source) { |
|||
source.data = $filter('orderBy')(source.data, source.query.order); |
|||
} |
|||
|
|||
function convertData(data) { |
|||
var rowsMap = {}; |
|||
for (var d = 0; d < data.length; d++) { |
|||
var columnData = data[d].data; |
|||
for (var i = 0; i < columnData.length; i++) { |
|||
var cellData = columnData[i]; |
|||
var timestamp = cellData[0]; |
|||
var row = rowsMap[timestamp]; |
|||
if (!row) { |
|||
row = []; |
|||
row[0] = timestamp; |
|||
for (var c = 0; c < data.length; c++) { |
|||
row[c+1] = undefined; |
|||
} |
|||
rowsMap[timestamp] = row; |
|||
} |
|||
row[d+1] = cellData[1]; |
|||
} |
|||
} |
|||
var rows = []; |
|||
for (var t in rowsMap) { |
|||
rows.push(rowsMap[t]); |
|||
} |
|||
return rows; |
|||
} |
|||
|
|||
function updateSourceData(source) { |
|||
source.data = convertData(source.rawData); |
|||
source.ts.count = source.data.length; |
|||
reorder(source); |
|||
updatePage(source); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
/** |
|||
* Copyright © 2016-2017 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. |
|||
*/ |
|||
|
|||
tb-timeseries-table-widget { |
|||
table.md-table thead.md-head>tr.md-row { |
|||
height: 40px; |
|||
} |
|||
|
|||
table.md-table tbody.md-body>tr.md-row, table.md-table tfoot.md-foot>tr.md-row { |
|||
height: 38px; |
|||
} |
|||
|
|||
.md-table-pagination>* { |
|||
height: 46px; |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2017 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-tabs md-selected="vm.sourceIndex" ng-class="{'tb-headless': vm.sources.length === 1}" |
|||
id="tabs" md-border-bottom flex class="tb-absolute-fill"> |
|||
<md-tab ng-repeat="source in vm.sources" label="{{ source.datasource.name }}"> |
|||
<md-table-container> |
|||
<table md-table> |
|||
<thead md-head md-order="source.query.order" md-on-reorder="vm.onReorder(source)"> |
|||
<tr md-row> |
|||
<th ng-show="vm.showTimestamp" md-column md-order-by="0"><span>Timestamp</span></th> |
|||
<th md-column md-order-by="{{ h.index }}" ng-repeat="h in source.ts.header"><span>{{ h.dataKey.label }}</span></th> |
|||
</tr> |
|||
</thead> |
|||
<tbody md-body> |
|||
<tr md-row ng-repeat="row in source.ts.data"> |
|||
<td ng-show="$index > 0 || ($index === 0 && vm.showTimestamp)" md-cell ng-repeat="d in row track by $index" ng-style="vm.cellStyle(source, $index, d)" ng-bind-html="vm.cellContent(source, $index, row, d)"> |
|||
</td> |
|||
</tr> |
|||
</tbody> |
|||
</table> |
|||
</md-table-container> |
|||
<md-table-pagination md-limit="source.query.limit" md-limit-options="[5, 10, 15]" |
|||
md-page="source.query.page" md-total="{{source.ts.count}}" |
|||
md-on-paginate="vm.onPaginate(source)" md-page-select> |
|||
</md-table-pagination> |
|||
</md-tab> |
|||
</md-tabs> |
|||
Loading…
Reference in new issue