Commit c2c2f55dc372a910086e46d155a39efb1fce2216

Authored by Vladyslav_Prykhodko
2 parents 9bb4ae8a 5afc39eb

Merge remote-tracking branch 'upstream/master' into feauture/dashboard/widget-select-content

Showing 39 changed files with 820 additions and 664 deletions
... ... @@ -18,8 +18,8 @@
18 18 "resources": [],
19 19 "templateHtml": "<div style=\"height: 100%; overflow-y: auto;\" id=\"device-terminal\"></div>",
20 20 "templateCss": ".cmd .cursor.blink {\n -webkit-animation-name: terminal-underline;\n -moz-animation-name: terminal-underline;\n -ms-animation-name: terminal-underline;\n animation-name: terminal-underline;\n}\n.terminal .inverted, .cmd .inverted {\n border-bottom-color: #aaa;\n}\n\n",
21   - "controllerScript": "var requestTimeout = 500;\nvar multiParams = false;\nvar useRowStyleFunction = false;\nvar styleObj = {};\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n var rpcEnabled = subscription.rpcEnabled;\n var deviceName = 'Simulated';\n var prompt;\n if (subscription.targetDeviceName && subscription.targetDeviceName.length) {\n deviceName = subscription.targetDeviceName;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n if (self.ctx.settings.multiParams) {\n multiParams = self.ctx.settings.multiParams;\n }\n if (self.ctx.settings.useRowStyleFunction && self.ctx.settings.rowStyleFunction) {\n try {\n var style = self.ctx.settings.rowStyleFunction;\n styleObj = JSON.parse(style);\n if ((typeof styleObj !== \"object\")) {\n styleObj = null;\n throw new URIError(`${style === null ? 'null' : typeof style} instead of style object`);\n }\n else if (typeof styleObj === \"object\" && (typeof styleObj.length) === \"number\") {\n styleObj = null;\n throw new URIError('Array instead of style object');\n }\n }\n catch (e) {\n console.log(`Row style function in widget ` +\n `returns '${e}'. Please check your row style function.`); \n }\n useRowStyleFunction = self.ctx.settings.useRowStyleFunction;\n \n }\n var greetings = 'Welcome to ThingsBoard RPC debug terminal.\\n\\n';\n if (!rpcEnabled) {\n greetings += 'Target device is not set!\\n\\n';\n prompt = '';\n } else {\n greetings += 'Current target device for RPC commands: [[b;#fff;]' + deviceName + ']\\n\\n';\n greetings += 'Please type [[b;#fff;]\\'help\\'] to see usage.\\n';\n prompt = '[[b;#8bc34a;]' + deviceName +']> ';\n }\n \n var terminal = $('#device-terminal', self.ctx.$container).terminal(\n function(command) {\n if (command !== '') {\n try {\n var localCommand = command.trim();\n var requestUUID = uuidv4();\n if (localCommand === 'help') {\n printUsage(this);\n } else {\n var cmdObj = $.terminal.parse_command(localCommand);\n if (cmdObj.args) {\n if (!multiParams && cmdObj.args.length > 1) {\n this.error(\"Wrong number of arguments!\");\n this.echo(' ');\n }\n else {\n if (cmdObj.args.length) {\n var params = getMultiParams(cmdObj.args);\n }\n performRpc(this, cmdObj.name, params, requestUUID);\n }\n }\n \n }\n } catch(e) {\n this.error(new String(e));\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: greetings,\n prompt: prompt,\n enabled: rpcEnabled\n });\n \n if (styleObj && styleObj !== null) {\n terminal.css(styleObj);\n }\n \n if (!rpcEnabled) {\n terminal.error('No RPC target detected!').pause();\n }\n}\n\n\nfunction printUsage(terminal) {\n var commandsListText = '\\n[[b;#fff;]Usage:]\\n';\n commandsListText += ' <method> [params body]]\\n\\n';\n commandsListText += '[[b;#fff;]Example 1 (multiParams===false):]\\n'; \n commandsListText += ' myRemoteMethod1 myText\\n\\n'; \n commandsListText += '[[b;#fff;]Example 2 (multiParams===false):]\\n'; \n commandsListText += ' myOtherRemoteMethod \"{\\\\\"key1\\\\\":2,\\\\\"key2\\\\\":\\\\\"myVal\\\\\"}\"\\n\\n'; \n commandsListText += '[[b;#fff;]Example 3 (multiParams===true)]\\n'; \n commandsListText += ' <method> [params body] = \"all the string after the method, including spaces\"]\\n';\n commandsListText += ' myOtherRemoteMethod \"{\\\\\"key1\\\\\": \"battery level\", \\\\\"key2\\\\\": \\\\\"myVal\\\\\"}\"\\n'; \n terminal.echo(new String(commandsListText));\n}\n\nfunction performRpc(terminal, method, params, requestUUID) {\n terminal.pause();\n self.ctx.controlApi.sendTwoWayCommand(method, params, requestTimeout, requestUUID).subscribe(\n function success(responseBody) {\n terminal.echo(JSON.stringify(responseBody));\n terminal.echo(' ');\n terminal.resume();\n },\n function fail() {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.echo(' ');\n terminal.resume();\n }\n );\n}\n\nfunction getMultiParams(cmdObj) {\n var params = \"\";\n cmdObj.forEach((element) => {\n try {\n params += \" \" + JSON.strigify(JSON.parse(element));\n } catch (e) {\n params += \" \" + element;\n }\n })\n return params.trim();\n}\n\n\nfunction uuidv4() {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n}\n\n \nself.onDestroy = function() {\n}",
22   - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"requestTimeout\": {\n \"title\": \"RPC request timeout (ms)\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"multiParams\": {\n \"title\": \"RPC params All line\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"useRowStyleFunction\": {\n \"title\": \"Use row style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"rowStyleFunction\": {\n \"title\": \"Row style function: f(entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": [\"requestTimeout\"]\n },\n \"form\": [\n \"requestTimeout\",\n \"multiParams\",\n \"useRowStyleFunction\",\n {\n \"key\": \"rowStyleFunction\",\n \"type\": \"javascript\",\n \"condition\": \"model.useRowStyleFunction === true\"\n }\n ]\n}",
  21 + "controllerScript": "var requestTimeout = 500;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n var rpcEnabled = subscription.rpcEnabled;\n var deviceName = 'Simulated';\n var prompt;\n if (subscription.targetDeviceName && subscription.targetDeviceName.length) {\n deviceName = subscription.targetDeviceName;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n var greetings = 'Welcome to ThingsBoard RPC debug terminal.\\n\\n';\n if (!rpcEnabled) {\n greetings += 'Target device is not set!\\n\\n';\n prompt = '';\n } else {\n greetings += 'Current target device for RPC commands: [[b;#fff;]' + deviceName + ']\\n\\n';\n greetings += 'Please type [[b;#fff;]\\'help\\'] to see usage.\\n';\n prompt = '[[b;#8bc34a;]' + deviceName +']> ';\n }\n \n var terminal = $('#device-terminal', self.ctx.$container).terminal(\n function(command) {\n if (command !== '') {\n try {\n var localCommand = command.trim();\n var requestUUID = uuidv4();\n if (localCommand === 'help') {\n printUsage(this);\n } else {\n var spaceIndex = localCommand.indexOf(' ');\n if (spaceIndex === -1 && !localCommand.length) {\n this.error(\"Wrong number of arguments!\");\n this.echo(' ');\n } else {\n var params;\n if (spaceIndex === -1) {\n spaceIndex = localCommand.length;\n }\n var name = localCommand.substr(0, spaceIndex);\n var args = localCommand.substr(spaceIndex + 1);\n if (args.length) {\n try {\n params = JSON.parse(args);\n } catch (e) {\n params = args;\n }\n }\n performRpc(this, name, params, requestUUID);\n }\n }\n } catch(e) {\n this.error(new String(e));\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: greetings,\n prompt: prompt,\n enabled: rpcEnabled\n });\n \n if (!rpcEnabled) {\n terminal.error('No RPC target detected!').pause();\n }\n}\n\n\nfunction printUsage(terminal) {\n var commandsListText = '\\n[[b;#fff;]Usage:]\\n';\n commandsListText += ' <method> [params body]]\\n\\n';\n commandsListText += '[[b;#fff;]Example 1:]\\n'; \n commandsListText += ' myRemoteMethod1 myText\\n\\n'; \n commandsListText += '[[b;#fff;]Example 2:]\\n'; \n commandsListText += ' myOtherRemoteMethod \"{\\\\\"key1\\\\\": 2, \\\\\"key2\\\\\": \\\\\"myVal\\\\\"}\"\\n'; \n terminal.echo(new String(commandsListText));\n}\n\n\nfunction performRpc(terminal, method, params, requestUUID) {\n terminal.pause();\n self.ctx.controlApi.sendTwoWayCommand(method, params, requestTimeout, requestUUID).subscribe(\n function success(responseBody) {\n terminal.echo(JSON.stringify(responseBody));\n terminal.echo(' ');\n terminal.resume();\n },\n function fail() {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.echo(' ');\n terminal.resume();\n }\n );\n}\n\n\nfunction uuidv4() {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n}\n\n \nself.onDestroy = function() {\n}",
  22 + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"requestTimeout\": {\n \"title\": \"RPC request timeout (ms)\",\n \"type\": \"number\",\n \"default\": 500\n }\n },\n \"required\": [\"requestTimeout\"]\n },\n \"form\": [\n \"requestTimeout\"\n ]\n}",
23 23 "dataKeySettingsSchema": "{}\n",
24 24 "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":true,\"backgroundColor\":\"#010101\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"parseGpioStatusFunction\":\"return body[pin] === true;\",\"gpioStatusChangeRequest\":{\"method\":\"setGpioStatus\",\"paramsBody\":\"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"},\"requestTimeout\":500,\"switchPanelBackgroundColor\":\"#b71c1c\",\"gpioStatusRequest\":{\"method\":\"getGpioStatus\",\"paramsBody\":\"{}\"},\"gpioList\":[{\"pin\":1,\"label\":\"GPIO 1\",\"row\":0,\"col\":0,\"_uniqueKey\":0},{\"pin\":2,\"label\":\"GPIO 2\",\"row\":0,\"col\":1,\"_uniqueKey\":1},{\"pin\":3,\"label\":\"GPIO 3\",\"row\":1,\"col\":0,\"_uniqueKey\":2}]},\"title\":\"RPC debug terminal\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
25 25 }
... ... @@ -151,4 +151,4 @@
151 151 }
152 152 }
153 153 ]
154   -}
\ No newline at end of file
  154 +}
... ...
... ... @@ -136,7 +136,7 @@ CREATE TABLE IF NOT EXISTS oauth2_mobile (
136 136 oauth2_params_id uuid NOT NULL,
137 137 created_time bigint NOT NULL,
138 138 pkg_name varchar(255),
139   - callback_url_scheme varchar(255),
  139 + app_secret varchar(2048),
140 140 CONSTRAINT fk_mobile_oauth2_params FOREIGN KEY (oauth2_params_id) REFERENCES oauth2_params(id) ON DELETE CASCADE,
141 141 CONSTRAINT oauth2_mobile_unq_key UNIQUE (oauth2_params_id, pkg_name)
142 142 );
... ...
... ... @@ -39,6 +39,7 @@ import org.springframework.web.util.UriComponentsBuilder;
39 39 import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
40 40 import org.thingsboard.server.dao.oauth2.OAuth2Service;
41 41 import org.thingsboard.server.service.security.auth.oauth2.TbOAuth2ParameterNames;
  42 +import org.thingsboard.server.service.security.model.token.OAuth2AppTokenFactory;
42 43 import org.thingsboard.server.utils.MiscUtils;
43 44
44 45 import javax.servlet.http.HttpServletRequest;
... ... @@ -69,6 +70,9 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza
69 70 @Autowired
70 71 private OAuth2Service oAuth2Service;
71 72
  73 + @Autowired
  74 + private OAuth2AppTokenFactory oAuth2AppTokenFactory;
  75 +
72 76 @Autowired(required = false)
73 77 private OAuth2Configuration oauth2Configuration;
74 78
... ... @@ -78,7 +82,8 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza
78 82 String registrationId = this.resolveRegistrationId(request);
79 83 String redirectUriAction = getAction(request, "login");
80 84 String appPackage = getAppPackage(request);
81   - return resolve(request, registrationId, redirectUriAction, appPackage);
  85 + String appToken = getAppToken(request);
  86 + return resolve(request, registrationId, redirectUriAction, appPackage, appToken);
82 87 }
83 88
84 89 @Override
... ... @@ -88,7 +93,8 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza
88 93 }
89 94 String redirectUriAction = getAction(request, "authorize");
90 95 String appPackage = getAppPackage(request);
91   - return resolve(request, registrationId, redirectUriAction, appPackage);
  96 + String appToken = getAppToken(request);
  97 + return resolve(request, registrationId, redirectUriAction, appPackage, appToken);
92 98 }
93 99
94 100 private String getAction(HttpServletRequest request, String defaultAction) {
... ... @@ -103,8 +109,12 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza
103 109 return request.getParameter("pkg");
104 110 }
105 111
  112 + private String getAppToken(HttpServletRequest request) {
  113 + return request.getParameter("appToken");
  114 + }
  115 +
106 116 @SuppressWarnings("deprecation")
107   - private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId, String redirectUriAction, String appPackage) {
  117 + private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId, String redirectUriAction, String appPackage, String appToken) {
108 118 if (registrationId == null) {
109 119 return null;
110 120 }
... ... @@ -117,10 +127,14 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza
117 127 Map<String, Object> attributes = new HashMap<>();
118 128 attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId());
119 129 if (!StringUtils.isEmpty(appPackage)) {
120   - String callbackUrlScheme = this.oAuth2Service.findCallbackUrlScheme(UUID.fromString(registrationId), appPackage);
121   - if (StringUtils.isEmpty(callbackUrlScheme)) {
122   - throw new IllegalArgumentException("Invalid package: " + appPackage + ". No package info found for Client Registration.");
  130 + if (StringUtils.isEmpty(appToken)) {
  131 + throw new IllegalArgumentException("Invalid application token.");
123 132 } else {
  133 + String appSecret = this.oAuth2Service.findAppSecret(UUID.fromString(registrationId), appPackage);
  134 + if (StringUtils.isEmpty(appSecret)) {
  135 + throw new IllegalArgumentException("Invalid package: " + appPackage + ". No application secret found for Client Registration with given application package.");
  136 + }
  137 + String callbackUrlScheme = this.oAuth2AppTokenFactory.validateTokenAndGetCallbackUrlScheme(appPackage, appToken, appSecret);
124 138 attributes.put(TbOAuth2ParameterNames.CALLBACK_URL_SCHEME, callbackUrlScheme);
125 139 }
126 140 }
... ...
... ... @@ -17,7 +17,6 @@ package org.thingsboard.server.controller;
17 17
18 18 import com.fasterxml.jackson.databind.ObjectMapper;
19 19 import lombok.extern.slf4j.Slf4j;
20   -import org.eclipse.leshan.core.SecurityMode;
21 20 import org.springframework.security.access.prepost.PreAuthorize;
22 21 import org.springframework.web.bind.annotation.PathVariable;
23 22 import org.springframework.web.bind.annotation.RequestBody;
... ... @@ -45,14 +44,11 @@ import java.util.Map;
45 44 public class Lwm2mController extends BaseController {
46 45
47 46 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
48   - @RequestMapping(value = "/lwm2m/deviceProfile/bootstrap/{securityMode}/{bootstrapServerIs}", method = RequestMethod.GET)
  47 + @RequestMapping(value = "/lwm2m/deviceProfile/bootstrap/{isBootstrapServer}", method = RequestMethod.GET)
49 48 @ResponseBody
50   - public ServerSecurityConfig getLwm2mBootstrapSecurityInfo(@PathVariable("securityMode") String strSecurityMode,
51   - @PathVariable("bootstrapServerIs") boolean bootstrapServer) throws ThingsboardException {
52   - checkNotNull(strSecurityMode);
  49 + public ServerSecurityConfig getLwm2mBootstrapSecurityInfo(@PathVariable("isBootstrapServer") boolean bootstrapServer) throws ThingsboardException {
53 50 try {
54   - SecurityMode securityMode = SecurityMode.valueOf(strSecurityMode);
55   - return lwM2MServerSecurityInfoRepository.getServerSecurityInfo(securityMode, bootstrapServer);
  51 + return lwM2MServerSecurityInfoRepository.getServerSecurityInfo(bootstrapServer);
56 52 } catch (Exception e) {
57 53 throw handleException(e);
58 54 }
... ...
... ... @@ -18,7 +18,6 @@ package org.thingsboard.server.service.lwm2m;
18 18
19 19 import lombok.RequiredArgsConstructor;
20 20 import lombok.extern.slf4j.Slf4j;
21   -import org.eclipse.leshan.core.SecurityMode;
22 21 import org.eclipse.leshan.core.util.Hex;
23 22 import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
24 23 import org.springframework.stereotype.Service;
... ... @@ -50,40 +49,20 @@ public class LwM2MServerSecurityInfoRepository {
50 49 private final LwM2MTransportServerConfig serverConfig;
51 50 private final LwM2MTransportBootstrapConfig bootstrapConfig;
52 51
53   - /**
54   - * @param securityMode
55   - * @param bootstrapServer
56   - * @return ServerSecurityConfig more value is default: Important - port, host, publicKey
57   - */
58   - public ServerSecurityConfig getServerSecurityInfo(SecurityMode securityMode, boolean bootstrapServer) {
59   - ServerSecurityConfig result = getServerSecurityConfig(bootstrapServer ? bootstrapConfig : serverConfig, securityMode);
  52 + public ServerSecurityConfig getServerSecurityInfo(boolean bootstrapServer) {
  53 + ServerSecurityConfig result = getServerSecurityConfig(bootstrapServer ? bootstrapConfig : serverConfig);
60 54 result.setBootstrapServerIs(bootstrapServer);
61 55 return result;
62 56 }
63 57
64   - private ServerSecurityConfig getServerSecurityConfig(LwM2MSecureServerConfig serverConfig, SecurityMode securityMode) {
  58 + private ServerSecurityConfig getServerSecurityConfig(LwM2MSecureServerConfig serverConfig) {
65 59 ServerSecurityConfig bsServ = new ServerSecurityConfig();
66 60 bsServ.setServerId(serverConfig.getId());
67   - switch (securityMode) {
68   - case NO_SEC:
69   - bsServ.setHost(serverConfig.getHost());
70   - bsServ.setPort(serverConfig.getPort());
71   - bsServ.setServerPublicKey("");
72   - break;
73   - case PSK:
74   - bsServ.setHost(serverConfig.getSecureHost());
75   - bsServ.setPort(serverConfig.getSecurePort());
76   - bsServ.setServerPublicKey("");
77   - break;
78   - case RPK:
79   - case X509:
80   - bsServ.setHost(serverConfig.getSecureHost());
81   - bsServ.setPort(serverConfig.getSecurePort());
82   - bsServ.setServerPublicKey(getPublicKey(serverConfig.getCertificateAlias(), this.serverConfig.getPublicX(), this.serverConfig.getPublicY()));
83   - break;
84   - default:
85   - break;
86   - }
  61 + bsServ.setHost(serverConfig.getHost());
  62 + bsServ.setPort(serverConfig.getPort());
  63 + bsServ.setSecurityHost(serverConfig.getSecureHost());
  64 + bsServ.setSecurityPort(serverConfig.getSecurePort());
  65 + bsServ.setServerPublicKey(getPublicKey(serverConfig.getCertificateAlias(), this.serverConfig.getPublicX(), this.serverConfig.getPublicY()));
87 66 return bsServ;
88 67 }
89 68
... ...
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.security.model.token;
  17 +
  18 +import io.jsonwebtoken.Claims;
  19 +import io.jsonwebtoken.ExpiredJwtException;
  20 +import io.jsonwebtoken.Jws;
  21 +import io.jsonwebtoken.Jwts;
  22 +import io.jsonwebtoken.MalformedJwtException;
  23 +import io.jsonwebtoken.SignatureException;
  24 +import io.jsonwebtoken.UnsupportedJwtException;
  25 +import io.micrometer.core.instrument.util.StringUtils;
  26 +import lombok.extern.slf4j.Slf4j;
  27 +import org.springframework.stereotype.Component;
  28 +
  29 +import java.util.Date;
  30 +import java.util.concurrent.TimeUnit;
  31 +
  32 +@Component
  33 +@Slf4j
  34 +public class OAuth2AppTokenFactory {
  35 +
  36 + private static final String CALLBACK_URL_SCHEME = "callbackUrlScheme";
  37 +
  38 + private static final long MAX_EXPIRATION_TIME_DIFF_MS = TimeUnit.MINUTES.toMillis(5);
  39 +
  40 + public String validateTokenAndGetCallbackUrlScheme(String appPackage, String appToken, String appSecret) {
  41 + Jws<Claims> jwsClaims;
  42 + try {
  43 + jwsClaims = Jwts.parser().setSigningKey(appSecret).parseClaimsJws(appToken);
  44 + }
  45 + catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException | SignatureException ex) {
  46 + throw new IllegalArgumentException("Invalid Application token: ", ex);
  47 + } catch (ExpiredJwtException expiredEx) {
  48 + throw new IllegalArgumentException("Application token expired", expiredEx);
  49 + }
  50 + Claims claims = jwsClaims.getBody();
  51 + Date expiration = claims.getExpiration();
  52 + if (expiration == null) {
  53 + throw new IllegalArgumentException("Application token must have expiration date");
  54 + }
  55 + long timeDiff = expiration.getTime() - System.currentTimeMillis();
  56 + if (timeDiff > MAX_EXPIRATION_TIME_DIFF_MS) {
  57 + throw new IllegalArgumentException("Application token expiration time can't be longer than 5 minutes");
  58 + }
  59 + if (!claims.getIssuer().equals(appPackage)) {
  60 + throw new IllegalArgumentException("Application token issuer doesn't match application package");
  61 + }
  62 + String callbackUrlScheme = claims.get(CALLBACK_URL_SCHEME, String.class);
  63 + if (StringUtils.isEmpty(callbackUrlScheme)) {
  64 + throw new IllegalArgumentException("Application token doesn't have callbackUrlScheme");
  65 + }
  66 + return callbackUrlScheme;
  67 + }
  68 +
  69 +}
... ...
... ... @@ -42,5 +42,5 @@ public interface OAuth2Service {
42 42
43 43 List<OAuth2Registration> findAllRegistrations();
44 44
45   - String findCallbackUrlScheme(UUID registrationId, String pkgName);
  45 + String findAppSecret(UUID registrationId, String pkgName);
46 46 }
... ...
... ... @@ -20,7 +20,9 @@ import lombok.Data;
20 20 @Data
21 21 public class ServerSecurityConfig {
22 22 String host;
  23 + String securityHost;
23 24 Integer port;
  25 + Integer securityPort;
24 26 String serverPublicKey;
25 27 boolean bootstrapServerIs = true;
26 28 Integer clientHoldOffTime = 1;
... ...
... ... @@ -31,12 +31,12 @@ public class OAuth2Mobile extends BaseData<OAuth2MobileId> {
31 31
32 32 private OAuth2ParamsId oauth2ParamsId;
33 33 private String pkgName;
34   - private String callbackUrlScheme;
  34 + private String appSecret;
35 35
36 36 public OAuth2Mobile(OAuth2Mobile mobile) {
37 37 super(mobile);
38 38 this.oauth2ParamsId = mobile.oauth2ParamsId;
39 39 this.pkgName = mobile.pkgName;
40   - this.callbackUrlScheme = mobile.callbackUrlScheme;
  40 + this.appSecret = mobile.appSecret;
41 41 }
42 42 }
... ...
... ... @@ -30,5 +30,5 @@ import lombok.ToString;
30 30 @Builder
31 31 public class OAuth2MobileInfo {
32 32 private String pkgName;
33   - private String callbackUrlScheme;
  33 + private String appSecret;
34 34 }
... ...
... ... @@ -418,7 +418,7 @@ public class ModelConstants {
418 418 public static final String OAUTH2_MOBILE_COLUMN_FAMILY_NAME = "oauth2_mobile";
419 419 public static final String OAUTH2_PARAMS_ID_PROPERTY = "oauth2_params_id";
420 420 public static final String OAUTH2_PKG_NAME_PROPERTY = "pkg_name";
421   - public static final String OAUTH2_CALLBACK_URL_SCHEME_PROPERTY = "callback_url_scheme";
  421 + public static final String OAUTH2_APP_SECRET_PROPERTY = "app_secret";
422 422
423 423 public static final String OAUTH2_CLIENT_REGISTRATION_INFO_COLUMN_FAMILY_NAME = "oauth2_client_registration_info";
424 424 public static final String OAUTH2_CLIENT_REGISTRATION_COLUMN_FAMILY_NAME = "oauth2_client_registration";
... ...
... ... @@ -40,8 +40,8 @@ public class OAuth2MobileEntity extends BaseSqlEntity<OAuth2Mobile> {
40 40 @Column(name = ModelConstants.OAUTH2_PKG_NAME_PROPERTY)
41 41 private String pkgName;
42 42
43   - @Column(name = ModelConstants.OAUTH2_CALLBACK_URL_SCHEME_PROPERTY)
44   - private String callbackUrlScheme;
  43 + @Column(name = ModelConstants.OAUTH2_APP_SECRET_PROPERTY)
  44 + private String appSecret;
45 45
46 46 public OAuth2MobileEntity() {
47 47 super();
... ... @@ -56,7 +56,7 @@ public class OAuth2MobileEntity extends BaseSqlEntity<OAuth2Mobile> {
56 56 this.oauth2ParamsId = mobile.getOauth2ParamsId().getId();
57 57 }
58 58 this.pkgName = mobile.getPkgName();
59   - this.callbackUrlScheme = mobile.getCallbackUrlScheme();
  59 + this.appSecret = mobile.getAppSecret();
60 60 }
61 61
62 62 @Override
... ... @@ -66,7 +66,7 @@ public class OAuth2MobileEntity extends BaseSqlEntity<OAuth2Mobile> {
66 66 mobile.setCreatedTime(createdTime);
67 67 mobile.setOauth2ParamsId(new OAuth2ParamsId(oauth2ParamsId));
68 68 mobile.setPkgName(pkgName);
69   - mobile.setCallbackUrlScheme(callbackUrlScheme);
  69 + mobile.setAppSecret(appSecret);
70 70 return mobile;
71 71 }
72 72 }
... ...
... ... @@ -29,6 +29,6 @@ public interface OAuth2RegistrationDao extends Dao<OAuth2Registration> {
29 29
30 30 List<OAuth2Registration> findByOAuth2ParamsId(UUID oauth2ParamsId);
31 31
32   - String findCallbackUrlScheme(UUID id, String pkgName);
  32 + String findAppSecret(UUID id, String pkgName);
33 33
34 34 }
... ...
... ... @@ -21,7 +21,23 @@ import org.springframework.stereotype.Service;
21 21 import org.springframework.util.StringUtils;
22 22 import org.thingsboard.server.common.data.BaseData;
23 23 import org.thingsboard.server.common.data.id.TenantId;
24   -import org.thingsboard.server.common.data.oauth2.*;
  24 +import org.thingsboard.server.common.data.oauth2.MapperType;
  25 +import org.thingsboard.server.common.data.oauth2.OAuth2BasicMapperConfig;
  26 +import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
  27 +import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig;
  28 +import org.thingsboard.server.common.data.oauth2.OAuth2Domain;
  29 +import org.thingsboard.server.common.data.oauth2.OAuth2DomainInfo;
  30 +import org.thingsboard.server.common.data.oauth2.OAuth2Info;
  31 +import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
  32 +import org.thingsboard.server.common.data.oauth2.OAuth2Mobile;
  33 +import org.thingsboard.server.common.data.oauth2.OAuth2MobileInfo;
  34 +import org.thingsboard.server.common.data.oauth2.OAuth2Params;
  35 +import org.thingsboard.server.common.data.oauth2.OAuth2ParamsInfo;
  36 +import org.thingsboard.server.common.data.oauth2.OAuth2Registration;
  37 +import org.thingsboard.server.common.data.oauth2.OAuth2RegistrationInfo;
  38 +import org.thingsboard.server.common.data.oauth2.PlatformType;
  39 +import org.thingsboard.server.common.data.oauth2.SchemeType;
  40 +import org.thingsboard.server.common.data.oauth2.TenantNameStrategyType;
25 41 import org.thingsboard.server.common.data.oauth2.deprecated.ClientRegistrationDto;
26 42 import org.thingsboard.server.common.data.oauth2.deprecated.DomainInfo;
27 43 import org.thingsboard.server.common.data.oauth2.deprecated.ExtendedOAuth2ClientRegistrationInfo;
... ... @@ -36,7 +52,11 @@ import org.thingsboard.server.dao.oauth2.deprecated.OAuth2ClientRegistrationDao;
36 52 import org.thingsboard.server.dao.oauth2.deprecated.OAuth2ClientRegistrationInfoDao;
37 53
38 54 import javax.transaction.Transactional;
39   -import java.util.*;
  55 +import java.util.ArrayList;
  56 +import java.util.Arrays;
  57 +import java.util.Comparator;
  58 +import java.util.List;
  59 +import java.util.UUID;
40 60 import java.util.function.Consumer;
41 61 import java.util.stream.Collectors;
42 62
... ... @@ -164,11 +184,11 @@ public class OAuth2ServiceImpl extends AbstractEntityService implements OAuth2Se
164 184 }
165 185
166 186 @Override
167   - public String findCallbackUrlScheme(UUID id, String pkgName) {
168   - log.trace("Executing findCallbackUrlScheme [{}][{}]", id, pkgName);
  187 + public String findAppSecret(UUID id, String pkgName) {
  188 + log.trace("Executing findAppSecret [{}][{}]", id, pkgName);
169 189 validateId(id, INCORRECT_CLIENT_REGISTRATION_ID + id);
170 190 validateString(pkgName, "Incorrect package name");
171   - return oauth2RegistrationDao.findCallbackUrlScheme(id, pkgName);
  191 + return oauth2RegistrationDao.findAppSecret(id, pkgName);
172 192 }
173 193
174 194
... ... @@ -323,8 +343,11 @@ public class OAuth2ServiceImpl extends AbstractEntityService implements OAuth2Se
323 343 if (StringUtils.isEmpty(mobileInfo.getPkgName())) {
324 344 throw new DataValidationException("Package should be specified!");
325 345 }
326   - if (StringUtils.isEmpty(mobileInfo.getCallbackUrlScheme())) {
327   - throw new DataValidationException("Callback URL scheme should be specified!");
  346 + if (StringUtils.isEmpty(mobileInfo.getAppSecret())) {
  347 + throw new DataValidationException("Application secret should be specified!");
  348 + }
  349 + if (mobileInfo.getAppSecret().length() < 16) {
  350 + throw new DataValidationException("Application secret should be at least 16 characters!");
328 351 }
329 352 }
330 353 oauth2Params.getMobileInfos().stream()
... ...
... ... @@ -148,7 +148,7 @@ public class OAuth2Utils {
148 148 public static OAuth2MobileInfo toOAuth2MobileInfo(OAuth2Mobile mobile) {
149 149 return OAuth2MobileInfo.builder()
150 150 .pkgName(mobile.getPkgName())
151   - .callbackUrlScheme(mobile.getCallbackUrlScheme())
  151 + .appSecret(mobile.getAppSecret())
152 152 .build();
153 153 }
154 154
... ... @@ -191,7 +191,7 @@ public class OAuth2Utils {
191 191 OAuth2Mobile mobile = new OAuth2Mobile();
192 192 mobile.setOauth2ParamsId(oauth2ParamsId);
193 193 mobile.setPkgName(mobileInfo.getPkgName());
194   - mobile.setCallbackUrlScheme(mobileInfo.getCallbackUrlScheme());
  194 + mobile.setAppSecret(mobileInfo.getAppSecret());
195 195 return mobile;
196 196 }
197 197
... ...
... ... @@ -57,8 +57,8 @@ public class JpaOAuth2RegistrationDao extends JpaAbstractDao<OAuth2RegistrationE
57 57 }
58 58
59 59 @Override
60   - public String findCallbackUrlScheme(UUID id, String pkgName) {
61   - return repository.findCallbackUrlScheme(id, pkgName);
  60 + public String findAppSecret(UUID id, String pkgName) {
  61 + return repository.findAppSecret(id, pkgName);
62 62 }
63 63
64 64 }
... ...
... ... @@ -42,12 +42,12 @@ public interface OAuth2RegistrationRepository extends CrudRepository<OAuth2Regis
42 42
43 43 List<OAuth2RegistrationEntity> findByOauth2ParamsId(UUID oauth2ParamsId);
44 44
45   - @Query("SELECT mobile.callbackUrlScheme " +
  45 + @Query("SELECT mobile.appSecret " +
46 46 "FROM OAuth2MobileEntity mobile " +
47 47 "LEFT JOIN OAuth2RegistrationEntity reg on mobile.oauth2ParamsId = reg.oauth2ParamsId " +
48 48 "WHERE reg.id = :registrationId " +
49 49 "AND mobile.pkgName = :pkgName")
50   - String findCallbackUrlScheme(@Param("registrationId") UUID id,
51   - @Param("pkgName") String pkgName);
  50 + String findAppSecret(@Param("registrationId") UUID id,
  51 + @Param("pkgName") String pkgName);
52 52
53 53 }
... ...
... ... @@ -431,7 +431,7 @@ CREATE TABLE IF NOT EXISTS oauth2_mobile (
431 431 oauth2_params_id uuid NOT NULL,
432 432 created_time bigint NOT NULL,
433 433 pkg_name varchar(255),
434   - callback_url_scheme varchar(255),
  434 + app_secret varchar(2048),
435 435 CONSTRAINT fk_mobile_oauth2_params FOREIGN KEY (oauth2_params_id) REFERENCES oauth2_params(id) ON DELETE CASCADE,
436 436 CONSTRAINT oauth2_mobile_unq_key UNIQUE (oauth2_params_id, pkg_name)
437 437 );
... ...
... ... @@ -468,7 +468,7 @@ CREATE TABLE IF NOT EXISTS oauth2_mobile (
468 468 oauth2_params_id uuid NOT NULL,
469 469 created_time bigint NOT NULL,
470 470 pkg_name varchar(255),
471   - callback_url_scheme varchar(255),
  471 + app_secret varchar(2048),
472 472 CONSTRAINT fk_mobile_oauth2_params FOREIGN KEY (oauth2_params_id) REFERENCES oauth2_params(id) ON DELETE CASCADE,
473 473 CONSTRAINT oauth2_mobile_unq_key UNIQUE (oauth2_params_id, pkg_name)
474 474 );
... ...
... ... @@ -15,8 +15,8 @@
15 15 */
16 16 package org.thingsboard.server.dao.service;
17 17
18   -import com.fasterxml.jackson.databind.node.ObjectNode;
19 18 import com.google.common.collect.Lists;
  19 +import org.apache.commons.lang3.RandomStringUtils;
20 20 import org.junit.After;
21 21 import org.junit.Assert;
22 22 import org.junit.Before;
... ... @@ -487,7 +487,7 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest {
487 487 }
488 488
489 489 @Test
490   - public void testFindCallbackUrlScheme() {
  490 + public void testFindAppSecret() {
491 491 OAuth2Info oAuth2Info = new OAuth2Info(true, Lists.newArrayList(
492 492 OAuth2ParamsInfo.builder()
493 493 .domainInfos(Lists.newArrayList(
... ... @@ -496,8 +496,8 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest {
496 496 OAuth2DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build()
497 497 ))
498 498 .mobileInfos(Lists.newArrayList(
499   - OAuth2MobileInfo.builder().pkgName("com.test.pkg1").callbackUrlScheme("testPkg1Callback").build(),
500   - OAuth2MobileInfo.builder().pkgName("com.test.pkg2").callbackUrlScheme("testPkg2Callback").build()
  499 + validMobileInfo("com.test.pkg1", "testPkg1AppSecret"),
  500 + validMobileInfo("com.test.pkg2", "testPkg2AppSecret")
501 501 ))
502 502 .clientRegistrations(Lists.newArrayList(
503 503 validRegistrationInfo(),
... ... @@ -527,14 +527,14 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest {
527 527 for (OAuth2ClientInfo clientInfo : firstDomainHttpClients) {
528 528 String[] segments = clientInfo.getUrl().split("/");
529 529 String registrationId = segments[segments.length-1];
530   - String callbackUrlScheme = oAuth2Service.findCallbackUrlScheme(UUID.fromString(registrationId), "com.test.pkg1");
531   - Assert.assertNotNull(callbackUrlScheme);
532   - Assert.assertEquals("testPkg1Callback", callbackUrlScheme);
533   - callbackUrlScheme = oAuth2Service.findCallbackUrlScheme(UUID.fromString(registrationId), "com.test.pkg2");
534   - Assert.assertNotNull(callbackUrlScheme);
535   - Assert.assertEquals("testPkg2Callback", callbackUrlScheme);
536   - callbackUrlScheme = oAuth2Service.findCallbackUrlScheme(UUID.fromString(registrationId), "com.test.pkg3");
537   - Assert.assertNull(callbackUrlScheme);
  530 + String appSecret = oAuth2Service.findAppSecret(UUID.fromString(registrationId), "com.test.pkg1");
  531 + Assert.assertNotNull(appSecret);
  532 + Assert.assertEquals("testPkg1AppSecret", appSecret);
  533 + appSecret = oAuth2Service.findAppSecret(UUID.fromString(registrationId), "com.test.pkg2");
  534 + Assert.assertNotNull(appSecret);
  535 + Assert.assertEquals("testPkg2AppSecret", appSecret);
  536 + appSecret = oAuth2Service.findAppSecret(UUID.fromString(registrationId), "com.test.pkg3");
  537 + Assert.assertNull(appSecret);
538 538 }
539 539 }
540 540
... ... @@ -548,8 +548,8 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest {
548 548 OAuth2DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build()
549 549 ))
550 550 .mobileInfos(Lists.newArrayList(
551   - OAuth2MobileInfo.builder().pkgName("com.test.pkg1").callbackUrlScheme("testPkg1Callback").build(),
552   - OAuth2MobileInfo.builder().pkgName("com.test.pkg2").callbackUrlScheme("testPkg2Callback").build()
  551 + validMobileInfo("com.test.pkg1", "testPkg1Callback"),
  552 + validMobileInfo("com.test.pkg2", "testPkg2Callback")
553 553 ))
554 554 .clientRegistrations(Lists.newArrayList(
555 555 validRegistrationInfo("Google", Arrays.asList(PlatformType.WEB, PlatformType.ANDROID)),
... ... @@ -651,4 +651,10 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest {
651 651 )
652 652 .build();
653 653 }
  654 +
  655 + private OAuth2MobileInfo validMobileInfo(String pkgName, String appSecret) {
  656 + return OAuth2MobileInfo.builder().pkgName(pkgName)
  657 + .appSecret(appSecret != null ? appSecret : RandomStringUtils.randomAlphanumeric(24))
  658 + .build();
  659 + }
654 660 }
... ...
... ... @@ -18,20 +18,22 @@ import { Injectable } from '@angular/core';
18 18 import { HttpClient } from '@angular/common/http';
19 19 import { PageLink } from '@shared/models/page/page-link';
20 20 import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
21   -import { Observable, throwError } from 'rxjs';
  21 +import { Observable, of, throwError } from 'rxjs';
22 22 import { PageData } from '@shared/models/page/page-data';
23 23 import { DeviceProfile, DeviceProfileInfo, DeviceTransportType } from '@shared/models/device.models';
24 24 import { isDefinedAndNotNull, isEmptyStr } from '@core/utils';
25 25 import { ObjectLwM2M, ServerSecurityConfig } from '@home/components/profile/device/lwm2m/lwm2m-profile-config.models';
26 26 import { SortOrder } from '@shared/models/page/sort-order';
27 27 import { OtaPackageService } from '@core/http/ota-package.service';
28   -import { mergeMap } from 'rxjs/operators';
  28 +import { mergeMap, tap } from 'rxjs/operators';
29 29
30 30 @Injectable({
31 31 providedIn: 'root'
32 32 })
33 33 export class DeviceProfileService {
34 34
  35 + private lwm2mBootstrapSecurityInfoInMemoryCache = new Map<boolean, ServerSecurityConfig>();
  36 +
35 37 constructor(
36 38 private http: HttpClient,
37 39 private otaPackageService: OtaPackageService
... ... @@ -58,12 +60,18 @@ export class DeviceProfileService {
58 60 return this.http.get<Array<ObjectLwM2M>>(url, defaultHttpOptionsFromConfig(config));
59 61 }
60 62
61   - public getLwm2mBootstrapSecurityInfo(securityMode: string, bootstrapServerIs: boolean,
62   - config?: RequestConfig): Observable<ServerSecurityConfig> {
63   - return this.http.get<ServerSecurityConfig>(
64   - `/api/lwm2m/deviceProfile/bootstrap/${securityMode}/${bootstrapServerIs}`,
65   - defaultHttpOptionsFromConfig(config)
66   - );
  63 + public getLwm2mBootstrapSecurityInfo(isBootstrapServer: boolean, config?: RequestConfig): Observable<ServerSecurityConfig> {
  64 + const securityConfig = this.lwm2mBootstrapSecurityInfoInMemoryCache.get(isBootstrapServer);
  65 + if (securityConfig) {
  66 + return of(securityConfig);
  67 + } else {
  68 + return this.http.get<ServerSecurityConfig>(
  69 + `/api/lwm2m/deviceProfile/bootstrap/${isBootstrapServer}`,
  70 + defaultHttpOptionsFromConfig(config)
  71 + ).pipe(
  72 + tap(serverConfig => this.lwm2mBootstrapSecurityInfoInMemoryCache.set(isBootstrapServer, serverConfig))
  73 + );
  74 + }
67 75 }
68 76
69 77 public getLwm2mObjectsPage(pageLink: PageLink, config?: RequestConfig): Observable<Array<ObjectLwM2M>> {
... ...
... ... @@ -445,3 +445,14 @@ export function validateEntityId(entityId: EntityId | null): boolean {
445 445 export function isMobileApp(): boolean {
446 446 return isDefined((window as any).flutter_inappwebview);
447 447 }
  448 +
  449 +const alphanumericCharacters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  450 +const alphanumericCharactersLength = alphanumericCharacters.length;
  451 +
  452 +export function randomAlphanumeric(length: number): string {
  453 + let result = '';
  454 + for ( let i = 0; i < length; i++ ) {
  455 + result += alphanumericCharacters.charAt(Math.floor(Math.random() * alphanumericCharactersLength));
  456 + }
  457 + return result;
  458 +}
... ...
... ... @@ -28,60 +28,45 @@
28 28 </mat-form-field>
29 29 <mat-form-field fxFlex>
30 30 <mat-label>{{ 'device-profile.lwm2m.server-host' | translate }}</mat-label>
31   - <input matInput type="text" formControlName="host" required
32   - matTooltip="{{'device-profile.lwm2m.server-host-tip' | translate}}"
33   - matTooltipPosition="above">
  31 + <input matInput type="text" formControlName="host" required>
34 32 <mat-error *ngIf="serverFormGroup.get('host').hasError('required')">
35   - {{ 'device-profile.lwm2m.server-host' | translate }}
36   - <strong>{{ 'device-profile.lwm2m.required' | translate }}</strong>
  33 + {{ 'device-profile.lwm2m.server-host-required' | translate }}
37 34 </mat-error>
38 35 </mat-form-field>
39 36 <mat-form-field fxFlex>
40 37 <mat-label>{{ 'device-profile.lwm2m.server-port' | translate }}</mat-label>
41   - <input matInput type="number" formControlName="port" required
42   - matTooltip="{{'device-profile.lwm2m.server-port-tip' | translate}}"
43   - matTooltipPosition="above">
  38 + <input matInput type="number" formControlName="port" required min="0">
44 39 <mat-error *ngIf="serverFormGroup.get('port').hasError('required')">
45   - {{ 'device-profile.lwm2m.server-port' | translate }}
46   - <strong>{{ 'device-profile.lwm2m.required' | translate }}</strong>
  40 + {{ 'device-profile.lwm2m.server-port-required' | translate }}
47 41 </mat-error>
48 42 </mat-form-field>
  43 + </div>
  44 + <div fxLayout="row" fxLayout.xs="column" fxLayoutGap="8px" fxLayoutGap.xs="0px">
49 45 <mat-form-field fxFlex>
50 46 <mat-label>{{ 'device-profile.lwm2m.short-id' | translate }}</mat-label>
51   - <input matInput type="number" formControlName="serverId" required
52   - matTooltip="{{'device-profile.lwm2m.short-id-tip' | translate}}"
53   - matTooltipPosition="above">
  47 + <input matInput type="number" formControlName="serverId" required min="0">
54 48 <mat-error *ngIf="serverFormGroup.get('serverId').hasError('required')">
55   - {{ 'device-profile.lwm2m.short-id' | translate }}
56   - <strong>{{ 'device-profile.lwm2m.required' | translate }}</strong>
  49 + {{ 'device-profile.lwm2m.short-id-required' | translate }}
57 50 </mat-error>
58 51 </mat-form-field>
59   - </div>
60   - <div fxLayout="row" fxLayout.xs="column" fxLayoutGap="8px" fxLayoutGap.xs="0px">
61 52 <mat-form-field fxFlex>
62 53 <mat-label>{{ 'device-profile.lwm2m.client-hold-off-time' | translate }}</mat-label>
63   - <input matInput type="number" formControlName="clientHoldOffTime" required
64   - matTooltip="{{'device-profile.lwm2m.client-hold-off-time-tip' | translate}}"
  54 + <input matInput type="number" formControlName="clientHoldOffTime" required min="0"
  55 + matTooltip="{{'device-profile.lwm2m.client-hold-off-time-tooltip' | translate}}"
65 56 matTooltipPosition="above">
66 57 <mat-error *ngIf="serverFormGroup.get('clientHoldOffTime').hasError('required')">
67   - {{ 'device-profile.lwm2m.client-hold-off-time' | translate }}
68   - <strong>{{ 'device-profile.lwm2m.required' | translate }}</strong>
  58 + {{ 'device-profile.lwm2m.client-hold-off-time-required' | translate }}
69 59 </mat-error>
70 60 </mat-form-field>
71 61 <mat-form-field fxFlex>
72   - <mat-label>{{ 'device-profile.lwm2m.bootstrap-server-account-timeout' | translate }}</mat-label>
73   - <input matInput type="number" formControlName="bootstrapServerAccountTimeout" required
74   - matTooltip="{{'device-profile.lwm2m.bootstrap-server-account-timeout-tip' | translate}}"
  62 + <mat-label>{{ 'device-profile.lwm2m.account-after-timeout' | translate }}</mat-label>
  63 + <input matInput type="number" formControlName="bootstrapServerAccountTimeout" required min="0"
  64 + matTooltip="{{'device-profile.lwm2m.account-after-timeout-tooltip' | translate}}"
75 65 matTooltipPosition="above">
76 66 <mat-error *ngIf="serverFormGroup.get('bootstrapServerAccountTimeout').hasError('required')">
77   - {{ 'device-profile.lwm2m.bootstrap-server-account-timeout' | translate }}
78   - <strong>{{ 'device-profile.lwm2m.required' | translate }}</strong>
  67 + {{ 'device-profile.lwm2m.account-after-timeout-required' | translate }}
79 68 </mat-error>
80 69 </mat-form-field>
81   - <mat-checkbox fxFlex formControlName="bootstrapServerIs" color="primary">
82   - {{ 'device-profile.lwm2m.bootstrap-server' | translate }}
83   - </mat-checkbox>
84   - <div fxFlex></div>
85 70 </div>
86 71 <div *ngIf="serverFormGroup.get('securityMode').value === securityConfigLwM2MType.RPK ||
87 72 serverFormGroup.get('securityMode').value === securityConfigLwM2MType.X509">
... ... @@ -89,32 +74,22 @@
89 74 <mat-label>{{ 'device-profile.lwm2m.server-public-key' | translate }}</mat-label>
90 75 <textarea matInput
91 76 #serverPublicKey
92   - maxlength="{{lenMaxServerPublicKey}}"
  77 + maxlength="{{maxLengthPublicKey}}"
93 78 cdkTextareaAutosize
94 79 cdkAutosizeMinRows="1"
95 80 cols="1" required
96   - style="overflow:hidden"
97 81 formControlName="serverPublicKey"
98   - matTooltip="{{'device-profile.lwm2m.server-public-key-tip' | translate}}"
99 82 ></textarea>
100   - <mat-hint align="end">{{serverPublicKey.value?.length || 0}}/{{lenMaxServerPublicKey}}</mat-hint>
  83 + <mat-hint align="end">{{serverPublicKey.value?.length || 0}}/{{maxLengthPublicKey}}</mat-hint>
101 84 <mat-error *ngIf="serverFormGroup.get('serverPublicKey').hasError('required')">
102   - {{ 'device-profile.lwm2m.server-public-key' | translate }}
103   - <strong>{{ 'device-profile.lwm2m.required' | translate }}</strong>
  85 + {{ 'device-profile.lwm2m.server-public-key-required' | translate }}
104 86 </mat-error>
105   - <mat-error *ngIf="serverFormGroup.get('serverPublicKey').hasError('pattern') &&
106   - (serverFormGroup.get('securityMode').value === securityConfigLwM2MType.RPK ||
107   - serverFormGroup.get('securityMode').value === securityConfigLwM2MType.X509)">
108   - {{ 'device-profile.lwm2m.server-public-key' | translate }}
109   - <strong>{{ 'device-profile.lwm2m.pattern_hex_dec' | translate: {
110   - count: 0} }}</strong>
  87 + <mat-error *ngIf="serverFormGroup.get('serverPublicKey').hasError('pattern')">
  88 + {{ 'device-profile.lwm2m.server-public-key-pattern' | translate }}
111 89 </mat-error>
112   - <mat-error *ngIf="(serverFormGroup.get('serverPublicKey').hasError('maxlength') ||
113   - serverFormGroup.get('serverPublicKey').hasError('minlength')) &&
114   - serverFormGroup.get('securityMode').value === securityConfigLwM2MType.RPK">
115   - {{ 'device-profile.lwm2m.server-public-key' | translate }}
116   - <strong>{{ 'device-profile.lwm2m.pattern_hex_dec' | translate: {
117   - count: lenMaxServerPublicKey } }}</strong>
  90 + <mat-error *ngIf="serverFormGroup.get('serverPublicKey').hasError('maxlength') ||
  91 + serverFormGroup.get('serverPublicKey').hasError('minlength')">
  92 + {{ 'device-profile.lwm2m.server-public-key-length' | translate: {count: maxLengthPublicKey } }}
118 93 </mat-error>
119 94 </mat-form-field>
120 95 </div>
... ...
... ... @@ -14,26 +14,24 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -import { Component, forwardRef, Inject, Input } from '@angular/core';
  17 +import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
18 18 import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
19 19 import {
20   - DEFAULT_CLIENT_HOLD_OFF_TIME,
21   - DEFAULT_ID_SERVER,
22 20 DEFAULT_PORT_BOOTSTRAP_NO_SEC,
23 21 DEFAULT_PORT_SERVER_NO_SEC,
24 22 KEY_REGEXP_HEX_DEC,
25 23 LEN_MAX_PUBLIC_KEY_RPK,
26 24 LEN_MAX_PUBLIC_KEY_X509,
27   - SECURITY_CONFIG_MODE,
28   - SECURITY_CONFIG_MODE_NAMES,
  25 + securityConfigMode,
  26 + securityConfigModeNames,
29 27 ServerSecurityConfig
30 28 } from './lwm2m-profile-config.models';
31   -import { coerceBooleanProperty } from '@angular/cdk/coercion';
32   -import { WINDOW } from '@core/services/window.service';
33   -import { pairwise, startWith } from 'rxjs/operators';
34 29 import { DeviceProfileService } from '@core/http/device-profile.service';
  30 +import { of, Subject } from 'rxjs';
  31 +import { map, mergeMap, takeUntil, tap } from 'rxjs/operators';
  32 +import { Observable } from 'rxjs/internal/Observable';
  33 +import { deepClone } from '@core/utils';
35 34
36   -// @dynamic
37 35 @Component({
38 36 selector: 'tb-profile-lwm2m-device-config-server',
39 37 templateUrl: './lwm2m-device-config-server.component.html',
... ... @@ -46,127 +44,78 @@ import { DeviceProfileService } from '@core/http/device-profile.service';
46 44 ]
47 45 })
48 46
49   -export class Lwm2mDeviceConfigServerComponent implements ControlValueAccessor {
  47 +export class Lwm2mDeviceConfigServerComponent implements OnInit, ControlValueAccessor, OnDestroy {
50 48
51   - private requiredValue: boolean;
52 49 private disabled = false;
  50 + private destroy$ = new Subject();
  51 +
  52 + private securityDefaultConfig: ServerSecurityConfig;
53 53
54   - valuePrev = null;
55 54 serverFormGroup: FormGroup;
56   - securityConfigLwM2MType = SECURITY_CONFIG_MODE;
57   - securityConfigLwM2MTypes = Object.keys(SECURITY_CONFIG_MODE);
58   - credentialTypeLwM2MNamesMap = SECURITY_CONFIG_MODE_NAMES;
59   - lenMinServerPublicKey = 0;
60   - lenMaxServerPublicKey = LEN_MAX_PUBLIC_KEY_RPK;
  55 + securityConfigLwM2MType = securityConfigMode;
  56 + securityConfigLwM2MTypes = Object.keys(securityConfigMode);
  57 + credentialTypeLwM2MNamesMap = securityConfigModeNames;
  58 + maxLengthPublicKey = LEN_MAX_PUBLIC_KEY_RPK;
61 59 currentSecurityMode = null;
62 60
63 61 @Input()
64   - bootstrapServerIs: boolean;
  62 + isBootstrapServer = false;
65 63
66   - get required(): boolean {
67   - return this.requiredValue;
68   - }
  64 + private propagateChange = (v: any) => { };
69 65
70   - @Input()
71   - set required(value: boolean) {
72   - this.requiredValue = coerceBooleanProperty(value);
  66 + constructor(public fb: FormBuilder,
  67 + private deviceProfileService: DeviceProfileService) {
73 68 }
74 69
75   - constructor(public fb: FormBuilder,
76   - private deviceProfileService: DeviceProfileService,
77   - @Inject(WINDOW) private window: Window) {
78   - this.serverFormGroup = this.initServerGroup();
  70 + ngOnInit(): void {
  71 + this.serverFormGroup = this.fb.group({
  72 + host: ['', Validators.required],
  73 + port: [this.isBootstrapServer ? DEFAULT_PORT_BOOTSTRAP_NO_SEC : DEFAULT_PORT_SERVER_NO_SEC, [Validators.required, Validators.min(0)]],
  74 + securityMode: [securityConfigMode.NO_SEC],
  75 + serverPublicKey: ['', Validators.required],
  76 + clientHoldOffTime: ['', [Validators.required, Validators.min(0)]],
  77 + serverId: ['', [Validators.required, Validators.min(0)]],
  78 + bootstrapServerAccountTimeout: ['', [Validators.required, Validators.min(0)]],
  79 + });
79 80 this.serverFormGroup.get('securityMode').valueChanges.pipe(
80   - startWith(null),
81   - pairwise()
82   - ).subscribe(([previousValue, currentValue]) => {
83   - if (previousValue === null || previousValue !== currentValue) {
84   - this.getLwm2mBootstrapSecurityInfo(currentValue);
85   - this.updateValidate(currentValue);
86   - this.serverFormGroup.updateValueAndValidity();
87   - }
88   -
  81 + tap(securityMode => this.updateValidate(securityMode)),
  82 + mergeMap(securityMode => this.getLwm2mBootstrapSecurityInfo(securityMode)),
  83 + takeUntil(this.destroy$)
  84 + ).subscribe(serverSecurityConfig => {
  85 + this.serverFormGroup.patchValue(serverSecurityConfig, {emitEvent: false});
89 86 });
90   - this.serverFormGroup.valueChanges.subscribe(value => {
91   - if (this.disabled !== undefined && !this.disabled) {
92   - this.propagateChangeState(value);
93   - }
  87 + this.serverFormGroup.valueChanges.pipe(
  88 + takeUntil(this.destroy$)
  89 + ).subscribe(value => {
  90 + this.propagateChangeState(value);
94 91 });
95 92 }
96 93
97   - private updateValueFields = (serverData: ServerSecurityConfig): void => {
98   - serverData.bootstrapServerIs = this.bootstrapServerIs;
99   - this.serverFormGroup.patchValue(serverData, {emitEvent: false});
100   - this.serverFormGroup.get('bootstrapServerIs').disable();
101   - const securityMode = this.serverFormGroup.get('securityMode').value as SECURITY_CONFIG_MODE;
102   - this.updateValidate(securityMode);
  94 + ngOnDestroy(): void {
  95 + this.destroy$.next();
  96 + this.destroy$.complete();
103 97 }
104 98
105   - private updateValidate = (securityMode: SECURITY_CONFIG_MODE): void => {
106   - switch (securityMode) {
107   - case SECURITY_CONFIG_MODE.NO_SEC:
108   - this.setValidatorsNoSecPsk();
109   - break;
110   - case SECURITY_CONFIG_MODE.PSK:
111   - this.setValidatorsNoSecPsk();
112   - break;
113   - case SECURITY_CONFIG_MODE.RPK:
114   - this.lenMinServerPublicKey = LEN_MAX_PUBLIC_KEY_RPK;
115   - this.lenMaxServerPublicKey = LEN_MAX_PUBLIC_KEY_RPK;
116   - this.setValidatorsRpkX509();
117   - break;
118   - case SECURITY_CONFIG_MODE.X509:
119   - this.lenMinServerPublicKey = 0;
120   - this.lenMaxServerPublicKey = LEN_MAX_PUBLIC_KEY_X509;
121   - this.setValidatorsRpkX509();
122   - break;
  99 + writeValue(serverData: ServerSecurityConfig): void {
  100 + if (serverData) {
  101 + this.serverFormGroup.patchValue(serverData, {emitEvent: false});
  102 + this.updateValidate(serverData.securityMode);
123 103 }
124   - this.serverFormGroup.updateValueAndValidity();
125   - }
126   -
127   - private setValidatorsNoSecPsk = (): void => {
128   - this.serverFormGroup.get('serverPublicKey').setValidators([]);
129   - }
130   -
131   - private setValidatorsRpkX509 = (): void => {
132   - this.serverFormGroup.get('serverPublicKey').setValidators([Validators.required,
133   - Validators.pattern(KEY_REGEXP_HEX_DEC),
134   - Validators.minLength(this.lenMinServerPublicKey),
135   - Validators.maxLength(this.lenMaxServerPublicKey)]);
136   - }
137   -
138   - writeValue(value: ServerSecurityConfig): void {
139   - if (value) {
140   - this.updateValueFields(value);
  104 + if (!this.securityDefaultConfig){
  105 + this.getLwm2mBootstrapSecurityInfo().subscribe(value => {
  106 + if (!serverData) {
  107 + this.serverFormGroup.patchValue(value);
  108 + }
  109 + });
141 110 }
142 111 }
143 112
144   - private propagateChange = (v: any) => {};
145   -
146 113 registerOnChange(fn: any): void {
147 114 this.propagateChange = fn;
148 115 }
149 116
150   - private propagateChangeState = (value: any): void => {
151   - if (value !== undefined) {
152   - if (this.valuePrev === null) {
153   - this.valuePrev = 'init';
154   - } else if (this.valuePrev === 'init') {
155   - this.valuePrev = value;
156   - } else if (JSON.stringify(value) !== JSON.stringify(this.valuePrev)) {
157   - this.valuePrev = value;
158   - if (this.serverFormGroup.valid) {
159   - this.propagateChange(value);
160   - } else {
161   - this.propagateChange(null);
162   - }
163   - }
164   - }
165   - }
166   -
167 117 setDisabledState(isDisabled: boolean): void {
168 118 this.disabled = isDisabled;
169   - this.valuePrev = null;
170 119 if (isDisabled) {
171 120 this.serverFormGroup.disable({emitEvent: false});
172 121 } else {
... ... @@ -177,33 +126,76 @@ export class Lwm2mDeviceConfigServerComponent implements ControlValueAccessor {
177 126 registerOnTouched(fn: any): void {
178 127 }
179 128
180   - private initServerGroup = (): FormGroup => {
181   - const port = this.bootstrapServerIs ? DEFAULT_PORT_BOOTSTRAP_NO_SEC : DEFAULT_PORT_SERVER_NO_SEC;
182   - return this.fb.group({
183   - host: [this.window.location.hostname, this.required ? Validators.required : ''],
184   - port: [port, this.required ? Validators.required : ''],
185   - bootstrapServerIs: [this.bootstrapServerIs, ''],
186   - securityMode: [this.fb.control(SECURITY_CONFIG_MODE.NO_SEC)],
187   - serverPublicKey: ['', this.required ? Validators.required : ''],
188   - clientHoldOffTime: [DEFAULT_CLIENT_HOLD_OFF_TIME, this.required ? Validators.required : ''],
189   - serverId: [DEFAULT_ID_SERVER, this.required ? Validators.required : ''],
190   - bootstrapServerAccountTimeout: ['', this.required ? Validators.required : ''],
191   - });
  129 + private updateValidate(securityMode: securityConfigMode): void {
  130 + switch (securityMode) {
  131 + case securityConfigMode.NO_SEC:
  132 + case securityConfigMode.PSK:
  133 + this.clearValidators();
  134 + break;
  135 + case securityConfigMode.RPK:
  136 + this.maxLengthPublicKey = LEN_MAX_PUBLIC_KEY_RPK;
  137 + this.setValidators(LEN_MAX_PUBLIC_KEY_RPK);
  138 + break;
  139 + case securityConfigMode.X509:
  140 + this.maxLengthPublicKey = LEN_MAX_PUBLIC_KEY_X509;
  141 + this.setValidators(0);
  142 + break;
  143 + }
  144 + this.serverFormGroup.get('serverPublicKey').updateValueAndValidity({emitEvent: false});
  145 + }
  146 +
  147 + private clearValidators(): void {
  148 + this.serverFormGroup.get('serverPublicKey').clearValidators();
  149 + }
  150 +
  151 + private setValidators(minLengthKey: number): void {
  152 + this.serverFormGroup.get('serverPublicKey').setValidators([
  153 + Validators.required,
  154 + Validators.pattern(KEY_REGEXP_HEX_DEC),
  155 + Validators.minLength(minLengthKey),
  156 + Validators.maxLength(this.maxLengthPublicKey)
  157 + ]);
192 158 }
193 159
194   - private getLwm2mBootstrapSecurityInfo = (mode: string): void => {
195   - this.deviceProfileService.getLwm2mBootstrapSecurityInfo(mode, this.serverFormGroup.get('bootstrapServerIs').value).subscribe(
196   - (serverSecurityConfig) => {
197   - this.serverFormGroup.patchValue({
198   - host: serverSecurityConfig.host,
199   - port: serverSecurityConfig.port,
200   - serverPublicKey: serverSecurityConfig.serverPublicKey,
201   - clientHoldOffTime: serverSecurityConfig.clientHoldOffTime,
202   - serverId: serverSecurityConfig.serverId,
203   - bootstrapServerAccountTimeout: serverSecurityConfig.bootstrapServerAccountTimeout
204   - },
205   - {emitEvent: true});
  160 + private propagateChangeState = (value: ServerSecurityConfig): void => {
  161 + if (value !== undefined) {
  162 + if (this.serverFormGroup.valid) {
  163 + this.propagateChange(value);
  164 + } else {
  165 + this.propagateChange(null);
206 166 }
  167 + }
  168 + }
  169 +
  170 + private getLwm2mBootstrapSecurityInfo(securityMode = securityConfigMode.NO_SEC): Observable<ServerSecurityConfig> {
  171 + if (this.securityDefaultConfig) {
  172 + return of(this.processingBootstrapSecurityInfo(this.securityDefaultConfig, securityMode));
  173 + }
  174 + return this.deviceProfileService.getLwm2mBootstrapSecurityInfo(this.isBootstrapServer).pipe(
  175 + map(securityInfo => {
  176 + this.securityDefaultConfig = securityInfo;
  177 + return this.processingBootstrapSecurityInfo(securityInfo, securityMode);
  178 + })
207 179 );
208 180 }
  181 +
  182 + private processingBootstrapSecurityInfo(securityConfig: ServerSecurityConfig, securityMode: securityConfigMode): ServerSecurityConfig {
  183 + const config = deepClone(securityConfig);
  184 + switch (securityMode) {
  185 + case securityConfigMode.PSK:
  186 + config.port = config.securityPort;
  187 + config.host = config.securityHost;
  188 + config.serverPublicKey = '';
  189 + break;
  190 + case securityConfigMode.RPK:
  191 + case securityConfigMode.X509:
  192 + config.port = config.securityPort;
  193 + config.host = config.securityHost;
  194 + break;
  195 + case securityConfigMode.NO_SEC:
  196 + config.serverPublicKey = '';
  197 + break;
  198 + }
  199 + return config;
  200 + }
209 201 }
... ...
... ... @@ -15,231 +15,172 @@
15 15 limitations under the License.
16 16
17 17 -->
18   -<section style="padding-bottom: 16px; margin: 0" mat-dialog-content>
  18 +<section style="padding-bottom: 16px; margin: 0">
19 19 <mat-tab-group dynamicHeight>
20 20 <mat-tab label="{{ 'device-profile.lwm2m.model-tab' | translate }}">
21 21 <ng-template matTabContent>
22 22 <section [formGroup]="lwm2mDeviceProfileFormGroup">
23   - <div class="mat-padding" style="padding-top: 0">
24   - <tb-profile-lwm2m-object-list
25   - (addList)="addObjectsList($event)"
26   - (removeList)="removeObjectsList($event)"
27   - [required]="required"
28   - formControlName="objectIds">
29   - </tb-profile-lwm2m-object-list>
30   - </div>
31   - <div class="mat-padding">
32   - <tb-profile-lwm2m-observe-attr-telemetry
33   - [required]="required"
34   - formControlName="observeAttrTelemetry">
35   - </tb-profile-lwm2m-observe-attr-telemetry>
36   - </div>
37   - </section>
38   - </ng-template>
39   - </mat-tab>
40   - <mat-tab label="{{ 'device-profile.lwm2m.bootstrap-tab' | translate }}">
41   - <ng-template matTabContent>
42   - <section [formGroup]="lwm2mDeviceProfileFormGroup">
43   - <div class="mat-padding">
44   - <mat-accordion multi="true" class="mat-body-1">
45   - <mat-expansion-panel>
46   - <mat-expansion-panel-header>
47   - <mat-panel-title>
48   - <div class="tb-panel-title">{{ 'device-profile.lwm2m.servers' | translate | uppercase }}</div>
49   - </mat-panel-title>
50   - </mat-expansion-panel-header>
51   - <ng-template matExpansionPanelContent>
52   - <div fxLayout="column">
53   - <div fxLayout="row" fxLayoutGap="8px">
54   - <mat-form-field fxFlex>
55   - <mat-label>{{ 'device-profile.lwm2m.short-id' | translate }}</mat-label>
56   - <input matInput type="number" formControlName="shortId" required>
57   - <mat-error *ngIf="lwm2mDeviceProfileFormGroup.get('shortId').hasError('required')">
58   - {{ 'device-profile.lwm2m.short-id' | translate }}
59   - <strong>{{ 'device-profile.lwm2m.required' | translate }}</strong>
60   - </mat-error>
61   - </mat-form-field>
62   - <mat-form-field fxFlex>
63   - <mat-label>{{ 'device-profile.lwm2m.lifetime' | translate }}</mat-label>
64   - <input matInput type="number" formControlName="lifetime" required>
65   - <mat-error
66   - *ngIf="lwm2mDeviceProfileFormGroup.get('lifetime').hasError('required')">
67   - {{ 'device-profile.lwm2m.lifetime' | translate }}
68   - <strong>{{ 'device-profile.lwm2m.required' | translate }}</strong>
69   - </mat-error>
70   - </mat-form-field>
71   - <mat-form-field fxFlex>
72   - <mat-label>{{ 'device-profile.lwm2m.default-min-period' | translate }}</mat-label>
73   - <input matInput type="number" formControlName="defaultMinPeriod" required>
74   - <mat-error
75   - *ngIf="lwm2mDeviceProfileFormGroup.get('defaultMinPeriod').hasError('required')">
76   - {{ 'device-profile.lwm2m.default-min-period' | translate }}
77   - <strong>{{ 'device-profile.lwm2m.required' | translate }}</strong>
78   - </mat-error>
79   - </mat-form-field>
80   - </div>
81   - <div fxLayout="row" fxLayoutGap="8px">
82   - <mat-form-field class="mat-block" fxFlex="100">
83   - <mat-label>{{ 'device-profile.lwm2m.binding' | translate }}</mat-label>
84   - <mat-select formControlName="binding">
85   - <mat-option *ngFor="let bindingMode of bindingModeTypes"
86   - [value]="bindingMode">
87   - {{ bindingModeTypeNamesMap.get(bindingModeType[bindingMode]) }}
88   - </mat-option>
89   - </mat-select>
90   - </mat-form-field>
91   - </div>
92   - <div>
93   - <mat-checkbox formControlName="notifIfDisabled" color="primary">
94   - {{ 'device-profile.lwm2m.notif-if-disabled' | translate }}
95   - </mat-checkbox>
96   - </div>
97   - </div>
98   - </ng-template>
99   - </mat-expansion-panel>
100   - </mat-accordion>
101   - <mat-accordion multi="true" class="mat-body-1">
102   - <mat-expansion-panel>
103   - <mat-expansion-panel-header>
104   - <mat-panel-title>
105   - <div
106   - class="tb-panel-title">{{ 'device-profile.lwm2m.bootstrap-server' | translate | uppercase }}</div>
107   - </mat-panel-title>
108   - </mat-expansion-panel-header>
109   - <ng-template matExpansionPanelContent>
110   - <div class="mat-padding">
111   - <tb-profile-lwm2m-device-config-server
112   - [required]="required"
113   - formControlName="bootstrapServer"
114   - [bootstrapServerIs]=true>
115   - </tb-profile-lwm2m-device-config-server>
116   - </div>
117   - </ng-template>
118   - </mat-expansion-panel>
119   - </mat-accordion>
120   - <mat-accordion multi="true" class="mat-body-1">
121   - <mat-expansion-panel>
122   - <mat-expansion-panel-header>
123   - <mat-panel-title>
124   - <div class="tb-panel-title">{{ 'device-profile.lwm2m.lwm2m-server' | translate | uppercase }}</div>
125   - </mat-panel-title>
126   - </mat-expansion-panel-header>
127   - <ng-template matExpansionPanelContent>
128   - <div class="mat-padding">
129   - <tb-profile-lwm2m-device-config-server
130   - [required]="required"
131   - formControlName="lwm2mServer"
132   - [bootstrapServerIs]=false>
133   - </tb-profile-lwm2m-device-config-server>
134   - </div>
135   - </ng-template>
136   - </mat-expansion-panel>
137   - </mat-accordion>
138   - </div>
  23 + <tb-profile-lwm2m-object-list
  24 + (addList)="addObjectsList($event)"
  25 + (removeList)="removeObjectsList($event)"
  26 + [required]="required"
  27 + formControlName="objectIds">
  28 + </tb-profile-lwm2m-object-list>
  29 + <tb-profile-lwm2m-observe-attr-telemetry
  30 + [required]="required"
  31 + formControlName="observeAttrTelemetry">
  32 + </tb-profile-lwm2m-observe-attr-telemetry>
139 33 </section>
140 34 </ng-template>
141 35 </mat-tab>
142   - <mat-tab label="{{ 'device-profile.lwm2m.others-tab' | translate }}">
  36 + <mat-tab label="{{ 'device-profile.lwm2m.servers' | translate }}">
143 37 <ng-template matTabContent>
144   - <section [formGroup]="lwm2mDeviceProfileFormGroup">
145   - <mat-accordion multi="true" class="mat-body-1">
146   - <div *ngIf="false">
147   - <mat-expansion-panel>
148   - <mat-expansion-panel-header>
149   - <mat-panel-title>
150   - <div
151   - class="tb-panel-title">{{ 'device-profile.lwm2m.client-strategy' | translate | uppercase }}</div>
152   - </mat-panel-title>
153   - </mat-expansion-panel-header>
154   - <ng-template matExpansionPanelContent>
155   - <div fxLayout="column">
156   - <mat-form-field class="mat-block">
157   - <mat-label>{{ 'device-profile.lwm2m.client-strategy-label' | translate }}</mat-label>
158   - <mat-select formControlName="clientStrategy"
159   - matTooltip="{{ 'device-profile.lwm2m.client-strategy-tip' | translate:
160   - { count: +lwm2mDeviceProfileFormGroup.get('clientStrategy').value } }}"
161   - matTooltipPosition="above">
162   - <mat-option value=1>{{ 'device-profile.lwm2m.client-strategy-connect' | translate:
163   - {count: 1} }}</mat-option>
164   - <mat-option value=2>{{ 'device-profile.lwm2m.client-strategy-connect' | translate:
165   - {count: 2} }}</mat-option>
166   - </mat-select>
167   - </mat-form-field>
168   - </div>
169   - </ng-template>
170   - </mat-expansion-panel>
171   - </div>
  38 + <section [formGroup]="lwm2mDeviceProfileFormGroup" style="padding: 4px 2px">
  39 + <mat-accordion multi="true">
172 40 <mat-expansion-panel>
173 41 <mat-expansion-panel-header>
174   - <mat-panel-title>
175   - <div
176   - class="tb-panel-title">{{ 'device-profile.lwm2m.ota-update-strategy' | translate | uppercase }}</div>
177   - </mat-panel-title>
  42 + <mat-panel-title>{{ 'device-profile.lwm2m.servers' | translate }}</mat-panel-title>
178 43 </mat-expansion-panel-header>
179 44 <ng-template matExpansionPanelContent>
180   - <div fxLayout="column">
181   - <mat-form-field class="mat-block" fxFlex>
182   - <mat-label>{{ 'device-profile.lwm2m.fw-update-strategy-label' | translate }}</mat-label>
183   - <mat-select formControlName="fwUpdateStrategy" (selectionChange)="changeFwUpdateStrategy($event)">
184   - <mat-option value=1>{{ 'device-profile.lwm2m.fw-update-strategy' | translate:
185   - {count: 1} }}</mat-option>
186   - <mat-option value=2>{{ 'device-profile.lwm2m.fw-update-strategy' | translate:
187   - {count: 2} }}</mat-option>
188   - <mat-option value=3>{{ 'device-profile.lwm2m.fw-update-strategy' | translate:
189   - {count: 3} }}</mat-option>
190   - </mat-select>
  45 + <div fxLayout="row" fxLayout.xs="column" fxLayoutGap="8px" fxLayoutGap.xs="0px">
  46 + <mat-form-field fxFlex>
  47 + <mat-label>{{ 'device-profile.lwm2m.short-id' | translate }}</mat-label>
  48 + <input matInput type="number" formControlName="shortId" required>
  49 + <mat-error *ngIf="lwm2mDeviceProfileFormGroup.get('shortId').hasError('required')">
  50 + {{ 'device-profile.lwm2m.short-id' | translate }}
  51 + <strong>{{ 'device-profile.lwm2m.required' | translate }}</strong>
  52 + </mat-error>
191 53 </mat-form-field>
192   - <mat-form-field class="mat-block" fxFlex>
193   - <mat-label>{{ 'device-profile.lwm2m.sw-update-strategy-label' | translate }}</mat-label>
194   - <mat-select formControlName="swUpdateStrategy" (selectionChange)="changeSwUpdateStrategy($event)">
195   - <mat-option value=1>{{ 'device-profile.lwm2m.sw-update-strategy' | translate:
196   - {count: 1} }}</mat-option>
197   - <mat-option value=2>{{ 'device-profile.lwm2m.sw-update-strategy' | translate:
198   - {count: 2} }}</mat-option>
199   - </mat-select>
  54 + <mat-form-field fxFlex>
  55 + <mat-label>{{ 'device-profile.lwm2m.lifetime' | translate }}</mat-label>
  56 + <input matInput type="number" formControlName="lifetime" required>
  57 + <mat-error
  58 + *ngIf="lwm2mDeviceProfileFormGroup.get('lifetime').hasError('required')">
  59 + {{ 'device-profile.lwm2m.lifetime' | translate }}
  60 + <strong>{{ 'device-profile.lwm2m.required' | translate }}</strong>
  61 + </mat-error>
  62 + </mat-form-field>
  63 + <mat-form-field fxFlex>
  64 + <mat-label>{{ 'device-profile.lwm2m.default-min-period' | translate }}</mat-label>
  65 + <input matInput type="number" formControlName="defaultMinPeriod" required>
  66 + <mat-error
  67 + *ngIf="lwm2mDeviceProfileFormGroup.get('defaultMinPeriod').hasError('required')">
  68 + {{ 'device-profile.lwm2m.default-min-period' | translate }}
  69 + <strong>{{ 'device-profile.lwm2m.required' | translate }}</strong>
  70 + </mat-error>
200 71 </mat-form-field>
201 72 </div>
  73 + <mat-form-field class="mat-block">
  74 + <mat-label>{{ 'device-profile.lwm2m.binding' | translate }}</mat-label>
  75 + <mat-select formControlName="binding">
  76 + <mat-option *ngFor="let bindingMode of bindingModeTypes"
  77 + [value]="bindingMode">
  78 + {{ bindingModeTypeNamesMap.get(bindingModeType[bindingMode]) }}
  79 + </mat-option>
  80 + </mat-select>
  81 + </mat-form-field>
  82 + <mat-checkbox formControlName="notifIfDisabled" color="primary">
  83 + {{ 'device-profile.lwm2m.notif-if-disabled' | translate }}
  84 + </mat-checkbox>
202 85 </ng-template>
203 86 </mat-expansion-panel>
204   - <mat-expansion-panel *ngIf="isFwUpdateStrategy || isSwUpdateStrategy">
  87 + <mat-expansion-panel>
205 88 <mat-expansion-panel-header>
206   - <mat-panel-title>
207   - <div
208   - class="tb-panel-title">{{ 'device-profile.lwm2m.ota-update-recourse' | translate | uppercase }}</div>
209   - </mat-panel-title>
  89 + <mat-panel-title>{{ 'device-profile.lwm2m.bootstrap-server' | translate }}</mat-panel-title>
210 90 </mat-expansion-panel-header>
211 91 <ng-template matExpansionPanelContent>
212   - <div fxLayout="column">
213   - <div *ngIf="isFwUpdateStrategy">
214   - <mat-form-field class="mat-block" fxFlex>
215   - <mat-label>{{ 'device-profile.lwm2m.fw-update-recourse' | translate }}</mat-label>
216   - <input matInput formControlName="fwUpdateRecourse" required>
217   - <mat-error *ngIf="lwm2mDeviceProfileFormGroup.get('fwUpdateRecourse').hasError('required')">
218   - {{ 'device-profile.lwm2m.fw-update-recourse' | translate }}
219   - <strong>{{ 'device-profile.lwm2m.required' | translate }}</strong>
220   - </mat-error>
221   - </mat-form-field>
222   - </div>
223   - <div *ngIf="isSwUpdateStrategy">
224   - <mat-form-field class="mat-block" fxFlex>
225   - <mat-label>{{ 'device-profile.lwm2m.sw-update-recourse' | translate }}</mat-label>
226   - <input matInput formControlName="swUpdateRecourse" required>
227   - <mat-error *ngIf="lwm2mDeviceProfileFormGroup.get('swUpdateRecourse').hasError('required')">
228   - {{ 'device-profile.lwm2m.sw-update-recourse' | translate }}
229   - <strong>{{ 'device-profile.lwm2m.required' | translate }}</strong>
230   - </mat-error>
231   - </mat-form-field>
232   - </div>
233   - </div>
  92 + <tb-profile-lwm2m-device-config-server
  93 + [required]="required"
  94 + formControlName="bootstrapServer"
  95 + [isBootstrapServer]="true">
  96 + </tb-profile-lwm2m-device-config-server>
  97 + </ng-template>
  98 + </mat-expansion-panel>
  99 + <mat-expansion-panel>
  100 + <mat-expansion-panel-header>
  101 + <mat-panel-title>{{ 'device-profile.lwm2m.lwm2m-server' | translate }}</mat-panel-title>
  102 + </mat-expansion-panel-header>
  103 + <ng-template matExpansionPanelContent>
  104 + <tb-profile-lwm2m-device-config-server
  105 + [required]="required"
  106 + formControlName="lwm2mServer"
  107 + [isBootstrapServer]="false">
  108 + </tb-profile-lwm2m-device-config-server>
234 109 </ng-template>
235 110 </mat-expansion-panel>
236 111 </mat-accordion>
237 112 </section>
238 113 </ng-template>
239 114 </mat-tab>
  115 + <mat-tab label="{{ 'device-profile.lwm2m.others-tab' | translate }}">
  116 + <ng-template matTabContent>
  117 + <section [formGroup]="lwm2mDeviceProfileFormGroup">
  118 + <fieldset class="fields-group">
  119 + <legend class="group-title" translate>device-profile.lwm2m.fw-update</legend>
  120 + <mat-form-field class="mat-block" fxFlex>
  121 + <mat-label>{{ 'device-profile.lwm2m.fw-update-strategy' | translate }}</mat-label>
  122 + <mat-select formControlName="fwUpdateStrategy">
  123 + <mat-option [value]=1>{{ 'device-profile.lwm2m.fw-update-strategy-package' | translate }}</mat-option>
  124 + <mat-option [value]=2>{{ 'device-profile.lwm2m.fw-update-strategy-package-uri' | translate }}</mat-option>
  125 + <mat-option [value]=3>{{ 'device-profile.lwm2m.fw-update-strategy-data' | translate }}</mat-option>
  126 + </mat-select>
  127 + </mat-form-field>
  128 + <mat-form-field class="mat-block" fxFlex *ngIf="isFwUpdateStrategy">
  129 + <mat-label>{{ 'device-profile.lwm2m.fw-update-recourse' | translate }}</mat-label>
  130 + <input matInput formControlName="fwUpdateRecourse" required>
  131 + <mat-error *ngIf="lwm2mDeviceProfileFormGroup.get('fwUpdateRecourse').hasError('required')">
  132 + {{ 'device-profile.lwm2m.fw-update-recourse-required' | translate }}
  133 + </mat-error>
  134 + </mat-form-field>
  135 + </fieldset>
  136 + <fieldset class="fields-group">
  137 + <legend class="group-title" translate>device-profile.lwm2m.sw-update</legend>
  138 + <mat-form-field class="mat-block" fxFlex>
  139 + <mat-label>{{ 'device-profile.lwm2m.sw-update-strategy' | translate }}</mat-label>
  140 + <mat-select formControlName="swUpdateStrategy">
  141 + <mat-option [value]=1>{{ 'device-profile.lwm2m.sw-update-strategy-package' | translate }}</mat-option>
  142 + <mat-option [value]=2>{{ 'device-profile.lwm2m.sw-update-strategy-package-uri' | translate }}</mat-option>
  143 + </mat-select>
  144 + </mat-form-field>
  145 + <mat-form-field class="mat-block" fxFlex *ngIf="isSwUpdateStrategy">
  146 + <mat-label>{{ 'device-profile.lwm2m.sw-update-recourse' | translate }}</mat-label>
  147 + <input matInput formControlName="swUpdateRecourse" required>
  148 + <mat-error *ngIf="lwm2mDeviceProfileFormGroup.get('swUpdateRecourse').hasError('required')">
  149 + {{ 'device-profile.lwm2m.sw-update-recourse-required' | translate }}
  150 + </mat-error>
  151 + </mat-form-field>
  152 + </fieldset>
  153 +<!-- <mat-accordion multi="true">-->
  154 +<!-- <div *ngIf="false">-->
  155 +<!-- <mat-expansion-panel>-->
  156 +<!-- <mat-expansion-panel-header>-->
  157 +<!-- <mat-panel-title>{{ 'device-profile.lwm2m.client-strategy' | translate }}</mat-panel-title>-->
  158 +<!-- </mat-expansion-panel-header>-->
  159 +<!-- <ng-template matExpansionPanelContent>-->
  160 +<!-- <div fxLayout="column">-->
  161 +<!-- <mat-form-field class="mat-block">-->
  162 +<!-- <mat-label>{{ 'device-profile.lwm2m.client-strategy-label' | translate }}</mat-label>-->
  163 +<!-- <mat-select formControlName="clientStrategy"-->
  164 +<!-- matTooltip="{{ 'device-profile.lwm2m.client-strategy-tip' | translate:-->
  165 +<!-- { count: +lwm2mDeviceProfileFormGroup.get('clientStrategy').value } }}"-->
  166 +<!-- matTooltipPosition="above">-->
  167 +<!-- <mat-option value=1>{{ 'device-profile.lwm2m.client-strategy-connect' | translate:-->
  168 +<!-- {count: 1} }}</mat-option>-->
  169 +<!-- <mat-option value=2>{{ 'device-profile.lwm2m.client-strategy-connect' | translate:-->
  170 +<!-- {count: 2} }}</mat-option>-->
  171 +<!-- </mat-select>-->
  172 +<!-- </mat-form-field>-->
  173 +<!-- </div>-->
  174 +<!-- </ng-template>-->
  175 +<!-- </mat-expansion-panel>-->
  176 +<!-- </div>-->
  177 +<!-- </mat-accordion>-->
  178 + </section>
  179 + </ng-template>
  180 + </mat-tab>
240 181 <mat-tab label="{{ 'device-profile.lwm2m.config-json-tab' | translate }}">
241 182 <ng-template matTabContent>
242   - <section [formGroup]="lwm2mDeviceConfigFormGroup" class="mat-padding">
  183 + <section [formGroup]="lwm2mDeviceConfigFormGroup" style="padding: 8px 0">
243 184 <tb-json-object-edit
244 185 [required]="required"
245 186 [sort]="sortFunction"
... ...
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host{
  17 + .fields-group {
  18 + padding: 8px;
  19 + margin: 10px 0;
  20 + border: 1px groove rgba(0, 0, 0, .25);
  21 + border-radius: 4px;
  22 +
  23 + legend {
  24 + color: rgba(0, 0, 0, .7);
  25 + }
  26 + }
  27 +}
... ...
... ... @@ -43,11 +43,11 @@ import { Direction } from '@shared/models/page/sort-order';
43 43 import _ from 'lodash';
44 44 import { Subject } from 'rxjs';
45 45 import { takeUntil } from 'rxjs/operators';
46   -import { MatSelectChange } from '@angular/material/select';
47 46
48 47 @Component({
49 48 selector: 'tb-profile-lwm2m-device-transport-configuration',
50 49 templateUrl: './lwm2m-device-profile-transport-configuration.component.html',
  50 + styleUrls: ['./lwm2m-device-profile-transport-configuration.component.scss'],
51 51 providers: [{
52 52 provide: NG_VALUE_ACCESSOR,
53 53 useExisting: forwardRef(() => Lwm2mDeviceProfileTransportConfigurationComponent),
... ... @@ -100,12 +100,38 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
100 100 clientStrategy: [1, []],
101 101 fwUpdateStrategy: [1, []],
102 102 swUpdateStrategy: [1, []],
103   - fwUpdateRecourse: ['', []],
104   - swUpdateRecourse: ['', []]
  103 + fwUpdateRecourse: [{value: '', disabled: true}, []],
  104 + swUpdateRecourse: [{value: '', disabled: true}, []]
105 105 });
106 106 this.lwm2mDeviceConfigFormGroup = this.fb.group({
107 107 configurationJson: [null, Validators.required]
108 108 });
  109 + this.lwm2mDeviceProfileFormGroup.get('fwUpdateStrategy').valueChanges.pipe(
  110 + takeUntil(this.destroy$)
  111 + ).subscribe((fwStrategy) => {
  112 + if (fwStrategy === 2) {
  113 + this.lwm2mDeviceProfileFormGroup.get('fwUpdateRecourse').enable({emitEvent: false});
  114 + this.lwm2mDeviceProfileFormGroup.get('fwUpdateRecourse').patchValue(DEFAULT_FW_UPDATE_RESOURCE, {emitEvent: false});
  115 + this.isFwUpdateStrategy = true;
  116 + } else {
  117 + this.lwm2mDeviceProfileFormGroup.get('fwUpdateRecourse').disable({emitEvent: false});
  118 + this.isFwUpdateStrategy = false;
  119 + }
  120 + this.otaUpdateFwStrategyValidate(true);
  121 + });
  122 + this.lwm2mDeviceProfileFormGroup.get('swUpdateStrategy').valueChanges.pipe(
  123 + takeUntil(this.destroy$)
  124 + ).subscribe((swStrategy) => {
  125 + if (swStrategy === 2) {
  126 + this.lwm2mDeviceProfileFormGroup.get('swUpdateRecourse').enable({emitEvent: false});
  127 + this.lwm2mDeviceProfileFormGroup.get('swUpdateRecourse').patchValue(DEFAULT_SW_UPDATE_RESOURCE, {emitEvent: false});
  128 + this.isSwUpdateStrategy = true;
  129 + } else {
  130 + this.isSwUpdateStrategy = false;
  131 + this.lwm2mDeviceProfileFormGroup.get('swUpdateRecourse').disable({emitEvent: false});
  132 + }
  133 + this.otaUpdateSwStrategyValidate(true);
  134 + });
109 135 this.lwm2mDeviceProfileFormGroup.valueChanges.pipe(
110 136 takeUntil(this.destroy$)
111 137 ).subscribe((value) => {
... ... @@ -176,11 +202,9 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
176 202 }
177 203
178 204 private updateWriteValue = (value: ModelValue): void => {
179   - let fwResource = this.configurationValue.clientLwM2mSettings.fwUpdateStrategy === '2' &&
180   - isDefinedAndNotNull(this.configurationValue.clientLwM2mSettings.fwUpdateRecourse) ?
  205 + const fwResource = isDefinedAndNotNull(this.configurationValue.clientLwM2mSettings.fwUpdateRecourse) ?
181 206 this.configurationValue.clientLwM2mSettings.fwUpdateRecourse : '';
182   - let swResource = this.configurationValue.clientLwM2mSettings.swUpdateStrategy === '2' &&
183   - isDefinedAndNotNull(this.configurationValue.clientLwM2mSettings.fwUpdateRecourse) ?
  207 + const swResource = isDefinedAndNotNull(this.configurationValue.clientLwM2mSettings.fwUpdateRecourse) ?
184 208 this.configurationValue.clientLwM2mSettings.swUpdateRecourse : '';
185 209 this.lwm2mDeviceProfileFormGroup.patchValue({
186 210 objectIds: value,
... ... @@ -193,16 +217,16 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
193 217 bootstrapServer: this.configurationValue.bootstrap.bootstrapServer,
194 218 lwm2mServer: this.configurationValue.bootstrap.lwm2mServer,
195 219 clientStrategy: this.configurationValue.clientLwM2mSettings.clientStrategy,
196   - fwUpdateStrategy: this.configurationValue.clientLwM2mSettings.fwUpdateStrategy,
197   - swUpdateStrategy: this.configurationValue.clientLwM2mSettings.swUpdateStrategy,
  220 + fwUpdateStrategy: this.configurationValue.clientLwM2mSettings.fwUpdateStrategy || 1,
  221 + swUpdateStrategy: this.configurationValue.clientLwM2mSettings.swUpdateStrategy || 1,
198 222 fwUpdateRecourse: fwResource,
199 223 swUpdateRecourse: swResource
200 224 },
201 225 {emitEvent: false});
202 226 this.configurationValue.clientLwM2mSettings.fwUpdateRecourse = fwResource;
203 227 this.configurationValue.clientLwM2mSettings.swUpdateRecourse = swResource;
204   - this.isFwUpdateStrategy = this.configurationValue.clientLwM2mSettings.fwUpdateStrategy === '2';
205   - this.isSwUpdateStrategy = this.configurationValue.clientLwM2mSettings.swUpdateStrategy === '2';
  228 + this.isFwUpdateStrategy = this.configurationValue.clientLwM2mSettings.fwUpdateStrategy === 2;
  229 + this.isSwUpdateStrategy = this.configurationValue.clientLwM2mSettings.swUpdateStrategy === 2;
206 230 this.otaUpdateSwStrategyValidate();
207 231 this.otaUpdateFwStrategyValidate();
208 232 }
... ... @@ -239,8 +263,8 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
239 263 this.configurationValue.clientLwM2mSettings.fwUpdateRecourse = config.fwUpdateRecourse;
240 264 this.configurationValue.clientLwM2mSettings.swUpdateRecourse = config.swUpdateRecourse;
241 265 this.upDateJsonAllConfig();
242   - this.updateModel();
243 266 }
  267 + this.updateModel();
244 268 }
245 269
246 270 private getObserveAttrTelemetryObjects = (objectList: ObjectLwM2M[]): object => {
... ... @@ -513,53 +537,22 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
513 537 });
514 538 }
515 539
516   - changeFwUpdateStrategy($event: MatSelectChange) {
517   - if ($event.value === '2') {
518   - this.isFwUpdateStrategy = true;
519   - if (isEmpty(this.lwm2mDeviceProfileFormGroup.get('fwUpdateRecourse').value)) {
520   - this.lwm2mDeviceProfileFormGroup.patchValue({
521   - fwUpdateRecourse: DEFAULT_FW_UPDATE_RESOURCE
522   - },
523   - {emitEvent: false});
524   - }
525   - } else {
526   - this.isFwUpdateStrategy = false;
527   - }
528   - this.otaUpdateFwStrategyValidate();
529   - }
530   -
531   - changeSwUpdateStrategy($event: MatSelectChange) {
532   - if ($event.value === '2') {
533   - this.isSwUpdateStrategy = true;
534   - if (isEmpty(this.lwm2mDeviceProfileFormGroup.get('swUpdateRecourse').value)) {
535   - this.lwm2mDeviceProfileFormGroup.patchValue({
536   - swUpdateRecourse: DEFAULT_SW_UPDATE_RESOURCE
537   - },
538   - {emitEvent: false});
539   -
540   - }
541   - } else {
542   - this.isSwUpdateStrategy = false;
543   - }
544   - this.otaUpdateSwStrategyValidate();
545   - }
546   -
547   - private otaUpdateFwStrategyValidate(): void {
  540 + private otaUpdateFwStrategyValidate(updated = false): void {
548 541 if (this.isFwUpdateStrategy) {
549 542 this.lwm2mDeviceProfileFormGroup.get('fwUpdateRecourse').setValidators([Validators.required]);
550 543 } else {
551 544 this.lwm2mDeviceProfileFormGroup.get('fwUpdateRecourse').clearValidators();
552 545 }
553   - this.lwm2mDeviceProfileFormGroup.get('fwUpdateRecourse').updateValueAndValidity();
  546 + this.lwm2mDeviceProfileFormGroup.get('fwUpdateRecourse').updateValueAndValidity({emitEvent: updated});
554 547 }
555 548
556   - private otaUpdateSwStrategyValidate(): void {
  549 + private otaUpdateSwStrategyValidate(updated = false): void {
557 550 if (this.isSwUpdateStrategy) {
558 551 this.lwm2mDeviceProfileFormGroup.get('swUpdateRecourse').setValidators([Validators.required]);
559 552 } else {
560 553 this.lwm2mDeviceProfileFormGroup.get('swUpdateRecourse').clearValidators();
561 554 }
562   - this.lwm2mDeviceProfileFormGroup.get('swUpdateRecourse').updateValueAndValidity();
  555 + this.lwm2mDeviceProfileFormGroup.get('swUpdateRecourse').updateValueAndValidity({emitEvent: updated});
563 556 }
564 557
565 558 }
... ...
... ... @@ -20,7 +20,6 @@ export const INSTANCE = 'instance';
20 20 export const RESOURCES = 'resources';
21 21 export const ATTRIBUTE_LWM2M = 'attributeLwm2m';
22 22 export const CLIENT_LWM2M = 'clientLwM2M';
23   -export const CLIENT_LWM2M_SETTINGS = 'clientLwM2mSettings';
24 23 export const OBSERVE_ATTR_TELEMETRY = 'observeAttrTelemetry';
25 24 export const OBSERVE = 'observe';
26 25 export const ATTRIBUTE = 'attribute';
... ... @@ -43,9 +42,9 @@ export const KEY_REGEXP_HEX_DEC = /^[-+]?[0-9A-Fa-f]+\.?[0-9A-Fa-f]*?$/;
43 42 export const KEY_REGEXP_NUMBER = /^(\-?|\+?)\d*$/;
44 43 export const INSTANCES_ID_VALUE_MIN = 0;
45 44 export const INSTANCES_ID_VALUE_MAX = 65535;
46   -export const DEFAULT_OTA_UPDATE_PROTOCOL = "coap://";
47   -export const DEFAULT_FW_UPDATE_RESOURCE = DEFAULT_OTA_UPDATE_PROTOCOL + DEFAULT_LOCAL_HOST_NAME + ":"+ DEFAULT_PORT_SERVER_NO_SEC;
48   -export const DEFAULT_SW_UPDATE_RESOURCE = DEFAULT_OTA_UPDATE_PROTOCOL + DEFAULT_LOCAL_HOST_NAME + ":"+ DEFAULT_PORT_SERVER_NO_SEC;
  45 +export const DEFAULT_OTA_UPDATE_PROTOCOL = 'coap://';
  46 +export const DEFAULT_FW_UPDATE_RESOURCE = DEFAULT_OTA_UPDATE_PROTOCOL + DEFAULT_LOCAL_HOST_NAME + ':' + DEFAULT_PORT_SERVER_NO_SEC;
  47 +export const DEFAULT_SW_UPDATE_RESOURCE = DEFAULT_OTA_UPDATE_PROTOCOL + DEFAULT_LOCAL_HOST_NAME + ':' + DEFAULT_PORT_SERVER_NO_SEC;
49 48
50 49
51 50 export enum BINDING_MODE {
... ... @@ -113,19 +112,19 @@ export const ATTRIBUTE_LWM2M_MAP = new Map<ATTRIBUTE_LWM2M_ENUM, string>(
113 112
114 113 export const ATTRIBUTE_KEYS = Object.keys(ATTRIBUTE_LWM2M_ENUM) as string[];
115 114
116   -export enum SECURITY_CONFIG_MODE {
  115 +export enum securityConfigMode {
117 116 PSK = 'PSK',
118 117 RPK = 'RPK',
119 118 X509 = 'X509',
120 119 NO_SEC = 'NO_SEC'
121 120 }
122 121
123   -export const SECURITY_CONFIG_MODE_NAMES = new Map<SECURITY_CONFIG_MODE, string>(
  122 +export const securityConfigModeNames = new Map<securityConfigMode, string>(
124 123 [
125   - [SECURITY_CONFIG_MODE.PSK, 'Pre-Shared Key'],
126   - [SECURITY_CONFIG_MODE.RPK, 'Raw Public Key'],
127   - [SECURITY_CONFIG_MODE.X509, 'X.509 Certificate'],
128   - [SECURITY_CONFIG_MODE.NO_SEC, 'No Security']
  124 + [securityConfigMode.PSK, 'Pre-Shared Key'],
  125 + [securityConfigMode.RPK, 'Raw Public Key'],
  126 + [securityConfigMode.X509, 'X.509 Certificate'],
  127 + [securityConfigMode.NO_SEC, 'No Security']
129 128 ]
130 129 );
131 130
... ... @@ -144,9 +143,10 @@ export interface BootstrapServersSecurityConfig {
144 143
145 144 export interface ServerSecurityConfig {
146 145 host?: string;
  146 + securityHost?: string;
147 147 port?: number;
148   - bootstrapServerIs?: boolean;
149   - securityMode: string;
  148 + securityPort?: number;
  149 + securityMode: securityConfigMode;
150 150 clientPublicKeyOrId?: string;
151 151 clientSecretKey?: string;
152 152 serverPublicKey?: string;
... ... @@ -169,8 +169,8 @@ export interface Lwm2mProfileConfigModels {
169 169
170 170 export interface ClientLwM2mSettings {
171 171 clientStrategy: string;
172   - fwUpdateStrategy: string;
173   - swUpdateStrategy: string;
  172 + fwUpdateStrategy: number;
  173 + swUpdateStrategy: number;
174 174 fwUpdateRecourse: string;
175 175 swUpdateRecourse: string;
176 176 }
... ... @@ -193,12 +193,11 @@ export function getDefaultBootstrapServersSecurityConfig(): BootstrapServersSecu
193 193 };
194 194 }
195 195
196   -export function getDefaultBootstrapServerSecurityConfig(hostname: any): ServerSecurityConfig {
  196 +export function getDefaultBootstrapServerSecurityConfig(hostname: string): ServerSecurityConfig {
197 197 return {
198 198 host: hostname,
199 199 port: DEFAULT_PORT_BOOTSTRAP_NO_SEC,
200   - bootstrapServerIs: true,
201   - securityMode: SECURITY_CONFIG_MODE.NO_SEC.toString(),
  200 + securityMode: securityConfigMode.NO_SEC,
202 201 serverPublicKey: '',
203 202 clientHoldOffTime: DEFAULT_CLIENT_HOLD_OFF_TIME,
204 203 serverId: DEFAULT_ID_BOOTSTRAP,
... ... @@ -208,7 +207,6 @@ export function getDefaultBootstrapServerSecurityConfig(hostname: any): ServerSe
208 207
209 208 export function getDefaultLwM2MServerSecurityConfig(hostname): ServerSecurityConfig {
210 209 const DefaultLwM2MServerSecurityConfig = getDefaultBootstrapServerSecurityConfig(hostname);
211   - DefaultLwM2MServerSecurityConfig.bootstrapServerIs = false;
212 210 DefaultLwM2MServerSecurityConfig.port = DEFAULT_PORT_SERVER_NO_SEC;
213 211 DefaultLwM2MServerSecurityConfig.serverId = DEFAULT_ID_SERVER;
214 212 return DefaultLwM2MServerSecurityConfig;
... ... @@ -242,9 +240,9 @@ export function getDefaultProfileConfig(hostname?: any): Lwm2mProfileConfigModel
242 240
243 241 function getDefaultProfileClientLwM2mSettingsConfig(): ClientLwM2mSettings {
244 242 return {
245   - clientStrategy: "1",
246   - fwUpdateStrategy: "1",
247   - swUpdateStrategy: "1",
  243 + clientStrategy: '1',
  244 + fwUpdateStrategy: 1,
  245 + swUpdateStrategy: 1,
248 246 fwUpdateRecourse: DEFAULT_FW_UPDATE_RESOURCE,
249 247 swUpdateRecourse: DEFAULT_SW_UPDATE_RESOURCE
250 248 };
... ...
... ... @@ -772,7 +772,7 @@ function drawDigitalLabel(context: DigitalGaugeCanvasRenderingContext2D, options
772 772 context.textAlign = 'center';
773 773 context.font = Drawings.font(options, 'Label', fontSizeFactor);
774 774 context.lineWidth = 0;
775   - drawText(context, options, 'Label', options.label.toUpperCase(), textX, textY);
  775 + drawText(context, options, 'Label', options.label, textX, textY);
776 776 }
777 777
778 778 function drawDigitalMinMax(context: DigitalGaugeCanvasRenderingContext2D, options: CanvasDigitalGaugeOptions) {
... ...
... ... @@ -48,20 +48,6 @@
48 48 <mat-label translate>device.label</mat-label>
49 49 <input matInput formControlName="label">
50 50 </mat-form-field>
51   - <mat-form-field class="mat-block" style="padding-bottom: 14px;">
52   - <mat-label translate>device-profile.transport-type</mat-label>
53   - <mat-select formControlName="transportType" required>
54   - <mat-option *ngFor="let type of deviceTransportTypes" [value]="type">
55   - {{deviceTransportTypeTranslations.get(type) | translate}}
56   - </mat-option>
57   - </mat-select>
58   - <mat-hint *ngIf="deviceWizardFormGroup.get('transportType').value">
59   - {{deviceTransportTypeHints.get(deviceWizardFormGroup.get('transportType').value) | translate}}
60   - </mat-hint>
61   - <mat-error *ngIf="deviceWizardFormGroup.get('transportType').hasError('required')">
62   - {{ 'device-profile.transport-type-required' | translate }}
63   - </mat-error>
64   - </mat-form-field>
65 51 <div fxLayout="row" fxLayoutGap="16px">
66 52 <mat-radio-group fxLayout="column" formControlName="addProfileType" fxLayoutAlign="space-around">
67 53 <mat-radio-button [value]="0" color="primary">
... ... @@ -74,13 +60,10 @@
74 60 <div fxLayout="column">
75 61 <tb-device-profile-autocomplete
76 62 [required]="!createProfile"
77   - [transportType]="deviceWizardFormGroup.get('transportType').value"
78 63 formControlName="deviceProfileId"
79 64 [ngClass]="{invisible: deviceWizardFormGroup.get('addProfileType').value !== 0}"
80   - (deviceProfileChanged)="$event?.transportType ? deviceWizardFormGroup.get('transportType').patchValue($event?.transportType) : {}"
81 65 [addNewProfile]="false"
82 66 [selectDefaultProfile]="true"
83   - [selectFirstProfile]="true"
84 67 [editProfileEnabled]="false">
85 68 </tb-device-profile-autocomplete>
86 69 <mat-form-field fxFlex class="mat-block"
... ... @@ -124,9 +107,22 @@
124 107 </fieldset>
125 108 </form>
126 109 </mat-step>
127   - <mat-step [stepControl]="transportConfigFormGroup" *ngIf="createTransportConfiguration">
  110 + <mat-step [stepControl]="transportConfigFormGroup" [optional]="true" *ngIf="createProfile">
128 111 <form [formGroup]="transportConfigFormGroup" style="padding-bottom: 16px;">
129   - <ng-template matStepLabel>{{ 'device-profile.transport-configuration' | translate }}</ng-template>
  112 + <ng-template matStepLabel>{{ 'device-profile.transport-configuration' | translate }}</ng-template><mat-form-field class="mat-block" style="padding-bottom: 14px;">
  113 + <mat-label translate>device-profile.transport-type</mat-label>
  114 + <mat-select formControlName="transportType" required>
  115 + <mat-option *ngFor="let type of deviceTransportTypes" [value]="type">
  116 + {{deviceTransportTypeTranslations.get(type) | translate}}
  117 + </mat-option>
  118 + </mat-select>
  119 + <mat-hint *ngIf="transportConfigFormGroup.get('transportType').value">
  120 + {{deviceTransportTypeHints.get(transportConfigFormGroup.get('transportType').value) | translate}}
  121 + </mat-hint>
  122 + <mat-error *ngIf="transportConfigFormGroup.get('transportType').hasError('required')">
  123 + {{ 'device-profile.transport-type-required' | translate }}
  124 + </mat-error>
  125 + </mat-form-field>
130 126 <tb-device-profile-transport-configuration
131 127 formControlName="transportConfiguration"
132 128 required>
... ...
... ... @@ -29,7 +29,6 @@ import {
29 29 DeviceProvisionConfiguration,
30 30 DeviceProvisionType,
31 31 DeviceTransportType,
32   - deviceTransportTypeConfigurationInfoMap,
33 32 deviceTransportTypeHintMap,
34 33 deviceTransportTypeTranslationMap
35 34 } from '@shared/models/device.models';
... ... @@ -66,7 +65,6 @@ export class DeviceWizardDialogComponent extends
66 65 showNext = true;
67 66
68 67 createProfile = false;
69   - createTransportConfiguration = false;
70 68
71 69 entityType = EntityType;
72 70
... ... @@ -88,7 +86,7 @@ export class DeviceWizardDialogComponent extends
88 86
89 87 customerFormGroup: FormGroup;
90 88
91   - labelPosition = 'end';
  89 + labelPosition: MatHorizontalStepper['labelPosition'] = 'end';
92 90
93 91 serviceType = ServiceType.TB_RULE_ENGINE;
94 92
... ... @@ -109,7 +107,6 @@ export class DeviceWizardDialogComponent extends
109 107 label: [''],
110 108 gateway: [false],
111 109 overwriteActivityTime: [false],
112   - transportType: [DeviceTransportType.DEFAULT, Validators.required],
113 110 addProfileType: [0],
114 111 deviceProfileId: [null, Validators.required],
115 112 newDeviceProfileTitle: [{value: null, disabled: true}],
... ... @@ -130,7 +127,6 @@ export class DeviceWizardDialogComponent extends
130 127 this.deviceWizardFormGroup.get('defaultQueueName').disable();
131 128 this.deviceWizardFormGroup.updateValueAndValidity();
132 129 this.createProfile = false;
133   - this.createTransportConfiguration = false;
134 130 } else {
135 131 this.deviceWizardFormGroup.get('deviceProfileId').setValidators(null);
136 132 this.deviceWizardFormGroup.get('deviceProfileId').disable();
... ... @@ -141,18 +137,18 @@ export class DeviceWizardDialogComponent extends
141 137
142 138 this.deviceWizardFormGroup.updateValueAndValidity();
143 139 this.createProfile = true;
144   - this.createTransportConfiguration = this.deviceWizardFormGroup.get('transportType').value &&
145   - deviceTransportTypeConfigurationInfoMap.get(this.deviceWizardFormGroup.get('transportType').value).hasProfileConfiguration;
146 140 }
147 141 }
148 142 ));
149 143
150 144 this.transportConfigFormGroup = this.fb.group(
151 145 {
  146 + transportType: [DeviceTransportType.DEFAULT, Validators.required],
152 147 transportConfiguration: [createDeviceProfileTransportConfiguration(DeviceTransportType.DEFAULT), Validators.required]
153 148 }
154 149 );
155   - this.subscriptions.push(this.deviceWizardFormGroup.get('transportType').valueChanges.subscribe((transportType) => {
  150 +
  151 + this.subscriptions.push(this.transportConfigFormGroup.get('transportType').valueChanges.subscribe((transportType) => {
156 152 this.deviceProfileTransportTypeChanged(transportType);
157 153 }));
158 154
... ... @@ -229,8 +225,6 @@ export class DeviceWizardDialogComponent extends
229 225 if (index > 0) {
230 226 if (!this.createProfile) {
231 227 index += 3;
232   - } else if (!this.createTransportConfiguration) {
233   - index += 1;
234 228 }
235 229 }
236 230 switch (index) {
... ... @@ -256,8 +250,6 @@ export class DeviceWizardDialogComponent extends
256 250 private deviceProfileTransportTypeChanged(deviceTransportType: DeviceTransportType): void {
257 251 this.transportConfigFormGroup.patchValue(
258 252 {transportConfiguration: createDeviceProfileTransportConfiguration(deviceTransportType)});
259   - this.createTransportConfiguration = this.createProfile && deviceTransportType &&
260   - deviceTransportTypeConfigurationInfoMap.get(deviceTransportType).hasProfileConfiguration;
261 253 }
262 254
263 255 add(): void {
... ... @@ -281,7 +273,7 @@ export class DeviceWizardDialogComponent extends
281 273 const deviceProfile: DeviceProfile = {
282 274 name: this.deviceWizardFormGroup.get('newDeviceProfileTitle').value,
283 275 type: DeviceProfileType.DEFAULT,
284   - transportType: this.deviceWizardFormGroup.get('transportType').value,
  276 + transportType: this.transportConfigFormGroup.get('transportType').value,
285 277 provisionType: deviceProvisionConfiguration.type,
286 278 provisionDeviceKey,
287 279 profileData: {
... ...
... ... @@ -90,23 +90,26 @@
90 90 <mat-form-field fxFlex class="mat-block">
91 91 <mat-label translate>admin.oauth2.redirect-uri-template</mat-label>
92 92 <input matInput [value]="redirectURI(domainInfo)" readonly>
93   - <button mat-icon-button color="primary" matSuffix type="button"
94   - ngxClipboard cbContent="{{ redirectURI(domainInfo) }}"
95   - matTooltip="{{ 'admin.oauth2.copy-redirect-uri' | translate }}"
96   - matTooltipPosition="above">
97   - <mat-icon class="material-icons" svgIcon="mdi:clipboard-arrow-left"></mat-icon>
98   - </button>
  93 + <tb-copy-button
  94 + matSuffix
  95 + color="primary"
  96 + [copyText]="redirectURI(domainInfo)"
  97 + tooltipText="{{ 'admin.oauth2.copy-redirect-uri' | translate }}"
  98 + tooltipPosition="above"
  99 + mdiIcon="mdi:clipboard-arrow-left">
  100 + </tb-copy-button>
99 101 </mat-form-field>
100   -
101 102 <mat-form-field fxFlex *ngIf="domainInfo.get('scheme').value === 'MIXED'" class="mat-block">
102 103 <mat-label></mat-label>
103 104 <input matInput [value]="redirectURIMixed(domainInfo)" readonly>
104   - <button mat-icon-button color="primary" matSuffix type="button"
105   - ngxClipboard cbContent="{{ redirectURIMixed(domainInfo) }}"
106   - matTooltip="{{ 'admin.oauth2.copy-redirect-uri' | translate }}"
107   - matTooltipPosition="above">
108   - <mat-icon class="material-icons" svgIcon="mdi:clipboard-arrow-left"></mat-icon>
109   - </button>
  105 + <tb-copy-button
  106 + matSuffix
  107 + color="primary"
  108 + [copyText]="redirectURIMixed(domainInfo)"
  109 + tooltipText="{{ 'admin.oauth2.copy-redirect-uri' | translate }}"
  110 + tooltipPosition="above"
  111 + mdiIcon="mdi:clipboard-arrow-left">
  112 + </tb-copy-button>
110 113 </mat-form-field>
111 114 </div>
112 115 </div>
... ... @@ -144,18 +147,32 @@
144 147 <div [formGroupName]="n" fxLayout="row" fxLayoutGap="8px">
145 148 <div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap="8px">
146 149 <div fxFlex fxLayout="column">
147   - <mat-form-field fxFlex class="mat-block">
  150 + <mat-form-field fxFlex class="mat-block" floatLabel="always">
148 151 <mat-label translate>admin.oauth2.mobile-package</mat-label>
149   - <input matInput formControlName="pkgName" required>
  152 + <input matInput formControlName="pkgName" placeholder="{{ 'admin.oauth2.mobile-package-placeholder' | translate }}" required>
  153 + <mat-hint translate>admin.oauth2.mobile-package-hint</mat-hint>
150 154 </mat-form-field>
151 155 <mat-error *ngIf="mobileInfo.hasError('unique')">
152 156 {{ 'admin.oauth2.mobile-package-unique' | translate }}
153 157 </mat-error>
154 158 </div>
155   - <mat-form-field fxFlex class="mat-block">
156   - <mat-label translate>admin.oauth2.mobile-callback-url-scheme</mat-label>
157   - <input matInput formControlName="callbackUrlScheme" required>
158   - </mat-form-field>
  159 + <div fxFlex fxLayout="row">
  160 + <mat-form-field fxFlex class="mat-block">
  161 + <mat-label translate>admin.oauth2.mobile-app-secret</mat-label>
  162 + <textarea matInput formControlName="appSecret" rows="1" required></textarea>
  163 + <tb-copy-button
  164 + matSuffix
  165 + color="primary"
  166 + [copyText]="mobileInfo.get('appSecret').value"
  167 + tooltipText="{{ 'admin.oauth2.copy-mobile-app-secret' | translate }}"
  168 + tooltipPosition="above"
  169 + mdiIcon="mdi:clipboard-arrow-left">
  170 + </tb-copy-button>
  171 + <mat-error *ngIf="mobileInfo.get('appSecret').invalid">
  172 + {{ 'admin.oauth2.invalid-mobile-app-secret' | translate }}
  173 + </mat-error>
  174 + </mat-form-field>
  175 + </div>
159 176 </div>
160 177 <div fxLayout="column" fxLayoutAlign="center start">
161 178 <button type="button" mat-icon-button color="primary"
... ...
... ... @@ -42,7 +42,7 @@ import { WINDOW } from '@core/services/window.service';
42 42 import { forkJoin, Subscription } from 'rxjs';
43 43 import { DialogService } from '@core/services/dialog.service';
44 44 import { TranslateService } from '@ngx-translate/core';
45   -import { isDefined, isDefinedAndNotNull } from '@core/utils';
  45 +import { isDefined, isDefinedAndNotNull, randomAlphanumeric } from '@core/utils';
46 46 import { OAuth2Service } from '@core/http/oauth2.service';
47 47 import { ActivatedRoute } from '@angular/router';
48 48
... ... @@ -275,7 +275,8 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
275 275 private buildMobileInfoForm(mobileInfo?: OAuth2MobileInfo): FormGroup {
276 276 return this.fb.group({
277 277 pkgName: [mobileInfo?.pkgName, [Validators.required]],
278   - callbackUrlScheme: [mobileInfo?.callbackUrlScheme, [Validators.required]],
  278 + appSecret: [mobileInfo?.appSecret, [Validators.required, Validators.minLength(16), Validators.maxLength(2048),
  279 + Validators.pattern(/^[A-Za-z0-9]+$/)]],
279 280 }, {validators: this.uniquePkgNameValidator});
280 281 }
281 282
... ... @@ -529,7 +530,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
529 530 addMobileInfo(control: AbstractControl): void {
530 531 this.mobileInfos(control).push(this.buildMobileInfoForm({
531 532 pkgName: '',
532   - callbackUrlScheme: ''
  533 + appSecret: randomAlphanumeric(24)
533 534 }));
534 535 }
535 536
... ...
... ... @@ -16,11 +16,16 @@
16 16
17 17 -->
18 18 <button mat-icon-button
  19 + type="button"
  20 + [color]="color"
19 21 [disabled]="disabled"
20 22 [matTooltip]="matTooltipText"
21 23 [matTooltipPosition]="matTooltipPosition"
22 24 (click)="copy($event)">
23   - <mat-icon [svgIcon]="mdiIconSymbol" [ngStyle]="style" [ngClass]="{'copied': copied}">
24   - {{ iconSymbol }}
  25 + <mat-icon [svgIcon]="mdiIcon" [ngStyle]="style" *ngIf="!copied; else copiedTemplate">
  26 + {{ icon }}
25 27 </mat-icon>
  28 + <ng-template #copiedTemplate>
  29 + <mat-icon [ngStyle]="style" class="copied">done</mat-icon>
  30 + </ng-template>
26 31 </button>
... ...
... ... @@ -26,7 +26,6 @@ import { TranslateService } from '@ngx-translate/core';
26 26 })
27 27 export class CopyButtonComponent {
28 28
29   - private copedIcon = '';
30 29 private timer;
31 30
32 31 copied = false;
... ... @@ -52,6 +51,9 @@ export class CopyButtonComponent {
52 51 @Input()
53 52 style: {[key: string]: any} = {};
54 53
  54 + @Input()
  55 + color: string;
  56 +
55 57 @Output()
56 58 successCopied = new EventEmitter<string>();
57 59
... ... @@ -67,23 +69,13 @@ export class CopyButtonComponent {
67 69 }
68 70 this.clipboardService.copy(this.copyText);
69 71 this.successCopied.emit(this.copyText);
70   - this.copedIcon = 'done';
71 72 this.copied = true;
72 73 this.timer = setTimeout(() => {
73   - this.copedIcon = null;
74 74 this.copied = false;
75 75 this.cd.detectChanges();
76 76 }, 1500);
77 77 }
78 78
79   - get iconSymbol(): string {
80   - return this.copedIcon || this.icon;
81   - }
82   -
83   - get mdiIconSymbol(): string {
84   - return this.copedIcon ? '' : this.mdiIcon;
85   - }
86   -
87 79 get matTooltipText(): string {
88 80 return this.copied ? this.translate.instant('ota-update.copied') : this.tooltipText;
89 81 }
... ...
... ... @@ -34,7 +34,7 @@ export interface OAuth2DomainInfo {
34 34
35 35 export interface OAuth2MobileInfo {
36 36 pkgName: string;
37   - callbackUrlScheme: string;
  37 + appSecret: string;
38 38 }
39 39
40 40 export enum DomainSchema{
... ...
... ... @@ -819,12 +819,14 @@ export function updateDatasourceFromEntityInfo(datasource: Datasource, entity: E
819 819 datasource.entityId = entity.id;
820 820 datasource.entityType = entity.entityType;
821 821 if (datasource.type === DatasourceType.entity || datasource.type === DatasourceType.entityCount) {
822   - datasource.entityName = entity.name;
823   - datasource.entityLabel = entity.label;
824   - datasource.name = entity.name;
825   - datasource.entityDescription = entity.entityDescription;
826   - datasource.entity.label = entity.label;
827   - datasource.entity.name = entity.name;
  822 + if (datasource.type === DatasourceType.entity) {
  823 + datasource.entityName = entity.name;
  824 + datasource.entityLabel = entity.label;
  825 + datasource.name = entity.name;
  826 + datasource.entityDescription = entity.entityDescription;
  827 + datasource.entity.label = entity.label;
  828 + datasource.entity.name = entity.name;
  829 + }
828 830 if (createFilter) {
829 831 datasource.entityFilter = {
830 832 type: AliasFilterType.singleEntity,
... ...
... ... @@ -224,8 +224,12 @@
224 224 "mobile-apps": "Mobile applications",
225 225 "no-mobile-apps": "No applications configured",
226 226 "mobile-package": "Application package",
  227 + "mobile-package-placeholder": "Ex.: my.example.app",
  228 + "mobile-package-hint": "For Android: your own unique Application ID. For iOS: Product bundle identifier.",
227 229 "mobile-package-unique": "Application package must be unique.",
228   - "mobile-callback-url-scheme": "Callback URL scheme",
  230 + "mobile-app-secret": "Application secret",
  231 + "invalid-mobile-app-secret": "Application secret must contain only alphanumeric characters and must be between 16 and 2048 characters long.",
  232 + "copy-mobile-app-secret": "Copy application secret",
229 233 "add-mobile-app": "Add application",
230 234 "delete-mobile-app": "Delete application info",
231 235 "providers": "Providers",
... ... @@ -1248,7 +1252,7 @@
1248 1252 "pattern_hex_dec": "{ count, plural, 0 {must be hex decimal format} other {must be # characters} }",
1249 1253 "servers": "Servers",
1250 1254 "short-id": "Short ID",
1251   - "short-id-tip": "Short Server ID",
  1255 + "short-id-required": "Short ID is required.",
1252 1256 "lifetime": "Lifetime of the registration for this LwM2M client",
1253 1257 "default-min-period": "Minimum Period between two notifications (sec)",
1254 1258 "notif-if-disabled": "Notification Storing When Disabled or Offline",
... ... @@ -1257,28 +1261,37 @@
1257 1261 "bootstrap-server": "Bootstrap Server",
1258 1262 "lwm2m-server": "LwM2M Server",
1259 1263 "server-host": "Host",
1260   - "server-host-tip": "Server Host",
  1264 + "server-host-required": "Host is required.",
1261 1265 "server-port": "Port",
1262   - "server-port-tip": "Server Port",
  1266 + "server-port-required": "Port is required.",
1263 1267 "server-public-key": "Server Public Key",
1264   - "server-public-key-tip": "Server Public Key only for X509, RPK",
  1268 + "server-public-key-required": "Server Public Key is required.",
  1269 + "server-public-key-pattern": "Server Public Key must be hex decimal format.",
  1270 + "server-public-key-length": "Server Public Key must be {{ count }} characters.",
1265 1271 "client-hold-off-time": "Hold Off Time",
1266   - "client-hold-off-time-tip": "Client Hold Off Time for use with a Bootstrap-Server only",
1267   - "bootstrap-server-account-timeout": "Account after the timeout",
1268   - "bootstrap-server-account-timeout-tip": "Bootstrap-Server Account after the timeout value given by this resource.",
1269   - "others-tab": "Other settings...",
  1272 + "client-hold-off-time-required": "Hold Off Time is required.",
  1273 + "client-hold-off-time-tooltip": "Client Hold Off Time for use with a Bootstrap-Server only",
  1274 + "account-after-timeout": "Account after the timeout",
  1275 + "account-after-timeout-required": "Account after the timeout is required.",
  1276 + "account-after-timeout-tooltip": "Bootstrap-Server Account after the timeout value given by this resource.",
  1277 + "others-tab": "Other settings",
1270 1278 "client-strategy": "Client strategy when connecting",
1271 1279 "client-strategy-label": "Strategy",
1272 1280 "client-strategy-connect": "{ count, plural, 1 {1: Only Observe Request to the client after the initial connection} other {2: Read All Resources & Observe Request to the client after registration} }",
1273 1281 "client-strategy-tip": "{ count, plural, 1 {Strategy 1: After the initial connection of the LWM2M Client, the server sends Observe resources Request to the client, those resources that are marked as observation in the Device profile and which exist on the LWM2M client.} other {Strategy 2: After the registration, request the client to read all the resource values for all objects that the LWM2M client has,\n then execute: the server sends Observe resources Request to the client, those resources that are marked as observation in the Device profile and which exist on the LWM2M client.} }",
1274   - "ota-update-strategy": "Ota update strategy",
1275   - "fw-update-strategy-label": "Firmware update strategy",
1276   - "fw-update-strategy": "{ count, plural, 1 {Push firmware update as binary file using Object 5 and Resource 0 (Package).} 2 {Auto-generate unique CoAP URL to download the package and push firmware update as Object 5 and Resource 1 (Package URI).} other {Push firmware update as binary file using Object 19 and Resource 0 (Data).} }",
1277   - "sw-update-strategy-label": "Software update strategy",
1278   - "sw-update-strategy": "{ count, plural, 1 {Push binary file using Object 9 and Resource 2 (Package).} other {Auto-generate unique CoAP URL to download the package and push software update using Object 9 and Resource 3 (Package URI).} }",
1279   - "ota-update-recourse": "Ota update Coap recourse",
1280   - "fw-update-recourse": "Firmware update Coap recourse",
1281   - "sw-update-recourse": "Software update Coap recourse",
  1282 + "fw-update": "Firmware update",
  1283 + "fw-update-strategy": "Firmware update strategy",
  1284 + "fw-update-strategy-data": "Push firmware update as binary file using Object 19 and Resource 0 (Data)",
  1285 + "fw-update-strategy-package": "Push firmware update as binary file using Object 5 and Resource 0 (Package)",
  1286 + "fw-update-strategy-package-uri": "Auto-generate unique CoAP URL to download the package and push firmware update as Object 5 and Resource 1 (Package URI)",
  1287 + "sw-update": "Software update",
  1288 + "sw-update-strategy": "Software update strategy",
  1289 + "sw-update-strategy-package": "Push binary file using Object 9 and Resource 2 (Package)",
  1290 + "sw-update-strategy-package-uri": "Auto-generate unique CoAP URL to download the package and push software update using Object 9 and Resource 3 (Package URI)",
  1291 + "fw-update-recourse": "Firmware update CoAP recourse",
  1292 + "fw-update-recourse-required": "Firmware update CoAP recourse is required.",
  1293 + "sw-update-recourse": "Software update CoAP recourse",
  1294 + "sw-update-recourse-required": "Software update CoAP recourse is required.",
1282 1295 "config-json-tab": "Json Config Profile Device"
1283 1296 }
1284 1297 },
... ...
... ... @@ -76,6 +76,7 @@
76 76 "general": "基本设置",
77 77 "general-policy": "基本策略",
78 78 "general-settings": "基本设置",
  79 + "home-settings": "首页设置",
79 80 "mail-from": "邮件来自",
80 81 "mail-from-required": "邮件发件人必填。",
81 82 "max-failed-login-attempts": "登录失败之前的最大登录尝试次数",
... ... @@ -115,7 +116,7 @@
115 116 "customer-name-pattern": "客户名称模式",
116 117 "default-dashboard-name": "默认仪表板名称",
117 118 "delete-domain": "删除域",
118   - "delete-domain-text": "注意,确认后域和所有 provider data 将不可用。",
  119 + "delete-domain-text": "请注意:确认后域和所有 provider data 将不可用。",
119 120 "delete-domain-title": "确实要删除域 '{{domainName}}' 的设置吗?",
120 121 "delete-provider": "删除 Provider",
121 122 "delete-registration-text": "请注意:确认后 provider data 将不可用。",
... ... @@ -413,7 +414,7 @@
413 414 "assignedToCustomer": "分配客户",
414 415 "copyId": "复制资产ID",
415 416 "delete": "删除资产",
416   - "delete-asset-text": "小心!确认后资产及其所有相关数据将不可恢复。",
  417 + "delete-asset-text": "请注意:确认后资产及其所有相关数据将不可恢复。",
417 418 "delete-asset-title": "确定要删除资产 '{{assetName}}'?",
418 419 "delete-assets": "删除资产",
419 420 "delete-assets-action-title": "删除 { count, plural, 1 {# 个资产} other {# 个资产} }",
... ... @@ -589,10 +590,10 @@
589 590 "default-customer": "默认客户",
590 591 "default-customer-required": "为了调试租户级别上的仪表板,需要默认客户。",
591 592 "delete": "删除此客户",
592   - "delete-customer-text": "小心!确认后,客户及其所有相关数据将不可恢复。",
  593 + "delete-customer-text": "请注意:确认后,客户及其所有相关数据将不可恢复。",
593 594 "delete-customer-title": "您确定要删除客户'{{customerTitle}}'吗?",
594 595 "delete-customers-action-title": "删除 { count, plural, 1 {# 个客户} other {# 个客户} }",
595   - "delete-customers-text": "小心!确认后,所有选定的客户将被删除,所有相关数据将不可恢复。",
  596 + "delete-customers-text": "请注意:确认后,所有选定的客户将被删除,所有相关数据将不可恢复。",
596 597 "delete-customers-title": "您确定要删除 { count, plural, 1 {# 个客户} other {# 个客户} }吗?",
597 598 "description": "说明",
598 599 "details": "详情",
... ... @@ -647,6 +648,7 @@
647 648 "background-color": "背景颜色",
648 649 "background-image": "背景图片",
649 650 "background-size-mode": "背景大小模式",
  651 + "cannot-upload-file": "无法上传文件",
650 652 "close-toolbar": "关闭工具栏",
651 653 "columns-count": "列数",
652 654 "columns-count-required": "需要列数。",
... ... @@ -659,14 +661,15 @@
659 661 "dashboard-details": "仪表板详情",
660 662 "dashboard-file": "仪表板文件",
661 663 "dashboard-import-missing-aliases-title": "配置导入仪表板使用的别名",
  664 + "dashboard-logo-image": "仪表板 Logo 图片",
662 665 "dashboard-required": "仪表板必填。",
663 666 "dashboards": "仪表板库",
664 667 "delete": "删除仪表板",
665   - "delete-dashboard-text": "小心!确认后仪表板及其所有相关数据将不可恢复。",
  668 + "delete-dashboard-text": "请注意:确认后仪表板及其所有相关数据将不可恢复。",
666 669 "delete-dashboard-title": "您确定要删除仪表板 '{{dashboardTitle}}'吗?",
667 670 "delete-dashboards": "删除仪表板",
668 671 "delete-dashboards-action-title": "删除 { count, plural, 1 {# 个仪表板} other {# 个仪表板} }",
669   - "delete-dashboards-text": "小心!确认后所有选定的仪表板将被删除,所有相关数据将不可恢复。",
  672 + "delete-dashboards-text": "请注意:确认后所有选定的仪表板将被删除,所有相关数据将不可恢复。",
670 673 "delete-dashboards-title": "确定要删除 { count, plural, 1 {# 个仪表板} other {# 个仪表板} }吗?",
671 674 "delete-state": "删除仪表板状态",
672 675 "delete-state-text": "确定要删除仪表板状态 '{{stateName}}' 吗?",
... ... @@ -674,6 +677,7 @@
674 677 "description": "说明",
675 678 "details": "详情",
676 679 "display-dashboard-export": "显示导出",
  680 + "display-dashboard-logo": "在仪表板全屏模式下显示 Logo",
677 681 "display-dashboard-timewindow": "显示时间窗口",
678 682 "display-dashboards-selection": "显示仪表板选项",
679 683 "display-entities-selection": "显示实体选项",
... ... @@ -684,6 +688,8 @@
684 688 "export": "导出仪表板",
685 689 "export-failed-error": "无法导出仪表板: {{error}}",
686 690 "hide-details": "隐藏详情",
  691 + "home-dashboard": "首页仪表板",
  692 + "home-dashboard-hide-toolbar": "隐藏首页仪表板工具栏",
687 693 "horizontal-margin": "水平边距",
688 694 "horizontal-margin-required": "需要水平边距值。",
689 695 "import": "导入仪表板",
... ... @@ -736,6 +742,7 @@
736 742 "select-state": "选择目标状态",
737 743 "select-widget-subtitle": "可用的部件类型列表",
738 744 "select-widget-title": "选择部件",
  745 + "select-widget-value": "{{title}}: 选择部件",
739 746 "selected-dashboards": "已选择 { count, plural, 1 {# 个仪表盘} other {# 个仪表盘} }",
740 747 "selected-states": "已选择 { count, plural, 1 {# 个仪表板状态} other {# 个仪表板状态} }",
741 748 "set-background": "设置背景",
... ... @@ -803,6 +810,7 @@
803 810 },
804 811 "datasource": {
805 812 "add-datasource-prompt": "请添加数据源",
  813 + "label": "标签",
806 814 "name": "名称",
807 815 "type": "数据源类型"
808 816 },
... ... @@ -843,6 +851,11 @@
843 851 "attributes-topic-filter": "Attributes topic filter",
844 852 "attributes-topic-filter-required": "Attributes topic 筛选器必填。",
845 853 "clear-alarm-rule": "清除报警规则",
  854 + "coap-device-payload-type": "CoAP 设备消息 Payload",
  855 + "coap-device-type": "CoAP 设备类型",
  856 + "coap-device-type-default": "默认",
  857 + "coap-device-type-efento": "Efento NB-IoT",
  858 + "coap-device-type-required": "CoAP 设备类型必填。",
846 859 "condition": "条件",
847 860 "condition-duration": "条件持续时间",
848 861 "condition-duration-time-unit": "时间单位",
... ... @@ -867,17 +880,19 @@
867 880 "copyId": "复制设备配置 ID",
868 881 "create-alarm-pattern": "创建 <b>{{alarmType}}</b> 报警",
869 882 "create-alarm-rules": "创建报警规则",
  883 + "create-device-profile": "创建设备配置",
870 884 "create-new-device-profile": "创建一个新的!",
871 885 "default": "默认",
872 886 "default-rule-chain": "默认规则链",
873 887 "delete": "删除设备配置",
874   - "delete-device-profile-text": "注意,确认后设备配置和所有相关数据将不可恢复。",
  888 + "delete-device-profile-text": "请注意:确认后设备配置和所有相关数据将不可恢复。",
875 889 "delete-device-profile-title": "是否确实要删除设备配置 '{{deviceProfileName}}'?",
876 890 "delete-device-profiles-text": "请注意:确认后,所有选定的设备配置将被删除,所有相关数据将不可恢复。",
877 891 "delete-device-profiles-title": "确定要删除 { count, plural, 1 {# 个设备配置} other {# 个设备配置} }?",
878 892 "description": "说明",
879 893 "device-profile": "设备配置",
880 894 "device-profile-details": "设备配置详情",
  895 + "device-profile-file": "设备配置文件",
881 896 "device-profile-required": "设备配置必填",
882 897 "device-profiles": "设备配置",
883 898 "device-provisioning": "设备预配置",
... ... @@ -886,7 +901,11 @@
886 901 "edit-alarm-rule-condition": "编辑报警规则条件",
887 902 "edit-schedule": "编辑报警日程表",
888 903 "enter-alarm-rule-condition-prompt": "请添加报警规则条件",
  904 + "export": "导出设备配置",
  905 + "export-failed-error": "无法导出设备配置文件: {{error}}",
889 906 "idCopiedMessage": "设备配置 ID 已复制到剪贴板",
  907 + "import": "导入设备配置",
  908 + "invalid-device-profile-file-error": "无法导入设备配置文件:无效的设备配置文件数据结构。",
890 909 "mqtt-device-payload-type": "MQTT 设备 Payload",
891 910 "mqtt-device-payload-type-json": "JSON",
892 911 "mqtt-device-payload-type-proto": "Protobuf",
... ... @@ -920,6 +939,11 @@
920 939 "provision-strategy-created-new": "允许创建新设备",
921 940 "provision-strategy-disabled": "禁用",
922 941 "provision-strategy-required": "预配置策略必填。",
  942 + "rpc-request-proto-schema": "RPC 请求 proto schema",
  943 + "rpc-request-proto-schema-hint": "RPC 请求消息应始终包含字段:string method = 1; int32 requestId = 2; 和params = 3的任何数据类型。",
  944 + "rpc-request-proto-schema-required": "RPC 请求 proto schema 必填。",
  945 + "rpc-response-proto-schema": "RPC 响应 proto schema",
  946 + "rpc-response-proto-schema-required": "RPC 响应 proto schema 必填。",
923 947 "rpc-response-topic-filter": "RPC响应 Topic 筛选器",
924 948 "rpc-response-topic-filter-required": "RPC响应 Topic 筛选器必填。",
925 949 "schedule": "启用规则:",
... ... @@ -957,6 +981,7 @@
957 981 "telemetry-topic-filter-required": "遥测数据 topic 筛选器必填。",
958 982 "transport-configuration": "传输配置",
959 983 "transport-type": "传输方式",
  984 + "transport-type-coap-hint": "启用高级 CoAP 传输设置",
960 985 "transport-type-default": "默认",
961 986 "transport-type-default-hint": "支持基本MQTT、HTTP和CoAP传输",
962 987 "transport-type-lwm2m": "LWM2M",
... ... @@ -964,6 +989,7 @@
964 989 "transport-type-mqtt": "MQTT",
965 990 "transport-type-mqtt-hint": "启用高级MQTT传输设置",
966 991 "transport-type-required": "传输方式必填。",
  992 + "transport-type-snmp-hint": "指定 SNMP 传输配置",
967 993 "type": "配置类型",
968 994 "type-default": "默认",
969 995 "type-required": "配置类型必填。"
... ... @@ -1000,11 +1026,11 @@
1000 1026 "credentials": "凭据",
1001 1027 "credentials-type": "凭据类型",
1002 1028 "delete": "删除设备",
1003   - "delete-device-text": "小心!确认后设备及其所有相关数据将不可恢复。",
  1029 + "delete-device-text": "请注意:确认后设备及其所有相关数据将不可恢复。",
1004 1030 "delete-device-title": "您确定要删除设备的{{deviceName}}吗?",
1005 1031 "delete-devices": "删除设备",
1006 1032 "delete-devices-action-title": "删除 {count,plural,1 {# 个设备} other {# 个设备} }",
1007   - "delete-devices-text": "小心!确认后所有选定的设备将被删除,所有相关数据将不可恢复。",
  1033 + "delete-devices-text": "请注意:确认后所有选定的设备将被删除,所有相关数据将不可恢复。",
1008 1034 "delete-devices-title": "确定要删除{count,plural,1 {# 个设备} other {# 个设备} } 吗?",
1009 1035 "description": "说明",
1010 1036 "details": "详情",
... ... @@ -1051,6 +1077,7 @@
1051 1077 "no-devices-text": "找不到设备",
1052 1078 "no-key-matching": "'{{key}}' 没有找到。",
1053 1079 "no-keys-found": "找不到密钥。",
  1080 + "overwrite-activity-time": "覆盖已连接设备的活动时间",
1054 1081 "password": "密码",
1055 1082 "public": "公开",
1056 1083 "remove-alias": "删除设备别名",
... ... @@ -1139,7 +1166,7 @@
1139 1166 "created-time": "创建时间",
1140 1167 "date-limits": "日期限制",
1141 1168 "delete": "删除实体视图",
1142   - "delete-entity-view-text": "小心!确认后实体视图及其所有相关数据将不可恢复。",
  1169 + "delete-entity-view-text": "请注意:确认后实体视图及其所有相关数据将不可恢复。",
1143 1170 "delete-entity-view-title": "确定要删除实体视图 '{{entityViewName}}'?",
1144 1171 "delete-entity-views": "删除实体视图",
1145 1172 "delete-entity-views-action-title": "删除 { count, plural, 1 {# 个实体视图} other {# 个实体视图} }",
... ... @@ -1235,6 +1262,7 @@
1235 1262 "duplicate-alias-error": "别名 '{{alias}}' 重复。<br>同一仪表板别名必须唯一。",
1236 1263 "enter-entity-type": "输入实体类型",
1237 1264 "entities": "实体",
  1265 + "entities-count": "实体数量",
1238 1266 "entity": "实体",
1239 1267 "entity-alias": "实体别名",
1240 1268 "entity-label": "实体标签",
... ... @@ -1334,12 +1362,15 @@
1334 1362 "body": "整体",
1335 1363 "data": "数据",
1336 1364 "data-type": "数据类型",
  1365 + "entity-id": "实体ID",
1337 1366 "error": "错误",
1338 1367 "errors-occurred": "错误发生",
1339 1368 "event": "事件",
1340 1369 "event-time": "事件时间",
1341 1370 "event-type": "事件类型",
  1371 + "events-filter": "事件筛选器",
1342 1372 "failed": "失败",
  1373 + "has-error": "有错误",
1343 1374 "message-id": "消息ID",
1344 1375 "message-type": "消息类型",
1345 1376 "messages-processed": "消息处理",
... ... @@ -1552,6 +1583,7 @@
1552 1583 "key-name-required": "键名必填。",
1553 1584 "key-type": {
1554 1585 "attribute": "属性",
  1586 + "constant": "常量",
1555 1587 "entity-field": "实体",
1556 1588 "key-type": "键类型",
1557 1589 "timeseries": "Timeseries"
... ... @@ -1603,6 +1635,51 @@
1603 1635 "value-type": "值类型"
1604 1636 }
1605 1637 },
  1638 + "firmware": {
  1639 + "add": "添加固件",
  1640 + "checksum": "校验和",
  1641 + "checksum-algorithm": "校验和算法",
  1642 + "checksum-copied-message": "固件校验和已复制到剪贴板",
  1643 + "checksum-required": "校验和为必填项。",
  1644 + "content-type": "Content type",
  1645 + "copy-checksum": "复制校验和",
  1646 + "copyId": "复制固件ID",
  1647 + "delete": "删除固件",
  1648 + "delete-firmware-text": "请注意:确认后固件将无法恢复。",
  1649 + "delete-firmware-title": "确定要删除固件'{{firmwareTitle}}'?",
  1650 + "delete-firmwares-action-title": "Delete { count, plural, 1 {# 个固件} other {# 个固件} }",
  1651 + "delete-firmwares-text": "请注意:确认后所有选定的资源都将被删除。",
  1652 + "delete-firmwares-title": "确定要删除固件 { count, plural, 1 {# 个固件} other {# 个固件} }?",
  1653 + "description": "描述",
  1654 + "download": "下载固件",
  1655 + "drop-file": "拖拽固件文件或单击以选择要上传的文件。",
  1656 + "empty": "固件为空",
  1657 + "file-name": "文件名",
  1658 + "file-size": "文件大小",
  1659 + "file-size-bytes": "文件大小(字节)",
  1660 + "firmware": "固件",
  1661 + "firmware-details": "固件详情",
  1662 + "firmware-required": "固件为必选项。",
  1663 + "idCopiedMessage": "固件ID已复制到剪贴板",
  1664 + "no-firmware-matching": "找不到与 '{{entity}}' 匹配的固件。",
  1665 + "no-firmware-text": "没有找到固件",
  1666 + "no-software-matching": "找不到与 '{{entity}}' 匹配的软件。",
  1667 + "no-software-text": "没有找到软件",
  1668 + "search": "查找固件",
  1669 + "selected-firmware": "已选择{ count, plural, 1 {# 个固件} other {# 个固件}",
  1670 + "software": "软件",
  1671 + "software-required": "软件为必选项。",
  1672 + "title": "标题",
  1673 + "title-required": "标题为必填项。",
  1674 + "type": "固件类型",
  1675 + "types": {
  1676 + "firmware": "固件",
  1677 + "software": "软件"
  1678 + },
  1679 + "version": "版本号",
  1680 + "version-required": "版本号为必填项。",
  1681 + "warning-after-save-no-edit": "保存固件后,将无法更改标题和版本号。"
  1682 +},
1606 1683 "fullscreen": {
1607 1684 "exit": "退出全屏",
1608 1685 "expand": "展开到全屏",
... ... @@ -1907,6 +1984,30 @@
1907 1984 "to-relations": "向内的关联",
1908 1985 "type": "类型"
1909 1986 },
  1987 + "resource": {
  1988 + "add": "添加资源",
  1989 + "delete": "删除资源",
  1990 + "delete-resource-text": "请注意:确认后资源将不可恢复。",
  1991 + "delete-resource-title": "确定要删除资源 '{{resourceTitle}}' 吗?",
  1992 + "delete-resources-action-title": "删除 { count, plural, 1 {# 个资源} other {# 个资源} }",
  1993 + "delete-resources-text": "请注意:确认后所有选定的资源都将被删除。",
  1994 + "delete-resources-title": "确定要删除 { count, plural, 1 {# 个资源} other {# 个资源} }?",
  1995 + "drop-file": "拖拽资源文件或单击以选择要上传的文件。",
  1996 + "empty": "资源为空",
  1997 + "export": "导出资源",
  1998 + "no-resource-matching": "找不到与 '{{widgetsBundle}}' 匹配的资源。",
  1999 + "no-resource-text": "找不到资源",
  2000 + "open-widgets-bundle": "打开部件库",
  2001 + "resource": "资源",
  2002 + "resource-library-details": "资源库详情",
  2003 + "resource-type": "资源类型",
  2004 + "resources-library": "资源库",
  2005 + "search": "查找资源",
  2006 + "selected-resources": "已选择{ count, plural, 1 {# 个资源} other {# 个资源} }",
  2007 + "system": "系统",
  2008 + "title": "标题",
  2009 + "title-required": "标题是必填项。"
  2010 +},
1910 2011 "rulechain": {
1911 2012 "add": "添加规则链",
1912 2013 "add-rulechain-text": "添加新的规则链",
... ... @@ -2098,10 +2199,10 @@
2098 2199 "admins": "管理员",
2099 2200 "copyId": "复制租户ID",
2100 2201 "delete": "删除租户",
2101   - "delete-tenant-text": "小心!确认后,租户和所有相关数据将不可恢复。",
  2202 + "delete-tenant-text": "请注意:确认后,租户和所有相关数据将不可恢复。",
2102 2203 "delete-tenant-title": "您确定要删除租户'{{tenantTitle}}'吗?",
2103 2204 "delete-tenants-action-title": "删除 { count, plural, 1 {# 个租户} other {# 个租户} }",
2104   - "delete-tenants-text": "小心!确认后,所有选定的租户将被删除,所有相关数据将不可恢复。",
  2205 + "delete-tenants-text": "请注意:确认后,所有选定的租户将被删除,所有相关数据将不可恢复。",
2105 2206 "delete-tenants-title": "确定要删除 {count,plural,1 {# 个租户} other {# 个租户} } 吗?",
2106 2207 "description": "说明",
2107 2208 "details": "详情",
... ... @@ -2178,10 +2279,10 @@
2178 2279 "customer-users": "客户用户",
2179 2280 "default-dashboard": "默认面板",
2180 2281 "delete": "删除用户",
2181   - "delete-user-text": "小心!确认后,用户和所有相关数据将不可恢复。",
  2282 + "delete-user-text": "请注意:确认后,用户和所有相关数据将不可恢复。",
2182 2283 "delete-user-title": "您确定要删除用户 '{{userEmail}}' 吗?",
2183 2284 "delete-users-action-title": "删除 { count, plural, 1 {# 个用户} other {# 个用户} }",
2184   - "delete-users-text": "小心!确认后,所有选定的用户将被删除,所有相关数据将不可恢复。",
  2285 + "delete-users-text": "请注意:确认后,所有选定的用户将被删除,所有相关数据将不可恢复。",
2185 2286 "delete-users-title": "确定要删除 { count, plural, 1 {# 个用户} other {# 个用户} } 吗?",
2186 2287 "description": "说明",
2187 2288 "details": "详情",
... ... @@ -2343,11 +2444,11 @@
2343 2444 "run": "运行部件",
2344 2445 "save": "保存部件",
2345 2446 "save-widget-type-as": "部件类型另存为",
2346   - "save-widget-type-as-text": "请输入新的部件标题或选择目标部件",
  2447 + "save-widget-type-as-text": "请输入新的部件标题或选择目标部件",
2347 2448 "saveAs": "部件另存为",
2348 2449 "search-data": "查找数据",
2349 2450 "select-widget-type": "选择窗口部件类型",
2350   - "select-widgets-bundle": "选择部件",
  2451 + "select-widgets-bundle": "选择部件",
2351 2452 "settings-schema": "设置模式",
2352 2453 "static": "静态部件",
2353 2454 "tidy": "Tidy",
... ... @@ -2358,7 +2459,7 @@
2358 2459 "type": "部件类型",
2359 2460 "unable-to-save-widget-error": "无法保存部件!控件有错误!",
2360 2461 "undo": "撤消部件更改",
2361   - "widget-bundle": "部件",
  2462 + "widget-bundle": "部件",
2362 2463 "widget-library": "部件库",
2363 2464 "widget-saved": "部件已保存",
2364 2465 "widget-template-load-failed-error": "无法加载部件模板!",
... ... @@ -2367,33 +2468,36 @@
2367 2468 "widget-type-not-found": "加载部件配置出错。<br>可能关联的部件已经删除了。"
2368 2469 },
2369 2470 "widgets-bundle": {
2370   - "add": "添加部件组",
2371   - "add-widgets-bundle-text": "添加新的部件组",
2372   - "create-new-widgets-bundle": "创建新的部件组",
  2471 + "add": "添加部件包",
  2472 + "add-widgets-bundle-text": "添加新的部件包",
  2473 + "create-new-widgets-bundle": "创建新的部件包",
2373 2474 "current": "当前组",
2374   - "delete": "删除部件组",
2375   - "delete-widgets-bundle-text": "小心!确认后,部件组和所有相关数据将不可恢复。",
2376   - "delete-widgets-bundle-title": "您确定要删除部件组 '{{widgetsBundleTitle}}'吗?",
2377   - "delete-widgets-bundles-action-title": "删除 { count, plural, 1 {# 个部件组} other {# 个部件组} }",
2378   - "delete-widgets-bundles-text": "小心!确认后,所有选定的部件组将被删除,所有相关数据将不可恢复。",
2379   - "delete-widgets-bundles-title": "确定要删除 { count, plural, 1 {# 个部件组} other {# 个部件组} } 吗?",
  2475 + "delete": "删除部件包",
  2476 + "delete-widgets-bundle-text": "请注意:确认后,部件包和所有相关数据将不可恢复。",
  2477 + "delete-widgets-bundle-title": "您确定要删除部件包 '{{widgetsBundleTitle}}'吗?",
  2478 + "delete-widgets-bundles-action-title": "删除 { count, plural, 1 {# 个部件包} other {# 个部件包} }",
  2479 + "delete-widgets-bundles-text": "请注意:确认后,所有选定的部件包将被删除,所有相关数据将不可恢复。",
  2480 + "delete-widgets-bundles-title": "确定要删除 { count, plural, 1 {# 个部件包} other {# 个部件包} } 吗?",
  2481 + "description": "描述",
2380 2482 "details": "详情",
2381   - "empty": "部件组是空的",
2382   - "export": "导出部件组",
2383   - "export-failed-error": "无法导出部件组: {{error}}",
2384   - "import": "导入部件组",
2385   - "invalid-widgets-bundle-file-error": "无法导入部件组:无效的部件组数据结构。",
2386   - "no-widgets-bundles-matching": "没有找到与 '{{widgetsBundle}}' 匹配的部件组。",
2387   - "no-widgets-bundles-text": "找不到部件组",
2388   - "open-widgets-bundle": "打开部件组",
2389   - "search": "查找部件组",
2390   - "selected-widgets-bundles": "已选择 { count, plural, 1 {# 个部件组} other {# 个部件组} }",
  2483 + "empty": "部件包是空的",
  2484 + "export": "导出部件包",
  2485 + "export-failed-error": "无法导出部件包: {{error}}",
  2486 + "image-preview": "图片预览",
  2487 + "import": "导入部件包",
  2488 + "invalid-widgets-bundle-file-error": "无法导入部件包:无效的部件包数据结构。",
  2489 + "loading-widgets-bundles": "加载部件包...",
  2490 + "no-widgets-bundles-matching": "没有找到与 '{{widgetsBundle}}' 匹配的部件包。",
  2491 + "no-widgets-bundles-text": "找不到部件包",
  2492 + "open-widgets-bundle": "打开部件包",
  2493 + "search": "查找部件包",
  2494 + "selected-widgets-bundles": "已选择 { count, plural, 1 {# 个部件包} other {# 个部件包} }",
2391 2495 "system": "系统",
2392 2496 "title": "标题",
2393 2497 "title-required": "标题必填。",
2394   - "widgets-bundle-details": "部件组详细信息",
2395   - "widgets-bundle-file": "部件组文件",
2396   - "widgets-bundle-required": "部件组必填。",
  2498 + "widgets-bundle-details": "部件包详细信息",
  2499 + "widgets-bundle-file": "部件包文件",
  2500 + "widgets-bundle-required": "部件包必填。",
2397 2501 "widgets-bundles": "部件包"
2398 2502 },
2399 2503 "widgets": {
... ...