Browse Source

Merge branch 'master' of github.com:thingsboard/thingsboard into edge/refactoring

pull/4410/head
Volodymyr Babak 6 years ago
parent
commit
c78823332b
  1. 18
      application/src/main/data/json/system/widget_bundles/input_widgets.json
  2. 2
      application/src/main/java/org/thingsboard/server/config/WebConfig.java
  3. 2
      application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
  4. 7
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapSecurityStore.java
  5. 5
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mQueuedRequest.java
  6. 21
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportRequest.java
  7. 19
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServiceImpl.java
  8. 5
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java
  9. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java
  10. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/event/AbstractEventInsertRepository.java
  11. 151
      dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
  12. 1
      msa/black-box-tests/README.md
  13. 9
      msa/tb/docker-cassandra/Dockerfile
  14. 9
      msa/tb/docker-postgres/Dockerfile
  15. 2
      msa/tb/pom.xml
  16. 2
      pom.xml
  17. 2
      rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java
  18. 23
      ui-ngx/src/app/core/http/resource.service.ts
  19. 2
      ui-ngx/src/app/core/services/time.service.ts
  20. 4
      ui-ngx/src/app/core/utils.ts
  21. 13
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  22. 4
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts
  23. 2
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html
  24. 36
      ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts
  25. 8
      ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts
  26. 18
      ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts
  27. 84
      ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.ts
  28. 29
      ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts
  29. 16
      ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts
  30. 33
      ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts
  31. 31
      ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts
  32. 69
      ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html
  33. 36
      ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.scss
  34. 227
      ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.ts
  35. 25
      ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts
  36. 15
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts
  37. 3
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  38. 4
      ui-ngx/src/app/modules/home/pages/asset/assets-table-config.resolver.ts
  39. 4
      ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts
  40. 4
      ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts
  41. 59
      ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts
  42. 24
      ui-ngx/src/app/modules/home/pages/edge/edge.component.html
  43. 4
      ui-ngx/src/app/modules/home/pages/edge/edges-table-config.resolver.ts
  44. 4
      ui-ngx/src/app/modules/home/pages/entity-view/entity-views-table-config.resolver.ts
  45. 4
      ui-ngx/src/app/modules/home/pages/profile/profile.component.html
  46. 9
      ui-ngx/src/app/modules/home/pages/profile/profile.component.ts
  47. 53
      ui-ngx/src/app/modules/home/pages/resource/resources-library-table-config.resolve.ts
  48. 10
      ui-ngx/src/app/modules/home/pages/resource/resources-library.component.html
  49. 23
      ui-ngx/src/app/modules/home/pages/resource/resources-library.component.ts
  50. 14
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.html
  51. 82
      ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts
  52. 4
      ui-ngx/src/app/shared/components/file-input.component.html
  53. 107
      ui-ngx/src/app/shared/components/file-input.component.ts
  54. 8
      ui-ngx/src/app/shared/components/json-object-edit.component.ts
  55. 8
      ui-ngx/src/app/shared/components/time/datetime-period.component.html
  56. 1
      ui-ngx/src/app/shared/components/time/quick-time-interval.component.ts
  57. 39
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.html
  58. 24
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.scss
  59. 5
      ui-ngx/src/app/shared/models/resource.models.ts
  60. 5
      ui-ngx/src/app/shared/shared.module.ts
  61. 2
      ui-ngx/src/assets/locale/locale.constant-zh_CN.json

18
application/src/main/data/json/system/widget_bundles/input_widgets.json

@ -455,6 +455,24 @@
"dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Photo camera input\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
}
},
{
"alias": "update_json_attribute",
"name": "Update JSON attribute",
"image": "data:image/png;base64,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",
"description": "Simple form to input new JSON value for pre-defined attribute/timeseries key.",
"descriptor": {
"type": "latest",
"sizeX": 7.5,
"sizeY": 3,
"resources": [],
"templateHtml": "<tb-json-input-widget \n [ctx]=\"ctx\">\n</tb-json-input-widget>",
"templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.jsonInputWidget.onDataUpdated();\n}\n\nself.onResize = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n}",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"AdvancedSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"widgetMode\": {\n \"title\": \"Widget mode\",\n \"type\": \"string\",\n \"default\": \"ATTRIBUTE\"\n },\n \"attributeScope\": {\n \"title\": \"Attribute scope\",\n \"type\": \"string\",\n \"default\": \"SERVER_SCOPE\"\n },\n \"showLabel\":{\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"labelValue\": {\n \"title\": \"Label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"attributeRequired\": {\n \"title\": \"Value required\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"showResultMessage\": {\n \"title\": \"Show result message\",\n \"type\": \"boolean\",\n \"default\": true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n {\n \"key\": \"widgetMode\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"ATTRIBUTE\",\n \"label\": \"Update attribute\"\n },\n {\n \"value\": \"TIME_SERIES\",\n \"label\": \"Update timeseries\"\n }\n ]\n },\n {\n \"key\": \"attributeScope\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"condition\": \"model.widgetMode === 'ATTRIBUTE'\",\n \"items\": [\n {\n \"value\": \"SERVER_SCOPE\",\n \"label\": \"Server attribute\"\n },\n {\n \"value\": \"SHARED_SCOPE\",\n \"label\": \"Shared attribute\"\n }\n ]\n },\n \"showLabel\",\n {\n \"key\": \"labelValue\",\n \"condition\": \"model.showLabel\"\n },\n \"attributeRequired\",\n \"showResultMessage\"\n ]\n}",
"dataKeySettingsSchema": "{}",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"attributeScope\":\"SERVER_SCOPE\",\"showLabel\":true,\"attributeRequired\":true,\"showResultMessage\":true},\"title\":\"Update JSON attribute\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}"
}
}
]
}

2
application/src/main/java/org/thingsboard/server/config/WebConfig.java

@ -21,7 +21,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class WebConfig {
@RequestMapping(value = "/{path:^(?!api$)(?!assets$)(?!static$)(?!webjars$)[^\\.]*}/**")
@RequestMapping(value = {"/assets", "/assets/", "/{path:^(?!api$)(?!assets$)(?!static$)(?!webjars$)[^\\.]*}/**"})
public String redirect() {
return "forward:/index.html";
}

2
application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java

@ -17,8 +17,10 @@ package org.thingsboard.server.service.install;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Profile;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

7
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapSecurityStore.java

@ -40,7 +40,7 @@ import org.thingsboard.server.transport.lwm2m.utils.TypeServer;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
@ -70,8 +70,7 @@ public class LwM2MBootstrapSecurityStore implements BootstrapSecurityStore {
@Override
public List<SecurityInfo> getAllByEndpoint(String endPoint) {
String endPointKey = endPoint;
ReadResultSecurityStore store = lwM2MCredentialsSecurityInfoValidator.createAndValidateCredentialsSecurityInfo(endPointKey, TypeServer.BOOTSTRAP);
ReadResultSecurityStore store = lwM2MCredentialsSecurityInfoValidator.createAndValidateCredentialsSecurityInfo(endPoint, TypeServer.BOOTSTRAP);
if (store.getBootstrapJsonCredential() != null && store.getSecurityMode() < LwM2MSecurityMode.DEFAULT_MODE.code) {
/** add value to store from BootstrapJson */
this.setBootstrapConfigScurityInfo(store);
@ -87,7 +86,7 @@ public class LwM2MBootstrapSecurityStore implements BootstrapSecurityStore {
} catch (InvalidConfigurationException e) {
log.error("", e);
}
return store.getSecurityInfo() == null ? null : Arrays.asList(store.getSecurityInfo());
return store.getSecurityInfo() == null ? null : Collections.singletonList(store.getSecurityInfo());
}
}
return null;

5
ui-ngx/src/app/shared/components/time/quick-time-interval.component.scss → common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mQueuedRequest.java

@ -13,7 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.lwm2m.server;
:host {
min-width: 364px;
public interface LwM2mQueuedRequest {
void send();
}

21
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportRequest.java

@ -16,6 +16,7 @@
package org.thingsboard.server.transport.lwm2m.server;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.coap.Response;
import org.eclipse.leshan.core.attributes.Attribute;
import org.eclipse.leshan.core.attributes.AttributeSet;
@ -34,6 +35,7 @@ import org.eclipse.leshan.core.request.ObserveRequest;
import org.eclipse.leshan.core.request.ReadRequest;
import org.eclipse.leshan.core.request.WriteAttributesRequest;
import org.eclipse.leshan.core.request.WriteRequest;
import org.eclipse.leshan.core.request.exception.ClientSleepingException;
import org.eclipse.leshan.core.response.CancelObservationResponse;
import org.eclipse.leshan.core.response.DeleteResponse;
import org.eclipse.leshan.core.response.DiscoverResponse;
@ -58,7 +60,6 @@ import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static org.eclipse.californium.core.coap.CoAP.ResponseCode.isSuccess;
import static org.eclipse.leshan.core.attributes.Attribute.MINIMUM_PERIOD;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.DEFAULT_TIMEOUT;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.GET_TYPE_OPER_DISCOVER;
@ -215,13 +216,19 @@ public class LwM2mTransportRequest {
request = new WriteAttributesRequest(resultIds.getObjectId(), attrSet);
}
break;
default:
}
if (request != null) {
this.sendRequest(registration, lwM2MClient, request, timeoutInMs);
}
else {
try {
this.sendRequest(registration, lwM2MClient, request, timeoutInMs);
} catch (ClientSleepingException e) {
DownlinkRequest finalRequest = request;
long finalTimeoutInMs = timeoutInMs;
lwM2MClient.getQueuedRequests().add(() -> sendRequest(registration, lwM2MClient, finalRequest, finalTimeoutInMs));
} catch (Exception e) {
log.error("[{}] [{}] [{}] Failed to send downlink.", registration.getEndpoint(), targetIdVer, typeOper, e);
}
} else {
log.error("[{}], [{}] - [{}] error SendRequest", registration.getEndpoint(), typeOper, targetIdVer);
}
}
@ -240,7 +247,7 @@ public class LwM2mTransportRequest {
if (!lwM2MClient.isInit()) {
lwM2MClient.initValue(this.serviceImpl, convertToIdVerFromObjectId(request.getPath().toString(), registration));
}
if (isSuccess(((Response) response.getCoapResponse()).getCode())) {
if (CoAP.ResponseCode.isSuccess(((Response) response.getCoapResponse()).getCode())) {
this.handleResponse(registration, request.getPath().toString(), response, request);
if (request instanceof WriteRequest && ((WriteRequest) request).isReplaceRequest()) {
String msg = String.format("%s: sendRequest Replace: CoapCde - %s Lwm2m code - %d name - %s Resource path - %s value - %s SendRequest to Client",
@ -283,7 +290,7 @@ public class LwM2mTransportRequest {
case FLOAT: // Double
return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, Double.parseDouble(value.toString())) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, Double.parseDouble(value.toString()));
case TIME: // Date
Date date = new Date(Long.decode(value.toString()));
Date date = new Date(Long.decode(value.toString()));
return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, date) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, date);
case OPAQUE: // byte[] value, base64
return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, Hex.decodeHex(value.toString().toCharArray())) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, Hex.decodeHex(value.toString().toCharArray()));

19
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServiceImpl.java

@ -146,9 +146,9 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService {
* Next -> Create new LwM2MClient for current session -> setModelClient...
*
* @param registration - Registration LwM2M Client
* @param previousObsersations - may be null
* @param previousObservations - may be null
*/
public void onRegistered(Registration registration, Collection<Observation> previousObsersations) {
public void onRegistered(Registration registration, Collection<Observation> previousObservations) {
executorRegistered.submit(() -> {
try {
log.warn("[{}] [{{}] Client: create after Registration", registration.getEndpoint(), registration.getId());
@ -188,6 +188,13 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService {
LwM2mClient lwM2MClient = this.lwM2mClientContext.getLwM2MClient(sessionInfo);
if (lwM2MClient.getDeviceId() == null && lwM2MClient.getProfileId() == null) {
initLwM2mClient(lwM2MClient, sessionInfo);
} else {
if (registration.getBindingMode().useQueueMode()) {
LwM2mQueuedRequest request;
while ((request = lwM2MClient.getQueuedRequests().poll()) != null) {
request.send();
}
}
}
log.info("Client: [{}] updatedReg [{}] name [{}] profile ", registration.getId(), registration.getEndpoint(), sessionInfo.getDeviceType());
@ -206,7 +213,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService {
* !!! Warn: if have not finishing unReg, then this operation will be finished on next Client`s connect
*/
public void unReg(Registration registration, Collection<Observation> observations) {
executorUnRegistered.submit(() -> {
executorUnRegistered.submit(() -> {
try {
this.setCancelObservations(registration);
this.sendLogsToThingsboard(LOG_LW2M_INFO + ": Client unRegistration", registration);
@ -239,8 +246,11 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService {
}
}
@Override
public void onSleepingDev(Registration registration) {
log.info("[{}] [{}] Received endpoint Sleeping version event", registration.getId(), registration.getEndpoint());
this.sendLogsToThingsboard(LOG_LW2M_INFO + ": Client is sleeping!", registration);
//TODO: associate endpointId with device information.
}
@ -417,6 +427,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService {
*/
protected void onAwakeDev(Registration registration) {
log.info("[{}] [{}] Received endpoint Awake version event", registration.getId(), registration.getEndpoint());
this.sendLogsToThingsboard(LOG_LW2M_INFO + ": Client is awake!", registration);
//TODO: associate endpointId with device information.
}
@ -612,7 +623,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService {
if (GET_TYPE_OPER_READ.equals(typeOper)) {
result = JacksonUtil.fromString(lwM2MClientProfile.getPostAttributeProfile().toString(), new TypeReference<>() {
});
result.addAll(JacksonUtil.fromString(lwM2MClientProfile.getPostTelemetryProfile().toString(), new TypeReference<>() {
result.addAll(JacksonUtil.convertValue(lwM2MClientProfile.getPostTelemetryProfile().toString(), new TypeReference<>() {
}));
} else {
result = JacksonUtil.fromString(lwM2MClientProfile.getPostObserveProfile().toString(), new TypeReference<>() {

5
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java

@ -25,13 +25,16 @@ import org.eclipse.leshan.server.registration.Registration;
import org.eclipse.leshan.server.security.SecurityInfo;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg;
import org.thingsboard.server.transport.lwm2m.server.LwM2mQueuedRequest;
import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportServiceImpl;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
@ -54,6 +57,7 @@ public class LwM2mClient implements Cloneable {
private final Map<String, ResourceValue> resources;
private final Map<String, TransportProtos.TsKvProto> delayedRequests;
private final List<String> pendingRequests;
private final Queue<LwM2mQueuedRequest> queuedRequests;
private boolean init;
public Object clone() throws CloneNotSupportedException {
@ -71,6 +75,7 @@ public class LwM2mClient implements Cloneable {
this.profileId = profileId;
this.sessionId = sessionId;
this.init = false;
this.queuedRequests = new ConcurrentLinkedQueue<>();
}
public boolean saveResourceValue(String pathRez, LwM2mResource rez, LwM2mModelProvider modelProvider) {

2
dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java

@ -51,11 +51,11 @@ public abstract class AbstractComponentDescriptorInsertRepository implements Com
TransactionStatus transaction = getTransactionStatus(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
try {
componentDescriptorEntity = processSaveOrUpdate(entity, insertOrUpdateOnUniqueKeyConflict);
transactionManager.commit(transaction);
} catch (Throwable th) {
log.trace("Could not execute the update statement for Component Descriptor with id {}, name {} and entityType {}", entity.getUuid(), entity.getName(), entity.getType());
transactionManager.rollback(transaction);
}
transactionManager.commit(transaction);
} else {
log.trace("Could not execute the insert statement for Component Descriptor with id {}, name {} and entityType {}", entity.getUuid(), entity.getName(), entity.getType());
}

2
dao/src/main/java/org/thingsboard/server/dao/sql/event/AbstractEventInsertRepository.java

@ -51,11 +51,11 @@ public abstract class AbstractEventInsertRepository implements EventInsertReposi
TransactionStatus transaction = getTransactionStatus(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
try {
eventEntity = processSaveOrUpdate(entity, insertOrUpdateOnUniqueKeyConflict);
transactionManager.commit(transaction);
} catch (Throwable th) {
log.trace("Could not execute the update statement for Entity with entityId {} and entityType {}", entity.getEventUid(), entity.getEventType());
transactionManager.rollback(transaction);
}
transactionManager.commit(transaction);
} else {
log.trace("Could not execute the insert statement for Entity with entityId {} and entityType {}", entity.getEventUid(), entity.getEventType());
}

151
dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java

@ -62,6 +62,8 @@ import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal;
@ -107,6 +109,7 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
private PreparedStatement[] fetchStmtsDesc;
private PreparedStatement deleteStmt;
private PreparedStatement deletePartitionStmt;
private final Lock stmtCreationLock = new ReentrantLock();
private boolean isInstall() {
return environment.acceptsProfiles(Profiles.of("install"));
@ -545,13 +548,20 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
private PreparedStatement getDeleteStmt() {
if (deleteStmt == null) {
deleteStmt = prepare("DELETE FROM " + ModelConstants.TS_KV_CF +
" WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.PARTITION_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.TS_COLUMN + " >= ? "
+ "AND " + ModelConstants.TS_COLUMN + " < ?");
stmtCreationLock.lock();
try {
if (deleteStmt == null) {
deleteStmt = prepare("DELETE FROM " + ModelConstants.TS_KV_CF +
" WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.PARTITION_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.TS_COLUMN + " >= ? "
+ "AND " + ModelConstants.TS_COLUMN + " < ?");
}
} finally {
stmtCreationLock.unlock();
}
}
return deleteStmt;
}
@ -585,27 +595,41 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
private PreparedStatement getDeletePartitionStmt() {
if (deletePartitionStmt == null) {
deletePartitionStmt = prepare("DELETE FROM " + ModelConstants.TS_KV_PARTITIONS_CF +
" WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.PARTITION_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM);
stmtCreationLock.lock();
try {
if (deletePartitionStmt == null) {
deletePartitionStmt = prepare("DELETE FROM " + ModelConstants.TS_KV_PARTITIONS_CF +
" WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.PARTITION_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM);
}
} finally {
stmtCreationLock.unlock();
}
}
return deletePartitionStmt;
}
private PreparedStatement getSaveStmt(DataType dataType) {
if (saveStmts == null) {
saveStmts = new PreparedStatement[DataType.values().length];
for (DataType type : DataType.values()) {
saveStmts[type.ordinal()] = prepare(INSERT_INTO + ModelConstants.TS_KV_CF +
"(" + ModelConstants.ENTITY_TYPE_COLUMN +
"," + ModelConstants.ENTITY_ID_COLUMN +
"," + ModelConstants.KEY_COLUMN +
"," + ModelConstants.PARTITION_COLUMN +
"," + ModelConstants.TS_COLUMN +
"," + getColumnName(type) + ")" +
" VALUES(?, ?, ?, ?, ?, ?)");
stmtCreationLock.lock();
try {
if (saveStmts == null) {
saveStmts = new PreparedStatement[DataType.values().length];
for (DataType type : DataType.values()) {
saveStmts[type.ordinal()] = prepare(INSERT_INTO + ModelConstants.TS_KV_CF +
"(" + ModelConstants.ENTITY_TYPE_COLUMN +
"," + ModelConstants.ENTITY_ID_COLUMN +
"," + ModelConstants.KEY_COLUMN +
"," + ModelConstants.PARTITION_COLUMN +
"," + ModelConstants.TS_COLUMN +
"," + getColumnName(type) + ")" +
" VALUES(?, ?, ?, ?, ?, ?)");
}
}
} finally {
stmtCreationLock.unlock();
}
}
return saveStmts[dataType.ordinal()];
@ -613,16 +637,23 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
private PreparedStatement getSaveTtlStmt(DataType dataType) {
if (saveTtlStmts == null) {
saveTtlStmts = new PreparedStatement[DataType.values().length];
for (DataType type : DataType.values()) {
saveTtlStmts[type.ordinal()] = prepare(INSERT_INTO + ModelConstants.TS_KV_CF +
"(" + ModelConstants.ENTITY_TYPE_COLUMN +
"," + ModelConstants.ENTITY_ID_COLUMN +
"," + ModelConstants.KEY_COLUMN +
"," + ModelConstants.PARTITION_COLUMN +
"," + ModelConstants.TS_COLUMN +
"," + getColumnName(type) + ")" +
" VALUES(?, ?, ?, ?, ?, ?) USING TTL ?");
stmtCreationLock.lock();
try {
if (saveTtlStmts == null) {
saveTtlStmts = new PreparedStatement[DataType.values().length];
for (DataType type : DataType.values()) {
saveTtlStmts[type.ordinal()] = prepare(INSERT_INTO + ModelConstants.TS_KV_CF +
"(" + ModelConstants.ENTITY_TYPE_COLUMN +
"," + ModelConstants.ENTITY_ID_COLUMN +
"," + ModelConstants.KEY_COLUMN +
"," + ModelConstants.PARTITION_COLUMN +
"," + ModelConstants.TS_COLUMN +
"," + getColumnName(type) + ")" +
" VALUES(?, ?, ?, ?, ?, ?) USING TTL ?");
}
}
} finally {
stmtCreationLock.unlock();
}
}
return saveTtlStmts[dataType.ordinal()];
@ -630,24 +661,38 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
private PreparedStatement getPartitionInsertStmt() {
if (partitionInsertStmt == null) {
partitionInsertStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF +
"(" + ModelConstants.ENTITY_TYPE_COLUMN +
"," + ModelConstants.ENTITY_ID_COLUMN +
"," + ModelConstants.PARTITION_COLUMN +
"," + ModelConstants.KEY_COLUMN + ")" +
" VALUES(?, ?, ?, ?)");
stmtCreationLock.lock();
try {
if (partitionInsertStmt == null) {
partitionInsertStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF +
"(" + ModelConstants.ENTITY_TYPE_COLUMN +
"," + ModelConstants.ENTITY_ID_COLUMN +
"," + ModelConstants.PARTITION_COLUMN +
"," + ModelConstants.KEY_COLUMN + ")" +
" VALUES(?, ?, ?, ?)");
}
} finally {
stmtCreationLock.unlock();
}
}
return partitionInsertStmt;
}
private PreparedStatement getPartitionInsertTtlStmt() {
if (partitionInsertTtlStmt == null) {
partitionInsertTtlStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF +
"(" + ModelConstants.ENTITY_TYPE_COLUMN +
"," + ModelConstants.ENTITY_ID_COLUMN +
"," + ModelConstants.PARTITION_COLUMN +
"," + ModelConstants.KEY_COLUMN + ")" +
" VALUES(?, ?, ?, ?) USING TTL ?");
stmtCreationLock.lock();
try {
if (partitionInsertTtlStmt == null) {
partitionInsertTtlStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF +
"(" + ModelConstants.ENTITY_TYPE_COLUMN +
"," + ModelConstants.ENTITY_ID_COLUMN +
"," + ModelConstants.PARTITION_COLUMN +
"," + ModelConstants.KEY_COLUMN + ")" +
" VALUES(?, ?, ?, ?) USING TTL ?");
}
} finally {
stmtCreationLock.unlock();
}
}
return partitionInsertTtlStmt;
}
@ -713,12 +758,26 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
switch (orderBy) {
case ASC_ORDER:
if (fetchStmtsAsc == null) {
fetchStmtsAsc = initFetchStmt(orderBy);
stmtCreationLock.lock();
try {
if (fetchStmtsAsc == null) {
fetchStmtsAsc = initFetchStmt(orderBy);
}
} finally {
stmtCreationLock.unlock();
}
}
return fetchStmtsAsc[aggType.ordinal()];
case DESC_ORDER:
if (fetchStmtsDesc == null) {
fetchStmtsDesc = initFetchStmt(orderBy);
stmtCreationLock.lock();
try {
if (fetchStmtsDesc == null) {
fetchStmtsDesc = initFetchStmt(orderBy);
}
} finally {
stmtCreationLock.unlock();
}
}
return fetchStmtsDesc[aggType.ordinal()];
default:

1
msa/black-box-tests/README.md

@ -10,6 +10,7 @@ To run the black box tests with using Docker, the local Docker images of Thingsb
As result, in REPOSITORY column, next images should be present:
thingsboard/tb-coap-transport
thingsboard/tb-lwm2m-transport
thingsboard/tb-http-transport
thingsboard/tb-mqtt-transport
thingsboard/tb-node

9
msa/tb/docker-cassandra/Dockerfile

@ -17,15 +17,14 @@
FROM thingsboard/openjdk11
RUN apt-get update
RUN apt-get install -y curl nmap procps
RUN echo 'deb http://ftp.us.debian.org/debian sid main' | tee --append /etc/apt/sources.list.d/debian.list > /dev/null
RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ sid-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null
RUN apt-get install -y curl nmap procps gnupg2
RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ buster-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null
RUN curl -L https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
RUN echo 'deb http://downloads.apache.org/cassandra/debian 40x main' | tee --append /etc/apt/sources.list.d/cassandra.list > /dev/null
RUN curl -L https://downloads.apache.org/cassandra/KEYS | apt-key add -
ENV PG_MAJOR=11
ENV PG_MAJOR=12
RUN apt-get update
RUN apt-get install -y cassandra cassandra-tools postgresql-11
RUN apt-get install -y cassandra cassandra-tools postgresql-12
RUN update-rc.d cassandra disable
RUN update-rc.d postgresql disable
RUN sed -i.old '/ulimit/d' /etc/init.d/cassandra

9
msa/tb/docker-postgres/Dockerfile

@ -17,13 +17,12 @@
FROM thingsboard/openjdk11
RUN apt-get update
RUN apt-get install -y curl
RUN echo 'deb http://ftp.us.debian.org/debian sid main' | tee --append /etc/apt/sources.list.d/debian.list > /dev/null
RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ sid-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null
RUN apt-get install -y curl gnupg2
RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ buster-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null
RUN curl -L https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
ENV PG_MAJOR 11
ENV PG_MAJOR 12
RUN apt-get update
RUN apt-get install -y postgresql-11
RUN apt-get install -y postgresql-12
RUN update-rc.d postgresql disable
COPY logback.xml ${pkg.name}.conf start-db.sh stop-db.sh start-tb.sh upgrade-tb.sh install-tb.sh ${pkg.name}.deb /tmp/

2
msa/tb/pom.xml

@ -39,7 +39,7 @@
<tb-postgres.docker.name>tb-postgres</tb-postgres.docker.name>
<tb-cassandra.docker.name>tb-cassandra</tb-cassandra.docker.name>
<pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder>
<pkg.upgradeVersion>2.4.2</pkg.upgradeVersion>
<pkg.upgradeVersion>3.3.0</pkg.upgradeVersion>
</properties>
<dependencies>

2
pom.xml

@ -99,7 +99,7 @@
org/thingsboard/server/extensions/core/plugin/telemetry/gen/**/*
</sonar.exclusions>
<elasticsearch.version>5.0.2</elasticsearch.version>
<delight-nashorn-sandbox.version>0.1.31</delight-nashorn-sandbox.version>
<delight-nashorn-sandbox.version>0.1.16</delight-nashorn-sandbox.version>
<kafka.version>2.6.0</kafka.version>
<bucket4j.version>4.1.1</bucket4j.version>
<fst.version>2.57</fst.version>

2
rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java

@ -1338,7 +1338,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
HttpEntity.EMPTY, DeviceProfile.class, deviceProfileId).getBody();
}
public PageData<DeviceProfile> getTenantDevices(PageLink pageLink) {
public PageData<DeviceProfile> getDeviceProfiles(PageLink pageLink) {
Map<String, String> params = new HashMap<>();
addPageLinkToParam(params, pageLink);
return restTemplate.exchange(

23
ui-ngx/src/app/core/http/resource.service.ts

@ -18,10 +18,10 @@ import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { PageLink } from '@shared/models/page/page-link';
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
import { Observable } from 'rxjs';
import { forkJoin, Observable, of } from 'rxjs';
import { PageData } from '@shared/models/page/page-data';
import { Resource, ResourceInfo } from '@shared/models/resource.models';
import { map } from 'rxjs/operators';
import { catchError, map, mergeMap } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
@ -70,6 +70,25 @@ export class ResourceService {
);
}
public saveResources(resources: Resource[], config?: RequestConfig): Observable<Resource[]> {
let partSize = 100;
partSize = resources.length > partSize ? partSize : resources.length;
const resourceObservables: Observable<Resource>[] = [];
for (let i = 0; i < partSize; i++) {
resourceObservables.push(this.saveResource(resources[i], config).pipe(catchError(() => of({} as Resource))));
}
return forkJoin(resourceObservables).pipe(
mergeMap((resource) => {
resources.splice(0, partSize);
if (resources.length) {
return this.saveResources(resources, config);
} else {
return of(resource);
}
})
);
}
public saveResource(resource: Resource, config?: RequestConfig): Observable<Resource> {
return this.http.post<Resource>('/api/resource', resource, defaultHttpOptionsFromConfig(config));
}

2
ui-ngx/src/app/core/services/time.service.ts

@ -79,7 +79,7 @@ export class TimeService {
public boundMinInterval(min: number): number {
if (isDefined(min)) {
min = Math.floor(min / 1000) * 1000;
min = Math.ceil(min / 1000) * 1000;
}
return this.toBound(min, MIN_INTERVAL, MAX_INTERVAL, MIN_INTERVAL);
}

4
ui-ngx/src/app/core/utils.ts

@ -127,6 +127,10 @@ export function isEmpty(obj: any): boolean {
return true;
}
export function isLiteralObject(value: any) {
return (!!value) && (value.constructor === Object);
}
export function formatValue(value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined {
if (isDefinedAndNotNull(value) && isNumeric(value) &&
(isDefinedAndNotNull(dec) || isDefinedAndNotNull(units) || Number(value).toString() === value)) {

13
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts

@ -90,7 +90,6 @@ import {
} from '@home/components/alias/entity-aliases-dialog.component';
import { EntityAliases } from '@app/shared/models/alias.models';
import { EditWidgetComponent } from '@home/components/dashboard-page/edit-widget.component';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import {
AddWidgetDialogComponent,
AddWidgetDialogData
@ -118,8 +117,7 @@ import { ComponentPortal } from '@angular/cdk/portal';
import {
DISPLAY_WIDGET_TYPES_PANEL_DATA,
DisplayWidgetTypesPanelComponent,
DisplayWidgetTypesPanelData,
WidgetTypes
DisplayWidgetTypesPanelData
} from '@home/components/dashboard-page/widget-types-panel.component';
import { DashboardWidgetSelectComponent } from '@home/components/dashboard-page/dashboard-widget-select.component';
import {AliasEntityType, EntityType} from "@shared/models/entity-type.models";
@ -1189,13 +1187,16 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
overlayRef.dispose();
});
const filterWidgetTypes = this.dashboardWidgetSelectComponent.filterWidgetTypes;
const widgetTypesList = Array.from(this.dashboardWidgetSelectComponent.widgetTypes.values()).map(type => {
return {type, display: filterWidgetTypes === null ? true : filterWidgetTypes.includes(type)};
});
const providers: StaticProvider[] = [
{
provide: DISPLAY_WIDGET_TYPES_PANEL_DATA,
useValue: {
types: Array.from(this.dashboardWidgetSelectComponent.widgetTypes.values()).map(type => {
return {type, display: true};
}),
types: widgetTypesList,
typesUpdated: (newTypes) => {
this.filterWidgetTypes = newTypes.filter(type => type.display).map(type => type.type);
}

4
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts

@ -77,6 +77,10 @@ export class DashboardWidgetSelectComponent implements OnInit {
this.filterWidgetTypes$.next(widgetTypes);
}
get filterWidgetTypes(): Array<widgetType> {
return this.filterWidgetTypes$.value;
}
@Output()
widgetSelected: EventEmitter<WidgetInfo> = new EventEmitter<WidgetInfo>();

2
ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html

@ -80,7 +80,7 @@
(mousedown)="widgetMouseDown($event, widget)"
(click)="widgetClicked($event, widget)"
(contextmenu)="openWidgetContextMenu($event, widget)">
<div *ngIf="widgetComponent.widgetContext?.inited" fxLayout="row" fxLayoutAlign="space-between start">
<div *ngIf="!!widgetComponent.widgetContext?.inited" fxLayout="row" fxLayoutAlign="space-between start">
<div class="tb-widget-title" fxLayout="column" fxLayoutAlign="center start" *ngIf="widget.showWidgetTitlePanel">
<span *ngIf="widget.showTitle"
[ngClass]="{'single-row': widget.hasTimewindow}"

36
ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts

@ -15,11 +15,21 @@
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import {
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import {
BooleanFilterPredicate,
BooleanOperation,
booleanOperationTranslationMap, EntityKeyValueType,
booleanOperationTranslationMap,
EntityKeyValueType,
FilterPredicateType
} from '@shared/models/query/query.models';
@ -32,10 +42,15 @@ import {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => BooleanFilterPredicateComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => BooleanFilterPredicateComponent),
multi: true
}
]
})
export class BooleanFilterPredicateComponent implements ControlValueAccessor, OnInit {
export class BooleanFilterPredicateComponent implements ControlValueAccessor, Validator, OnInit {
@Input() disabled: boolean;
@ -73,7 +88,7 @@ export class BooleanFilterPredicateComponent implements ControlValueAccessor, On
registerOnTouched(fn: any): void {
}
setDisabledState?(isDisabled: boolean): void {
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.booleanFilterPredicateFormGroup.disable({emitEvent: false});
@ -82,17 +97,20 @@ export class BooleanFilterPredicateComponent implements ControlValueAccessor, On
}
}
validate(): ValidationErrors | null {
return this.booleanFilterPredicateFormGroup ? null : {
booleanFilterPredicate: {valid: false}
};
}
writeValue(predicate: BooleanFilterPredicate): void {
this.booleanFilterPredicateFormGroup.get('operation').patchValue(predicate.operation, {emitEvent: false});
this.booleanFilterPredicateFormGroup.get('value').patchValue(predicate.value, {emitEvent: false});
}
private updateModel() {
let predicate: BooleanFilterPredicate = null;
if (this.booleanFilterPredicateFormGroup.valid) {
predicate = this.booleanFilterPredicateFormGroup.getRawValue();
predicate.type = FilterPredicateType.BOOLEAN;
}
const predicate: BooleanFilterPredicate = this.booleanFilterPredicateFormGroup.getRawValue();
predicate.type = FilterPredicateType.BOOLEAN;
this.propagateChange(predicate);
}

8
ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts

@ -16,11 +16,7 @@
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import {
ComplexFilterPredicate,
ComplexFilterPredicateInfo,
EntityKeyValueType
} from '@shared/models/query/query.models';
import { ComplexFilterPredicateInfo, EntityKeyValueType } from '@shared/models/query/query.models';
import { MatDialog } from '@angular/material/dialog';
import {
ComplexFilterPredicateDialogComponent,
@ -71,7 +67,7 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On
registerOnTouched(fn: any): void {
}
setDisabledState?(isDisabled: boolean): void {
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}

18
ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts

@ -21,7 +21,10 @@ import {
FormArray,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { Observable, of, Subscription } from 'rxjs';
@ -49,10 +52,15 @@ import { map } from 'rxjs/operators';
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FilterPredicateListComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => FilterPredicateListComponent),
multi: true
}
]
})
export class FilterPredicateListComponent implements ControlValueAccessor, OnInit {
export class FilterPredicateListComponent implements ControlValueAccessor, Validator, OnInit {
@Input() disabled: boolean;
@ -108,6 +116,12 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni
}
}
validate(control: AbstractControl): ValidationErrors | null {
return this.filterListFormGroup.valid ? null : {
filterList: {valid: false}
};
}
writeValue(predicates: Array<KeyFilterPredicateInfo>): void {
if (this.valueChangeSubscription) {
this.valueChangeSubscription.unsubscribe();
@ -178,7 +192,7 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni
private updateModel() {
const predicates: Array<KeyFilterPredicateInfo> = this.filterListFormGroup.getRawValue().predicates;
if (this.filterListFormGroup.valid && predicates.length) {
if (predicates.length) {
this.propagateChange(predicates);
} else {
this.propagateChange(null);

84
ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.ts

@ -19,7 +19,10 @@ import {
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
ValidatorFn,
Validators
} from '@angular/forms';
@ -39,10 +42,15 @@ import {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FilterPredicateValueComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => FilterPredicateValueComponent),
multi: true
}
]
})
export class FilterPredicateValueComponent implements ControlValueAccessor, OnInit {
export class FilterPredicateValueComponent implements ControlValueAccessor, Validator, OnInit {
private readonly inheritModeForSources: DynamicValueSourceType[] = [
DynamicValueSourceType.CURRENT_CUSTOMER,
@ -62,7 +70,22 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn
}
}
@Input() onlyUserDynamicSource = false;
private onlyUserDynamicSourceValue = false;
@Input()
set onlyUserDynamicSource(dynamicMode: boolean) {
this.onlyUserDynamicSourceValue = dynamicMode;
if (this.filterPredicateValueFormGroup) {
this.updateValidationDynamicMode();
setTimeout(() => {
this.updateModel();
}, 0);
}
}
get onlyUserDynamicSource(): boolean {
return this.onlyUserDynamicSourceValue;
}
@Input()
valueType: EntityKeyValueType;
@ -83,6 +106,7 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn
allow = true;
private propagateChange = null;
private propagateChangePending = false;
constructor(private fb: FormBuilder) {
}
@ -126,6 +150,7 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn
this.updateShowInheritMode(sourceType);
}
);
this.updateValidationDynamicMode();
this.filterPredicateValueFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
@ -133,12 +158,18 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn
registerOnChange(fn: any): void {
this.propagateChange = fn;
if (this.propagateChangePending) {
this.propagateChangePending = false;
setTimeout(() => {
this.updateModel();
}, 0);
}
}
registerOnTouched(fn: any): void {
}
setDisabledState?(isDisabled: boolean): void {
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.filterPredicateValueFormGroup.disable({emitEvent: false});
@ -147,28 +178,35 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn
}
}
validate(): ValidationErrors | null {
return this.filterPredicateValueFormGroup.valid ? null : {
filterPredicateValue: {valid: false}
};
}
writeValue(predicateValue: FilterPredicateValue<string | number | boolean>): void {
this.propagateChangePending = false;
this.filterPredicateValueFormGroup.get('defaultValue').patchValue(predicateValue.defaultValue, {emitEvent: false});
this.filterPredicateValueFormGroup.get('dynamicValue.sourceType').patchValue(predicateValue.dynamicValue ?
predicateValue.dynamicValue.sourceType : null, {emitEvent: false});
this.filterPredicateValueFormGroup.get('dynamicValue.sourceAttribute').patchValue(predicateValue.dynamicValue ?
predicateValue.dynamicValue.sourceAttribute : null, {emitEvent: false});
this.filterPredicateValueFormGroup.get('dynamicValue.inherit').patchValue(predicateValue.dynamicValue ?
predicateValue.dynamicValue.inherit : false, {emitEvent: false});
this.filterPredicateValueFormGroup.get('dynamicValue').patchValue({
sourceType: predicateValue.dynamicValue ? predicateValue.dynamicValue.sourceType : null,
sourceAttribute: predicateValue.dynamicValue ? predicateValue.dynamicValue.sourceAttribute : null,
inherit: predicateValue.dynamicValue ? predicateValue.dynamicValue.inherit : false
}, {emitEvent: this.onlyUserDynamicSource});
this.updateShowInheritMode(predicateValue?.dynamicValue?.sourceType);
}
private updateModel() {
let predicateValue: FilterPredicateValue<string | number | boolean> = null;
if (this.filterPredicateValueFormGroup.valid) {
predicateValue = this.filterPredicateValueFormGroup.getRawValue();
if (predicateValue.dynamicValue) {
if (!predicateValue.dynamicValue.sourceType || !predicateValue.dynamicValue.sourceAttribute) {
predicateValue.dynamicValue = null;
}
const predicateValue: FilterPredicateValue<string | number | boolean> = this.filterPredicateValueFormGroup.getRawValue();
if (predicateValue.dynamicValue) {
if (!predicateValue.dynamicValue.sourceType || !predicateValue.dynamicValue.sourceAttribute) {
predicateValue.dynamicValue = null;
}
}
this.propagateChange(predicateValue);
if (this.propagateChange) {
this.propagateChange(predicateValue);
} else {
this.propagateChangePending = true;
}
}
private updateShowInheritMode(sourceType: DynamicValueSourceType) {
@ -179,4 +217,16 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn
this.inheritMode = false;
}
}
private updateValidationDynamicMode() {
if (this.onlyUserDynamicSource) {
this.filterPredicateValueFormGroup.get('dynamicValue.sourceType').setValidators(Validators.required);
this.filterPredicateValueFormGroup.get('dynamicValue.sourceAttribute').setValidators(Validators.required);
} else {
this.filterPredicateValueFormGroup.get('dynamicValue.sourceType').clearValidators();
this.filterPredicateValueFormGroup.get('dynamicValue.sourceAttribute').clearValidators();
}
this.filterPredicateValueFormGroup.get('dynamicValue.sourceType').updateValueAndValidity({emitEvent: false});
this.filterPredicateValueFormGroup.get('dynamicValue.sourceAttribute').updateValueAndValidity({emitEvent: false});
}
}

29
ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts

@ -15,11 +15,17 @@
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import {
EntityKeyValueType,
FilterPredicateType, KeyFilterPredicate, KeyFilterPredicateInfo
} from '@shared/models/query/query.models';
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { EntityKeyValueType, FilterPredicateType, KeyFilterPredicateInfo } from '@shared/models/query/query.models';
@Component({
selector: 'tb-filter-predicate',
@ -30,10 +36,15 @@ import {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FilterPredicateComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => FilterPredicateComponent),
multi: true
}
]
})
export class FilterPredicateComponent implements ControlValueAccessor, OnInit {
export class FilterPredicateComponent implements ControlValueAccessor, Validator, OnInit {
@Input() disabled: boolean;
@ -75,7 +86,7 @@ export class FilterPredicateComponent implements ControlValueAccessor, OnInit {
registerOnTouched(fn: any): void {
}
setDisabledState?(isDisabled: boolean): void {
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.filterPredicateFormGroup.disable({emitEvent: false});
@ -84,6 +95,12 @@ export class FilterPredicateComponent implements ControlValueAccessor, OnInit {
}
}
validate(): ValidationErrors | null {
return this.filterPredicateFormGroup.valid ? null : {
filterPredicate: {valid: false}
};
}
writeValue(predicate: KeyFilterPredicateInfo): void {
this.type = predicate.keyFilterPredicate.type;
this.filterPredicateFormGroup.get('predicate').patchValue(predicate.keyFilterPredicate, {emitEvent: false});

16
ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts

@ -22,7 +22,10 @@ import {
FormBuilder,
FormControl,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { Observable, Subscription } from 'rxjs';
@ -46,10 +49,15 @@ import { EntityId } from '@shared/models/id/entity-id';
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => KeyFilterListComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => KeyFilterListComponent),
multi: true
}
]
})
export class KeyFilterListComponent implements ControlValueAccessor, OnInit {
export class KeyFilterListComponent implements ControlValueAccessor, Validator, OnInit {
@Input() disabled: boolean;
@ -104,6 +112,12 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit {
}
}
validate(): ValidationErrors | null {
return this.keyFilterListFormGroup.valid && this.keyFiltersControl.valid ? null : {
keyFilterList: {valid: false}
};
}
writeValue(keyFilters: Array<KeyFilterInfo>): void {
if (this.valueChangeSubscription) {
this.valueChangeSubscription.unsubscribe();

33
ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts

@ -15,7 +15,16 @@
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import {
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import {
EntityKeyValueType,
FilterPredicateType,
@ -33,10 +42,15 @@ import {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => NumericFilterPredicateComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => NumericFilterPredicateComponent),
multi: true
}
]
})
export class NumericFilterPredicateComponent implements ControlValueAccessor, OnInit {
export class NumericFilterPredicateComponent implements ControlValueAccessor, Validator, OnInit {
@Input() disabled: boolean;
@ -76,7 +90,7 @@ export class NumericFilterPredicateComponent implements ControlValueAccessor, On
registerOnTouched(fn: any): void {
}
setDisabledState?(isDisabled: boolean): void {
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.numericFilterPredicateFormGroup.disable({emitEvent: false});
@ -85,17 +99,20 @@ export class NumericFilterPredicateComponent implements ControlValueAccessor, On
}
}
validate(): ValidationErrors | null {
return this.numericFilterPredicateFormGroup.valid ? null : {
numericFilterPredicate: {valid: false}
};
}
writeValue(predicate: NumericFilterPredicate): void {
this.numericFilterPredicateFormGroup.get('operation').patchValue(predicate.operation, {emitEvent: false});
this.numericFilterPredicateFormGroup.get('value').patchValue(predicate.value, {emitEvent: false});
}
private updateModel() {
let predicate: NumericFilterPredicate = null;
if (this.numericFilterPredicateFormGroup.valid) {
predicate = this.numericFilterPredicateFormGroup.getRawValue();
predicate.type = FilterPredicateType.NUMERIC;
}
const predicate: NumericFilterPredicate = this.numericFilterPredicateFormGroup.getRawValue();
predicate.type = FilterPredicateType.NUMERIC;
this.propagateChange(predicate);
}

31
ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts

@ -15,7 +15,16 @@
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import {
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import {
EntityKeyValueType,
FilterPredicateType,
@ -33,10 +42,15 @@ import {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => StringFilterPredicateComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => StringFilterPredicateComponent),
multi: true
}
]
})
export class StringFilterPredicateComponent implements ControlValueAccessor, OnInit {
export class StringFilterPredicateComponent implements ControlValueAccessor, Validator, OnInit {
@Input() disabled: boolean;
@ -90,12 +104,15 @@ export class StringFilterPredicateComponent implements ControlValueAccessor, OnI
this.stringFilterPredicateFormGroup.get('ignoreCase').patchValue(predicate.ignoreCase, {emitEvent: false});
}
validate(c): ValidationErrors {
return this.stringFilterPredicateFormGroup.valid ? null : {
stringFilterPredicate: {valid: false}
};
}
private updateModel() {
let predicate: StringFilterPredicate = null;
if (this.stringFilterPredicateFormGroup.valid) {
predicate = this.stringFilterPredicateFormGroup.getRawValue();
predicate.type = FilterPredicateType.STRING;
}
const predicate: StringFilterPredicate = this.stringFilterPredicateFormGroup.getRawValue();
predicate.type = FilterPredicateType.STRING;
this.propagateChange(predicate);
}

69
ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html

@ -0,0 +1,69 @@
<!--
Copyright © 2016-2021 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.
-->
<div class="tb-json-input" tb-toast toastTarget="{{ toastTargetId }}">
<form *ngIf="attributeUpdateFormGroup"
fxLayout="column"
class="tb-json-input__form"
[formGroup]="attributeUpdateFormGroup"
(ngSubmit)="save()">
<div fxLayout="column" fxLayoutGap="10px" fxFlex *ngIf="entityDetected && isValidParameter && dataKeyDetected">
<fieldset fxFlex>
<tb-json-object-edit
[editorStyle]="{minHeight: '100px'}"
fillHeight="true"
[required]="settings.attributeRequired"
label="{{ settings.showLabel ? labelValue : '' }}"
formControlName="currentValue"
(focusin)="isFocused = true;"
(focusout)="isFocused = false;"
></tb-json-object-edit>
</fieldset>
<div class="tb-json-input-form__actions" fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="20px">
<button mat-button color="primary"
type="button"
[disabled]="!attributeUpdateFormGroup.dirty"
(click)="discard()"
matTooltip="{{ 'widgets.input-widgets.discard-changes' | translate }}"
matTooltipPosition="above">
{{ "action.undo" | translate }}
</button>
<button mat-button mat-raised-button color="primary"
type="submit"
[disabled]="attributeUpdateFormGroup.invalid || !attributeUpdateFormGroup.dirty">
{{ "action.save" | translate }}
</button>
</div>
</div>
<div fxLayout="column" fxLayoutAlign="center center" fxFlex *ngIf="!entityDetected || !dataKeyDetected || !isValidParameter">
<div class="tb-json-input__error"
*ngIf="!entityDetected">
{{ 'widgets.input-widgets.no-entity-selected' | translate }}
</div>
<div class="tb-json-input__error"
*ngIf="entityDetected && !dataKeyDetected">
{{ 'widgets.input-widgets.no-datakey-selected' | translate }}
</div>
<div class="tb-json-input__error"
*ngIf="dataKeyDetected && !isValidParameter">
{{ errorMessage | translate }}
</div>
</div>
</form>
</div>

36
ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.scss

@ -0,0 +1,36 @@
/**
* Copyright © 2016-2021 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-json-input {
width: 100%;
height: 100%;
padding: 5px;
&__form {
overflow: auto;
height: 100%;
}
&__error {
text-align: center;
font-size: 18px;
color: #a0a0a0;
}
}
.tb-toast {
font-size: 14px!important;
}

227
ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.ts

@ -0,0 +1,227 @@
///
/// Copyright © 2016-2021 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 { Component, Input, OnInit } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { WidgetContext } from '@home/models/widget-component.models';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { Datasource, DatasourceData, DatasourceType, WidgetConfig } from '@shared/models/widget.models';
import { IWidgetSubscription } from '@core/api/widget-api.models';
import { FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms';
import { AttributeService } from '@core/http/attribute.service';
import { AttributeData, AttributeScope, DataKeyType, LatestTelemetry } from '@shared/models/telemetry/telemetry.models';
import { EntityId } from '@shared/models/id/entity-id';
import { EntityType } from '@shared/models/entity-type.models';
import { createLabelFromDatasource } from '@core/utils';
import { Observable } from 'rxjs';
enum JsonInputWidgetMode {
ATTRIBUTE = 'ATTRIBUTE',
TIME_SERIES = 'TIME_SERIES',
}
interface JsonInputWidgetSettings {
widgetTitle: string;
widgetMode: JsonInputWidgetMode;
attributeScope?: AttributeScope;
showLabel: boolean;
labelValue?: string;
attributeRequired: boolean;
showResultMessage: boolean;
}
@Component({
selector: 'tb-json-input-widget ',
templateUrl: './json-input-widget.component.html',
styleUrls: ['./json-input-widget.component.scss']
})
export class JsonInputWidgetComponent extends PageComponent implements OnInit {
@Input()
ctx: WidgetContext;
public settings: JsonInputWidgetSettings;
private widgetConfig: WidgetConfig;
private subscription: IWidgetSubscription;
private datasource: Datasource;
labelValue: string;
entityDetected = false;
dataKeyDetected = false;
isValidParameter = false;
errorMessage: string;
isFocused: boolean;
originalValue: any;
attributeUpdateFormGroup: FormGroup;
toastTargetId = 'json-input-widget' + this.utils.guid();
constructor(protected store: Store<AppState>,
private utils: UtilsService,
private fb: FormBuilder,
private attributeService: AttributeService,
private translate: TranslateService) {
super(store);
}
ngOnInit(): void {
this.ctx.$scope.jsonInputWidget = this;
this.settings = this.ctx.settings;
this.widgetConfig = this.ctx.widgetConfig;
this.subscription = this.ctx.defaultSubscription;
this.datasource = this.subscription.datasources[0];
this.initializeConfig();
this.validateDatasources();
this.buildForm();
this.ctx.updateWidgetParams();
}
private initializeConfig() {
if (this.settings.widgetTitle && this.settings.widgetTitle.length) {
const title = createLabelFromDatasource(this.datasource, this.settings.widgetTitle);
this.ctx.widgetTitle = this.utils.customTranslation(title, title);
} else {
this.ctx.widgetTitle = this.ctx.widgetConfig.title;
}
if (this.settings.labelValue && this.settings.labelValue.length) {
const label = createLabelFromDatasource(this.datasource, this.settings.labelValue);
this.labelValue = this.utils.customTranslation(label, label);
} else {
this.labelValue = this.translate.instant('widgets.input-widgets.value');
}
}
private validateDatasources() {
if (this.datasource?.type === DatasourceType.entity) {
this.entityDetected = true;
if (this.datasource.dataKeys.length) {
this.dataKeyDetected = true;
if (this.settings.widgetMode === JsonInputWidgetMode.ATTRIBUTE) {
if (this.datasource.dataKeys[0].type === DataKeyType.attribute) {
if (this.settings.attributeScope === AttributeScope.SERVER_SCOPE || this.datasource.entityType === EntityType.DEVICE) {
this.isValidParameter = true;
} else {
this.errorMessage = 'widgets.input-widgets.not-allowed-entity';
}
} else {
this.errorMessage = 'widgets.input-widgets.no-attribute-selected';
}
} else {
if (this.datasource.dataKeys[0].type === DataKeyType.timeseries) {
this.isValidParameter = true;
} else {
this.errorMessage = 'widgets.input-widgets.no-timeseries-selected';
}
}
}
}
}
private buildForm() {
const validators: ValidatorFn[] = [];
if (this.settings.attributeRequired) {
validators.push(Validators.required);
}
this.attributeUpdateFormGroup = this.fb.group({
currentValue: [{}, validators]
});
this.attributeUpdateFormGroup.valueChanges.subscribe( () => {
this.ctx.detectChanges();
});
}
private updateWidgetData(data: Array<DatasourceData>) {
if (this.isValidParameter) {
let value = {};
if (data[0].data[0][1] !== '') {
try {
value = JSON.parse(data[0].data[0][1]);
} catch (e) {
value = data[0].data[0][1];
}
}
this.originalValue = value;
if (!this.isFocused) {
this.attributeUpdateFormGroup.get('currentValue').patchValue(this.originalValue);
this.ctx.detectChanges();
}
}
}
public onDataUpdated() {
this.updateWidgetData(this.subscription.data);
}
public save() {
this.isFocused = false;
const attributeToSave: AttributeData = {
key: this.datasource.dataKeys[0].name,
value: this.attributeUpdateFormGroup.get('currentValue').value
};
const entityId: EntityId = {
entityType: this.datasource.entityType,
id: this.datasource.entityId
};
let saveAttributeObservable: Observable<any>;
if (this.settings.widgetMode === JsonInputWidgetMode.ATTRIBUTE) {
saveAttributeObservable = this.attributeService.saveEntityAttributes(
entityId,
this.settings.attributeScope,
[ attributeToSave ],
{}
);
} else {
saveAttributeObservable = this.attributeService.saveEntityTimeseries(
entityId,
LatestTelemetry.LATEST_TELEMETRY,
[ attributeToSave ],
{}
);
}
saveAttributeObservable.subscribe(
() => {
this.attributeUpdateFormGroup.markAsPristine();
this.ctx.detectChanges();
if (this.settings.showResultMessage) {
this.ctx.showSuccessToast(this.translate.instant('widgets.input-widgets.update-successful'),
1000, 'bottom', 'left', this.toastTargetId);
}
},
() => {
if (this.settings.showResultMessage) {
this.ctx.showErrorToast(this.translate.instant('widgets.input-widgets.update-failed'),
'bottom', 'left', this.toastTargetId);
}
});
}
public discard() {
this.attributeUpdateFormGroup.reset({currentValue: this.originalValue}, {emitEvent: false});
this.attributeUpdateFormGroup.markAsPristine();
this.isFocused = false;
}
}

25
ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts

@ -19,7 +19,8 @@ import {
createLabelFromDatasource,
hashCode,
isDefined,
isDefinedAndNotNull, isFunction,
isDefinedAndNotNull,
isFunction,
isNumber,
isUndefined,
padValue
@ -30,7 +31,7 @@ import { Datasource, DatasourceData } from '@shared/models/widget.models';
import _ from 'lodash';
import { mapProviderSchema, providerSets } from '@home/components/widget/lib/maps/schemes';
import { addCondition, mergeSchemes } from '@core/schema-utils';
import L, {Projection} from "leaflet";
import L from 'leaflet';
export function getProviderSchema(mapProvider: MapProviders, ignoreImageMap = false) {
const providerSchema = _.cloneDeep(mapProviderSchema);
@ -318,22 +319,24 @@ export const parseWithTranslation = {
};
export function parseData(input: DatasourceData[]): FormattedData[] {
return _(input).groupBy(el => el?.datasource?.entityName)
return _(input).groupBy(el => el?.datasource.entityId + el?.datasource.entityType)
.values().value().map((entityArray, i) => {
const obj: FormattedData = {
entityName: entityArray[0]?.datasource?.entityName,
entityId: entityArray[0]?.datasource?.entityId,
entityType: entityArray[0]?.datasource?.entityType,
$datasource: entityArray[0]?.datasource,
entityId: entityArray[0].datasource.entityId,
entityType: entityArray[0].datasource.entityType,
$datasource: entityArray[0].datasource,
dsIndex: i,
deviceType: null
};
entityArray.filter(el => el.data.length).forEach(el => {
const indexDate = el?.data?.length ? el.data.length - 1 : 0;
obj[el?.dataKey?.label] = el?.data[indexDate][1];
obj[el?.dataKey?.label + '|ts'] = el?.data[indexDate][0];
if (el?.dataKey?.label === 'type') {
obj.deviceType = el?.data[indexDate][1];
const indexDate = el.data.length ? el.data.length - 1 : 0;
if (!obj.hasOwnProperty(el.dataKey.label) || el.data[indexDate][1] !== '') {
obj[el.dataKey.label] = el.data[indexDate][1];
obj[el.dataKey.label + '|ts'] = el.data[indexDate][0];
if (el.dataKey.label === 'type') {
obj.deviceType = el.data[indexDate][1];
}
}
});
return obj;

15
ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts

@ -121,7 +121,7 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy
this.switchResize$ = new ResizeObserver(() => {
this.resize();
})
});
this.switchResize$.observe(this.switchContainerRef.nativeElement);
this.init();
}
@ -202,13 +202,13 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy
let width = this.switchContainer.width();
let height = this.switchContainer.height();
if (this.showOnOffLabels) {
height = height*2/3;
height = height * 2 / 3;
}
const ratio = width/height;
const ratio = width / height;
if (ratio > switchAspectRation) {
width = height*switchAspectRation;
width = height * switchAspectRation;
} else {
height = width/switchAspectRation;
height = width / switchAspectRation;
}
this.switchElement.css({width, height});
this.matSlideToggle.css({width, height, minWidth: width});
@ -232,11 +232,11 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy
fontSize--;
textWidth = this.measureTextWidth(text, fontSize);
}
element.css({fontSize: fontSize+'px', lineHeight: fontSize+'px'});
element.css({fontSize: fontSize + 'px', lineHeight: fontSize + 'px'});
}
private measureTextWidth(text: string, fontSize: number): number {
this.textMeasure.css({fontSize: fontSize+'px', lineHeight: fontSize+'px'});
this.textMeasure.css({fontSize: fontSize + 'px', lineHeight: fontSize + 'px'});
this.textMeasure.text(text);
return this.textMeasure.width();
}
@ -260,6 +260,7 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy
this.ctx.controlApi.sendTwoWayCommand(this.getValueMethod, null, this.requestTimeout).subscribe(
(responseBody) => {
this.setValue(this.parseValueFunction(responseBody));
this.ctx.detectChanges();
},
() => {
const errorText = this.ctx.defaultSubscription.rpcErrorText;

3
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -38,6 +38,7 @@ import { ImportExportService } from '@home/components/import-export/import-expor
import { NavigationCardsWidgetComponent } from '@home/components/widget/lib/navigation-cards-widget.component';
import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navigation-card-widget.component';
import { EdgesOverviewWidgetComponent } from '@home/components/widget/lib/edges-overview-widget.component';
import { JsonInputWidgetComponent } from '@home/components/widget/lib/json-input-widget.component';
@NgModule({
declarations:
@ -51,6 +52,7 @@ import { EdgesOverviewWidgetComponent } from '@home/components/widget/lib/edges-
EdgesOverviewWidgetComponent,
DateRangeNavigatorWidgetComponent,
DateRangeNavigatorPanelComponent,
JsonInputWidgetComponent,
MultipleInputWidgetComponent,
TripAnimationComponent,
PhotoCameraInputWidgetComponent,
@ -72,6 +74,7 @@ import { EdgesOverviewWidgetComponent } from '@home/components/widget/lib/edges-
EdgesOverviewWidgetComponent,
RpcWidgetsModule,
DateRangeNavigatorWidgetComponent,
JsonInputWidgetComponent,
MultipleInputWidgetComponent,
TripAnimationComponent,
PhotoCameraInputWidgetComponent,

4
ui-ngx/src/app/modules/home/pages/asset/assets-table-config.resolver.ts

@ -104,7 +104,7 @@ export class AssetsTableConfigResolver implements Resolve<EntityTableConfig<Asse
));
};
this.config.onEntityAction = action => this.onAssetAction(action);
this.config.detailsReadonly = () => this.config.componentsData.assetScope === 'customer_user';
this.config.detailsReadonly = () => (this.config.componentsData.assetScope === 'customer_user' || this.config.componentsData.assetScope === 'edge_customer_user');
this.config.headerComponent = AssetTableHeaderComponent;
@ -151,7 +151,7 @@ export class AssetsTableConfigResolver implements Resolve<EntityTableConfig<Asse
this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.assetScope);
this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.assetScope);
this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.assetScope);
this.config.addEnabled = this.config.componentsData.assetScope !== 'customer_user';
this.config.addEnabled = !(this.config.componentsData.assetScope === 'customer_user' || this.config.componentsData.assetScope === 'edge_customer_user');
this.config.entitiesDeleteEnabled = this.config.componentsData.assetScope === 'tenant';
this.config.deleteEnabled = () => this.config.componentsData.assetScope === 'tenant';
return this.config;

4
ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts

@ -103,7 +103,7 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<
return this.dashboardService.saveDashboard(dashboard as Dashboard);
};
this.config.onEntityAction = action => this.onDashboardAction(action);
this.config.detailsReadonly = () => this.config.componentsData.dashboardScope === 'customer_user';
this.config.detailsReadonly = () => (this.config.componentsData.dashboardScope === 'customer_user' || this.config.componentsData.dashboardScope === 'edge_customer_user');
}
resolve(route: ActivatedRouteSnapshot): Observable<EntityTableConfig<DashboardInfo | Dashboard>> {
@ -147,7 +147,7 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<
this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.dashboardScope);
this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.dashboardScope);
this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.dashboardScope);
this.config.addEnabled = this.config.componentsData.dashboardScope !== 'customer_user';
this.config.addEnabled = !(this.config.componentsData.dashboardScope === 'customer_user' || this.config.componentsData.dashboardScope === 'edge_customer_user');
this.config.entitiesDeleteEnabled = this.config.componentsData.dashboardScope === 'tenant';
this.config.deleteEnabled = () => this.config.componentsData.dashboardScope === 'tenant';
return this.config;

4
ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts

@ -112,7 +112,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
));
};
this.config.onEntityAction = action => this.onDeviceAction(action);
this.config.detailsReadonly = () => this.config.componentsData.deviceScope === 'customer_user';
this.config.detailsReadonly = () => (this.config.componentsData.deviceScope === 'customer_user' || this.config.componentsData.deviceScope === 'edge_customer_user');
this.config.headerComponent = DeviceTableHeaderComponent;
@ -161,7 +161,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.deviceScope);
this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.deviceScope);
this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.deviceScope);
this.config.addEnabled = this.config.componentsData.deviceScope !== 'customer_user';
this.config.addEnabled = !(this.config.componentsData.deviceScope === 'customer_user' || this.config.componentsData.deviceScope === 'edge_customer_user');
this.config.entitiesDeleteEnabled = this.config.componentsData.deviceScope === 'tenant';
this.config.deleteEnabled = () => this.config.componentsData.deviceScope === 'tenant';
return this.config;

59
ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts

@ -58,21 +58,6 @@ const routes: Routes = [
entitiesTableConfig: EdgesTableConfigResolver
}
},
{
path: ':edgeId/ruleChains',
component: EntitiesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN],
ruleChainsType: 'edge',
breadcrumb: {
label: 'edge.edge-rulechains',
icon: 'settings_ethernet'
},
},
resolve: {
entitiesTableConfig: RuleChainsTableConfigResolver
}
},
{
path: ':edgeId/assets',
component: EntitiesTableComponent,
@ -156,6 +141,50 @@ const routes: Routes = [
}
]
},
{
path: ':edgeId/ruleChains',
data: {
breadcrumb: {
label: 'edge.edge-rulechains',
icon: 'settings_ethernet'
}
},
children: [
{
path: '',
component: EntitiesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN],
title: 'edge.rulechains',
ruleChainsType: 'edge'
},
resolve: {
entitiesTableConfig: RuleChainsTableConfigResolver
}
},
{
path: ':ruleChainId',
component: RuleChainPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
breadcrumb: {
labelFunction: ruleChainBreadcumbLabelFunction,
icon: 'settings_ethernet'
} as BreadCrumbConfig<RuleChainPageComponent>,
auth: [Authority.TENANT_ADMIN],
title: 'rulechain.edge-rulechain',
import: false,
ruleChainType: RuleChainType.EDGE
},
resolve: {
ruleChain: RuleChainResolver,
ruleChainMetaData: ResolvedRuleChainMetaDataResolver,
ruleNodeComponents: RuleNodeComponentsResolver,
tooltipster: TooltipsterResolver
}
}
]
},
{
path: 'ruleChains',
data: {

24
ui-ngx/src/app/modules/home/pages/edge/edge.component.html

@ -44,31 +44,31 @@
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'openEdgeAssets')"
[fxShow]="!isEdit">
[fxShow]="!isEdit && edgeScope !== 'customer'">
{{'edge.manage-edge-assets' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'openEdgeDevices')"
[fxShow]="!isEdit">
[fxShow]="!isEdit && edgeScope !== 'customer'">
{{'edge.manage-edge-devices' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'openEdgeEntityViews')"
[fxShow]="!isEdit">
[fxShow]="!isEdit && edgeScope !== 'customer'">
{{'edge.manage-edge-entity-views' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'openEdgeDashboards')"
[fxShow]="!isEdit">
[fxShow]="!isEdit && edgeScope !== 'customer'">
{{'edge.manage-edge-dashboards' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'openEdgeRuleChains')"
[fxShow]="!isEdit && (edgeScope === 'tenant' || edgeScope === 'customer')">
[fxShow]="!isEdit && edgeScope === 'tenant'">
{{'edge.manage-edge-rulechains' | translate }}
</button>
</div>
@ -85,7 +85,7 @@
ngxClipboard
(cbOnSuccess)="onEdgeInfoCopied('key')"
[cbContent]="entity?.routingKey"
[fxShow]="!isEdit && (edgeScope === 'tenant' || edgeScope === 'customer')">
[fxShow]="!isEdit && edgeScope !== 'customer_user'">
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
<span translate>edge.copy-edge-key</span>
</button>
@ -93,7 +93,7 @@
ngxClipboard
(cbOnSuccess)="onEdgeInfoCopied('secret')"
[cbContent]="entity?.secret"
[fxShow]="!isEdit && (edgeScope === 'tenant' || edgeScope === 'customer')">
[fxShow]="!isEdit && edgeScope !== 'customer_user'">
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
<span translate>edge.copy-edge-secret</span>
</button>
@ -101,7 +101,7 @@
ngxClipboard
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'syncEdge')"
[fxShow]="!isEdit && (edgeScope === 'tenant' || edgeScope === 'customer')">
[fxShow]="!isEdit && edgeScope !== 'customer_user'">
<mat-icon svgIcon="mdi:sync"></mat-icon>
<span translate>edge.sync</span>
</button>
@ -132,7 +132,7 @@
[required]="true"
[entityType]="entityType.EDGE">
</tb-entity-subtype-autocomplete>
<div [fxShow]="(edgeScope === 'tenant' || edgeScope === 'customer')">
<div [fxShow]="edgeScope !== 'customer_user'">
<div class="tb-hint" [innerHTML]="'edge.edge-license-key-hint' | translate"></div>
<mat-form-field class="mat-block">
<mat-label translate>edge.edge-license-key</mat-label>
@ -142,7 +142,7 @@
</mat-error>
</mat-form-field>
</div>
<div [fxShow]="(edgeScope === 'tenant' || edgeScope === 'customer')">
<div [fxShow]="edgeScope !== 'customer_user'">
<div translate class="tb-hint">edge.cloud-endpoint-hint</div>
<mat-form-field class="mat-block">
<mat-label translate>edge.cloud-endpoint</mat-label>
@ -153,7 +153,7 @@
</mat-form-field>
</div>
</fieldset>
<div fxLayout="row" [fxShow]="(edgeScope === 'tenant' || edgeScope === 'customer')">
<div fxLayout="row" [fxShow]="edgeScope !== 'customer_user'">
<mat-form-field class="mat-block" fxFlex>
<mat-label translate>edge.edge-key</mat-label>
<input matInput formControlName="routingKey">
@ -164,7 +164,7 @@
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
</button>
</div>
<div fxLayout="row" [fxShow]="(edgeScope === 'tenant' || edgeScope === 'customer')">
<div fxLayout="row" [fxShow]="edgeScope !== 'customer_user'">
<mat-form-field class="mat-block" fxFlex>
<mat-label translate>edge.edge-secret</mat-label>
<input matInput formControlName="secret">

4
ui-ngx/src/app/modules/home/pages/edge/edges-table-config.resolver.ts

@ -477,8 +477,8 @@ export class EdgesTableConfigResolver implements Resolve<EntityTableConfig<EdgeI
$event.stopPropagation();
}
this.dialogService.confirm(
this.translate.instant('edge.unassign-edge-title', {count: edges.length}),
this.translate.instant('edge.unassign-edge-text'),
this.translate.instant('edge.unassign-edges-title', {count: edges.length}),
this.translate.instant('edge.unassign-edges-text'),
this.translate.instant('action.no'),
this.translate.instant('action.yes'),
true

4
ui-ngx/src/app/modules/home/pages/entity-view/entity-views-table-config.resolver.ts

@ -104,7 +104,7 @@ export class EntityViewsTableConfigResolver implements Resolve<EntityTableConfig
));
};
this.config.onEntityAction = action => this.onEntityViewAction(action);
this.config.detailsReadonly = () => this.config.componentsData.entityViewScope === 'customer_user';
this.config.detailsReadonly = () => (this.config.componentsData.entityViewScope === 'customer_user' || this.config.componentsData.entityViewScope === 'edge_customer_user');
this.config.headerComponent = EntityViewTableHeaderComponent;
@ -152,7 +152,7 @@ export class EntityViewsTableConfigResolver implements Resolve<EntityTableConfig
this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.entityViewScope);
this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.entityViewScope);
this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.entityViewScope);
this.config.addEnabled = this.config.componentsData.entityViewScope !== 'customer_user';
this.config.addEnabled = !(this.config.componentsData.entityViewScope === 'customer_user' || this.config.componentsData.entityViewScope !== 'edge_customer_user');
this.config.entitiesDeleteEnabled = this.config.componentsData.entityViewScope === 'tenant';
this.config.deleteEnabled = () => this.config.componentsData.entityViewScope === 'tenant';
return this.config;

4
ui-ngx/src/app/modules/home/pages/profile/profile.component.html

@ -57,13 +57,13 @@
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>language.language</mat-label>
<mat-select matInput formControlName="language">
<mat-select formControlName="language">
<mat-option *ngFor="let lang of languageList" [value]="lang">
{{ lang ? ('language.locales.' + lang | translate) : ''}}
</mat-option>
</mat-select>
</mat-form-field>
<section class="tb-home-dashboard" fxFlex fxLayout="column" fxLayout.gt-sm="row">
<section class="tb-home-dashboard" fxFlex fxLayout="column" fxLayout.gt-sm="row" *ngIf="!isSysAdmin()">
<tb-dashboard-autocomplete
fxFlex
placeholder="{{ 'dashboard.home-dashboard' | translate }}"

9
ui-ngx/src/app/modules/home/pages/profile/profile.component.ts

@ -16,7 +16,7 @@
import { Component, OnInit } from '@angular/core';
import { UserService } from '@core/http/user.service';
import { User } from '@shared/models/user.model';
import { AuthUser, User } from '@shared/models/user.model';
import { Authority } from '@shared/models/authority.enum';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
@ -33,6 +33,7 @@ import { DialogService } from '@core/services/dialog.service';
import { AuthService } from '@core/auth/auth.service';
import { ActivatedRoute } from '@angular/router';
import { isDefinedAndNotNull } from '@core/utils';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
@Component({
selector: 'tb-profile',
@ -45,6 +46,7 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
profile: FormGroup;
user: User;
languageList = env.supportedLangs;
private readonly authUser: AuthUser;
constructor(protected store: Store<AppState>,
private route: ActivatedRoute,
@ -55,6 +57,7 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
public dialogService: DialogService,
public fb: FormBuilder) {
super(store);
this.authUser = getCurrentAuthUser(this.store);
}
ngOnInit() {
@ -134,4 +137,8 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
return this.profile;
}
isSysAdmin(): boolean {
return this.authUser.authority === Authority.SYS_ADMIN;
}
}

53
ui-ngx/src/app/modules/home/pages/resource/resources-library-table-config.resolve.ts

@ -24,7 +24,6 @@ import {
import { Resolve } from '@angular/router';
import { Resource, ResourceInfo, ResourceTypeTranslationMap } from '@shared/models/resource.models';
import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models';
import { Direction } from '@shared/models/page/sort-order';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { DatePipe } from '@angular/common';
import { TranslateService } from '@ngx-translate/core';
@ -34,13 +33,14 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Authority } from '@shared/models/authority.enum';
import { ResourcesLibraryComponent } from '@home/pages/resource/resources-library.component';
import { Observable } from 'rxjs/internal/Observable';
import { PageData } from '@shared/models/page/page-data';
import { PageLink } from '@shared/models/page/page-link';
import { EntityAction } from '@home/models/entity/entity-component.models';
import { map } from 'rxjs/operators';
@Injectable()
export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableConfig<Resource>> {
export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableConfig<Resource, PageLink, ResourceInfo>> {
private readonly config: EntityTableConfig<Resource> = new EntityTableConfig<Resource>();
private readonly config: EntityTableConfig<Resource, PageLink, ResourceInfo> = new EntityTableConfig<Resource, PageLink, ResourceInfo>();
private readonly resourceTypesTranslationMap = ResourceTypeTranslationMap;
constructor(private store: Store<AppState>,
@ -52,17 +52,16 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
this.config.entityComponent = ResourcesLibraryComponent;
this.config.entityTranslations = entityTypeTranslations.get(EntityType.TB_RESOURCE);
this.config.entityResources = entityTypeResources.get(EntityType.TB_RESOURCE);
this.config.defaultSortOrder = {property: 'title', direction: Direction.ASC};
this.config.entityTitle = (resource) => resource ?
resource.title : '';
this.config.columns.push(
new DateEntityTableColumn<ResourceInfo>('createdTime', 'common.created-time', this.datePipe, '150px'),
new EntityTableColumn<ResourceInfo>('title', 'widgets-bundle.title', '60%'),
new EntityTableColumn<ResourceInfo>('title', 'resource.title', '60%'),
new EntityTableColumn<ResourceInfo>('resourceType', 'resource.resource-type', '40%',
entity => this.resourceTypesTranslationMap.get(entity.resourceType)),
new EntityTableColumn<ResourceInfo>('tenantId', 'widgets-bundle.system', '60px',
new EntityTableColumn<ResourceInfo>('tenantId', 'resource.system', '60px',
entity => {
return checkBoxCell(entity.tenantId.id === NULL_UUID);
}),
@ -83,13 +82,34 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
this.config.deleteEntitiesTitle = count => this.translate.instant('resource.delete-resources-title', {count});
this.config.deleteEntitiesContent = () => this.translate.instant('resource.delete-resources-text');
this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink) as Observable<PageData<Resource>>;
this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink);
this.config.loadEntity = id => this.resourceService.getResource(id.id);
this.config.saveEntity = resource => this.resourceService.saveResource(resource);
this.config.saveEntity = resource => this.saveResource(resource);
this.config.deleteEntity = id => this.resourceService.deleteResource(id.id);
this.config.onEntityAction = action => this.onResourceAction(action);
}
saveResource(resource) {
if (Array.isArray(resource.data)) {
const resources = [];
resource.data.forEach((data, index) => {
resources.push({
resourceType: resource.resourceType,
data,
fileName: resource.fileName[index],
title: resource.title
});
});
return this.resourceService.saveResources(resources, {resendRequest: true}).pipe(
map((response) => response[0])
);
} else {
return this.resourceService.saveResource(resource);
}
}
resolve(): EntityTableConfig<Resource> {
resolve(): EntityTableConfig<Resource, PageLink, ResourceInfo> {
this.config.tableTitle = this.translate.instant('resource.resources-library');
const authUser = getCurrentAuthUser(this.store);
this.config.deleteEnabled = (resource) => this.isResourceEditable(resource, authUser.authority);
@ -105,7 +125,16 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
this.resourceService.downloadResource(resource.id.id).subscribe();
}
private isResourceEditable(resource: Resource, authority: Authority): boolean {
onResourceAction(action: EntityAction<ResourceInfo>): boolean {
switch (action.action) {
case 'uploadResource':
this.exportResource(action.event, action.entity);
return true;
}
return false;
}
private isResourceEditable(resource: ResourceInfo, authority: Authority): boolean {
if (authority === Authority.TENANT_ADMIN) {
return resource && resource.tenantId && resource.tenantId.id !== NULL_UUID;
} else {

10
ui-ngx/src/app/modules/home/pages/resource/resources-library.component.html

@ -16,6 +16,12 @@
-->
<div class="tb-details-buttons" fxLayout.xs="column">
<button mat-raised-button color="primary" fxFlex.xs
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'uploadResource')"
[fxShow]="!isEdit">
{{'resource.export' | translate }}
</button>
<button mat-raised-button color="primary" fxFlex.xs
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')"
@ -44,9 +50,11 @@
<tb-file-input
formControlName="data"
required
[convertToBase64]="true"
[readAsBinary]="true"
[allowedExtensions]="getAllowedExtensions()"
[contentConvertFunction]="convertToBase64File"
[accept]="getAcceptType()"
[multipleFile]="entityForm.get('resourceType').value === resourceType.LWM2M_MODEL"
dropLabel="{{'resource.drop-file' | translate}}"
[existingFileName]="entityForm.get('fileName')?.value"
(fileNameChanged)="entityForm?.get('fileName').patchValue($event)">

23
ui-ngx/src/app/modules/home/pages/resource/resources-library.component.ts

@ -29,7 +29,7 @@ import {
ResourceTypeMIMETypes,
ResourceTypeTranslationMap
} from '@shared/models/resource.models';
import { distinctUntilChanged, takeUntil } from 'rxjs/operators';
import { pairwise, startWith, takeUntil } from 'rxjs/operators';
@Component({
selector: 'tb-resources-library',
@ -54,15 +54,22 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
ngOnInit() {
super.ngOnInit();
this.entityForm.get('resourceType').valueChanges.pipe(
distinctUntilChanged((oldValue, newValue) => [oldValue, newValue].includes(this.resourceType.LWM2M_MODEL)),
startWith(ResourceType.LWM2M_MODEL),
pairwise(),
takeUntil(this.destroy$)
).subscribe((type) => {
).subscribe(([previousType, type]) => {
if (previousType === this.resourceType.LWM2M_MODEL) {
this.entityForm.get('title').setValidators(Validators.required);
this.entityForm.get('title').updateValueAndValidity({emitEvent: false});
}
if (type === this.resourceType.LWM2M_MODEL) {
this.entityForm.get('title').clearValidators();
} else {
this.entityForm.get('title').setValidators(Validators.required);
this.entityForm.get('title').updateValueAndValidity({emitEvent: false});
}
this.entityForm.get('title').updateValueAndValidity({emitEvent: false});
this.entityForm.patchValue({
data: null,
fileName: null
}, {emitEvent: false});
});
}
@ -119,4 +126,8 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
return '*/*';
}
}
convertToBase64File(data: string): string {
return window.btoa(data);
}
}

14
ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.html

@ -19,19 +19,19 @@
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'open')"
[fxShow]="!isEdit && (ruleChainScope === 'tenant' || ruleChainScope === 'edges')">
[fxShow]="!isEdit">
{{'rulechain.open-rulechain' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'export')"
[fxShow]="!isEdit && (ruleChainScope === 'tenant' || ruleChainScope === 'edges')">
[fxShow]="!isEdit">
{{'rulechain.export' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'setRoot')"
[fxShow]="!isEdit && !entity?.root && ruleChainScope === 'tenant'">
[fxShow]="!isEdit && ((!entity?.root && ruleChainScope === 'tenant') || (!isEdgeRootRuleChain() && ruleChainScope === 'edge'))">
{{'rulechain.set-root' | translate }}
</button>
<button mat-raised-button color="primary"
@ -52,12 +52,6 @@
[fxShow]="!isEdit && isAutoAssignToEdgeRuleChain() && ruleChainScope === 'edges'">
{{'rulechain.unset-auto-assign-to-edge' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'setRoot')"
[fxShow]="!isEdit && !isEdgeRootRuleChain() && ruleChainScope === 'edge'">
{{'rulechain.set-root' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'unassignFromEdge')"
@ -67,7 +61,7 @@
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')"
[fxShow]="!hideDelete() && !isEdit && (ruleChainScope === 'tenant' || ruleChainScope === 'edges')">
[fxShow]="!hideDelete() && !isEdit && ruleChainScope !== 'edge'">
{{'rulechain.delete' | translate }}
</button>
<div fxLayout="row">

82
ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts

@ -211,53 +211,51 @@ export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig<
configureCellActions(ruleChainScope: string): Array<CellActionDescriptor<RuleChain>> {
const actions: Array<CellActionDescriptor<RuleChain>> = [];
if (ruleChainScope === 'tenant' || ruleChainScope === 'edges') {
actions.push(
{
name: this.translate.instant('rulechain.open-rulechain'),
icon: 'settings_ethernet',
isEnabled: () => true,
onAction: ($event, entity) => this.openRuleChain($event, entity)
},
{
name: this.translate.instant('rulechain.export'),
icon: 'file_download',
isEnabled: () => true,
onAction: ($event, entity) => this.exportRuleChain($event, entity)
}
);
if (ruleChainScope === 'tenant') {
actions.push(
{
name: this.translate.instant('rulechain.open-rulechain'),
icon: 'settings_ethernet',
isEnabled: () => true,
onAction: ($event, entity) => this.openRuleChain($event, entity)
name: this.translate.instant('rulechain.set-root'),
icon: 'flag',
isEnabled: (entity) => this.isNonRootRuleChain(entity),
onAction: ($event, entity) => this.setRootRuleChain($event, entity)
}
);
}
if (ruleChainScope === 'edges') {
actions.push(
{
name: this.translate.instant('rulechain.set-edge-template-root-rulechain'),
icon: 'flag',
isEnabled: (entity) => this.isNonRootRuleChain(entity),
onAction: ($event, entity) => this.setEdgeTemplateRootRuleChain($event, entity)
},
{
name: this.translate.instant('rulechain.export'),
icon: 'file_download',
isEnabled: () => true,
onAction: ($event, entity) => this.exportRuleChain($event, entity)
name: this.translate.instant('rulechain.set-auto-assign-to-edge'),
icon: 'bookmark_outline',
isEnabled: (entity) => this.isNotAutoAssignToEdgeRuleChain(entity),
onAction: ($event, entity) => this.setAutoAssignToEdgeRuleChain($event, entity)
},
{
name: this.translate.instant('rulechain.unset-auto-assign-to-edge'),
icon: 'bookmark',
isEnabled: (entity) => this.isAutoAssignToEdgeRuleChain(entity),
onAction: ($event, entity) => this.unsetAutoAssignToEdgeRuleChain($event, entity)
}
);
if (ruleChainScope === 'tenant') {
actions.push(
{
name: this.translate.instant('rulechain.set-root'),
icon: 'flag',
isEnabled: (entity) => this.isNonRootRuleChain(entity),
onAction: ($event, entity) => this.setRootRuleChain($event, entity)
}
);
}
if (ruleChainScope === 'edges') {
actions.push(
{
name: this.translate.instant('rulechain.set-edge-template-root-rulechain'),
icon: 'flag',
isEnabled: (entity) => this.isNonRootRuleChain(entity),
onAction: ($event, entity) => this.setEdgeTemplateRootRuleChain($event, entity)
},
{
name: this.translate.instant('rulechain.set-auto-assign-to-edge'),
icon: 'bookmark_outline',
isEnabled: (entity) => this.isNotAutoAssignToEdgeRuleChain(entity),
onAction: ($event, entity) => this.setAutoAssignToEdgeRuleChain($event, entity)
},
{
name: this.translate.instant('rulechain.unset-auto-assign-to-edge'),
icon: 'bookmark',
isEnabled: (entity) => this.isAutoAssignToEdgeRuleChain(entity),
onAction: ($event, entity) => this.unsetAutoAssignToEdgeRuleChain($event, entity)
}
);
}
}
if (ruleChainScope === 'edge') {
actions.push(
@ -301,6 +299,8 @@ export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig<
}
if (this.config.componentsData.ruleChainScope === 'edges') {
this.router.navigateByUrl(`edges/ruleChains/${ruleChain.id.id}`);
} else if (this.config.componentsData.ruleChainScope === 'edge') {
this.router.navigateByUrl(`edges/${this.config.componentsData.edgeId}/ruleChains/${ruleChain.id.id}`);
} else {
this.router.navigateByUrl(`ruleChains/${ruleChain.id.id}`);
}

4
ui-ngx/src/app/shared/components/file-input.component.html

@ -18,7 +18,7 @@
<div class="tb-container">
<label class="tb-title">{{ label }}</label>
<ng-container #flow="flow"
[flowConfig]="{singleFile: true, allowDuplicateUploads: true}">
[flowConfig]="{allowDuplicateUploads: true}">
<div class="tb-file-select-container">
<div class="tb-file-clear-container">
<button mat-button mat-icon-button color="primary"
@ -34,7 +34,7 @@
flowDrop
[flow]="flow.flowJs">
<label for="{{inputId}}">{{ dropLabel }}</label>
<input class="file-input" flowButton type="file" [flow]="flow.flowJs" [flowAttributes]="{accept: accept}" id="{{inputId}}">
<input class="file-input" flowButton #flowInput type="file" [flow]="flow.flowJs" [flowAttributes]="{accept: accept}" id="{{inputId}}">
</div>
</div>
</ng-container>

107
ui-ngx/src/app/shared/components/file-input.component.ts

@ -17,6 +17,7 @@
import {
AfterViewInit,
Component,
ElementRef,
EventEmitter,
forwardRef,
Input,
@ -102,17 +103,34 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
existingFileName: string;
@Input()
convertToBase64 = false;
readAsBinary = false;
private multipleFileValue = false;
@Input()
set multipleFile(value: boolean) {
this.multipleFileValue = value;
if (this.flow?.flowJs) {
this.updateMultipleFileMode(this.multipleFile);
}
}
get multipleFile(): boolean {
return this.multipleFileValue;
}
@Output()
fileNameChanged = new EventEmitter<string>();
fileNameChanged = new EventEmitter<string|string[]>();
fileName: string;
fileName: string | string[];
fileContent: any;
@ViewChild('flow', {static: true})
flow: FlowDirective;
@ViewChild('flowInput', {static: true})
flowInput: ElementRef;
autoUploadSubscription: Subscription;
private propagateChange = null;
@ -125,34 +143,60 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
ngAfterViewInit() {
this.autoUploadSubscription = this.flow.events$.subscribe(event => {
if (event.type === 'fileAdded') {
const file = event.event[0] as flowjs.FlowFile;
if (this.filterFile(file)) {
const reader = new FileReader();
reader.onload = (loadEvent) => {
if (typeof reader.result === 'string') {
const fileContent = this.convertToBase64 ? window.btoa(reader.result) : reader.result;
if (fileContent && fileContent.length > 0) {
if (this.contentConvertFunction) {
this.fileContent = this.contentConvertFunction(fileContent);
} else {
this.fileContent = fileContent;
}
if (this.fileContent) {
this.fileName = file.name;
} else {
this.fileName = null;
}
this.updateModel();
}
if (event.type === 'filesAdded') {
const readers = [];
(event.event[0] as flowjs.FlowFile[]).forEach(file => {
if (this.filterFile(file)) {
readers.push(this.readerAsFile(file));
}
});
if (readers.length) {
Promise.all(readers).then((filesContent) => {
filesContent = filesContent.filter(content => content.fileContent != null);
if (filesContent.length === 1) {
this.fileContent = filesContent[0].fileContent;
this.fileName = filesContent[0].fileName;
this.updateModel();
} else if (filesContent.length > 1) {
this.fileContent = filesContent.map(content => content.fileContent);
this.fileName = filesContent.map(content => content.fileName);
this.updateModel();
}
});
}
}
});
if (!this.multipleFile) {
this.updateMultipleFileMode(this.multipleFile);
}
}
private readerAsFile(file: flowjs.FlowFile): Promise<any> {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = () => {
let fileName = null;
let fileContent = null;
if (typeof reader.result === 'string') {
fileContent = reader.result;
if (fileContent && fileContent.length > 0) {
if (this.contentConvertFunction) {
fileContent = this.contentConvertFunction(fileContent);
}
if (fileContent) {
fileName = file.name;
}
};
if (this.convertToBase64) {
reader.readAsBinaryString(file.file);
} else {
reader.readAsText(file.file);
}
}
resolve({fileContent, fileName});
};
reader.onerror = () => {
resolve({fileContent: null, fileName: null});
};
if (this.readAsBinary) {
reader.readAsBinaryString(file.file);
} else {
reader.readAsText(file.file);
}
});
}
@ -207,4 +251,11 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
this.fileContent = null;
this.updateModel();
}
private updateMultipleFileMode(multiple: boolean) {
this.flow.flowJs.opts.singleFile = !multiple;
if (!multiple) {
this.flowInput.nativeElement.removeAttribute('multiple');
}
}
}

8
ui-ngx/src/app/shared/components/json-object-edit.component.ts

@ -22,7 +22,7 @@ import { ActionNotificationHide, ActionNotificationShow } from '@core/notificati
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
import { guid, isUndefined } from '@core/utils';
import { guid, isUndefined, isDefinedAndNotNull, isLiteralObject } from '@core/utils';
import { ResizeObserver } from '@juggle/resize-observer';
import { getAce } from '@shared/models/ace/ace.models';
@ -230,8 +230,7 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
this.contentValue = '';
this.objectValid = false;
try {
if (this.modelValue) {
if (isDefinedAndNotNull(this.modelValue)) {
this.contentValue = JSON.stringify(this.modelValue, isUndefined(this.sort) ? undefined :
(key, objectValue) => {
return this.sort(key, objectValue);
@ -260,6 +259,9 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
if (this.contentValue && this.contentValue.length > 0) {
try {
data = JSON.parse(this.contentValue);
if (!isLiteralObject(data)) {
throw new TypeError(`Value is not a valid JSON`);
}
this.objectValid = true;
this.validationError = '';
} catch (ex) {

8
ui-ngx/src/app/shared/components/time/datetime-period.component.html

@ -15,8 +15,9 @@
limitations under the License.
-->
<section fxLayout="column" fxLayoutAlign="start start">
<section fxLayout="row" fxLayout.xs="row wrap" fxLayoutAlign="start start" fxLayoutGap="16px">
<section fxLayout="column" fxLayoutAlign="start stretch">
<section fxLayout="row" fxLayoutAlign="start start" fxLayoutGap="16px"
fxLayout.xs="column" fxLayoutAlign.xs="start stretch" fxLayoutGap.xs="0">
<mat-form-field>
<mat-placeholder translate>datetime.date-from</mat-placeholder>
<mat-datetimepicker-toggle [for]="startDatePicker" matPrefix></mat-datetimepicker-toggle>
@ -30,7 +31,8 @@
<input matInput [disabled]="disabled" [(ngModel)]="startDate" [matDatetimepicker]="startTimePicker" (ngModelChange)="onStartDateChange()">
</mat-form-field>
</section>
<section fxLayout="row" fxLayout.xs="row wrap" fxLayoutAlign="start start" fxLayoutGap="16px">
<section fxLayout="row" fxLayoutAlign="start start" fxLayoutGap="16px"
fxLayout.xs="column" fxLayoutAlign.xs="start stretch" fxLayoutGap.xs="0">
<mat-form-field>
<mat-placeholder translate>datetime.date-to</mat-placeholder>
<mat-datetimepicker-toggle [for]="endDatePicker" matPrefix></mat-datetimepicker-toggle>

1
ui-ngx/src/app/shared/components/time/quick-time-interval.component.ts

@ -21,7 +21,6 @@ import { QuickTimeInterval, QuickTimeIntervalTranslationMap } from '@shared/mode
@Component({
selector: 'tb-quick-time-interval',
templateUrl: './quick-time-interval.component.html',
styleUrls: ['./quick-time-interval.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,

39
ui-ngx/src/app/shared/components/time/timewindow-panel.component.html

@ -51,7 +51,7 @@
[fxShow]="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL"
[required]="timewindow.selectedTab === timewindowTypes.REALTIME &&
timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL"
style="padding-top: 8px; min-width: 364px"></tb-quick-time-interval>
style="padding-top: 8px"></tb-quick-time-interval>
</section>
</mat-radio-button>
</mat-radio-group>
@ -75,6 +75,7 @@
<tb-timeinterval
formControlName="timewindowMs"
predefinedName="timewindow.last"
class="history-time-input"
[fxShow]="timewindowForm.get('history.historyType').value === historyTypes.LAST_INTERVAL"
[required]="timewindow.selectedTab === timewindowTypes.HISTORY &&
timewindowForm.get('history.historyType').value === historyTypes.LAST_INTERVAL"
@ -86,6 +87,7 @@
<span translate>timewindow.time-period</span>
<tb-datetime-period
formControlName="fixedTimewindow"
class="history-time-input"
[fxShow]="timewindowForm.get('history.historyType').value === historyTypes.FIXED"
[required]="timewindow.selectedTab === timewindowTypes.HISTORY &&
timewindowForm.get('history.historyType').value === historyTypes.FIXED"
@ -97,10 +99,11 @@
<span translate>timewindow.interval</span>
<tb-quick-time-interval
formControlName="quickInterval"
class="history-time-input"
[fxShow]="timewindowForm.get('history.historyType').value === historyTypes.INTERVAL"
[required]="timewindow.selectedTab === timewindowTypes.HISTORY &&
timewindowForm.get('history.historyType').value === historyTypes.INTERVAL"
style="padding-top: 8px; min-width: 364px"></tb-quick-time-interval>
style="padding-top: 8px"></tb-quick-time-interval>
</section>
</mat-radio-button>
</mat-radio-group>
@ -134,21 +137,23 @@
(ngModelChange)="onHideAggIntervalChanged()"></mat-checkbox>
</section>
<section fxLayout="column" fxFlex [fxShow]="isEdit || !timewindow.hideAggInterval">
<div class="limit-slider-container"
fxLayout="row" fxLayoutAlign="start center">
<span translate>aggregation.limit</span>
<mat-slider fxFlex formControlName="limit"
thumbLabel
[value]="timewindowForm.get('aggregation.limit').value"
min="{{minDatapointsLimit()}}"
max="{{maxDatapointsLimit()}}">
</mat-slider>
<mat-form-field style="max-width: 80px;">
<input matInput formControlName="limit" type="number" step="1"
[value]="timewindowForm.get('aggregation.limit').value"
min="{{minDatapointsLimit()}}"
max="{{maxDatapointsLimit()}}"/>
</mat-form-field>
<div class="limit-slider-container" fxLayout="row" fxLayoutAlign="start center"
fxLayout.xs="column" fxLayoutAlign.xs="stretch">
<label translate>aggregation.limit</label>
<div fxLayout="row" fxLayoutAlign="start center" fxFlex>
<mat-slider fxFlex formControlName="limit"
thumbLabel
[value]="timewindowForm.get('aggregation.limit').value"
min="{{minDatapointsLimit()}}"
max="{{maxDatapointsLimit()}}">
</mat-slider>
<mat-form-field class="limit-slider-value">
<input matInput formControlName="limit" type="number" step="1"
[value]="timewindowForm.get('aggregation.limit').value"
min="{{minDatapointsLimit()}}"
max="{{maxDatapointsLimit()}}"/>
</mat-form-field>
</div>
</div>
</section>
</section>

24
ui-ngx/src/app/shared/components/time/timewindow-panel.component.scss

@ -13,6 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import "../../../../scss/constants";
:host {
width: 100%;
height: 100%;
@ -40,21 +42,29 @@
}
.limit-slider-container {
>:first-child {
margin-right: 16px;
}
>:last-child {
.limit-slider-value {
margin-left: 16px;
}
>:first-child, >:last-child {
min-width: 25px;
max-width: 42px;
max-width: 80px;
}
mat-form-field input[type=number] {
text-align: center;
}
}
@media #{$mat-gt-sm} {
.history-time-input {
min-width: 364px;
}
.limit-slider-container {
> label {
margin-right: 16px;
width: min-content;
max-width: 40%;
}
}
}
}
:host ::ng-deep {

5
ui-ngx/src/app/shared/models/resource.models.ts

@ -59,3 +59,8 @@ export interface Resource extends ResourceInfo {
data: string;
fileName: string;
}
export interface Resources extends ResourceInfo {
data: string|string[];
fileName: string|string[];
}

5
ui-ngx/src/app/shared/shared.module.ts

@ -141,6 +141,7 @@ import { FileSizePipe } from '@shared/pipe/file-size.pipe';
import { WidgetsBundleSearchComponent } from '@shared/components/widgets-bundle-search.component';
import { SelectableColumnsPipe } from '@shared/pipe/selectable-columns.pipe';
import { QuickTimeIntervalComponent } from '@shared/components/time/quick-time-interval.component';
import { MAT_DATE_LOCALE } from '@angular/material/core';
@NgModule({
providers: [
@ -154,6 +155,10 @@ import { QuickTimeIntervalComponent } from '@shared/components/time/quick-time-i
{
provide: FlowInjectionToken,
useValue: Flow
},
{
provide: MAT_DATE_LOCALE,
useValue: 'en-GB'
}
],
declarations: [

2
ui-ngx/src/assets/locale/locale.constant-zh_CN.json

@ -1943,7 +1943,7 @@
"selected-rulechains": "已选择 { count, plural, 1 {# 个规则链} other {# 个规则链} }",
"set-root": "设置为根规则链",
"set-root-rulechain-text": "确认之后,规则链将变为根规格链,并将处理所有传入的传输消息。",
"set-root-rulechain-title": "您确定要生成规则链'{{RuleChainName}}'根吗?",
"set-root-rulechain-title": "您确定要生成规则链'{{ruleChainName}}'根吗?",
"system": "系统"
},
"rulenode": {

Loading…
Cancel
Save