Commit c2c2f55dc372a910086e46d155a39efb1fce2216
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 | +} | ... | ... |
... | ... | @@ -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 | } | ... | ... |
... | ... | @@ -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 | } | ... | ... |
... | ... | @@ -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": { | ... | ... |