Merge branch 'master' into feature/api-limits
This commit is contained in:
commit
64b2b9ce64
File diff suppressed because one or more lines are too long
@ -62,6 +62,7 @@ public final class PluginProcessingContext implements PluginContext {
|
||||
private static final Executor executor = Executors.newSingleThreadExecutor();
|
||||
public static final String CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION = "Customer user is not allowed to perform this operation!";
|
||||
public static final String SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION = "System administrator is not allowed to perform this operation!";
|
||||
public static final String DEVICE_WITH_REQUESTED_ID_NOT_FOUND = "Device with requested id wasn't found!";
|
||||
|
||||
private final SharedPluginProcessingContext pluginCtx;
|
||||
private final Optional<PluginApiCallSecurityContext> securityCtx;
|
||||
@ -309,7 +310,7 @@ public final class PluginProcessingContext implements PluginContext {
|
||||
ListenableFuture<Device> deviceFuture = pluginCtx.deviceService.findDeviceByIdAsync(new DeviceId(entityId.getId()));
|
||||
Futures.addCallback(deviceFuture, getCallback(callback, device -> {
|
||||
if (device == null) {
|
||||
return ValidationResult.entityNotFound("Device with requested id wasn't found!");
|
||||
return ValidationResult.entityNotFound(DEVICE_WITH_REQUESTED_ID_NOT_FOUND);
|
||||
} else {
|
||||
if (!device.getTenantId().equals(ctx.getTenantId())) {
|
||||
return ValidationResult.accessDenied("Device doesn't belong to the current Tenant!");
|
||||
|
||||
@ -169,6 +169,13 @@ cassandra:
|
||||
# Specify partitioning size for timestamp key-value storage. Example MINUTES, HOURS, DAYS, MONTHS
|
||||
ts_key_value_partitioning: "${TS_KV_PARTITIONING:MONTHS}"
|
||||
|
||||
# SQL configuration parameters
|
||||
sql:
|
||||
# Specify executor service type used to perform timeseries insert tasks: SINGLE FIXED CACHED
|
||||
ts_inserts_executor_type: "${SQL_TS_INSERTS_EXECUTOR_TYPE:fixed}"
|
||||
# Specify thread pool size for FIXED executor service type
|
||||
ts_inserts_fixed_thread_pool_size: "${SQL_TS_INSERTS_FIXED_THREAD_POOL_SIZE:10}"
|
||||
|
||||
# Actor system parameters
|
||||
actors:
|
||||
tenant:
|
||||
|
||||
@ -106,6 +106,11 @@ public abstract class AbstractControllerTest {
|
||||
protected static final String CUSTOMER_USER_EMAIL = "testcustomer@thingsboard.org";
|
||||
private static final String CUSTOMER_USER_PASSWORD = "customer";
|
||||
|
||||
/** See {@link org.springframework.test.web.servlet.DefaultMvcResult#getAsyncResult(long)}
|
||||
* and {@link org.springframework.mock.web.MockAsyncContext#getTimeout()}
|
||||
*/
|
||||
private static final long DEFAULT_TIMEOUT = -1L;
|
||||
|
||||
protected MediaType contentType = new MediaType(MediaType.APPLICATION_JSON.getType(),
|
||||
MediaType.APPLICATION_JSON.getSubtype(),
|
||||
Charset.forName("utf8"));
|
||||
@ -336,7 +341,7 @@ public abstract class AbstractControllerTest {
|
||||
}
|
||||
|
||||
protected <T> T doPost(String urlTemplate, T content, Class<T> responseClass, ResultMatcher resultMatcher, String... params) throws Exception {
|
||||
return readResponse(doPost(urlTemplate, params).andExpect(resultMatcher), responseClass);
|
||||
return readResponse(doPost(urlTemplate, content, params).andExpect(resultMatcher), responseClass);
|
||||
}
|
||||
|
||||
protected <T> T doPost(String urlTemplate, T content, Class<T> responseClass, String... params) throws Exception {
|
||||
@ -344,7 +349,11 @@ public abstract class AbstractControllerTest {
|
||||
}
|
||||
|
||||
protected <T> T doPostAsync(String urlTemplate, T content, Class<T> responseClass, ResultMatcher resultMatcher, String... params) throws Exception {
|
||||
return readResponse(doPostAsync(urlTemplate, content, params).andExpect(resultMatcher), responseClass);
|
||||
return readResponse(doPostAsync(urlTemplate, content, DEFAULT_TIMEOUT, params).andExpect(resultMatcher), responseClass);
|
||||
}
|
||||
|
||||
protected <T> T doPostAsync(String urlTemplate, T content, Class<T> responseClass, ResultMatcher resultMatcher, Long timeout, String... params) throws Exception {
|
||||
return readResponse(doPostAsync(urlTemplate, content, timeout, params).andExpect(resultMatcher), responseClass);
|
||||
}
|
||||
|
||||
protected <T> T doDelete(String urlTemplate, Class<T> responseClass, String... params) throws Exception {
|
||||
@ -366,12 +375,13 @@ public abstract class AbstractControllerTest {
|
||||
return mockMvc.perform(postRequest);
|
||||
}
|
||||
|
||||
protected <T> ResultActions doPostAsync(String urlTemplate, T content, String... params) throws Exception {
|
||||
protected <T> ResultActions doPostAsync(String urlTemplate, T content, Long timeout, String... params) throws Exception {
|
||||
MockHttpServletRequestBuilder postRequest = post(urlTemplate);
|
||||
setJwtToken(postRequest);
|
||||
String json = json(content);
|
||||
postRequest.contentType(contentType).content(json);
|
||||
MvcResult result = mockMvc.perform(postRequest).andReturn();
|
||||
result.getAsyncResult(timeout);
|
||||
return mockMvc.perform(asyncDispatch(result));
|
||||
}
|
||||
|
||||
@ -384,8 +394,8 @@ public abstract class AbstractControllerTest {
|
||||
|
||||
protected void populateParams(MockHttpServletRequestBuilder request, String... params) {
|
||||
if (params != null && params.length > 0) {
|
||||
Assert.assertEquals(params.length % 2, 0);
|
||||
MultiValueMap<String, String> paramsMap = new LinkedMultiValueMap<String, String>();
|
||||
Assert.assertEquals(0, params.length % 2);
|
||||
MultiValueMap<String, String> paramsMap = new LinkedMultiValueMap<>();
|
||||
for (int i = 0; i < params.length; i += 2) {
|
||||
paramsMap.add(params[i], params[i + 1]);
|
||||
}
|
||||
|
||||
@ -15,21 +15,23 @@
|
||||
*/
|
||||
package org.thingsboard.server.mqtt.rpc;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import com.datastax.driver.core.utils.UUIDs;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.eclipse.paho.client.mqttv3.*;
|
||||
import org.junit.*;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.thingsboard.server.actors.plugin.PluginProcessingContext;
|
||||
import org.thingsboard.server.common.data.Device;
|
||||
import org.thingsboard.server.common.data.Tenant;
|
||||
import org.thingsboard.server.common.data.User;
|
||||
import org.thingsboard.server.common.data.page.TextPageData;
|
||||
import org.thingsboard.server.common.data.plugin.PluginMetaData;
|
||||
import org.thingsboard.server.common.data.security.Authority;
|
||||
import org.thingsboard.server.common.data.security.DeviceCredentials;
|
||||
import org.thingsboard.server.controller.AbstractControllerTest;
|
||||
import org.thingsboard.server.dao.service.DaoNoSqlTest;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
@ -42,15 +44,19 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractControllerTest {
|
||||
|
||||
private static final String MQTT_URL = "tcp://localhost:1883";
|
||||
private static final String FAIL_MSG_IF_HTTP_CLIENT_ERROR_NOT_ENCOUNTERED = "HttpClientErrorException expected, but not encountered";
|
||||
private static final Long TIME_TO_HANDLE_REQUEST = 500L;
|
||||
|
||||
private Tenant savedTenant;
|
||||
private User tenantAdmin;
|
||||
private Long asyncContextTimeoutToUseRpcPlugin;
|
||||
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws Exception {
|
||||
loginSysAdmin();
|
||||
|
||||
asyncContextTimeoutToUseRpcPlugin = getAsyncContextTimeoutToUseRpcPlugin();
|
||||
|
||||
Tenant tenant = new Tenant();
|
||||
tenant.setTitle("My tenant");
|
||||
savedTenant = doPost("/api/tenant", tenant, Tenant.class);
|
||||
@ -70,8 +76,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractC
|
||||
public void afterTest() throws Exception {
|
||||
loginSysAdmin();
|
||||
if (savedTenant != null) {
|
||||
doDelete("/api/tenant/" + savedTenant.getId().getId().toString())
|
||||
.andExpect(status().isOk());
|
||||
doDelete("/api/tenant/" + savedTenant.getId().getId().toString()).andExpect(status().isOk());
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,7 +107,6 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractC
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore // TODO: figure out the right error code for this case. Ignored due to failure: expected 408 but was: 200
|
||||
public void testServerMqttOneWayRpcDeviceOffline() throws Exception {
|
||||
Device device = new Device();
|
||||
device.setName("Test One-Way Server-Side RPC Device Offline");
|
||||
@ -115,29 +119,19 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractC
|
||||
|
||||
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}";
|
||||
String deviceId = savedDevice.getId().getId().toString();
|
||||
try {
|
||||
doPost("/api/plugins/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().is(408));
|
||||
Assert.fail(FAIL_MSG_IF_HTTP_CLIENT_ERROR_NOT_ENCOUNTERED);
|
||||
} catch (HttpClientErrorException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
Assert.assertEquals(HttpStatus.REQUEST_TIMEOUT, e.getStatusCode());
|
||||
Assert.assertEquals("408 null", e.getMessage());
|
||||
}
|
||||
|
||||
doPostAsync("/api/plugins/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().isRequestTimeout(),
|
||||
asyncContextTimeoutToUseRpcPlugin);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore // TODO: figure out the right error code for this case. Ignored due to failure: expected 400 (404?) but was: 401
|
||||
public void testServerMqttOneWayRpcDeviceDoesNotExist() throws Exception {
|
||||
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}";
|
||||
String nonExistentDeviceId = UUID.randomUUID().toString();
|
||||
try {
|
||||
doPostAsync("/api/plugins/rpc/oneway/" + nonExistentDeviceId, setGpioRequest, String.class, status().is(400));
|
||||
Assert.fail(FAIL_MSG_IF_HTTP_CLIENT_ERROR_NOT_ENCOUNTERED);
|
||||
} catch (HttpClientErrorException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
Assert.assertEquals(HttpStatus.BAD_REQUEST, e.getStatusCode());
|
||||
Assert.assertEquals("400 null", e.getMessage());
|
||||
}
|
||||
String nonExistentDeviceId = UUIDs.timeBased().toString();
|
||||
|
||||
String result = doPostAsync("/api/plugins/rpc/oneway/" + nonExistentDeviceId, setGpioRequest, String.class,
|
||||
status().isNotFound());
|
||||
Assert.assertEquals(PluginProcessingContext.DEVICE_WITH_REQUESTED_ID_NOT_FOUND, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -168,7 +162,6 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractC
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore // TODO: figure out the right error code for this case. Ignored due to failure: expected 408 but was: 200
|
||||
public void testServerMqttTwoWayRpcDeviceOffline() throws Exception {
|
||||
Device device = new Device();
|
||||
device.setName("Test Two-Way Server-Side RPC Device Offline");
|
||||
@ -181,29 +174,19 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractC
|
||||
|
||||
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}";
|
||||
String deviceId = savedDevice.getId().getId().toString();
|
||||
try {
|
||||
doPost("/api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().is(408));
|
||||
Assert.fail(FAIL_MSG_IF_HTTP_CLIENT_ERROR_NOT_ENCOUNTERED);
|
||||
} catch (HttpClientErrorException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
Assert.assertEquals(HttpStatus.REQUEST_TIMEOUT, e.getStatusCode());
|
||||
Assert.assertEquals("408 null", e.getMessage());
|
||||
}
|
||||
|
||||
doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isRequestTimeout(),
|
||||
asyncContextTimeoutToUseRpcPlugin);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore // TODO: figure out the right error code for this case. Ignored due to failure: expected 400 (404?) but was: 401
|
||||
public void testServerMqttTwoWayRpcDeviceDoesNotExist() throws Exception {
|
||||
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}";
|
||||
String nonExistentDeviceId = UUID.randomUUID().toString();
|
||||
try {
|
||||
doPostAsync("/api/plugins/rpc/oneway/" + nonExistentDeviceId, setGpioRequest, String.class, status().is(400));
|
||||
Assert.fail(FAIL_MSG_IF_HTTP_CLIENT_ERROR_NOT_ENCOUNTERED);
|
||||
} catch (HttpClientErrorException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
Assert.assertEquals(HttpStatus.BAD_REQUEST, e.getStatusCode());
|
||||
Assert.assertEquals("400 null", e.getMessage());
|
||||
}
|
||||
String nonExistentDeviceId = UUIDs.timeBased().toString();
|
||||
|
||||
String result = doPostAsync("/api/plugins/rpc/twoway/" + nonExistentDeviceId, setGpioRequest, String.class,
|
||||
status().isNotFound());
|
||||
Assert.assertEquals(PluginProcessingContext.DEVICE_WITH_REQUESTED_ID_NOT_FOUND, result);
|
||||
}
|
||||
|
||||
private Device getSavedDevice(Device device) throws Exception {
|
||||
@ -214,6 +197,13 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractC
|
||||
return doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class);
|
||||
}
|
||||
|
||||
private Long getAsyncContextTimeoutToUseRpcPlugin() throws Exception {
|
||||
TextPageData<PluginMetaData> plugins = doGetTyped("/api/plugin/system?limit=1&textSearch=system rpc plugin",
|
||||
new TypeReference<TextPageData<PluginMetaData>>(){});
|
||||
Long systemRpcPluginTimeout = plugins.getData().iterator().next().getConfiguration().get("defaultTimeout").asLong();
|
||||
return systemRpcPluginTimeout + TIME_TO_HANDLE_REQUEST;
|
||||
}
|
||||
|
||||
private static class TestMqttCallback implements MqttCallback {
|
||||
|
||||
private final MqttAsyncClient client;
|
||||
@ -228,10 +218,10 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractC
|
||||
|
||||
@Override
|
||||
public void messageArrived(String requestTopic, MqttMessage mqttMessage) throws Exception {
|
||||
log.info("Message Arrived: " + mqttMessage.getPayload().toString());
|
||||
log.info("Message Arrived: " + Arrays.toString(mqttMessage.getPayload()));
|
||||
MqttMessage message = new MqttMessage();
|
||||
String responseTopic = requestTopic.replace("request", "response");
|
||||
message.setPayload("{\"value1\":\"A\", \"value2\":\"B\"}".getBytes());
|
||||
message.setPayload("{\"value1\":\"A\", \"value2\":\"B\"}".getBytes("UTF-8"));
|
||||
client.publish(responseTopic, message);
|
||||
}
|
||||
|
||||
|
||||
@ -75,6 +75,7 @@ public abstract class AbstractCassandraCluster {
|
||||
private Environment environment;
|
||||
|
||||
private Cluster cluster;
|
||||
private Cluster.Builder clusterBuilder;
|
||||
|
||||
@Getter(AccessLevel.NONE) private Session session;
|
||||
|
||||
@ -88,29 +89,27 @@ public abstract class AbstractCassandraCluster {
|
||||
|
||||
protected void init(String keyspaceName) {
|
||||
this.keyspaceName = keyspaceName;
|
||||
Cluster.Builder builder = Cluster.builder()
|
||||
this.clusterBuilder = Cluster.builder()
|
||||
.addContactPointsWithPorts(getContactPoints(url))
|
||||
.withClusterName(clusterName)
|
||||
.withSocketOptions(socketOpts.getOpts())
|
||||
.withPoolingOptions(new PoolingOptions()
|
||||
.setMaxRequestsPerConnection(HostDistance.LOCAL, 32768)
|
||||
.setMaxRequestsPerConnection(HostDistance.REMOTE, 32768));
|
||||
builder.withQueryOptions(queryOpts.getOpts());
|
||||
builder.withCompression(StringUtils.isEmpty(compression) ? Compression.NONE : Compression.valueOf(compression.toUpperCase()));
|
||||
this.clusterBuilder.withQueryOptions(queryOpts.getOpts());
|
||||
this.clusterBuilder.withCompression(StringUtils.isEmpty(compression) ? Compression.NONE : Compression.valueOf(compression.toUpperCase()));
|
||||
if (ssl) {
|
||||
builder.withSSL();
|
||||
this.clusterBuilder.withSSL();
|
||||
}
|
||||
if (!jmx) {
|
||||
builder.withoutJMXReporting();
|
||||
this.clusterBuilder.withoutJMXReporting();
|
||||
}
|
||||
if (!metrics) {
|
||||
builder.withoutMetrics();
|
||||
this.clusterBuilder.withoutMetrics();
|
||||
}
|
||||
if (credentials) {
|
||||
builder.withCredentials(username, password);
|
||||
this.clusterBuilder.withCredentials(username, password);
|
||||
}
|
||||
cluster = builder.build();
|
||||
cluster.init();
|
||||
if (!isInstall()) {
|
||||
initSession();
|
||||
}
|
||||
@ -139,7 +138,8 @@ public abstract class AbstractCassandraCluster {
|
||||
long endTime = System.currentTimeMillis() + initTimeout;
|
||||
while (System.currentTimeMillis() < endTime) {
|
||||
try {
|
||||
|
||||
cluster = clusterBuilder.build();
|
||||
cluster.init();
|
||||
if (this.keyspaceName != null) {
|
||||
session = cluster.connect(keyspaceName);
|
||||
} else {
|
||||
|
||||
@ -20,6 +20,7 @@ import com.google.common.collect.Lists;
|
||||
import com.google.common.util.concurrent.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.thingsboard.server.common.data.UUIDConverter;
|
||||
@ -31,14 +32,17 @@ import org.thingsboard.server.dao.model.sql.TsKvLatestCompositeKey;
|
||||
import org.thingsboard.server.dao.model.sql.TsKvLatestEntity;
|
||||
import org.thingsboard.server.dao.sql.JpaAbstractDaoListeningExecutorService;
|
||||
import org.thingsboard.server.dao.timeseries.TimeseriesDao;
|
||||
import org.thingsboard.server.dao.timeseries.TsInsertExecutorType;
|
||||
import org.thingsboard.server.dao.util.SqlDao;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ -50,7 +54,13 @@ import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID;
|
||||
@SqlDao
|
||||
public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService implements TimeseriesDao {
|
||||
|
||||
private ListeningExecutorService insertService = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
|
||||
@Value("${sql.ts_inserts_executor_type}")
|
||||
private String insertExecutorType;
|
||||
|
||||
@Value("${sql.ts_inserts_fixed_thread_pool_size}")
|
||||
private int insertFixedThreadPoolSize;
|
||||
|
||||
private ListeningExecutorService insertService;
|
||||
|
||||
@Autowired
|
||||
private TsKvRepository tsKvRepository;
|
||||
@ -58,6 +68,32 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp
|
||||
@Autowired
|
||||
private TsKvLatestRepository tsKvLatestRepository;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
Optional<TsInsertExecutorType> executorTypeOptional = TsInsertExecutorType.parse(insertExecutorType);
|
||||
TsInsertExecutorType executorType;
|
||||
if (executorTypeOptional.isPresent()) {
|
||||
executorType = executorTypeOptional.get();
|
||||
} else {
|
||||
executorType = TsInsertExecutorType.FIXED;
|
||||
}
|
||||
switch (executorType) {
|
||||
case SINGLE:
|
||||
insertService = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
|
||||
break;
|
||||
case FIXED:
|
||||
int poolSize = insertFixedThreadPoolSize;
|
||||
if (poolSize <= 0) {
|
||||
poolSize = 10;
|
||||
}
|
||||
insertService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(poolSize));
|
||||
break;
|
||||
case CACHED:
|
||||
insertService = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListenableFuture<List<TsKvEntry>> findAllAsync(EntityId entityId, List<TsKvQuery> queries) {
|
||||
List<ListenableFuture<List<TsKvEntry>>> futures = queries
|
||||
@ -234,7 +270,7 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp
|
||||
entity.setDoubleValue(tsKvEntry.getDoubleValue().orElse(null));
|
||||
entity.setLongValue(tsKvEntry.getLongValue().orElse(null));
|
||||
entity.setBooleanValue(tsKvEntry.getBooleanValue().orElse(null));
|
||||
log.trace("Saving entity: " + entity);
|
||||
log.trace("Saving entity: {}", entity);
|
||||
return insertService.submit(() -> {
|
||||
tsKvRepository.save(entity);
|
||||
return null;
|
||||
@ -265,7 +301,9 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp
|
||||
|
||||
@PreDestroy
|
||||
void onDestroy() {
|
||||
insertService.shutdown();
|
||||
if (insertService != null) {
|
||||
insertService.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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.dao.timeseries;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public enum TsInsertExecutorType {
|
||||
SINGLE,
|
||||
FIXED,
|
||||
CACHED;
|
||||
|
||||
public static Optional<TsInsertExecutorType> parse(String name) {
|
||||
TsInsertExecutorType executorType = null;
|
||||
if (name != null) {
|
||||
for (TsInsertExecutorType type : TsInsertExecutorType.values()) {
|
||||
if (type.name().equalsIgnoreCase(name)) {
|
||||
executorType = type;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Optional.of(executorType);
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,7 @@
|
||||
database.type=sql
|
||||
database.type=sql
|
||||
|
||||
sql.ts_inserts_executor_type=fixed
|
||||
sql.ts_inserts_fixed_thread_pool_size=10
|
||||
|
||||
spring.jpa.show-sql=false
|
||||
spring.jpa.hibernate.ddl-auto=validate
|
||||
|
||||
@ -74,13 +74,13 @@ echo "Generating SSL Key Pair..."
|
||||
|
||||
keytool -genkeypair -v \
|
||||
-alias $CLIENT_KEY_ALIAS \
|
||||
-dname "CN=$DOMAIN_SUFFIX, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \
|
||||
-keystore $CLIENT_FILE_PREFIX.jks \
|
||||
-keypass $CLIENT_KEY_PASSWORD \
|
||||
-storepass $CLIENT_KEYSTORE_PASSWORD \
|
||||
-keyalg RSA \
|
||||
-keysize 2048 \
|
||||
-validity 9999
|
||||
-validity 9999 \
|
||||
-dname "CN=$DOMAIN_SUFFIX, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE"
|
||||
|
||||
echo "Converting keystore to pkcs12"
|
||||
keytool -importkeystore \
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
DOMAIN_SUFFIX="$(hostname)"
|
||||
ORGANIZATIONAL_UNIT=Thingsboard
|
||||
ORGANIZATION=Thingsboard
|
||||
CITY=San Francisco
|
||||
CITY=SF
|
||||
STATE_OR_PROVINCE=CA
|
||||
TWO_LETTER_COUNTRY_CODE=US
|
||||
|
||||
|
||||
@ -1214,6 +1214,7 @@ export default angular.module('thingsboard.locale', [])
|
||||
"remove-widget-text": "After the confirmation the widget and all related data will become unrecoverable.",
|
||||
"timeseries": "Time series",
|
||||
"search-data": "Search data",
|
||||
"no-data-found": "No data found",
|
||||
"latest-values": "Latest values",
|
||||
"rpc": "Control widget",
|
||||
"alarm": "Alarm widget",
|
||||
|
||||
@ -340,7 +340,11 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
|
||||
}
|
||||
if (!style.width) {
|
||||
var columnWidth = vm.columnWidth[key.label];
|
||||
style.width = columnWidth;
|
||||
if(columnWidth !== "0px") {
|
||||
style.width = columnWidth;
|
||||
} else {
|
||||
style.width = "auto";
|
||||
}
|
||||
}
|
||||
return style;
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@ import tinycolor from 'tinycolor2';
|
||||
import TbGoogleMap from './google-map';
|
||||
import TbOpenStreetMap from './openstreet-map';
|
||||
import TbImageMap from './image-map';
|
||||
import TbTencentMap from './tencent-map';
|
||||
|
||||
import {processPattern, arraysEqual, toLabelValueMap, fillPattern, fillPatternWithActions} from './widget-utils';
|
||||
|
||||
@ -83,6 +84,8 @@ export default class TbMapWidgetV2 {
|
||||
settings.posFunction,
|
||||
settings.imageEntityAlias,
|
||||
settings.imageUrlAttribute);
|
||||
} else if (mapProvider === 'tencent-map') {
|
||||
this.map = new TbTencentMap($element,this.utils, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, minZoomLevel, settings.tmApiKey, settings.tmDefaultMapType);
|
||||
}
|
||||
}
|
||||
|
||||
@ -466,6 +469,8 @@ export default class TbMapWidgetV2 {
|
||||
schema = angular.copy(openstreetMapSettingsSchema);
|
||||
} else if (mapProvider === 'image-map') {
|
||||
return imageMapSettingsSchema;
|
||||
} else if (mapProvider === 'tencent-map') {
|
||||
schema = angular.copy(tencentMapSettingsSchema);
|
||||
}
|
||||
angular.merge(schema.schema.properties, commonMapSettingsSchema.schema.properties);
|
||||
schema.schema.required = schema.schema.required.concat(commonMapSettingsSchema.schema.required);
|
||||
@ -544,7 +549,51 @@ const googleMapSettingsSchema =
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
const tencentMapSettingsSchema =
|
||||
{
|
||||
"schema":{
|
||||
"title":"Tencent Map Configuration",
|
||||
"type":"object",
|
||||
"properties":{
|
||||
"tmApiKey":{
|
||||
"title":"Tencent Maps API Key",
|
||||
"type":"string"
|
||||
},
|
||||
"tmDefaultMapType":{
|
||||
"title":"Default map type",
|
||||
"type":"string",
|
||||
"default":"roadmap"
|
||||
}
|
||||
},
|
||||
"required":[
|
||||
"tmApiKey"
|
||||
]
|
||||
},
|
||||
"form":[
|
||||
"tmApiKey",
|
||||
{
|
||||
"key":"tmDefaultMapType",
|
||||
"type":"rc-select",
|
||||
"multiple":false,
|
||||
"items":[
|
||||
{
|
||||
"value":"roadmap",
|
||||
"label":"Roadmap"
|
||||
},
|
||||
{
|
||||
"value":"satellite",
|
||||
"label":"Satellite"
|
||||
},
|
||||
{
|
||||
"value":"hybrid",
|
||||
"label":"Hybrid"
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const openstreetMapSettingsSchema =
|
||||
{
|
||||
"schema":{
|
||||
|
||||
391
ui/src/app/widget/lib/tencent-map.js
Normal file
391
ui/src/app/widget/lib/tencent-map.js
Normal file
@ -0,0 +1,391 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
var tmGlobals = {
|
||||
loadingTmId: null,
|
||||
tmApiKeys: {}
|
||||
}
|
||||
|
||||
export default class TbTencentMap {
|
||||
constructor($containerElement,utils, initCallback, defaultZoomLevel, dontFitMapBounds, minZoomLevel, tmApiKey, tmDefaultMapType) {
|
||||
var tbMap = this;
|
||||
this.utils = utils;
|
||||
this.defaultZoomLevel = defaultZoomLevel;
|
||||
this.dontFitMapBounds = dontFitMapBounds;
|
||||
this.minZoomLevel = minZoomLevel;
|
||||
this.tooltips = [];
|
||||
this.defaultMapType = tmDefaultMapType;
|
||||
|
||||
function clearGlobalId() {
|
||||
if (tmGlobals.loadingTmId && tmGlobals.loadingTmId === tbMap.mapId) {
|
||||
tmGlobals.loadingTmId = null;
|
||||
}
|
||||
}
|
||||
|
||||
function displayError(message) {
|
||||
$containerElement.html( // eslint-disable-line angular/angularelement
|
||||
"<div class='error'>"+ message + "</div>"
|
||||
);
|
||||
}
|
||||
|
||||
function initTencentMap() {
|
||||
tbMap.map = new qq.maps.Map($containerElement[0], { // eslint-disable-line no-undef
|
||||
scrollwheel: true,
|
||||
mapTypeId: getTencentMapTypeId(tbMap.defaultMapType),
|
||||
zoom: tbMap.defaultZoomLevel || 8
|
||||
});
|
||||
|
||||
if (initCallback) {
|
||||
initCallback();
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
function getTencentMapTypeId(mapType) {
|
||||
var mapTypeId =qq.maps.MapTypeId.ROADMAP;
|
||||
if (mapType) {
|
||||
if (mapType === 'hybrid') {
|
||||
mapTypeId = qq.maps.MapTypeId.HYBRID;
|
||||
} else if (mapType === 'satellite') {
|
||||
mapTypeId = qq.maps.MapTypeId.SATELLITE;
|
||||
} else if (mapType === 'terrain') {
|
||||
mapTypeId = qq.maps.MapTypeId.ROADMAP;
|
||||
}
|
||||
}
|
||||
return mapTypeId;
|
||||
}
|
||||
|
||||
/* eslint-enable no-undef */
|
||||
|
||||
this.mapId = '' + Math.random().toString(36).substr(2, 9);
|
||||
this.apiKey = tmApiKey || '84d6d83e0e51e481e50454ccbe8986b';
|
||||
|
||||
window.tm_authFailure = function() { // eslint-disable-line no-undef, angular/window-service
|
||||
if (tmGlobals.loadingTmId && tmGlobals.loadingTmId === tbMap.mapId) {
|
||||
tmGlobals.loadingTmId = null;
|
||||
tmGlobals.tmApiKeys[tbMap.apiKey].error = 'Unable to authentificate for tencent Map API.</br>Please check your API key.';
|
||||
displayError(tmGlobals.tmApiKeys[tbMap.apiKey].error);
|
||||
}
|
||||
};
|
||||
|
||||
this.initMapFunctionName = 'initTencentMap_' + this.mapId;
|
||||
|
||||
window[this.initMapFunctionName] = function() { // eslint-disable-line no-undef, angular/window-service
|
||||
tmGlobals.tmApiKeys[tbMap.apiKey].loaded = true;
|
||||
initTencentMap();
|
||||
for (var p = 0; p < tmGlobals.tmApiKeys[tbMap.apiKey].pendingInits.length; p++) {
|
||||
var pendingInit = tmGlobals.tmApiKeys[tbMap.apiKey].pendingInits[p];
|
||||
pendingInit();
|
||||
}
|
||||
tmGlobals.tmApiKeys[tbMap.apiKey].pendingInits = [];
|
||||
};
|
||||
if (this.apiKey && this.apiKey.length > 0) {
|
||||
if (tmGlobals.tmApiKeys[this.apiKey]) {
|
||||
if (tmGlobals.tmApiKeys[this.apiKey].error) {
|
||||
displayError(tmGlobals.tmApiKeys[this.apiKey].error);
|
||||
} else if (tmGlobals.tmApiKeys[this.apiKey].loaded) {
|
||||
initTencentMap();
|
||||
} else {
|
||||
tmGlobals.tmApiKeys[this.apiKey].pendingInits.push(initTencentMap);
|
||||
}
|
||||
} else {
|
||||
tmGlobals.tmApiKeys[this.apiKey] = {
|
||||
loaded: false,
|
||||
pendingInits: []
|
||||
};
|
||||
var tencentMapScriptRes = 'http://map.qq.com/api/js?v=2.exp&key='+this.apiKey+'&callback='+this.initMapFunctionName;
|
||||
|
||||
tmGlobals.loadingTmId = this.mapId;
|
||||
lazyLoad.load({ type: 'js', path: tencentMapScriptRes }).then( // eslint-disable-line no-undef
|
||||
function success() {
|
||||
setTimeout(clearGlobalId, 2000); // eslint-disable-line no-undef, angular/timeout-service
|
||||
},
|
||||
function fail(e) {
|
||||
clearGlobalId();
|
||||
tmGlobals.tmApiKeys[tbMap.apiKey].error = 'tencent map api load failed!</br>'+e;
|
||||
displayError(tmGlobals.tmApiKeys[tbMap.apiKey].error);
|
||||
}
|
||||
);
|
||||
}
|
||||
} else {
|
||||
displayError('No tencent Map Api Key provided!');
|
||||
}
|
||||
}
|
||||
|
||||
inited() {
|
||||
return angular.isDefined(this.map);
|
||||
}
|
||||
|
||||
createMarkerLabelStyle(settings) {
|
||||
return {
|
||||
width: "200px",
|
||||
textAlign: "center",
|
||||
color: settings.labelColor,
|
||||
background: "none",
|
||||
border: "none",
|
||||
fontSize: "12px",
|
||||
fontFamily: "\"Helvetica Neue\", Arial, Helvetica, sans-serif",
|
||||
fontWeight: "bold"
|
||||
};
|
||||
}
|
||||
|
||||
/* eslint-disable no-undef,no-unused-vars*/
|
||||
updateMarkerLabel(marker, settings) {
|
||||
if (marker.label) {
|
||||
marker.label.setContent(settings.labelText);
|
||||
marker.label.setStyle(this.createMarkerLabelStyle(settings));
|
||||
}
|
||||
}
|
||||
/* eslint-enable no-undef,no-unused-vars */
|
||||
|
||||
/* eslint-disable no-undef,no-unused-vars */
|
||||
updateMarkerColor(marker, color) {
|
||||
this.createDefaultMarkerIcon(marker, color, (iconInfo) => {
|
||||
marker.setIcon(iconInfo.icon);
|
||||
});
|
||||
}
|
||||
/* eslint-enable no-undef,,no-unused-vars */
|
||||
|
||||
/* eslint-disable no-undef */
|
||||
updateMarkerIcon(marker, settings) {
|
||||
this.createMarkerIcon(marker, settings, (iconInfo) => {
|
||||
marker.setIcon(iconInfo.icon);
|
||||
if (marker.label) {
|
||||
marker.label.setOffset(new qq.maps.Size(-100, -iconInfo.size[1]-20));
|
||||
}
|
||||
});
|
||||
}
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
/* eslint-disable no-undef */
|
||||
createMarkerIcon(marker, settings, onMarkerIconReady) {
|
||||
var currentImage = settings.currentImage;
|
||||
var tMap = this;
|
||||
if (currentImage && currentImage.url) {
|
||||
this.utils.loadImageAspect(currentImage.url).then(
|
||||
(aspect) => {
|
||||
if (aspect) {
|
||||
var width;
|
||||
var height;
|
||||
if (aspect > 1) {
|
||||
width = currentImage.size;
|
||||
height = currentImage.size / aspect;
|
||||
} else {
|
||||
width = currentImage.size * aspect;
|
||||
height = currentImage.size;
|
||||
}
|
||||
var icon = new qq.maps.MarkerImage(currentImage.url,
|
||||
new qq.maps.Size(width, height),
|
||||
new qq.maps.Point(0,0),
|
||||
new qq.maps.Point(width/2, height),
|
||||
new qq.maps.Size(width, height));
|
||||
var iconInfo = {
|
||||
size: [width, height],
|
||||
icon: icon
|
||||
};
|
||||
onMarkerIconReady(iconInfo);
|
||||
} else {
|
||||
tMap.createDefaultMarkerIcon(marker, settings.color, onMarkerIconReady);
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.createDefaultMarkerIcon(marker, settings.color, onMarkerIconReady);
|
||||
}
|
||||
}
|
||||
/* eslint-enable no-undef */
|
||||
|
||||
/* eslint-disable no-undef */
|
||||
createDefaultMarkerIcon(marker, color, onMarkerIconReady) {
|
||||
var pinColor = color.substr(1);
|
||||
var icon = new qq.maps.MarkerImage("https://chart.apis.google.com/chart?chst=d_map_pin_letter_withshadow&chld=%E2%80%A2|" + pinColor,
|
||||
new qq.maps.Size(40, 37),
|
||||
new qq.maps.Point(0,0),
|
||||
new qq.maps.Point(10, 37));
|
||||
var iconInfo = {
|
||||
size: [40, 37],
|
||||
icon: icon
|
||||
};
|
||||
onMarkerIconReady(iconInfo);
|
||||
}
|
||||
/* eslint-enable no-undef */
|
||||
|
||||
/* eslint-disable no-undef */
|
||||
createMarker(location, settings, onClickListener, markerArgs) {
|
||||
var marker = new qq.maps.Marker({
|
||||
position: location
|
||||
});
|
||||
var tMap = this;
|
||||
this.createMarkerIcon(marker, settings, (iconInfo) => {
|
||||
marker.setIcon(iconInfo.icon);
|
||||
marker.setMap(tMap.map);
|
||||
if (settings.showLabel) {
|
||||
marker.label = new qq.maps.Label({
|
||||
clickable: false,
|
||||
content: settings.labelText,
|
||||
offset: new qq.maps.Size(-100, -iconInfo.size[1]-20),
|
||||
style: tMap.createMarkerLabelStyle(settings),
|
||||
visible: true,
|
||||
position: location,
|
||||
map: tMap.map,
|
||||
zIndex: 1000
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (settings.displayTooltip) {
|
||||
this.createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo, settings.autocloseTooltip, markerArgs);
|
||||
}
|
||||
|
||||
if (onClickListener) {
|
||||
qq.maps.event.addListener(marker, 'click', onClickListener);
|
||||
}
|
||||
|
||||
return marker;
|
||||
}
|
||||
|
||||
/* eslint-disable no-undef */
|
||||
removeMarker(marker) {
|
||||
marker.setMap(null);
|
||||
if (marker.label) {
|
||||
marker.label.setMap(null);
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-enable no-undef */
|
||||
|
||||
/* eslint-disable no-undef */
|
||||
createTooltip(marker, pattern, replaceInfo, autoClose, markerArgs) {
|
||||
var popup = new qq.maps.InfoWindow({
|
||||
map :this.map
|
||||
});
|
||||
var map = this;
|
||||
qq.maps.event.addListener(marker, 'click', function() {
|
||||
if (autoClose) {
|
||||
map.tooltips.forEach((tooltip) => {
|
||||
tooltip.popup.close();
|
||||
});
|
||||
}
|
||||
popup.open();
|
||||
popup.setPosition(marker);
|
||||
});
|
||||
this.tooltips.push( {
|
||||
markerArgs: markerArgs,
|
||||
popup: popup,
|
||||
pattern: pattern,
|
||||
replaceInfo: replaceInfo
|
||||
});
|
||||
}
|
||||
/* eslint-enable no-undef */
|
||||
|
||||
/* eslint-disable no-undef */
|
||||
updatePolylineColor(polyline, settings, color) {
|
||||
var options = {
|
||||
path: polyline.getPath(),
|
||||
strokeColor: color,
|
||||
strokeOpacity: settings.strokeOpacity,
|
||||
strokeWeight: settings.strokeWeight,
|
||||
map: this.map
|
||||
};
|
||||
polyline.setOptions(options);
|
||||
}
|
||||
/* eslint-enable no-undef */
|
||||
|
||||
/* eslint-disable no-undef */
|
||||
createPolyline(locations, settings) {
|
||||
var polyline = new qq.maps.Polyline({
|
||||
path: locations,
|
||||
strokeColor: settings.color,
|
||||
strokeOpacity: settings.strokeOpacity,
|
||||
strokeWeight: settings.strokeWeight,
|
||||
map: this.map
|
||||
});
|
||||
|
||||
return polyline;
|
||||
}
|
||||
/* eslint-enable no-undef */
|
||||
|
||||
removePolyline(polyline) {
|
||||
polyline.setMap(null);
|
||||
}
|
||||
|
||||
/* eslint-disable no-undef ,no-unused-vars*/
|
||||
fitBounds(bounds) {
|
||||
if (this.dontFitMapBounds && this.defaultZoomLevel) {
|
||||
this.map.setZoom(this.defaultZoomLevel);
|
||||
this.map.setCenter(bounds.getCenter());
|
||||
} else {
|
||||
var tbMap = this;
|
||||
qq.maps.event.addListenerOnce(this.map, 'bounds_changed', function() { // eslint-disable-line no-undef
|
||||
if (!tbMap.defaultZoomLevel && tbMap.map.getZoom() > tbMap.minZoomLevel) {
|
||||
tbMap.map.setZoom(tbMap.minZoomLevel);
|
||||
}
|
||||
});
|
||||
this.map.fitBounds(bounds);
|
||||
}
|
||||
}
|
||||
/* eslint-enable no-undef,no-unused-vars */
|
||||
|
||||
createLatLng(lat, lng) {
|
||||
return new qq.maps.LatLng(lat, lng); // eslint-disable-line no-undef
|
||||
}
|
||||
|
||||
extendBoundsWithMarker(bounds, marker) {
|
||||
bounds.extend(marker.getPosition());
|
||||
}
|
||||
|
||||
getMarkerPosition(marker) {
|
||||
return marker.getPosition();
|
||||
}
|
||||
|
||||
setMarkerPosition(marker, latLng) {
|
||||
marker.setPosition(latLng);
|
||||
if (marker.label) {
|
||||
marker.label.setPosition(latLng);
|
||||
}
|
||||
}
|
||||
|
||||
getPolylineLatLngs(polyline) {
|
||||
return polyline.getPath().getArray();
|
||||
}
|
||||
|
||||
setPolylineLatLngs(polyline, latLngs) {
|
||||
polyline.setPath(latLngs);
|
||||
}
|
||||
|
||||
createBounds() {
|
||||
return new qq.maps.LatLngBounds(); // eslint-disable-line no-undef
|
||||
}
|
||||
|
||||
extendBounds(bounds, polyline) {
|
||||
if (polyline && polyline.getPath()) {
|
||||
var locations = polyline.getPath();
|
||||
for (var i = 0; i < locations.getLength(); i++) {
|
||||
bounds.extend(locations.getAt(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
invalidateSize() {
|
||||
qq.maps.event.trigger(this.map, "resize"); // eslint-disable-line no-undef
|
||||
}
|
||||
|
||||
getTooltips() {
|
||||
return this.tooltips;
|
||||
}
|
||||
|
||||
}
|
||||
@ -41,7 +41,7 @@
|
||||
<md-tabs flex md-selected="vm.sourceIndex" ng-class="{'tb-headless': vm.sources.length === 1}"
|
||||
id="tabs" md-border-bottom flex>
|
||||
<md-tab ng-repeat="source in vm.sources" label="{{ source.datasource.name }}">
|
||||
<md-table-container>
|
||||
<md-table-container class="tb-absolute-fill layout-column">
|
||||
<table md-table>
|
||||
<thead md-head md-order="source.query.order" md-on-reorder="vm.onReorder(source)">
|
||||
<tr md-row>
|
||||
@ -70,16 +70,20 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<md-divider></md-divider>
|
||||
<span ng-show="!vm.sources[vm.sourceIndex].data.length"
|
||||
layout-align="center center"
|
||||
class="no-data-found" translate>widget.no-data-found</span>
|
||||
</md-table-container>
|
||||
<md-table-pagination ng-if="vm.displayPagination"
|
||||
md-limit="source.query.limit"
|
||||
md-limit-options="vm.limitOptions"
|
||||
md-page="source.query.page"
|
||||
md-total="{{source.data.length}}"
|
||||
md-on-paginate="vm.onPaginate(source)"
|
||||
md-page-select>
|
||||
</md-table-pagination>
|
||||
</md-tab>
|
||||
</md-tabs>
|
||||
<md-table-pagination ng-if="vm.displayPagination"
|
||||
md-limit="vm.sources[vm.sourceIndex].query.limit"
|
||||
md-limit-options="vm.limitOptions"
|
||||
md-page="vm.sources[vm.sourceIndex].query.page"
|
||||
md-total="{{vm.sources[vm.sourceIndex].data.length}}"
|
||||
md-on-paginate="vm.onPaginate(vm.sources[vm.sourceIndex])"
|
||||
md-page-select>
|
||||
</md-table-pagination>
|
||||
</div>
|
||||
</div>
|
||||
Loading…
x
Reference in New Issue
Block a user