thingsboard-developers
Changes
application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java 34(+34 -0)
application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java 161(+161 -0)
application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java 66(+66 -0)
application/src/main/java/org/thingsboard/server/controller/plugin/PluginApiController.java 108(+108 -0)
application/src/main/java/org/thingsboard/server/controller/plugin/PluginNotFoundException.java 30(+30 -0)
application/src/main/java/org/thingsboard/server/controller/plugin/PluginWebSocketHandler.java 203(+203 -0)
application/src/main/java/org/thingsboard/server/controller/plugin/PluginWebSocketMsgEndpoint.java 28(+28 -0)
application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java 116(+116 -0)
application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java 133(+133 -0)
application/src/main/java/org/thingsboard/server/service/environment/EnvironmentLogService.java 39(+39 -0)
application/src/main/java/org/thingsboard/server/service/security/auth/AbstractJwtAuthenticationToken.java 66(+66 -0)
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtHeaderTokenExtractor.java 42(+42 -0)
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtQueryTokenExtractor.java 43(+43 -0)
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/TokenExtractor.java 22(+22 -0)
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java 58(+58 -0)
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtTokenAuthenticationProcessingFilter.java 71(+71 -0)
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java 79(+79 -0)
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenProcessingFilter.java 94(+94 -0)
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenRepository.java 38(+38 -0)
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenRequest.java 32(+32 -0)
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/SkipPathRequestMatcher.java 45(+45 -0)
application/src/main/java/org/thingsboard/server/service/security/auth/JwtAuthenticationToken.java 32(+32 -0)
application/src/main/java/org/thingsboard/server/service/security/auth/RefreshAuthenticationToken.java 32(+32 -0)
application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginRequest.java 38(+38 -0)
application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java 79(+79 -0)
application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationFailureHandler.java 44(+44 -0)
application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java 85(+85 -0)
application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java 93(+93 -0)
application/src/main/java/org/thingsboard/server/service/security/device/DefaultDeviceAuthService.java 70(+70 -0)
application/src/main/java/org/thingsboard/server/service/security/exception/AuthMethodNotSupportedException.java 26(+26 -0)
application/src/main/java/org/thingsboard/server/service/security/exception/JwtExpiredTokenException.java 38(+38 -0)
application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java 64(+64 -0)
application/src/main/java/org/thingsboard/server/service/security/model/token/AccessJwtToken.java 38(+38 -0)
application/src/main/java/org/thingsboard/server/service/security/model/token/JwtToken.java 20(+20 -0)
application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java 158(+158 -0)
application/src/main/java/org/thingsboard/server/service/security/model/token/RawAccessJwtToken.java 56(+56 -0)
dao/pom.xml 176(+176 -0)
dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java 133(+133 -0)
dao/src/main/java/org/thingsboard/server/dao/model/type/ComponentLifecycleStateCodec.java 28(+28 -0)
dao/src/main/resources/demo-data.cql 402(+402 -0)
dao/src/main/resources/schema.cql 425(+425 -0)
dao/src/main/resources/system-data.cql 258(+258 -0)
dao/src/test/java/org/thingsboard/server/dao/service/DeviceCredentialsServiceImplTest.java 195(+195 -0)
dao/src/test/resources/cassandra-test.yaml 590(+590 -0)
dao/src/test/resources/logback.xml 19(+19 -0)
ui/.babelrc 9(+9 -0)
ui/.eslintrc 15(+15 -0)
ui/.gitignore 2(+2 -0)
ui/.jshintrc 13(+13 -0)
ui/package.json 123(+123 -0)
ui/pom.xml 142(+142 -0)
ui/server.js 75(+75 -0)
ui/src/app/admin/admin.controller.js 54(+54 -0)
ui/src/app/admin/admin.routes.js 73(+73 -0)
ui/src/app/admin/general-settings.tpl.html 45(+45 -0)
ui/src/app/admin/index.js 36(+36 -0)
ui/src/app/api/admin.service.js 63(+63 -0)
ui/src/app/api/customer.service.js 85(+85 -0)
ui/src/app/api/dashboard.service.js 129(+129 -0)
ui/src/app/api/datasource.service.js 490(+490 -0)
ui/src/app/api/device.service.js 381(+381 -0)
ui/src/app/api/event.service.js 50(+50 -0)
ui/src/app/api/login.service.js 95(+95 -0)
ui/src/app/api/plugin.service.js 216(+216 -0)
ui/src/app/api/rule.service.js 182(+182 -0)
ui/src/app/api/telemetry-websocket.service.js 170(+170 -0)
ui/src/app/api/tenant.service.js 85(+85 -0)
ui/src/app/api/user.service.js 334(+334 -0)
ui/src/app/api/widget.service.js 601(+601 -0)
ui/src/app/app.config.js 147(+147 -0)
ui/src/app/app.js 105(+105 -0)
ui/src/app/app.run.js 166(+166 -0)
ui/src/app/common/types.constant.js 151(+151 -0)
ui/src/app/common/utils.service.js 265(+265 -0)
ui/src/app/component/component.directive.js 75(+75 -0)
ui/src/app/component/component.tpl.html 58(+58 -0)
ui/src/app/component/component-dialog.controller.js 105(+105 -0)
ui/src/app/component/index.js 28(+28 -0)
ui/src/app/components/contact.directive.js 300(+300 -0)
ui/src/app/components/contact.tpl.html 59(+59 -0)
ui/src/app/components/dashboard.directive.js 427(+427 -0)
ui/src/app/components/dashboard.scss 108(+108 -0)
ui/src/app/components/dashboard.tpl.html 80(+80 -0)
ui/src/app/components/dashboard-select.directive.js 114(+114 -0)
ui/src/app/components/dashboard-select.scss 30(+30 -0)
ui/src/app/components/datakey-config.directive.js 139(+139 -0)
ui/src/app/components/datakey-config.scss 28(+28 -0)
ui/src/app/components/datasource.scss 54(+54 -0)
ui/src/app/components/datasource.tpl.html 46(+46 -0)
ui/src/app/components/datasource-device.directive.js 248(+248 -0)
ui/src/app/components/datasource-device.scss 29(+29 -0)
ui/src/app/components/datasource-device.tpl.html 127(+127 -0)
ui/src/app/components/datasource-func.directive.js 182(+182 -0)
ui/src/app/components/datasource-func.scss 29(+29 -0)
ui/src/app/components/datetime-period.directive.js 108(+108 -0)
ui/src/app/components/datetime-period.scss 28(+28 -0)
ui/src/app/components/details-sidenav.scss 49(+49 -0)
ui/src/app/components/expand-fullscreen.directive.js 140(+140 -0)
ui/src/app/components/expand-fullscreen.scss 53(+53 -0)
ui/src/app/components/grid.directive.js 607(+607 -0)
ui/src/app/components/grid.scss 37(+37 -0)
ui/src/app/components/grid.tpl.html 102(+102 -0)
ui/src/app/components/js-func.directive.js 203(+203 -0)
ui/src/app/components/js-func.scss 31(+31 -0)
ui/src/app/components/js-func.tpl.html 33(+33 -0)
ui/src/app/components/json-form.directive.js 243(+243 -0)
ui/src/app/components/json-form.scss 19(+19 -0)
ui/src/app/components/json-form.tpl.html 18(+18 -0)
ui/src/app/components/led-light.directive.js 110(+110 -0)
ui/src/app/components/menu-link.directive.js 76(+76 -0)
ui/src/app/components/menu-link.scss 32(+32 -0)
ui/src/app/components/menu-link.tpl.html 22(+22 -0)
ui/src/app/components/menu-toggle.tpl.html 33(+33 -0)
ui/src/app/components/plugin-select.directive.js 115(+115 -0)
ui/src/app/components/plugin-select.scss 37(+37 -0)
ui/src/app/components/plugin-select.tpl.html 44(+44 -0)
ui/src/app/components/react/json-form.scss 101(+101 -0)
ui/src/app/components/react/json-form-ace-editor.jsx 136(+136 -0)
ui/src/app/components/react/json-form-array.jsx 165(+165 -0)
ui/src/app/components/react/json-form-color.jsx 161(+161 -0)
ui/src/app/components/react/json-form-rc-select.jsx 123(+123 -0)
ui/src/app/components/side-menu.directive.js 50(+50 -0)
ui/src/app/components/side-menu.scss 84(+84 -0)
ui/src/app/components/side-menu.tpl.html 23(+23 -0)
ui/src/app/components/tb-event-directives.js 59(+59 -0)
ui/src/app/components/timeinterval.directive.js 213(+213 -0)
ui/src/app/components/timeinterval.scss 34(+34 -0)
ui/src/app/components/timeinterval.tpl.html 47(+47 -0)
ui/src/app/components/timewindow.directive.js 242(+242 -0)
ui/src/app/components/timewindow.scss 27(+27 -0)
ui/src/app/components/timewindow.tpl.html 23(+23 -0)
ui/src/app/components/truncate.filter.js 43(+43 -0)
ui/src/app/components/widget.controller.js 478(+478 -0)
ui/src/app/components/widget.directive.js 127(+127 -0)
ui/src/app/components/widget-config.directive.js 340(+340 -0)
ui/src/app/components/widget-config.tpl.html 163(+163 -0)
ui/src/app/customer/add-customer.tpl.html 46(+46 -0)
ui/src/app/customer/customer.controller.js 164(+164 -0)
ui/src/app/customer/customer.directive.js 42(+42 -0)
ui/src/app/customer/customer.routes.js 47(+47 -0)
ui/src/app/customer/customer-card.tpl.html 18(+18 -0)
ui/src/app/customer/customers.tpl.html 29(+29 -0)
ui/src/app/customer/index.js 36(+36 -0)
ui/src/app/dashboard/add-dashboard.tpl.html 45(+45 -0)
ui/src/app/dashboard/add-widget.controller.js 124(+124 -0)
ui/src/app/dashboard/add-widget.tpl.html 59(+59 -0)
ui/src/app/dashboard/dashboard.controller.js 424(+424 -0)
ui/src/app/dashboard/dashboard.directive.js 42(+42 -0)
ui/src/app/dashboard/dashboard.routes.js 107(+107 -0)
ui/src/app/dashboard/dashboard.scss 55(+55 -0)
ui/src/app/dashboard/dashboard.tpl.html 177(+177 -0)
ui/src/app/dashboard/dashboard-card.tpl.html 18(+18 -0)
ui/src/app/dashboard/dashboards.controller.js 379(+379 -0)
ui/src/app/dashboard/dashboards.tpl.html 29(+29 -0)
ui/src/app/dashboard/device-aliases.controller.js 183(+183 -0)
ui/src/app/dashboard/device-aliases.scss 32(+32 -0)
ui/src/app/dashboard/device-aliases.tpl.html 131(+131 -0)
ui/src/app/dashboard/edit-widget.directive.js 111(+111 -0)
ui/src/app/dashboard/edit-widget.tpl.html 28(+28 -0)
ui/src/app/dashboard/index.js 67(+67 -0)
ui/src/app/device/add-device.tpl.html 45(+45 -0)
ui/src/app/device/assign-to-customer.controller.js 123(+123 -0)
ui/src/app/device/attribute/attribute-table.tpl.html 208(+208 -0)
ui/src/app/device/device.controller.js 429(+429 -0)
ui/src/app/device/device.directive.js 69(+69 -0)
ui/src/app/device/device.routes.js 68(+68 -0)
ui/src/app/device/device-card.tpl.html 18(+18 -0)
ui/src/app/device/device-fieldset.tpl.html 60(+60 -0)
ui/src/app/device/devices.tpl.html 55(+55 -0)
ui/src/app/device/index.js 52(+52 -0)
ui/src/app/event/event.scss 72(+72 -0)
ui/src/app/event/event-header.directive.js 66(+66 -0)
ui/src/app/event/event-header-alarm.tpl.html 20(+20 -0)
ui/src/app/event/event-header-error.tpl.html 21(+21 -0)
ui/src/app/event/event-header-stats.tpl.html 21(+21 -0)
ui/src/app/event/event-row.directive.js 90(+90 -0)
ui/src/app/event/event-row-alarm.tpl.html 32(+32 -0)
ui/src/app/event/event-row-error.tpl.html 33(+33 -0)
ui/src/app/event/event-row-lc-event.tpl.html 34(+34 -0)
ui/src/app/event/event-row-stats.tpl.html 21(+21 -0)
ui/src/app/event/event-table.directive.js 212(+212 -0)
ui/src/app/event/event-table.tpl.html 47(+47 -0)
ui/src/app/event/index.js 30(+30 -0)
ui/src/app/global-interceptor.service.js 182(+182 -0)
ui/src/app/help/help.directive.js 75(+75 -0)
ui/src/app/help/help.scss 22(+22 -0)
ui/src/app/help/help-links.constant.js 129(+129 -0)
ui/src/app/home/home-links.controller.js 20(+20 -0)
ui/src/app/home/home-links.routes.js 45(+45 -0)
ui/src/app/home/home-links.tpl.html 38(+38 -0)
ui/src/app/home/index.js 26(+26 -0)
ui/src/app/jsonform/index.js 32(+32 -0)
ui/src/app/jsonform/jsonform.controller.js 117(+117 -0)
ui/src/app/jsonform/jsonform.routes.js 44(+44 -0)
ui/src/app/jsonform/jsonform.scss 17(+17 -0)
ui/src/app/jsonform/jsonform.tpl.html 54(+54 -0)
ui/src/app/layout/breadcrumb.tpl.html 38(+38 -0)
ui/src/app/layout/breadcrumb-icon.filter.js 24(+24 -0)
ui/src/app/layout/breadcrumb-label.filter.js 45(+45 -0)
ui/src/app/layout/home.controller.js 152(+152 -0)
ui/src/app/layout/home.routes.js 50(+50 -0)
ui/src/app/layout/home.scss 93(+93 -0)
ui/src/app/layout/home.tpl.html 96(+96 -0)
ui/src/app/layout/index.js 78(+78 -0)
ui/src/app/login/create-password.tpl.html 56(+56 -0)
ui/src/app/login/index.js 40(+40 -0)
ui/src/app/login/login.controller.js 46(+46 -0)
ui/src/app/login/login.routes.js 80(+80 -0)
ui/src/app/login/login.scss 23(+23 -0)
ui/src/app/login/login.tpl.html 56(+56 -0)
ui/src/app/login/reset-password.tpl.html 56(+56 -0)
ui/src/app/plugin/add-plugin.tpl.html 48(+48 -0)
ui/src/app/plugin/index.js 38(+38 -0)
ui/src/app/plugin/plugin.controller.js 177(+177 -0)
ui/src/app/plugin/plugin.directive.js 89(+89 -0)
ui/src/app/plugin/plugin.routes.js 46(+46 -0)
ui/src/app/plugin/plugin.scss 18(+18 -0)
ui/src/app/plugin/plugin-card.tpl.html 19(+19 -0)
ui/src/app/plugin/plugin-fieldset.tpl.html 71(+71 -0)
ui/src/app/plugin/plugins.tpl.html 42(+42 -0)
ui/src/app/profile/change-password.tpl.html 64(+64 -0)
ui/src/app/profile/index.js 38(+38 -0)
ui/src/app/profile/profile.controller.js 58(+58 -0)
ui/src/app/profile/profile.routes.js 45(+45 -0)
ui/src/app/profile/profile.tpl.html 56(+56 -0)
ui/src/app/rule/add-rule.tpl.html 48(+48 -0)
ui/src/app/rule/index.js 42(+42 -0)
ui/src/app/rule/rule.controller.js 170(+170 -0)
ui/src/app/rule/rule.directive.js 183(+183 -0)
ui/src/app/rule/rule.routes.js 46(+46 -0)
ui/src/app/rule/rule.scss 55(+55 -0)
ui/src/app/rule/rule-card.tpl.html 19(+19 -0)
ui/src/app/rule/rule-fieldset.tpl.html 200(+200 -0)
ui/src/app/rule/rules.tpl.html 42(+42 -0)
ui/src/app/services/error-toast.tpl.html 25(+25 -0)
ui/src/app/services/menu.service.js 321(+321 -0)
ui/src/app/services/success-toast.tpl.html 25(+25 -0)
ui/src/app/services/toast.controller.js 27(+27 -0)
ui/src/app/services/toast.js 24(+24 -0)
ui/src/app/services/toast.scss 26(+26 -0)
ui/src/app/services/toast.service.js 85(+85 -0)
ui/src/app/tenant/add-tenant.tpl.html 48(+48 -0)
ui/src/app/tenant/index.js 36(+36 -0)
ui/src/app/tenant/tenant.controller.js 132(+132 -0)
ui/src/app/tenant/tenant.directive.js 40(+40 -0)
ui/src/app/tenant/tenant.routes.js 46(+46 -0)
ui/src/app/tenant/tenant-card.tpl.html 18(+18 -0)
ui/src/app/tenant/tenant-fieldset.tpl.html 36(+36 -0)
ui/src/app/tenant/tenants.tpl.html 27(+27 -0)
ui/src/app/user/add-user.tpl.html 45(+45 -0)
ui/src/app/user/index.js 34(+34 -0)
ui/src/app/user/user.controller.js 153(+153 -0)
ui/src/app/user/user.directive.js 40(+40 -0)
ui/src/app/user/user.routes.js 69(+69 -0)
ui/src/app/user/user-card.tpl.html 18(+18 -0)
ui/src/app/user/user-fieldset.tpl.html 47(+47 -0)
ui/src/app/user/users.tpl.html 27(+27 -0)
ui/src/app/widget/index.js 59(+59 -0)
ui/src/app/widget/lib/analogue-linear-gauge.js 184(+184 -0)
ui/src/app/widget/lib/analogue-radial-gauge.js 193(+193 -0)
ui/src/app/widget/lib/digital-gauge.js 586(+586 -0)
ui/src/app/widget/widget-editor.controller.js 645(+645 -0)
ui/src/app/widget/widget-editor.scss 151(+151 -0)
ui/src/app/widget/widget-editor.tpl.html 258(+258 -0)
ui/src/app/widget/widget-library.controller.js 176(+176 -0)
ui/src/app/widget/widget-library.routes.js 107(+107 -0)
ui/src/app/widget/widget-library.tpl.html 48(+48 -0)
ui/src/app/widget/widgets-bundle.controller.js 148(+148 -0)
ui/src/app/widget/widgets-bundles.tpl.html 24(+24 -0)
ui/src/font/Segment7Standard.otf 0(+0 -0)
ui/src/index.html 37(+37 -0)
ui/src/locale/en_US.json 631(+631 -0)
ui/src/scss/animations.scss 44(+44 -0)
ui/src/scss/constants.scss 43(+43 -0)
ui/src/scss/main.scss 402(+402 -0)
ui/src/scss/mixins.scss 34(+34 -0)
ui/src/svg/logo_title_white.svg 42(+42 -0)
ui/src/svg/logo_white.svg 10(+10 -0)
ui/src/svg/mdi.svg 1(+1 -0)
ui/src/thingsboard.ico 0(+0 -0)
ui/src/vendor/css.js/css.js 672(+672 -0)
ui/src/vendor/css.js/css.min.js 2(+2 -0)
ui/webpack.config.dev.js 129(+129 -0)
ui/webpack.config.js 22(+22 -0)
ui/webpack.config.prod.js 125(+125 -0)
Details
diff --git a/application/src/main/java/org/thingsboard/server/config/JwtSettings.java b/application/src/main/java/org/thingsboard/server/config/JwtSettings.java
new file mode 100644
index 0000000..9a74785
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/config/JwtSettings.java
@@ -0,0 +1,75 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import org.thingsboard.server.service.security.model.token.JwtToken;
+
+@Configuration
+@ConfigurationProperties(prefix = "security.jwt")
+public class JwtSettings {
+ /**
+ * {@link JwtToken} will expire after this time.
+ */
+ private Integer tokenExpirationTime;
+
+ /**
+ * Token issuer.
+ */
+ private String tokenIssuer;
+
+ /**
+ * Key is used to sign {@link JwtToken}.
+ */
+ private String tokenSigningKey;
+
+ /**
+ * {@link JwtToken} can be refreshed during this timeframe.
+ */
+ private Integer refreshTokenExpTime;
+
+ public Integer getRefreshTokenExpTime() {
+ return refreshTokenExpTime;
+ }
+
+ public void setRefreshTokenExpTime(Integer refreshTokenExpTime) {
+ this.refreshTokenExpTime = refreshTokenExpTime;
+ }
+
+ public Integer getTokenExpirationTime() {
+ return tokenExpirationTime;
+ }
+
+ public void setTokenExpirationTime(Integer tokenExpirationTime) {
+ this.tokenExpirationTime = tokenExpirationTime;
+ }
+
+ public String getTokenIssuer() {
+ return tokenIssuer;
+ }
+ public void setTokenIssuer(String tokenIssuer) {
+ this.tokenIssuer = tokenIssuer;
+ }
+
+ public String getTokenSigningKey() {
+ return tokenSigningKey;
+ }
+
+ public void setTokenSigningKey(String tokenSigningKey) {
+ this.tokenSigningKey = tokenSigningKey;
+ }
+}
\ No newline at end of file
diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java
new file mode 100644
index 0000000..99bec5b
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.config;
+
+import org.springframework.context.MessageSource;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.support.ResourceBundleMessageSource;
+
+@Configuration
+public class ThingsboardMessageConfiguration {
+
+ @Bean
+ public MessageSource messageSource() {
+ ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
+ messageSource.setBasename("i18n/messages");
+ messageSource.setDefaultEncoding("UTF-8");
+ return messageSource;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
new file mode 100644
index 0000000..ec6ca81
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
@@ -0,0 +1,161 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.security.SecurityProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.annotation.Order;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
+import org.thingsboard.server.service.security.auth.rest.RestAuthenticationProvider;
+import org.thingsboard.server.service.security.auth.rest.RestLoginProcessingFilter;
+import org.thingsboard.server.service.security.auth.jwt.*;
+import org.thingsboard.server.service.security.auth.jwt.extractor.TokenExtractor;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@Configuration
+@EnableWebSecurity
+@EnableGlobalMethodSecurity(prePostEnabled=true)
+@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
+public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapter {
+
+ public static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization";
+ public static final String JWT_TOKEN_QUERY_PARAM = "token";
+
+ public static final String DEVICE_API_ENTRY_POINT = "/api/v1/**";
+ public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login";
+ public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token";
+ public static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/static/**", "/api/noauth/**"};
+ public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**";
+ public static final String WS_TOKEN_BASED_AUTH_ENTRY_POINT = "/api/ws/**";
+
+ @Autowired private ThingsboardErrorResponseHandler restAccessDeniedHandler;
+ @Autowired private AuthenticationSuccessHandler successHandler;
+ @Autowired private AuthenticationFailureHandler failureHandler;
+ @Autowired private RestAuthenticationProvider restAuthenticationProvider;
+ @Autowired private JwtAuthenticationProvider jwtAuthenticationProvider;
+ @Autowired private RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider;
+
+ @Autowired
+ @Qualifier("jwtHeaderTokenExtractor")
+ private TokenExtractor jwtHeaderTokenExtractor;
+
+ @Autowired
+ @Qualifier("jwtQueryTokenExtractor")
+ private TokenExtractor jwtQueryTokenExtractor;
+
+ @Autowired private AuthenticationManager authenticationManager;
+
+ @Autowired private ObjectMapper objectMapper;
+
+ @Bean
+ protected RestLoginProcessingFilter buildRestLoginProcessingFilter() throws Exception {
+ RestLoginProcessingFilter filter = new RestLoginProcessingFilter(FORM_BASED_LOGIN_ENTRY_POINT, successHandler, failureHandler, objectMapper);
+ filter.setAuthenticationManager(this.authenticationManager);
+ return filter;
+ }
+
+ @Bean
+ protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception {
+ List<String> pathsToSkip = new ArrayList(Arrays.asList(NON_TOKEN_BASED_AUTH_ENTRY_POINTS));
+ pathsToSkip.addAll(Arrays.asList(WS_TOKEN_BASED_AUTH_ENTRY_POINT, TOKEN_REFRESH_ENTRY_POINT, FORM_BASED_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT));
+ SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT);
+ JwtTokenAuthenticationProcessingFilter filter
+ = new JwtTokenAuthenticationProcessingFilter(failureHandler, jwtHeaderTokenExtractor, matcher);
+ filter.setAuthenticationManager(this.authenticationManager);
+ return filter;
+ }
+
+ @Bean
+ protected RefreshTokenProcessingFilter buildRefreshTokenProcessingFilter() throws Exception {
+ RefreshTokenProcessingFilter filter = new RefreshTokenProcessingFilter(TOKEN_REFRESH_ENTRY_POINT, successHandler, failureHandler, objectMapper);
+ filter.setAuthenticationManager(this.authenticationManager);
+ return filter;
+ }
+
+ @Bean
+ protected JwtTokenAuthenticationProcessingFilter buildWsJwtTokenAuthenticationProcessingFilter() throws Exception {
+ AntPathRequestMatcher matcher = new AntPathRequestMatcher(WS_TOKEN_BASED_AUTH_ENTRY_POINT);
+ JwtTokenAuthenticationProcessingFilter filter
+ = new JwtTokenAuthenticationProcessingFilter(failureHandler, jwtQueryTokenExtractor, matcher);
+ filter.setAuthenticationManager(this.authenticationManager);
+ return filter;
+ }
+
+ @Bean
+ @Override
+ public AuthenticationManager authenticationManagerBean() throws Exception {
+ return super.authenticationManagerBean();
+ }
+
+ @Override
+ protected void configure(AuthenticationManagerBuilder auth) {
+ auth.authenticationProvider(restAuthenticationProvider);
+ auth.authenticationProvider(jwtAuthenticationProvider);
+ auth.authenticationProvider(refreshTokenAuthenticationProvider);
+ }
+
+ @Bean
+ protected BCryptPasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+
+ @Override
+ protected void configure(HttpSecurity http) throws Exception {
+ http.headers().frameOptions().disable()
+ .and()
+ .csrf().disable()
+ .exceptionHandling()
+ .and()
+ .sessionManagement()
+ .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+ .and()
+ .authorizeRequests()
+ .antMatchers(DEVICE_API_ENTRY_POINT).permitAll() // Device HTTP Transport API
+ .antMatchers(FORM_BASED_LOGIN_ENTRY_POINT).permitAll() // Login end-point
+ .antMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll() // Token refresh end-point
+ .antMatchers(NON_TOKEN_BASED_AUTH_ENTRY_POINTS).permitAll() // static resources, user activation and password reset end-points
+ .and()
+ .authorizeRequests()
+ .antMatchers(WS_TOKEN_BASED_AUTH_ENTRY_POINT).authenticated() // Protected WebSocket API End-points
+ .antMatchers(TOKEN_BASED_AUTH_ENTRY_POINT).authenticated() // Protected API End-points
+ .and()
+ .exceptionHandling().accessDeniedHandler(restAccessDeniedHandler)
+ .and()
+ .addFilterBefore(buildRestLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
+ .addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
+ .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
+ .addFilterBefore(buildWsJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class);
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/config/WebConfig.java b/application/src/main/java/org/thingsboard/server/config/WebConfig.java
new file mode 100644
index 0000000..3a2234a
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/config/WebConfig.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.config;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+@Controller
+public class WebConfig {
+
+ @RequestMapping(value = "/{path:^(?!api$)(?!static$)[^\\.]*}/**")
+ public String redirect() {
+ return "forward:/index.html";
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java b/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java
new file mode 100644
index 0000000..b6b4d0e
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java
@@ -0,0 +1,96 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.config;
+
+import java.util.Map;
+
+import org.thingsboard.server.exception.ThingsboardErrorCode;
+import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.controller.plugin.PluginWebSocketHandler;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.server.ServerHttpRequest;
+import org.springframework.http.server.ServerHttpResponse;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.config.annotation.EnableWebSocket;
+import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
+import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
+import org.springframework.web.socket.server.HandshakeInterceptor;
+import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;
+import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
+
+@Configuration
+@EnableWebSocket
+public class WebSocketConfiguration implements WebSocketConfigurer {
+
+ public static final String WS_PLUGIN_PREFIX = "/api/ws/plugins/";
+ public static final String WS_SECURITY_USER_ATTRIBUTE = "SECURITY_USER";
+ private static final String WS_PLUGIN_MAPPING = WS_PLUGIN_PREFIX + "**";
+
+ @Bean
+ public ServletServerContainerFactoryBean createWebSocketContainer() {
+ ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
+ container.setMaxTextMessageBufferSize(8192);
+ container.setMaxBinaryMessageBufferSize(8192);
+ return container;
+ }
+
+ @Override
+ public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
+ registry.addHandler(pluginWsHandler(), WS_PLUGIN_MAPPING).setAllowedOrigins("*")
+ .addInterceptors(new HttpSessionHandshakeInterceptor(), new HandshakeInterceptor() {
+
+ @Override
+ public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
+ Map<String, Object> attributes) throws Exception {
+ SecurityUser user = null;
+ try {
+ user = getCurrentUser();
+ } catch (ThingsboardException ex) {}
+ if (user == null) {
+ response.setStatusCode(HttpStatus.UNAUTHORIZED);
+ return false;
+ } else {
+ attributes.put(WS_SECURITY_USER_ATTRIBUTE, user);
+ return true;
+ }
+ }
+
+ @Override
+ public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
+ Exception exception) {
+ }
+ });
+ }
+
+ @Bean
+ public WebSocketHandler pluginWsHandler() {
+ return new PluginWebSocketHandler();
+ }
+
+ protected SecurityUser getCurrentUser() throws ThingsboardException {
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+ if (authentication != null && authentication.getPrincipal() instanceof SecurityUser) {
+ return (SecurityUser) authentication.getPrincipal();
+ } else {
+ throw new ThingsboardException("You aren't authorized to perform this operation!", ThingsboardErrorCode.AUTHENTICATION);
+ }
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/AdminController.java b/application/src/main/java/org/thingsboard/server/controller/AdminController.java
new file mode 100644
index 0000000..cace783
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java
@@ -0,0 +1,75 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import org.thingsboard.server.common.data.AdminSettings;
+import org.thingsboard.server.dao.settings.AdminSettingsService;
+import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.service.mail.MailService;
+
+@RestController
+@RequestMapping("/api/admin")
+public class AdminController extends BaseController {
+
+ @Autowired
+ private MailService mailService;
+
+ @Autowired
+ private AdminSettingsService adminSettingsService;
+
+ @PreAuthorize("hasAuthority('SYS_ADMIN')")
+ @RequestMapping(value = "/settings/{key}", method = RequestMethod.GET)
+ @ResponseBody
+ public AdminSettings getAdminSettings(@PathVariable("key") String key) throws ThingsboardException {
+ try {
+ return checkNotNull(adminSettingsService.findAdminSettingsByKey(key));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('SYS_ADMIN')")
+ @RequestMapping(value = "/settings", method = RequestMethod.POST)
+ @ResponseBody
+ public AdminSettings saveAdminSettings(@RequestBody AdminSettings adminSettings) throws ThingsboardException {
+ try {
+ adminSettings = checkNotNull(adminSettingsService.saveAdminSettings(adminSettings));
+ if (adminSettings.getKey().equals("mail")) {
+ mailService.updateMailConfiguration();
+ }
+ return adminSettings;
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('SYS_ADMIN')")
+ @RequestMapping(value = "/settings/testMail", method = RequestMethod.POST)
+ public void sendTestMail(@RequestBody AdminSettings adminSettings) throws ThingsboardException {
+ try {
+ adminSettings = checkNotNull(adminSettings);
+ if (adminSettings.getKey().equals("mail")) {
+ String email = getCurrentUser().getEmail();
+ mailService.sendTestMail(adminSettings.getJsonValue(), email);
+ }
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java
new file mode 100644
index 0000000..b704c86
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java
@@ -0,0 +1,236 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.web.bind.annotation.*;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.security.UserCredentials;
+import org.thingsboard.server.dao.user.UserService;
+import org.thingsboard.server.exception.ThingsboardErrorCode;
+import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.service.mail.MailService;
+import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.security.model.token.JwtToken;
+import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
+
+import javax.servlet.http.HttpServletRequest;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+@RestController
+@RequestMapping("/api")
+@Slf4j
+public class AuthController extends BaseController {
+
+
+
+ @Autowired
+ private BCryptPasswordEncoder passwordEncoder;
+
+ @Autowired
+ private JwtTokenFactory tokenFactory;
+
+ @Autowired
+ private RefreshTokenRepository refreshTokenRepository;
+
+ @Autowired
+ private UserService userService;
+
+ @Autowired
+ private MailService mailService;
+
+ @PreAuthorize("isAuthenticated()")
+ @RequestMapping(value = "/auth/user", method = RequestMethod.GET)
+ public @ResponseBody User getUser() throws ThingsboardException {
+ try {
+ SecurityUser securityUser = getCurrentUser();
+ return userService.findUserById(securityUser.getId());
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("isAuthenticated()")
+ @RequestMapping(value = "/auth/changePassword", method = RequestMethod.POST)
+ @ResponseStatus(value = HttpStatus.OK)
+ public void changePassword (
+ @RequestParam(value = "currentPassword") String currentPassword,
+ @RequestParam(value = "newPassword") String newPassword) throws ThingsboardException {
+ try {
+ SecurityUser securityUser = getCurrentUser();
+ UserCredentials userCredentials = userService.findUserCredentialsByUserId(securityUser.getId());
+ if (!passwordEncoder.matches(currentPassword, userCredentials.getPassword())) {
+ throw new ThingsboardException("Current password doesn't match!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
+ }
+ userCredentials.setPassword(passwordEncoder.encode(newPassword));
+ userService.saveUserCredentials(userCredentials);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @RequestMapping(value = "/noauth/activate", params = { "activateToken" }, method = RequestMethod.GET)
+ public ResponseEntity<String> checkActivateToken(
+ @RequestParam(value = "activateToken") String activateToken) {
+ HttpHeaders headers = new HttpHeaders();
+ HttpStatus responseStatus;
+ UserCredentials userCredentials = userService.findUserCredentialsByActivateToken(activateToken);
+ if (userCredentials != null) {
+ String createPasswordURI = "/login/createPassword";
+ try {
+ URI location = new URI(createPasswordURI + "?activateToken=" + activateToken);
+ headers.setLocation(location);
+ responseStatus = HttpStatus.PERMANENT_REDIRECT;
+ } catch (URISyntaxException e) {
+ log.error("Unable to create URI with address [{}]", createPasswordURI);
+ responseStatus = HttpStatus.BAD_REQUEST;
+ }
+ } else {
+ responseStatus = HttpStatus.CONFLICT;
+ }
+ return new ResponseEntity<>(headers, responseStatus);
+ }
+
+ @RequestMapping(value = "/noauth/resetPasswordByEmail", method = RequestMethod.POST)
+ @ResponseStatus(value = HttpStatus.OK)
+ public void requestResetPasswordByEmail (
+ @RequestParam(value = "email") String email,
+ HttpServletRequest request) throws ThingsboardException {
+ try {
+ UserCredentials userCredentials = userService.requestPasswordReset(email);
+
+ String baseUrl = String.format("%s://%s:%d",
+ request.getScheme(),
+ request.getServerName(),
+ request.getServerPort());
+ String resetPasswordUrl = String.format("%s/api/noauth/resetPassword?resetToken=%s", baseUrl,
+ userCredentials.getResetToken());
+
+ mailService.sendResetPasswordEmail(resetPasswordUrl, email);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @RequestMapping(value = "/noauth/resetPassword", params = { "resetToken" }, method = RequestMethod.GET)
+ public ResponseEntity<String> checkResetToken(
+ @RequestParam(value = "resetToken") String resetToken) {
+ HttpHeaders headers = new HttpHeaders();
+ HttpStatus responseStatus;
+ String resetPasswordURI = "/login/resetPassword";
+ UserCredentials userCredentials = userService.findUserCredentialsByResetToken(resetToken);
+ if (userCredentials != null) {
+ try {
+ URI location = new URI(resetPasswordURI + "?resetToken=" + resetToken);
+ headers.setLocation(location);
+ responseStatus = HttpStatus.PERMANENT_REDIRECT;
+ } catch (URISyntaxException e) {
+ log.error("Unable to create URI with address [{}]", resetPasswordURI);
+ responseStatus = HttpStatus.BAD_REQUEST;
+ }
+ } else {
+ responseStatus = HttpStatus.CONFLICT;
+ }
+ return new ResponseEntity<>(headers, responseStatus);
+ }
+
+ @RequestMapping(value = "/noauth/activate", method = RequestMethod.POST)
+ @ResponseStatus(value = HttpStatus.OK)
+ @ResponseBody
+ public JsonNode activateUser(
+ @RequestParam(value = "activateToken") String activateToken,
+ @RequestParam(value = "password") String password,
+ HttpServletRequest request) throws ThingsboardException {
+ try {
+ String encodedPassword = passwordEncoder.encode(password);
+ UserCredentials credentials = userService.activateUserCredentials(activateToken, encodedPassword);
+ User user = userService.findUserById(credentials.getUserId());
+ SecurityUser securityUser = new SecurityUser(user, credentials.isEnabled());
+ String baseUrl = String.format("%s://%s:%d",
+ request.getScheme(),
+ request.getServerName(),
+ request.getServerPort());
+ String loginUrl = String.format("%s/login", baseUrl);
+ String email = user.getEmail();
+ mailService.sendAccountActivatedEmail(loginUrl, email);
+
+ JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);
+ JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser);
+
+ ObjectMapper objectMapper = new ObjectMapper();
+ ObjectNode tokenObject = objectMapper.createObjectNode();
+ tokenObject.put("token", accessToken.getToken());
+ tokenObject.put("refreshToken", refreshToken.getToken());
+ return tokenObject;
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @RequestMapping(value = "/noauth/resetPassword", method = RequestMethod.POST)
+ @ResponseStatus(value = HttpStatus.OK)
+ @ResponseBody
+ public JsonNode resetPassword(
+ @RequestParam(value = "resetToken") String resetToken,
+ @RequestParam(value = "password") String password,
+ HttpServletRequest request) throws ThingsboardException {
+ try {
+ UserCredentials userCredentials = userService.findUserCredentialsByResetToken(resetToken);
+ if (userCredentials != null) {
+ String encodedPassword = passwordEncoder.encode(password);
+ userCredentials.setPassword(encodedPassword);
+ userCredentials.setResetToken(null);
+ userCredentials = userService.saveUserCredentials(userCredentials);
+ User user = userService.findUserById(userCredentials.getUserId());
+ SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled());
+ String baseUrl = String.format("%s://%s:%d",
+ request.getScheme(),
+ request.getServerName(),
+ request.getServerPort());
+ String loginUrl = String.format("%s/login", baseUrl);
+ String email = user.getEmail();
+ mailService.sendPasswordWasResetEmail(loginUrl, email);
+
+ JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);
+ JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser);
+
+ ObjectMapper objectMapper = new ObjectMapper();
+ ObjectNode tokenObject = objectMapper.createObjectNode();
+ tokenObject.put("token", accessToken.getToken());
+ tokenObject.put("refreshToken", refreshToken.getToken());
+ return tokenObject;
+ } else {
+ throw new ThingsboardException("Invalid reset token!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
+ }
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
new file mode 100644
index 0000000..db6380b
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
@@ -0,0 +1,377 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.thingsboard.server.actors.service.ActorService;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.Dashboard;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.id.*;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.data.plugin.PluginMetaData;
+import org.thingsboard.server.common.data.rule.RuleMetaData;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.common.data.widget.WidgetType;
+import org.thingsboard.server.common.data.widget.WidgetsBundle;
+import org.thingsboard.server.dao.customer.CustomerService;
+import org.thingsboard.server.dao.dashboard.DashboardService;
+import org.thingsboard.server.dao.device.DeviceCredentialsService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.exception.IncorrectParameterException;
+import org.thingsboard.server.dao.model.ModelConstants;
+import org.thingsboard.server.dao.plugin.PluginService;
+import org.thingsboard.server.dao.rule.RuleService;
+import org.thingsboard.server.dao.user.UserService;
+import org.thingsboard.server.dao.widget.WidgetTypeService;
+import org.thingsboard.server.dao.widget.WidgetsBundleService;
+import org.thingsboard.server.exception.ThingsboardErrorCode;
+import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
+import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.service.component.ComponentDiscoveryService;
+import org.thingsboard.server.service.security.model.SecurityUser;
+
+import javax.mail.MessagingException;
+import javax.servlet.http.HttpServletResponse;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+import static org.thingsboard.server.dao.service.Validator.validateId;
+
+@Slf4j
+public abstract class BaseController {
+
+ @Autowired
+ private ThingsboardErrorResponseHandler errorResponseHandler;
+
+ @Autowired
+ protected CustomerService customerService;
+
+ @Autowired
+ protected UserService userService;
+
+ @Autowired
+ protected DeviceService deviceService;
+
+ @Autowired
+ protected DeviceCredentialsService deviceCredentialsService;
+
+ @Autowired
+ protected WidgetsBundleService widgetsBundleService;
+
+ @Autowired
+ protected WidgetTypeService widgetTypeService;
+
+ @Autowired
+ protected DashboardService dashboardService;
+
+ @Autowired
+ protected ComponentDiscoveryService componentDescriptorService;
+
+ @Autowired
+ protected RuleService ruleService;
+
+ @Autowired
+ protected PluginService pluginService;
+
+ @Autowired
+ protected ActorService actorService;
+
+
+ @ExceptionHandler(ThingsboardException.class)
+ public void handleThingsboardException(ThingsboardException ex, HttpServletResponse response) {
+ errorResponseHandler.handle(ex, response);
+ }
+
+ ThingsboardException handleException(Exception exception) {
+ return handleException(exception, true);
+ }
+
+ private ThingsboardException handleException(Exception exception, boolean logException) {
+ if (logException) {
+ log.error("Error [{}]", exception.getMessage());
+ }
+
+ String cause = "";
+ if (exception.getCause() != null) {
+ cause = exception.getCause().getClass().getCanonicalName();
+ }
+
+ if (exception instanceof ThingsboardException) {
+ return (ThingsboardException) exception;
+ } else if (exception instanceof IllegalArgumentException || exception instanceof IncorrectParameterException
+ || exception instanceof DataValidationException || cause.contains("IncorrectParameterException")) {
+ return new ThingsboardException(exception.getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS);
+ } else if (exception instanceof MessagingException) {
+ return new ThingsboardException("Unable to send mail: " + exception.getMessage(), ThingsboardErrorCode.GENERAL);
+ } else {
+ return new ThingsboardException(exception.getMessage(), ThingsboardErrorCode.GENERAL);
+ }
+ }
+
+ <T> T checkNotNull(T reference) throws ThingsboardException {
+ if (reference == null) {
+ throw new ThingsboardException("Requested item wasn't found!", ThingsboardErrorCode.ITEM_NOT_FOUND);
+ }
+ return reference;
+ }
+
+ <T> T checkNotNull(Optional<T> reference) throws ThingsboardException {
+ if (reference.isPresent()) {
+ return reference.get();
+ } else {
+ throw new ThingsboardException("Requested item wasn't found!", ThingsboardErrorCode.ITEM_NOT_FOUND);
+ }
+ }
+
+ void checkParameter(String name, String param) throws ThingsboardException {
+ if (StringUtils.isEmpty(param)) {
+ throw new ThingsboardException("Parameter '" + name + "' can't be empty!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
+ }
+ }
+
+ UUID toUUID(String id) {
+ return UUID.fromString(id);
+ }
+
+ TimePageLink createPageLink(int limit, Long startTime, Long endTime, boolean ascOrder, String idOffset) {
+ UUID idOffsetUuid = null;
+ if (StringUtils.isNotEmpty(idOffset)) {
+ idOffsetUuid = toUUID(idOffset);
+ }
+ return new TimePageLink(limit, startTime, endTime, ascOrder, idOffsetUuid);
+ }
+
+
+ TextPageLink createPageLink(int limit, String textSearch, String idOffset, String textOffset) {
+ UUID idOffsetUuid = null;
+ if (StringUtils.isNotEmpty(idOffset)) {
+ idOffsetUuid = toUUID(idOffset);
+ }
+ return new TextPageLink(limit, textSearch, idOffsetUuid, textOffset);
+ }
+
+ protected SecurityUser getCurrentUser() throws ThingsboardException {
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+ if (authentication != null && authentication.getPrincipal() instanceof SecurityUser) {
+ return (SecurityUser) authentication.getPrincipal();
+ } else {
+ throw new ThingsboardException("You aren't authorized to perform this operation!", ThingsboardErrorCode.AUTHENTICATION);
+ }
+ }
+
+ void checkTenantId(TenantId tenantId) throws ThingsboardException {
+ validateId(tenantId, "Incorrect tenantId " + tenantId);
+ SecurityUser authUser = getCurrentUser();
+ if (authUser.getAuthority() != Authority.SYS_ADMIN &&
+ (authUser.getTenantId() == null || !authUser.getTenantId().equals(tenantId))) {
+ throw new ThingsboardException("You don't have permission to perform this operation!",
+ ThingsboardErrorCode.PERMISSION_DENIED);
+ }
+ }
+
+ protected TenantId getTenantId() throws ThingsboardException {
+ return getCurrentUser().getTenantId();
+ }
+
+ Customer checkCustomerId(CustomerId customerId) throws ThingsboardException {
+ try {
+ validateId(customerId, "Incorrect customerId " + customerId);
+ SecurityUser authUser = getCurrentUser();
+ if (authUser.getAuthority() == Authority.SYS_ADMIN ||
+ (authUser.getAuthority() != Authority.TENANT_ADMIN &&
+ (authUser.getCustomerId() == null || !authUser.getCustomerId().equals(customerId)))) {
+ throw new ThingsboardException("You don't have permission to perform this operation!",
+ ThingsboardErrorCode.PERMISSION_DENIED);
+ }
+ Customer customer = customerService.findCustomerById(customerId);
+ checkCustomer(customer);
+ return customer;
+ } catch (Exception e) {
+ throw handleException(e, false);
+ }
+ }
+
+ private void checkCustomer(Customer customer) throws ThingsboardException {
+ checkNotNull(customer);
+ checkTenantId(customer.getTenantId());
+ }
+
+ User checkUserId(UserId userId) throws ThingsboardException {
+ try {
+ validateId(userId, "Incorrect userId " + userId);
+ User user = userService.findUserById(userId);
+ checkUser(user);
+ return user;
+ } catch (Exception e) {
+ throw handleException(e, false);
+ }
+ }
+
+ private void checkUser(User user) throws ThingsboardException {
+ checkNotNull(user);
+ checkTenantId(user.getTenantId());
+ if (user.getAuthority() == Authority.CUSTOMER_USER) {
+ checkCustomerId(user.getCustomerId());
+ }
+ }
+
+ Device checkDeviceId(DeviceId deviceId) throws ThingsboardException {
+ try {
+ validateId(deviceId, "Incorrect deviceId " + deviceId);
+ Device device = deviceService.findDeviceById(deviceId);
+ checkDevice(device);
+ return device;
+ } catch (Exception e) {
+ throw handleException(e, false);
+ }
+ }
+
+ private void checkDevice(Device device) throws ThingsboardException {
+ checkNotNull(device);
+ checkTenantId(device.getTenantId());
+ if (device.getCustomerId() != null && !device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
+ checkCustomerId(device.getCustomerId());
+ }
+ }
+
+ WidgetsBundle checkWidgetsBundleId(WidgetsBundleId widgetsBundleId, boolean modify) throws ThingsboardException {
+ try {
+ validateId(widgetsBundleId, "Incorrect widgetsBundleId " + widgetsBundleId);
+ WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleById(widgetsBundleId);
+ checkWidgetsBundle(widgetsBundle, modify);
+ return widgetsBundle;
+ } catch (Exception e) {
+ throw handleException(e, false);
+ }
+ }
+
+ private void checkWidgetsBundle(WidgetsBundle widgetsBundle, boolean modify) throws ThingsboardException {
+ checkNotNull(widgetsBundle);
+ if (widgetsBundle.getTenantId() != null && !widgetsBundle.getTenantId().getId().equals(ModelConstants.NULL_UUID)) {
+ checkTenantId(widgetsBundle.getTenantId());
+ } else if (modify && getCurrentUser().getAuthority() != Authority.SYS_ADMIN) {
+ throw new ThingsboardException("You don't have permission to perform this operation!",
+ ThingsboardErrorCode.PERMISSION_DENIED);
+ }
+ }
+
+ WidgetType checkWidgetTypeId(WidgetTypeId widgetTypeId, boolean modify) throws ThingsboardException {
+ try {
+ validateId(widgetTypeId, "Incorrect widgetTypeId " + widgetTypeId);
+ WidgetType widgetType = widgetTypeService.findWidgetTypeById(widgetTypeId);
+ checkWidgetType(widgetType, modify);
+ return widgetType;
+ } catch (Exception e) {
+ throw handleException(e, false);
+ }
+ }
+
+ void checkWidgetType(WidgetType widgetType, boolean modify) throws ThingsboardException {
+ checkNotNull(widgetType);
+ if (widgetType.getTenantId() != null && !widgetType.getTenantId().getId().equals(ModelConstants.NULL_UUID)) {
+ checkTenantId(widgetType.getTenantId());
+ } else if (modify && getCurrentUser().getAuthority() != Authority.SYS_ADMIN) {
+ throw new ThingsboardException("You don't have permission to perform this operation!",
+ ThingsboardErrorCode.PERMISSION_DENIED);
+ }
+ }
+
+ Dashboard checkDashboardId(DashboardId dashboardId) throws ThingsboardException {
+ try {
+ validateId(dashboardId, "Incorrect dashboardId " + dashboardId);
+ Dashboard dashboard = dashboardService.findDashboardById(dashboardId);
+ checkDashboard(dashboard);
+ return dashboard;
+ } catch (Exception e) {
+ throw handleException(e, false);
+ }
+ }
+
+ private void checkDashboard(Dashboard dashboard) throws ThingsboardException {
+ checkNotNull(dashboard);
+ checkTenantId(dashboard.getTenantId());
+ if (dashboard.getCustomerId() != null && !dashboard.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
+ checkCustomerId(dashboard.getCustomerId());
+ }
+ }
+
+ ComponentDescriptor checkComponentDescriptorByClazz(String clazz) throws ThingsboardException {
+ try {
+ log.debug("[{}] Lookup component descriptor", clazz);
+ ComponentDescriptor componentDescriptor = checkNotNull(componentDescriptorService.getComponent(clazz));
+ return componentDescriptor;
+ } catch (Exception e) {
+ throw handleException(e, false);
+ }
+ }
+
+ List<ComponentDescriptor> checkComponentDescriptorsByType(ComponentType type) throws ThingsboardException {
+ try {
+ log.debug("[{}] Lookup component descriptors", type);
+ return componentDescriptorService.getComponents(type);
+ } catch (Exception e) {
+ throw handleException(e, false);
+ }
+ }
+
+ List<ComponentDescriptor> checkPluginActionsByPluginClazz(String pluginClazz) throws ThingsboardException {
+ try {
+ checkComponentDescriptorByClazz(pluginClazz);
+ log.debug("[{}] Lookup plugin actions", pluginClazz);
+ return componentDescriptorService.getPluginActions(pluginClazz);
+ } catch (Exception e) {
+ throw handleException(e, false);
+ }
+ }
+
+ protected PluginMetaData checkPlugin(PluginMetaData plugin) throws ThingsboardException {
+ checkNotNull(plugin);
+ SecurityUser authUser = getCurrentUser();
+ TenantId tenantId = plugin.getTenantId();
+ validateId(tenantId, "Incorrect tenantId " + tenantId);
+ if (authUser.getAuthority() != Authority.SYS_ADMIN) {
+ if (authUser.getTenantId() == null ||
+ !tenantId.getId().equals(ModelConstants.NULL_UUID) && !authUser.getTenantId().equals(tenantId)) {
+ throw new ThingsboardException("You don't have permission to perform this operation!",
+ ThingsboardErrorCode.PERMISSION_DENIED);
+
+ } else if (tenantId.getId().equals(ModelConstants.NULL_UUID)) {
+ plugin.setConfiguration(null);
+ }
+ }
+ return plugin;
+ }
+
+ protected RuleMetaData checkRule(RuleMetaData rule) throws ThingsboardException {
+ checkNotNull(rule);
+ checkTenantId(rule.getTenantId());
+ return rule;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java
new file mode 100644
index 0000000..0744dd1
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java
@@ -0,0 +1,66 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.exception.ThingsboardException;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api")
+public class ComponentDescriptorController extends BaseController {
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')")
+ @RequestMapping(value = "/component/{componentDescriptorClazz:.+}", method = RequestMethod.GET)
+ @ResponseBody
+ public ComponentDescriptor getComponentDescriptorByClazz(@PathVariable("componentDescriptorClazz") String strComponentDescriptorClazz) throws ThingsboardException {
+ checkParameter("strComponentDescriptorClazz", strComponentDescriptorClazz);
+ try {
+ return checkComponentDescriptorByClazz(strComponentDescriptorClazz);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')")
+ @RequestMapping(value = "/components/{componentType}", method = RequestMethod.GET)
+ @ResponseBody
+ public List<ComponentDescriptor> getComponentDescriptorsByType(@PathVariable("componentType") String strComponentType) throws ThingsboardException {
+ checkParameter("componentType", strComponentType);
+ try {
+ return checkComponentDescriptorsByType(ComponentType.valueOf(strComponentType));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')")
+ @RequestMapping(value = "/components/actions/{pluginClazz:.+}", method = RequestMethod.GET)
+ @ResponseBody
+ public List<ComponentDescriptor> getPluginActionsByPluginClazz(@PathVariable("pluginClazz") String pluginClazz) throws ThingsboardException {
+ checkParameter("pluginClazz", pluginClazz);
+ try {
+ return checkPluginActionsByPluginClazz(pluginClazz);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java
new file mode 100644
index 0000000..93b4267
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java
@@ -0,0 +1,87 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.exception.ThingsboardException;
+
+@RestController
+@RequestMapping("/api")
+public class CustomerController extends BaseController {
+
+ @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/customer/{customerId}", method = RequestMethod.GET)
+ @ResponseBody
+ public Customer getCustomerById(@PathVariable("customerId") String strCustomerId) throws ThingsboardException {
+ checkParameter("customerId", strCustomerId);
+ try {
+ CustomerId customerId = new CustomerId(toUUID(strCustomerId));
+ return checkCustomerId(customerId);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/customer", method = RequestMethod.POST)
+ @ResponseBody
+ public Customer saveCustomer(@RequestBody Customer customer) throws ThingsboardException {
+ try {
+ customer.setTenantId(getCurrentUser().getTenantId());
+ return checkNotNull(customerService.saveCustomer(customer));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/customer/{customerId}", method = RequestMethod.DELETE)
+ @ResponseStatus(value = HttpStatus.OK)
+ public void deleteCustomer(@PathVariable("customerId") String strCustomerId) throws ThingsboardException {
+ checkParameter("customerId", strCustomerId);
+ try {
+ CustomerId customerId = new CustomerId(toUUID(strCustomerId));
+ checkCustomerId(customerId);
+ customerService.deleteCustomer(customerId);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/customers", params = { "limit" }, method = RequestMethod.GET)
+ @ResponseBody
+ public TextPageData<Customer> getCustomers(@RequestParam int limit,
+ @RequestParam(required = false) String textSearch,
+ @RequestParam(required = false) String idOffset,
+ @RequestParam(required = false) String textOffset) throws ThingsboardException {
+ try {
+ TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
+ TenantId tenantId = getCurrentUser().getTenantId();
+ return checkNotNull(customerService.findCustomersByTenantId(tenantId, pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
new file mode 100644
index 0000000..6ce92c9
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
@@ -0,0 +1,148 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import org.thingsboard.server.common.data.Dashboard;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DashboardId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.dao.exception.IncorrectParameterException;
+import org.thingsboard.server.dao.model.ModelConstants;
+import org.thingsboard.server.exception.ThingsboardException;
+
+@RestController
+@RequestMapping("/api")
+public class DashboardController extends BaseController {
+
+ @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/dashboard/{dashboardId}", method = RequestMethod.GET)
+ @ResponseBody
+ public Dashboard getDashboardById(@PathVariable("dashboardId") String strDashboardId) throws ThingsboardException {
+ checkParameter("dashboardId", strDashboardId);
+ try {
+ DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
+ return checkDashboardId(dashboardId);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/dashboard", method = RequestMethod.POST)
+ @ResponseBody
+ public Dashboard saveDashboard(@RequestBody Dashboard dashboard) throws ThingsboardException {
+ try {
+ dashboard.setTenantId(getCurrentUser().getTenantId());
+ return checkNotNull(dashboardService.saveDashboard(dashboard));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/dashboard/{dashboardId}", method = RequestMethod.DELETE)
+ @ResponseStatus(value = HttpStatus.OK)
+ public void deleteDashboard(@PathVariable("dashboardId") String strDashboardId) throws ThingsboardException {
+ checkParameter("dashboardId", strDashboardId);
+ try {
+ DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
+ checkDashboardId(dashboardId);
+ dashboardService.deleteDashboard(dashboardId);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/customer/{customerId}/dashboard/{dashboardId}", method = RequestMethod.POST)
+ @ResponseBody
+ public Dashboard assignDashboardToCustomer(@PathVariable("customerId") String strCustomerId,
+ @PathVariable("dashboardId") String strDashboardId) throws ThingsboardException {
+ checkParameter("customerId", strCustomerId);
+ checkParameter("dashboardId", strDashboardId);
+ try {
+ CustomerId customerId = new CustomerId(toUUID(strCustomerId));
+ checkCustomerId(customerId);
+
+ DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
+ checkDashboardId(dashboardId);
+
+ return checkNotNull(dashboardService.assignDashboardToCustomer(dashboardId, customerId));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/customer/dashboard/{dashboardId}", method = RequestMethod.DELETE)
+ @ResponseBody
+ public Dashboard unassignDashboardFromCustomer(@PathVariable("dashboardId") String strDashboardId) throws ThingsboardException {
+ checkParameter("dashboardId", strDashboardId);
+ try {
+ DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
+ Dashboard dashboard = checkDashboardId(dashboardId);
+ if (dashboard.getCustomerId() == null || dashboard.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
+ throw new IncorrectParameterException("Dashboard isn't assigned to any customer!");
+ }
+ return checkNotNull(dashboardService.unassignDashboardFromCustomer(dashboardId));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/tenant/dashboards", params = { "limit" }, method = RequestMethod.GET)
+ @ResponseBody
+ public TextPageData<Dashboard> getTenantDashboards(
+ @RequestParam int limit,
+ @RequestParam(required = false) String textSearch,
+ @RequestParam(required = false) String idOffset,
+ @RequestParam(required = false) String textOffset) throws ThingsboardException {
+ try {
+ TenantId tenantId = getCurrentUser().getTenantId();
+ TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
+ return checkNotNull(dashboardService.findDashboardsByTenantId(tenantId, pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/customer/{customerId}/dashboards", params = { "limit" }, method = RequestMethod.GET)
+ @ResponseBody
+ public TextPageData<Dashboard> getCustomerDashboards(
+ @PathVariable("customerId") String strCustomerId,
+ @RequestParam int limit,
+ @RequestParam(required = false) String textSearch,
+ @RequestParam(required = false) String idOffset,
+ @RequestParam(required = false) String textOffset) throws ThingsboardException {
+ checkParameter("customerId", strCustomerId);
+ try {
+ TenantId tenantId = getCurrentUser().getTenantId();
+ CustomerId customerId = new CustomerId(toUUID(strCustomerId));
+ checkCustomerId(customerId);
+ TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
+ return checkNotNull(dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
new file mode 100644
index 0000000..1c0a7be
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
@@ -0,0 +1,176 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.security.DeviceCredentials;
+import org.thingsboard.server.dao.exception.IncorrectParameterException;
+import org.thingsboard.server.dao.model.ModelConstants;
+import org.thingsboard.server.exception.ThingsboardException;
+
+@RestController
+@RequestMapping("/api")
+public class DeviceController extends BaseController {
+
+ @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/device/{deviceId}", method = RequestMethod.GET)
+ @ResponseBody
+ public Device getDeviceById(@PathVariable("deviceId") String strDeviceId) throws ThingsboardException {
+ checkParameter("deviceId", strDeviceId);
+ try {
+ DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
+ return checkDeviceId(deviceId);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/device", method = RequestMethod.POST)
+ @ResponseBody
+ public Device saveDevice(@RequestBody Device device) throws ThingsboardException {
+ try {
+ device.setTenantId(getCurrentUser().getTenantId());
+ return checkNotNull(deviceService.saveDevice(device));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/device/{deviceId}", method = RequestMethod.DELETE)
+ @ResponseStatus(value = HttpStatus.OK)
+ public void deleteDevice(@PathVariable("deviceId") String strDeviceId) throws ThingsboardException {
+ checkParameter("deviceId", strDeviceId);
+ try {
+ DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
+ checkDeviceId(deviceId);
+ deviceService.deleteDevice(deviceId);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/customer/{customerId}/device/{deviceId}", method = RequestMethod.POST)
+ @ResponseBody
+ public Device assignDeviceToCustomer(@PathVariable("customerId") String strCustomerId,
+ @PathVariable("deviceId") String strDeviceId) throws ThingsboardException {
+ checkParameter("customerId", strCustomerId);
+ checkParameter("deviceId", strDeviceId);
+ try {
+ CustomerId customerId = new CustomerId(toUUID(strCustomerId));
+ checkCustomerId(customerId);
+
+ DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
+ checkDeviceId(deviceId);
+
+ return checkNotNull(deviceService.assignDeviceToCustomer(deviceId, customerId));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/customer/device/{deviceId}", method = RequestMethod.DELETE)
+ @ResponseBody
+ public Device unassignDeviceFromCustomer(@PathVariable("deviceId") String strDeviceId) throws ThingsboardException {
+ checkParameter("deviceId", strDeviceId);
+ try {
+ DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
+ Device device = checkDeviceId(deviceId);
+ if (device.getCustomerId() == null || device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
+ throw new IncorrectParameterException("Device isn't assigned to any customer!");
+ }
+ return checkNotNull(deviceService.unassignDeviceFromCustomer(deviceId));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/device/{deviceId}/credentials", method = RequestMethod.GET)
+ @ResponseBody
+ public DeviceCredentials getDeviceCredentialsByDeviceId(@PathVariable("deviceId") String strDeviceId) throws ThingsboardException {
+ checkParameter("deviceId", strDeviceId);
+ try {
+ DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
+ checkDeviceId(deviceId);
+ return checkNotNull(deviceCredentialsService.findDeviceCredentialsByDeviceId(deviceId));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/device/credentials", method = RequestMethod.POST)
+ @ResponseBody
+ public DeviceCredentials saveDeviceCredentials(@RequestBody DeviceCredentials deviceCredentials) throws ThingsboardException {
+ checkNotNull(deviceCredentials);
+ try {
+ checkDeviceId(deviceCredentials.getDeviceId());
+ return checkNotNull(deviceCredentialsService.updateDeviceCredentials(deviceCredentials));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/tenant/devices", params = { "limit" }, method = RequestMethod.GET)
+ @ResponseBody
+ public TextPageData<Device> getTenantDevices(
+ @RequestParam int limit,
+ @RequestParam(required = false) String textSearch,
+ @RequestParam(required = false) String idOffset,
+ @RequestParam(required = false) String textOffset) throws ThingsboardException {
+ try {
+ TenantId tenantId = getCurrentUser().getTenantId();
+ TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
+ return checkNotNull(deviceService.findDevicesByTenantId(tenantId, pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/customer/{customerId}/devices", params = { "limit" }, method = RequestMethod.GET)
+ @ResponseBody
+ public TextPageData<Device> getCustomerDevices(
+ @PathVariable("customerId") String strCustomerId,
+ @RequestParam int limit,
+ @RequestParam(required = false) String textSearch,
+ @RequestParam(required = false) String idOffset,
+ @RequestParam(required = false) String textOffset) throws ThingsboardException {
+ checkParameter("customerId", strCustomerId);
+ try {
+ TenantId tenantId = getCurrentUser().getTenantId();
+ CustomerId customerId = new CustomerId(toUUID(strCustomerId));
+ checkCustomerId(customerId);
+ TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
+ return checkNotNull(deviceService.findDevicesByTenantIdAndCustomerId(tenantId, customerId, pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/EventController.java b/application/src/main/java/org/thingsboard/server/controller/EventController.java
new file mode 100644
index 0000000..2ff1a73
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/EventController.java
@@ -0,0 +1,116 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.data.id.*;
+import org.thingsboard.server.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.dao.event.EventService;
+import org.thingsboard.server.dao.model.ModelConstants;
+import org.thingsboard.server.exception.ThingsboardErrorCode;
+import org.thingsboard.server.exception.ThingsboardException;
+
+@RestController
+@RequestMapping("/api")
+public class EventController extends BaseController {
+
+ @Autowired
+ private EventService eventService;
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/events/{entityType}/{entityId}/{eventType}", method = RequestMethod.GET)
+ @ResponseBody
+ public TimePageData<Event> getEvents(
+ @PathVariable("entityType") String strEntityType,
+ @PathVariable("entityId") String strEntityId,
+ @PathVariable("eventType") String eventType,
+ @RequestParam("tenantId") String strTenantId,
+ @RequestParam int limit,
+ @RequestParam(required = false) Long startTime,
+ @RequestParam(required = false) Long endTime,
+ @RequestParam(required = false, defaultValue = "false") boolean ascOrder,
+ @RequestParam(required = false) String offset
+ ) throws ThingsboardException {
+ checkParameter("EntityId", strEntityId);
+ checkParameter("EntityType", strEntityType);
+ try {
+ TenantId tenantId = new TenantId(toUUID(strTenantId));
+ if (!tenantId.getId().equals(ModelConstants.NULL_UUID) &&
+ !tenantId.equals(getCurrentUser().getTenantId())) {
+ throw new ThingsboardException("You don't have permission to perform this operation!",
+ ThingsboardErrorCode.PERMISSION_DENIED);
+ }
+ TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset);
+ return checkNotNull(eventService.findEvents(tenantId, getEntityId(strEntityType, strEntityId), eventType, pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/events/{entityType}/{entityId}", method = RequestMethod.GET)
+ @ResponseBody
+ public TimePageData<Event> getEvents(
+ @PathVariable("entityType") String strEntityType,
+ @PathVariable("entityId") String strEntityId,
+ @RequestParam("tenantId") String strTenantId,
+ @RequestParam int limit,
+ @RequestParam(required = false) Long startTime,
+ @RequestParam(required = false) Long endTime,
+ @RequestParam(required = false, defaultValue = "false") boolean ascOrder,
+ @RequestParam(required = false) String offset
+ ) throws ThingsboardException {
+ checkParameter("EntityId", strEntityId);
+ checkParameter("EntityType", strEntityType);
+ try {
+ TenantId tenantId = new TenantId(toUUID(strTenantId));
+ if (!tenantId.getId().equals(ModelConstants.NULL_UUID) &&
+ !tenantId.equals(getCurrentUser().getTenantId())) {
+ throw new ThingsboardException("You don't have permission to perform this operation!",
+ ThingsboardErrorCode.PERMISSION_DENIED);
+ }
+ TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset);
+ return checkNotNull(eventService.findEvents(tenantId, getEntityId(strEntityType, strEntityId), pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+
+ private EntityId getEntityId(String strEntityType, String strEntityId) throws ThingsboardException {
+ EntityId entityId;
+ EntityType entityType = EntityType.valueOf(strEntityType);
+ switch (entityType) {
+ case RULE:
+ entityId = new RuleId(toUUID(strEntityId));
+ break;
+ case PLUGIN:
+ entityId = new PluginId(toUUID(strEntityId));
+ break;
+ case DEVICE:
+ entityId = new DeviceId(toUUID(strEntityId));
+ break;
+ default:
+ throw new ThingsboardException("EntityType ['" + entityType + "'] is incorrect!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
+ }
+ return entityId;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/PluginApiController.java b/application/src/main/java/org/thingsboard/server/controller/plugin/PluginApiController.java
new file mode 100644
index 0000000..d9b0ab4
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/plugin/PluginApiController.java
@@ -0,0 +1,108 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller.plugin;
+
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.RequestEntity;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.context.request.async.DeferredResult;
+import org.thingsboard.server.actors.service.ActorService;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.PluginMetaData;
+import org.thingsboard.server.controller.BaseController;
+import org.thingsboard.server.dao.model.ModelConstants;
+import org.thingsboard.server.dao.plugin.PluginService;
+import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.extensions.api.plugins.PluginApiCallSecurityContext;
+import org.thingsboard.server.extensions.api.plugins.PluginConstants;
+import org.thingsboard.server.extensions.api.plugins.rest.BasicPluginRestMsg;
+import org.thingsboard.server.extensions.api.plugins.rest.RestRequest;
+
+import javax.servlet.http.HttpServletRequest;
+
+@RestController
+@RequestMapping(PluginConstants.PLUGIN_URL_PREFIX)
+@Slf4j
+public class PluginApiController extends BaseController {
+
+ @Autowired
+ private ActorService actorService;
+
+ @Autowired
+ private PluginService pluginService;
+
+ @SuppressWarnings("rawtypes")
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/{pluginToken}/**")
+ @ResponseStatus(value = HttpStatus.OK)
+ public DeferredResult<ResponseEntity> processRequest(
+ @PathVariable("pluginToken") String pluginToken,
+ RequestEntity<byte[]> requestEntity,
+ HttpServletRequest request)
+ throws ThingsboardException {
+ log.debug("[{}] Going to process requst uri: {}", pluginToken, requestEntity.getUrl());
+ DeferredResult<ResponseEntity> result = new DeferredResult<ResponseEntity>();
+ PluginMetaData pluginMd = pluginService.findPluginByApiToken(pluginToken);
+ if (pluginMd == null) {
+ result.setErrorResult(new PluginNotFoundException("Plugin with token: " + pluginToken + " not found!"));
+ } else {
+ TenantId tenantId = getCurrentUser().getTenantId();
+ CustomerId customerId = getCurrentUser().getCustomerId();
+ if (validatePluginAccess(pluginMd, tenantId, customerId)) {
+ if(ModelConstants.NULL_UUID.equals(tenantId.getId())){
+ tenantId = null;
+ }
+ PluginApiCallSecurityContext securityCtx = new PluginApiCallSecurityContext(pluginMd.getTenantId(), pluginMd.getId(), tenantId, customerId);
+ actorService.process(new BasicPluginRestMsg(securityCtx, new RestRequest(requestEntity, request), result));
+ } else {
+ result.setResult(new ResponseEntity<>(HttpStatus.FORBIDDEN));
+ }
+
+ }
+ return result;
+ }
+
+ public static boolean validatePluginAccess(PluginMetaData pluginMd, TenantId tenantId, CustomerId customerId) {
+ boolean systemAdministrator = tenantId == null || ModelConstants.NULL_UUID.equals(tenantId.getId());
+ boolean tenantAdministrator = !systemAdministrator && (customerId == null || ModelConstants.NULL_UUID.equals(customerId.getId()));
+ boolean systemPlugin = ModelConstants.NULL_UUID.equals(pluginMd.getTenantId().getId());
+
+ boolean validUser = false;
+ if (systemPlugin) {
+ if (pluginMd.isPublicAccess() || systemAdministrator) {
+ // All users can access public system plugins. Only system
+ // users can access private system plugins
+ validUser = true;
+ }
+ } else {
+ if ((pluginMd.isPublicAccess() || tenantAdministrator) && tenantId.equals(pluginMd.getTenantId())) {
+ // All tenant users can access public tenant plugins. Only tenant
+ // administrator can access private tenant plugins
+ validUser = true;
+ }
+ }
+ return validUser;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/PluginNotFoundException.java b/application/src/main/java/org/thingsboard/server/controller/plugin/PluginNotFoundException.java
new file mode 100644
index 0000000..b105bb8
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/plugin/PluginNotFoundException.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller.plugin;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(HttpStatus.NOT_FOUND)
+public class PluginNotFoundException extends RuntimeException {
+
+ private static final long serialVersionUID = 1L;
+
+ public PluginNotFoundException(String message){
+ super(message);
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/PluginWebSocketHandler.java b/application/src/main/java/org/thingsboard/server/controller/plugin/PluginWebSocketHandler.java
new file mode 100644
index 0000000..dbc1c81
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/plugin/PluginWebSocketHandler.java
@@ -0,0 +1,203 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller.plugin;
+
+import java.io.IOException;
+import java.net.URI;
+import java.security.InvalidParameterException;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.thingsboard.server.actors.service.ActorService;
+import org.thingsboard.server.config.WebSocketConfiguration;
+import org.thingsboard.server.extensions.api.plugins.PluginConstants;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.PluginMetaData;
+import org.thingsboard.server.dao.plugin.PluginService;
+import org.thingsboard.server.extensions.api.plugins.PluginApiCallSecurityContext;
+import org.thingsboard.server.extensions.api.plugins.ws.BasicPluginWebsocketSessionRef;
+import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
+import org.thingsboard.server.extensions.api.plugins.ws.SessionEvent;
+import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg;
+import org.thingsboard.server.extensions.api.plugins.ws.msg.SessionEventPluginWebSocketMsg;
+import org.thingsboard.server.extensions.api.plugins.ws.msg.TextPluginWebSocketMsg;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+import org.springframework.web.socket.handler.TextWebSocketHandler;
+
+@Service
+@Slf4j
+public class PluginWebSocketHandler extends TextWebSocketHandler implements PluginWebSocketMsgEndpoint {
+
+ private static final ConcurrentMap<String, SessionMetaData> internalSessionMap = new ConcurrentHashMap<>();
+ private static final ConcurrentMap<String, String> externalSessionMap = new ConcurrentHashMap<>();
+
+ @Autowired @Lazy
+ private ActorService actorService;
+
+ @Autowired @Lazy
+ private PluginService pluginService;
+
+ @Override
+ public void handleTextMessage(WebSocketSession session, TextMessage message) {
+ try {
+ log.info("[{}] Processing {}", session.getId(), message);
+ SessionMetaData sessionMd = internalSessionMap.get(session.getId());
+ if (sessionMd != null) {
+ actorService.process(new TextPluginWebSocketMsg(sessionMd.sessionRef, message.getPayload()));
+ } else {
+ log.warn("[{}] Failed to find session", session.getId());
+ session.close(CloseStatus.SERVER_ERROR.withReason("Session not found!"));
+ }
+ session.sendMessage(message);
+ } catch (IOException e) {
+ log.warn("IO error", e);
+ }
+ }
+
+ @Override
+ public void afterConnectionEstablished(WebSocketSession session) throws Exception {
+ super.afterConnectionEstablished(session);
+ try {
+ String internalSessionId = session.getId();
+ PluginWebsocketSessionRef sessionRef = toRef(session);
+ String externalSessionId = sessionRef.getSessionId();
+ internalSessionMap.put(internalSessionId, new SessionMetaData(session, sessionRef));
+ externalSessionMap.put(externalSessionId, internalSessionId);
+ actorService.process(new SessionEventPluginWebSocketMsg(sessionRef, SessionEvent.onEstablished()));
+ log.info("[{}][{}] Session is started", externalSessionId, session.getId());
+ } catch (InvalidParameterException e) {
+ log.warn("[[{}] Failed to start session", session.getId(), e);
+ session.close(CloseStatus.BAD_DATA.withReason(e.getMessage()));
+ } catch (Exception e) {
+ log.warn("[{}] Failed to start session", session.getId(), e);
+ session.close(CloseStatus.SERVER_ERROR.withReason(e.getMessage()));
+ }
+ }
+
+ @Override
+ public void handleTransportError(WebSocketSession session, Throwable tError) throws Exception {
+ super.handleTransportError(session, tError);
+ SessionMetaData sessionMd = internalSessionMap.get(session.getId());
+ if (sessionMd != null) {
+ actorService.process(new SessionEventPluginWebSocketMsg(sessionMd.sessionRef, SessionEvent.onError(tError)));
+ } else {
+ log.warn("[{}] Failed to find session", session.getId());
+ }
+ log.trace("[{}] Session transport error", session.getId(), tError);
+ }
+
+ @Override
+ public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
+ super.afterConnectionClosed(session, closeStatus);
+ SessionMetaData sessionMd = internalSessionMap.remove(session.getId());
+ if (sessionMd != null) {
+ externalSessionMap.remove(sessionMd.sessionRef.getSessionId());
+ actorService.process(new SessionEventPluginWebSocketMsg(sessionMd.sessionRef, SessionEvent.onClosed()));
+ }
+ log.info("[{}] Session is closed", session.getId());
+ }
+
+ private PluginWebsocketSessionRef toRef(WebSocketSession session) throws IOException {
+ URI sessionUri = session.getUri();
+ String path = sessionUri.getPath();
+ path = path.substring(WebSocketConfiguration.WS_PLUGIN_PREFIX.length());
+ if (path.length() == 0) {
+ throw new IllegalArgumentException("URL should contain plugin token!");
+ }
+ String[] pathElements = path.split("/");
+ String pluginToken = pathElements[0];
+ // TODO: cache
+ PluginMetaData pluginMd = pluginService.findPluginByApiToken(pluginToken);
+ if (pluginMd == null) {
+ throw new InvalidParameterException("Can't find plugin with specified token!");
+ } else {
+ SecurityUser currentUser = (SecurityUser) session.getAttributes().get(WebSocketConfiguration.WS_SECURITY_USER_ATTRIBUTE);
+ TenantId tenantId = currentUser.getTenantId();
+ CustomerId customerId = currentUser.getCustomerId();
+ if (PluginApiController.validatePluginAccess(pluginMd, tenantId, customerId)) {
+ PluginApiCallSecurityContext securityCtx = new PluginApiCallSecurityContext(pluginMd.getTenantId(), pluginMd.getId(), tenantId,
+ currentUser.getCustomerId());
+ return new BasicPluginWebsocketSessionRef(UUID.randomUUID().toString(), securityCtx, session.getUri(), session.getAttributes(),
+ session.getLocalAddress(), session.getRemoteAddress());
+ } else {
+ throw new SecurityException("Current user is not allowed to use this plugin!");
+ }
+ }
+ }
+
+ private static class SessionMetaData {
+ private final WebSocketSession session;
+ private final PluginWebsocketSessionRef sessionRef;
+
+ public SessionMetaData(WebSocketSession session, PluginWebsocketSessionRef sessionRef) {
+ super();
+ this.session = session;
+ this.sessionRef = sessionRef;
+ }
+ }
+
+ @Override
+ public void send(PluginWebsocketMsg<?> wsMsg) throws IOException {
+ PluginWebsocketSessionRef sessionRef = wsMsg.getSessionRef();
+ String externalId = sessionRef.getSessionId();
+ log.debug("[{}] Processing {}", externalId, wsMsg);
+ String internalId = externalSessionMap.get(externalId);
+ if (internalId != null) {
+ SessionMetaData sessionMd = internalSessionMap.get(internalId);
+ if (sessionMd != null) {
+ if (wsMsg instanceof TextPluginWebSocketMsg) {
+ String payload = ((TextPluginWebSocketMsg) wsMsg).getPayload();
+ sessionMd.session.sendMessage(new TextMessage(payload));
+ }
+ } else {
+ log.warn("[{}][{}] Failed to find session by internal id", externalId, internalId);
+ }
+ } else {
+ log.warn("[{}] Failed to find session by external id", externalId);
+ }
+ }
+
+ @Override
+ public void close(PluginWebsocketSessionRef sessionRef) throws IOException {
+ String externalId = sessionRef.getSessionId();
+ log.debug("[{}] Processing close request", externalId);
+ String internalId = externalSessionMap.get(externalId);
+ if (internalId != null) {
+ SessionMetaData sessionMd = internalSessionMap.get(internalId);
+ if (sessionMd != null) {
+ sessionMd.session.close(CloseStatus.NORMAL);
+ } else {
+ log.warn("[{}][{}] Failed to find session by internal id", externalId, internalId);
+ }
+ } else {
+ log.warn("[{}] Failed to find session by external id", externalId);
+ }
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/PluginWebSocketMsgEndpoint.java b/application/src/main/java/org/thingsboard/server/controller/plugin/PluginWebSocketMsgEndpoint.java
new file mode 100644
index 0000000..75b6012
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/plugin/PluginWebSocketMsgEndpoint.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller.plugin;
+
+import java.io.IOException;
+
+import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
+import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg;
+
+public interface PluginWebSocketMsgEndpoint {
+
+ void send(PluginWebsocketMsg<?> wsMsg) throws IOException;
+
+ void close(PluginWebsocketSessionRef sessionRef) throws IOException;
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/PluginController.java b/application/src/main/java/org/thingsboard/server/controller/PluginController.java
new file mode 100644
index 0000000..6f06eb3
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/PluginController.java
@@ -0,0 +1,197 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.data.plugin.PluginMetaData;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.common.data.widget.WidgetsBundle;
+import org.thingsboard.server.dao.model.ModelConstants;
+import org.thingsboard.server.exception.ThingsboardException;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api")
+public class PluginController extends BaseController {
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @RequestMapping(value = "/plugin/{pluginId}", method = RequestMethod.GET)
+ @ResponseBody
+ public PluginMetaData getPluginById(@PathVariable("pluginId") String strPluginId) throws ThingsboardException {
+ checkParameter("pluginId", strPluginId);
+ try {
+ PluginId pluginId = new PluginId(toUUID(strPluginId));
+ return checkPlugin(pluginService.findPluginById(pluginId));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @RequestMapping(value = "/plugin/token/{pluginToken}", method = RequestMethod.GET)
+ @ResponseBody
+ public PluginMetaData getPluginByToken(@PathVariable("pluginToken") String pluginToken) throws ThingsboardException {
+ checkParameter("pluginToken", pluginToken);
+ try {
+ return checkPlugin(pluginService.findPluginByApiToken(pluginToken));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @RequestMapping(value = "/plugin", method = RequestMethod.POST)
+ @ResponseBody
+ public PluginMetaData savePlugin(@RequestBody PluginMetaData source) throws ThingsboardException {
+ try {
+ boolean created = source.getId() == null;
+ source.setTenantId(getCurrentUser().getTenantId());
+ PluginMetaData plugin = checkNotNull(pluginService.savePlugin(source));
+ actorService.onPluginStateChange(plugin.getTenantId(), plugin.getId(),
+ created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
+ return plugin;
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @RequestMapping(value = "/plugin/{pluginId}/activate", method = RequestMethod.POST)
+ @ResponseStatus(value = HttpStatus.OK)
+ public void activatePluginById(@PathVariable("pluginId") String strPluginId) throws ThingsboardException {
+ checkParameter("pluginId", strPluginId);
+ try {
+ PluginId pluginId = new PluginId(toUUID(strPluginId));
+ PluginMetaData plugin = checkPlugin(pluginService.findPluginById(pluginId));
+ pluginService.activatePluginById(pluginId);
+ actorService.onPluginStateChange(plugin.getTenantId(), plugin.getId(), ComponentLifecycleEvent.ACTIVATED);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @RequestMapping(value = "/plugin/{pluginId}/suspend", method = RequestMethod.POST)
+ @ResponseStatus(value = HttpStatus.OK)
+ public void suspendPluginById(@PathVariable("pluginId") String strPluginId) throws ThingsboardException {
+ checkParameter("pluginId", strPluginId);
+ try {
+ PluginId pluginId = new PluginId(toUUID(strPluginId));
+ PluginMetaData plugin = checkPlugin(pluginService.findPluginById(pluginId));
+ pluginService.suspendPluginById(pluginId);
+ actorService.onPluginStateChange(plugin.getTenantId(), plugin.getId(), ComponentLifecycleEvent.SUSPENDED);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('SYS_ADMIN')")
+ @RequestMapping(value = "/plugin/system", params = {"limit"}, method = RequestMethod.GET)
+ @ResponseBody
+ public TextPageData<PluginMetaData> getSystemPlugins(
+ @RequestParam int limit,
+ @RequestParam(required = false) String textSearch,
+ @RequestParam(required = false) String idOffset,
+ @RequestParam(required = false) String textOffset) throws ThingsboardException {
+ try {
+ TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
+ return checkNotNull(pluginService.findSystemPlugins(pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('SYS_ADMIN')")
+ @RequestMapping(value = "/plugin/tenant/{tenantId}", params = {"limit"}, method = RequestMethod.GET)
+ @ResponseBody
+ public TextPageData<PluginMetaData> getTenantPlugins(
+ @PathVariable("tenantId") String strTenantId,
+ @RequestParam int limit,
+ @RequestParam(required = false) String textSearch,
+ @RequestParam(required = false) String idOffset,
+ @RequestParam(required = false) String textOffset) throws ThingsboardException {
+ checkParameter("tenantId", strTenantId);
+ try {
+ TenantId tenantId = new TenantId(toUUID(strTenantId));
+ TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
+ return checkNotNull(pluginService.findTenantPlugins(tenantId, pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @RequestMapping(value = "/plugins", method = RequestMethod.GET)
+ @ResponseBody
+ public List<PluginMetaData> getPlugins() throws ThingsboardException {
+ try {
+ if (getCurrentUser().getAuthority() == Authority.SYS_ADMIN) {
+ return checkNotNull(pluginService.findSystemPlugins());
+ } else {
+ TenantId tenantId = getCurrentUser().getTenantId();
+ List<PluginMetaData> plugins = checkNotNull(pluginService.findAllTenantPluginsByTenantId(tenantId));
+ plugins.stream()
+ .filter(plugin -> plugin.getTenantId().getId().equals(ModelConstants.NULL_UUID))
+ .forEach(plugin -> plugin.setConfiguration(null));
+ return plugins;
+ }
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/plugin", params = {"limit"}, method = RequestMethod.GET)
+ @ResponseBody
+ public TextPageData<PluginMetaData> getTenantPlugins(
+ @RequestParam int limit,
+ @RequestParam(required = false) String textSearch,
+ @RequestParam(required = false) String idOffset,
+ @RequestParam(required = false) String textOffset) throws ThingsboardException {
+ try {
+ TenantId tenantId = getCurrentUser().getTenantId();
+ TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
+ return checkNotNull(pluginService.findTenantPlugins(tenantId, pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @RequestMapping(value = "/plugin/{pluginId}", method = RequestMethod.DELETE)
+ @ResponseStatus(value = HttpStatus.OK)
+ public void deletePlugin(@PathVariable("pluginId") String strPluginId) throws ThingsboardException {
+ checkParameter("pluginId", strPluginId);
+ try {
+ PluginId pluginId = new PluginId(toUUID(strPluginId));
+ PluginMetaData plugin = checkPlugin(pluginService.findPluginById(pluginId));
+ pluginService.deletePluginById(pluginId);
+ actorService.onPluginStateChange(plugin.getTenantId(), plugin.getId(), ComponentLifecycleEvent.DELETED);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleController.java b/application/src/main/java/org/thingsboard/server/controller/RuleController.java
new file mode 100644
index 0000000..3c04619
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/RuleController.java
@@ -0,0 +1,193 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.data.plugin.PluginMetaData;
+import org.thingsboard.server.common.data.rule.RuleMetaData;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.exception.ThingsboardException;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api")
+public class RuleController extends BaseController {
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @RequestMapping(value = "/rule/{ruleId}", method = RequestMethod.GET)
+ @ResponseBody
+ public RuleMetaData getRuleById(@PathVariable("ruleId") String strRuleId) throws ThingsboardException {
+ checkParameter("ruleId", strRuleId);
+ try {
+ RuleId ruleId = new RuleId(toUUID(strRuleId));
+ return checkRule(ruleService.findRuleById(ruleId));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @RequestMapping(value = "/rule/token/{pluginToken}", method = RequestMethod.GET)
+ @ResponseBody
+ public List<RuleMetaData> getRulesByPluginToken(@PathVariable("pluginToken") String pluginToken) throws ThingsboardException {
+ checkParameter("pluginToken", pluginToken);
+ try {
+ PluginMetaData plugin = checkPlugin(pluginService.findPluginByApiToken(pluginToken));
+ return ruleService.findPluginRules(plugin.getApiToken());
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @RequestMapping(value = "/rule", method = RequestMethod.POST)
+ @ResponseBody
+ public RuleMetaData saveRule(@RequestBody RuleMetaData source) throws ThingsboardException {
+ try {
+ boolean created = source.getId() == null;
+ source.setTenantId(getCurrentUser().getTenantId());
+ RuleMetaData rule = checkNotNull(ruleService.saveRule(source));
+ actorService.onRuleStateChange(rule.getTenantId(), rule.getId(),
+ created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
+ return rule;
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @RequestMapping(value = "/rule/{ruleId}/activate", method = RequestMethod.POST)
+ @ResponseStatus(value = HttpStatus.OK)
+ public void activateRuleById(@PathVariable("ruleId") String strRuleId) throws ThingsboardException {
+ checkParameter("ruleId", strRuleId);
+ try {
+ RuleId ruleId = new RuleId(toUUID(strRuleId));
+ RuleMetaData rule = checkRule(ruleService.findRuleById(ruleId));
+ ruleService.activateRuleById(ruleId);
+ actorService.onRuleStateChange(rule.getTenantId(), rule.getId(), ComponentLifecycleEvent.ACTIVATED);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @RequestMapping(value = "/rule/{ruleId}/suspend", method = RequestMethod.POST)
+ @ResponseStatus(value = HttpStatus.OK)
+ public void suspendRuleById(@PathVariable("ruleId") String strRuleId) throws ThingsboardException {
+ checkParameter("ruleId", strRuleId);
+ try {
+ RuleId ruleId = new RuleId(toUUID(strRuleId));
+ RuleMetaData rule = checkRule(ruleService.findRuleById(ruleId));
+ ruleService.suspendRuleById(ruleId);
+ actorService.onRuleStateChange(rule.getTenantId(), rule.getId(), ComponentLifecycleEvent.SUSPENDED);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('SYS_ADMIN')")
+ @RequestMapping(value = "/rule/system", params = {"limit"}, method = RequestMethod.GET)
+ @ResponseBody
+ public TextPageData<RuleMetaData> getSystemRules(
+ @RequestParam int limit,
+ @RequestParam(required = false) String textSearch,
+ @RequestParam(required = false) String idOffset,
+ @RequestParam(required = false) String textOffset) throws ThingsboardException {
+ try {
+ TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
+ return checkNotNull(ruleService.findSystemRules(pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('SYS_ADMIN')")
+ @RequestMapping(value = "/rule/tenant/{tenantId}", params = {"limit"}, method = RequestMethod.GET)
+ @ResponseBody
+ public TextPageData<RuleMetaData> getTenantRules(
+ @PathVariable("tenantId") String strTenantId,
+ @RequestParam int limit,
+ @RequestParam(required = false) String textSearch,
+ @RequestParam(required = false) String idOffset,
+ @RequestParam(required = false) String textOffset) throws ThingsboardException {
+ checkParameter("tenantId", strTenantId);
+ try {
+ TenantId tenantId = new TenantId(toUUID(strTenantId));
+ TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
+ return checkNotNull(ruleService.findTenantRules(tenantId, pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @RequestMapping(value = "/rules", method = RequestMethod.GET)
+ @ResponseBody
+ public List<RuleMetaData> getRules() throws ThingsboardException {
+ try {
+ if (getCurrentUser().getAuthority() == Authority.SYS_ADMIN) {
+ return checkNotNull(ruleService.findSystemRules());
+ } else {
+ TenantId tenantId = getCurrentUser().getTenantId();
+ return checkNotNull(ruleService.findAllTenantRulesByTenantId(tenantId));
+ }
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/rule", params = {"limit"}, method = RequestMethod.GET)
+ @ResponseBody
+ public TextPageData<RuleMetaData> getTenantRules(
+ @RequestParam int limit,
+ @RequestParam(required = false) String textSearch,
+ @RequestParam(required = false) String idOffset,
+ @RequestParam(required = false) String textOffset) throws ThingsboardException {
+ try {
+ TenantId tenantId = getCurrentUser().getTenantId();
+ TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
+ return checkNotNull(ruleService.findTenantRules(tenantId, pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @RequestMapping(value = "/rule/{ruleId}", method = RequestMethod.DELETE)
+ @ResponseStatus(value = HttpStatus.OK)
+ public void deleteRule(@PathVariable("ruleId") String strRuleId) throws ThingsboardException {
+ checkParameter("ruleId", strRuleId);
+ try {
+ RuleId ruleId = new RuleId(toUUID(strRuleId));
+ RuleMetaData rule = checkRule(ruleService.findRuleById(ruleId));
+ ruleService.deleteRuleById(ruleId);
+ actorService.onRuleStateChange(rule.getTenantId(), rule.getId(), ComponentLifecycleEvent.DELETED);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantController.java b/application/src/main/java/org/thingsboard/server/controller/TenantController.java
new file mode 100644
index 0000000..6dd38a3
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/TenantController.java
@@ -0,0 +1,89 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.dao.tenant.TenantService;
+import org.thingsboard.server.exception.ThingsboardException;
+
+@RestController
+@RequestMapping("/api")
+public class TenantController extends BaseController {
+
+ @Autowired
+ private TenantService tenantService;
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @RequestMapping(value = "/tenant/{tenantId}", method = RequestMethod.GET)
+ @ResponseBody
+ public Tenant getTenantById(@PathVariable("tenantId") String strTenantId) throws ThingsboardException {
+ checkParameter("tenantId", strTenantId);
+ try {
+ TenantId tenantId = new TenantId(toUUID(strTenantId));
+ checkTenantId(tenantId);
+ return checkNotNull(tenantService.findTenantById(tenantId));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('SYS_ADMIN')")
+ @RequestMapping(value = "/tenant", method = RequestMethod.POST)
+ @ResponseBody
+ public Tenant saveTenant(@RequestBody Tenant tenant) throws ThingsboardException {
+ try {
+ return checkNotNull(tenantService.saveTenant(tenant));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('SYS_ADMIN')")
+ @RequestMapping(value = "/tenant/{tenantId}", method = RequestMethod.DELETE)
+ @ResponseStatus(value = HttpStatus.OK)
+ public void deleteTenant(@PathVariable("tenantId") String strTenantId) throws ThingsboardException {
+ checkParameter("tenantId", strTenantId);
+ try {
+ TenantId tenantId = new TenantId(toUUID(strTenantId));
+ tenantService.deleteTenant(tenantId);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('SYS_ADMIN')")
+ @RequestMapping(value = "/tenants", params = { "limit" }, method = RequestMethod.GET)
+ @ResponseBody
+ public TextPageData<Tenant> getTenants(@RequestParam int limit,
+ @RequestParam(required = false) String textSearch,
+ @RequestParam(required = false) String idOffset,
+ @RequestParam(required = false) String textOffset) throws ThingsboardException {
+ try {
+ TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
+ return checkNotNull(tenantService.findTenants(pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java
new file mode 100644
index 0000000..2c436f0
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java
@@ -0,0 +1,179 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UserId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.common.data.security.UserCredentials;
+import org.thingsboard.server.exception.ThingsboardErrorCode;
+import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.service.mail.MailService;
+import org.thingsboard.server.service.security.model.SecurityUser;
+
+import javax.servlet.http.HttpServletRequest;
+
+@RestController
+@RequestMapping("/api")
+public class UserController extends BaseController {
+
+ @Autowired
+ private MailService mailService;
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/user/{userId}", method = RequestMethod.GET)
+ @ResponseBody
+ public User getUserById(@PathVariable("userId") String strUserId) throws ThingsboardException {
+ checkParameter("userId", strUserId);
+ try {
+ UserId userId = new UserId(toUUID(strUserId));
+ SecurityUser authUser = getCurrentUser();
+ if (authUser.getAuthority() == Authority.CUSTOMER_USER && !authUser.getId().equals(userId)) {
+ throw new ThingsboardException("You don't have permission to perform this operation!",
+ ThingsboardErrorCode.PERMISSION_DENIED);
+ }
+ return checkUserId(userId);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/user", method = RequestMethod.POST)
+ @ResponseBody
+ public User saveUser(@RequestBody User user,
+ HttpServletRequest request) throws ThingsboardException {
+ try {
+ SecurityUser authUser = getCurrentUser();
+ if (authUser.getAuthority() == Authority.CUSTOMER_USER && !authUser.getId().equals(user.getId())) {
+ throw new ThingsboardException("You don't have permission to perform this operation!",
+ ThingsboardErrorCode.PERMISSION_DENIED);
+ }
+ boolean sendEmail = user.getId() == null;
+ if (getCurrentUser().getAuthority() == Authority.TENANT_ADMIN) {
+ user.setTenantId(getCurrentUser().getTenantId());
+ }
+ User savedUser = checkNotNull(userService.saveUser(user));
+ if (sendEmail) {
+ UserCredentials userCredentials = userService.findUserCredentialsByUserId(savedUser.getId());
+ String baseUrl = String.format("%s://%s:%d",
+ request.getScheme(),
+ request.getServerName(),
+ request.getServerPort());
+ String activateUrl = String.format("%s/api/noauth/activate?activateToken=%s", baseUrl,
+ userCredentials.getActivateToken());
+ String email = savedUser.getEmail();
+ try {
+ mailService.sendActivationEmail(activateUrl, email);
+ } catch (ThingsboardException e) {
+ userService.deleteUser(savedUser.getId());
+ throw e;
+ }
+ }
+ return savedUser;
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @RequestMapping(value = "/user/sendActivationMail", method = RequestMethod.POST)
+ @ResponseStatus(value = HttpStatus.OK)
+ public void sendActivationEmail(
+ @RequestParam(value = "email") String email,
+ HttpServletRequest request) throws ThingsboardException {
+ try {
+ User user = checkNotNull(userService.findUserByEmail(email));
+ UserCredentials userCredentials = userService.findUserCredentialsByUserId(user.getId());
+ if (!userCredentials.isEnabled()) {
+ String baseUrl = String.format("%s://%s:%d",
+ request.getScheme(),
+ request.getServerName(),
+ request.getServerPort());
+ String activateUrl = String.format("%s/api/noauth/activate?activateToken=%s", baseUrl,
+ userCredentials.getActivateToken());
+ mailService.sendActivationEmail(activateUrl, email);
+ } else {
+ throw new ThingsboardException("User is already active!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
+ }
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @RequestMapping(value = "/user/{userId}", method = RequestMethod.DELETE)
+ @ResponseStatus(value = HttpStatus.OK)
+ public void deleteUser(@PathVariable("userId") String strUserId) throws ThingsboardException {
+ checkParameter("userId", strUserId);
+ try {
+ UserId userId = new UserId(toUUID(strUserId));
+ checkUserId(userId);
+ userService.deleteUser(userId);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('SYS_ADMIN')")
+ @RequestMapping(value = "/tenant/{tenantId}/users", params = { "limit" }, method = RequestMethod.GET)
+ @ResponseBody
+ public TextPageData<User> getTenantAdmins(
+ @PathVariable("tenantId") String strTenantId,
+ @RequestParam int limit,
+ @RequestParam(required = false) String textSearch,
+ @RequestParam(required = false) String idOffset,
+ @RequestParam(required = false) String textOffset) throws ThingsboardException {
+ checkParameter("tenantId", strTenantId);
+ try {
+ TenantId tenantId = new TenantId(toUUID(strTenantId));
+ TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
+ return checkNotNull(userService.findTenantAdmins(tenantId, pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/customer/{customerId}/users", params = { "limit" }, method = RequestMethod.GET)
+ @ResponseBody
+ public TextPageData<User> getCustomerUsers(
+ @PathVariable("customerId") String strCustomerId,
+ @RequestParam int limit,
+ @RequestParam(required = false) String textSearch,
+ @RequestParam(required = false) String idOffset,
+ @RequestParam(required = false) String textOffset) throws ThingsboardException {
+ checkParameter("customerId", strCustomerId);
+ try {
+ CustomerId customerId = new CustomerId(toUUID(strCustomerId));
+ checkCustomerId(customerId);
+ TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
+ TenantId tenantId = getCurrentUser().getTenantId();
+ return checkNotNull(userService.findCustomerUsers(tenantId, customerId, pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
new file mode 100644
index 0000000..e06962b
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
@@ -0,0 +1,116 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.WidgetsBundleId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.common.data.widget.WidgetsBundle;
+import org.thingsboard.server.dao.model.ModelConstants;
+import org.thingsboard.server.exception.ThingsboardException;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api")
+public class WidgetsBundleController extends BaseController {
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @RequestMapping(value = "/widgetsBundle/{widgetsBundleId}", method = RequestMethod.GET)
+ @ResponseBody
+ public WidgetsBundle getWidgetsBundleById(@PathVariable("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException {
+ checkParameter("widgetsBundleId", strWidgetsBundleId);
+ try {
+ WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId));
+ return checkWidgetsBundleId(widgetsBundleId, false);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @RequestMapping(value = "/widgetsBundle", method = RequestMethod.POST)
+ @ResponseBody
+ public WidgetsBundle saveWidgetsBundle(@RequestBody WidgetsBundle widgetsBundle) throws ThingsboardException {
+ try {
+ if (getCurrentUser().getAuthority() == Authority.SYS_ADMIN) {
+ widgetsBundle.setTenantId(new TenantId(ModelConstants.NULL_UUID));
+ } else {
+ widgetsBundle.setTenantId(getCurrentUser().getTenantId());
+ }
+ return checkNotNull(widgetsBundleService.saveWidgetsBundle(widgetsBundle));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @RequestMapping(value = "/widgetsBundle/{widgetsBundleId}", method = RequestMethod.DELETE)
+ @ResponseStatus(value = HttpStatus.OK)
+ public void deleteWidgetsBundle(@PathVariable("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException {
+ checkParameter("widgetsBundleId", strWidgetsBundleId);
+ try {
+ WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId));
+ checkWidgetsBundleId(widgetsBundleId, true);
+ widgetsBundleService.deleteWidgetsBundle(widgetsBundleId);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @RequestMapping(value = "/widgetsBundles", params = { "limit" }, method = RequestMethod.GET)
+ @ResponseBody
+ public TextPageData<WidgetsBundle> getWidgetsBundles(
+ @RequestParam int limit,
+ @RequestParam(required = false) String textSearch,
+ @RequestParam(required = false) String idOffset,
+ @RequestParam(required = false) String textOffset) throws ThingsboardException {
+ try {
+ TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
+ if (getCurrentUser().getAuthority() == Authority.SYS_ADMIN) {
+ return checkNotNull(widgetsBundleService.findSystemWidgetsBundlesByPageLink(pageLink));
+ } else {
+ TenantId tenantId = getCurrentUser().getTenantId();
+ return checkNotNull(widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, pageLink));
+ }
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @RequestMapping(value = "/widgetsBundles", method = RequestMethod.GET)
+ @ResponseBody
+ public List<WidgetsBundle> getWidgetsBundles() throws ThingsboardException {
+ try {
+ if (getCurrentUser().getAuthority() == Authority.SYS_ADMIN) {
+ return checkNotNull(widgetsBundleService.findSystemWidgetsBundles());
+ } else {
+ TenantId tenantId = getCurrentUser().getTenantId();
+ return checkNotNull(widgetsBundleService.findAllTenantWidgetsBundlesByTenantId(tenantId));
+ }
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
new file mode 100644
index 0000000..c3ded12
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
@@ -0,0 +1,118 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.WidgetTypeId;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.common.data.widget.WidgetType;
+import org.thingsboard.server.dao.model.ModelConstants;
+import org.thingsboard.server.exception.ThingsboardException;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api")
+public class WidgetTypeController extends BaseController {
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @RequestMapping(value = "/widgetType/{widgetTypeId}", method = RequestMethod.GET)
+ @ResponseBody
+ public WidgetType getWidgetTypeById(@PathVariable("widgetTypeId") String strWidgetTypeId) throws ThingsboardException {
+ checkParameter("widgetTypeId", strWidgetTypeId);
+ try {
+ WidgetTypeId widgetTypeId = new WidgetTypeId(toUUID(strWidgetTypeId));
+ return checkWidgetTypeId(widgetTypeId, false);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @RequestMapping(value = "/widgetType", method = RequestMethod.POST)
+ @ResponseBody
+ public WidgetType saveWidgetType(@RequestBody WidgetType widgetType) throws ThingsboardException {
+ try {
+ if (getCurrentUser().getAuthority() == Authority.SYS_ADMIN) {
+ widgetType.setTenantId(new TenantId(ModelConstants.NULL_UUID));
+ } else {
+ widgetType.setTenantId(getCurrentUser().getTenantId());
+ }
+ return checkNotNull(widgetTypeService.saveWidgetType(widgetType));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @RequestMapping(value = "/widgetType/{widgetTypeId}", method = RequestMethod.DELETE)
+ @ResponseStatus(value = HttpStatus.OK)
+ public void deleteWidgetType(@PathVariable("widgetTypeId") String strWidgetTypeId) throws ThingsboardException {
+ checkParameter("widgetTypeId", strWidgetTypeId);
+ try {
+ WidgetTypeId widgetTypeId = new WidgetTypeId(toUUID(strWidgetTypeId));
+ checkWidgetTypeId(widgetTypeId, true);
+ widgetTypeService.deleteWidgetType(widgetTypeId);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @RequestMapping(value = "/widgetTypes", params = { "isSystem", "bundleAlias"}, method = RequestMethod.GET)
+ @ResponseBody
+ public List<WidgetType> getBundleWidgetTypes(
+ @RequestParam boolean isSystem,
+ @RequestParam String bundleAlias) throws ThingsboardException {
+ try {
+ TenantId tenantId;
+ if (isSystem) {
+ tenantId = new TenantId(ModelConstants.NULL_UUID);
+ } else {
+ tenantId = getCurrentUser().getTenantId();
+ }
+ return checkNotNull(widgetTypeService.findWidgetTypesByTenantIdAndBundleAlias(tenantId, bundleAlias));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/widgetType", params = { "isSystem", "bundleAlias", "alias" }, method = RequestMethod.GET)
+ @ResponseBody
+ public WidgetType getWidgetType(
+ @RequestParam boolean isSystem,
+ @RequestParam String bundleAlias,
+ @RequestParam String alias) throws ThingsboardException {
+ try {
+ TenantId tenantId;
+ if (isSystem) {
+ tenantId = new TenantId(ModelConstants.NULL_UUID);
+ } else {
+ tenantId = getCurrentUser().getTenantId();
+ }
+ WidgetType widgetType = widgetTypeService.findWidgetTypeByTenantIdBundleAliasAndAlias(tenantId, bundleAlias, alias);
+ checkWidgetType(widgetType, false);
+ return widgetType;
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorCode.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorCode.java
new file mode 100644
index 0000000..9c505f6
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorCode.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.exception;
+
+import com.fasterxml.jackson.annotation.JsonValue;
+
+public enum ThingsboardErrorCode {
+
+ GENERAL(2),
+ AUTHENTICATION(10),
+ JWT_TOKEN_EXPIRED(11),
+ PERMISSION_DENIED(20),
+ INVALID_ARGUMENTS(30),
+ BAD_REQUEST_PARAMS(31),
+ ITEM_NOT_FOUND(32);
+
+ private int errorCode;
+
+ ThingsboardErrorCode(int errorCode) {
+ this.errorCode = errorCode;
+ }
+
+ @JsonValue
+ public int getErrorCode() {
+ return errorCode;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java
new file mode 100644
index 0000000..9960a56
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java
@@ -0,0 +1,60 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.exception;
+
+import org.springframework.http.HttpStatus;
+
+import java.util.Date;
+
+public class ThingsboardErrorResponse {
+ // HTTP Response Status Code
+ private final HttpStatus status;
+
+ // General Error message
+ private final String message;
+
+ // Error code
+ private final ThingsboardErrorCode errorCode;
+
+ private final Date timestamp;
+
+ protected ThingsboardErrorResponse(final String message, final ThingsboardErrorCode errorCode, HttpStatus status) {
+ this.message = message;
+ this.errorCode = errorCode;
+ this.status = status;
+ this.timestamp = new java.util.Date();
+ }
+
+ public static ThingsboardErrorResponse of(final String message, final ThingsboardErrorCode errorCode, HttpStatus status) {
+ return new ThingsboardErrorResponse(message, errorCode, status);
+ }
+
+ public Integer getStatus() {
+ return status.value();
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public ThingsboardErrorCode getErrorCode() {
+ return errorCode;
+ }
+
+ public Date getTimestamp() {
+ return timestamp;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java
new file mode 100644
index 0000000..ad60ea2
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java
@@ -0,0 +1,133 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.exception;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.access.AccessDeniedHandler;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException;
+import org.thingsboard.server.service.security.exception.JwtExpiredTokenException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+@Component
+@Slf4j
+public class ThingsboardErrorResponseHandler implements AccessDeniedHandler {
+
+ @Autowired
+ private ObjectMapper mapper;
+
+ @Override
+ public void handle(HttpServletRequest request, HttpServletResponse response,
+ AccessDeniedException accessDeniedException) throws IOException,
+ ServletException {
+ if (!response.isCommitted()) {
+ response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+ response.setStatus(HttpStatus.FORBIDDEN.value());
+ mapper.writeValue(response.getWriter(),
+ ThingsboardErrorResponse.of("You don't have permission to perform this operation!",
+ ThingsboardErrorCode.PERMISSION_DENIED, HttpStatus.FORBIDDEN));
+ }
+ }
+
+ public void handle(Exception exception, HttpServletResponse response) {
+ log.debug("Processing exception {}", exception.getMessage(), exception);
+ if (!response.isCommitted()) {
+ try {
+ response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+
+ if (exception instanceof ThingsboardException) {
+ handleThingsboardException((ThingsboardException) exception, response);
+ } else if (exception instanceof AccessDeniedException) {
+ handleAccessDeniedException(response);
+ } else if (exception instanceof AuthenticationException) {
+ handleAuthenticationException((AuthenticationException) exception, response);
+ } else {
+ response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
+ mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of(exception.getMessage(),
+ ThingsboardErrorCode.GENERAL, HttpStatus.INTERNAL_SERVER_ERROR));
+ }
+ } catch (IOException e) {
+ log.error("Can't handle exception", e);
+ }
+ }
+ }
+
+ private void handleThingsboardException(ThingsboardException thingsboardException, HttpServletResponse response) throws IOException {
+
+ ThingsboardErrorCode errorCode = thingsboardException.getErrorCode();
+ HttpStatus status;
+
+ switch (errorCode) {
+ case AUTHENTICATION:
+ status = HttpStatus.UNAUTHORIZED;
+ break;
+ case PERMISSION_DENIED:
+ status = HttpStatus.FORBIDDEN;
+ break;
+ case INVALID_ARGUMENTS:
+ status = HttpStatus.BAD_REQUEST;
+ break;
+ case ITEM_NOT_FOUND:
+ status = HttpStatus.NOT_FOUND;
+ break;
+ case BAD_REQUEST_PARAMS:
+ status = HttpStatus.BAD_REQUEST;
+ break;
+ case GENERAL:
+ status = HttpStatus.INTERNAL_SERVER_ERROR;
+ break;
+ default:
+ status = HttpStatus.INTERNAL_SERVER_ERROR;
+ break;
+ }
+
+ response.setStatus(status.value());
+ mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of(thingsboardException.getMessage(), errorCode, status));
+ }
+
+ private void handleAccessDeniedException(HttpServletResponse response) throws IOException {
+ response.setStatus(HttpStatus.FORBIDDEN.value());
+ mapper.writeValue(response.getWriter(),
+ ThingsboardErrorResponse.of("You don't have permission to perform this operation!",
+ ThingsboardErrorCode.PERMISSION_DENIED, HttpStatus.FORBIDDEN));
+
+ }
+
+ private void handleAuthenticationException(AuthenticationException authenticationException, HttpServletResponse response) throws IOException {
+ response.setStatus(HttpStatus.UNAUTHORIZED.value());
+ if (authenticationException instanceof BadCredentialsException) {
+ mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Invalid username or password", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
+ } else if (authenticationException instanceof JwtExpiredTokenException) {
+ mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Token has expired", ThingsboardErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED));
+ } else if (authenticationException instanceof AuthMethodNotSupportedException) {
+ mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of(authenticationException.getMessage(), ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
+ }
+ mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardException.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardException.java
new file mode 100644
index 0000000..0da0414
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardException.java
@@ -0,0 +1,51 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.exception;
+
+public class ThingsboardException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ private ThingsboardErrorCode errorCode;
+
+ public ThingsboardException() {
+ super();
+ }
+
+ public ThingsboardException(ThingsboardErrorCode errorCode) {
+ this.errorCode = errorCode;
+ }
+
+ public ThingsboardException(String message, ThingsboardErrorCode errorCode) {
+ super(message);
+ this.errorCode = errorCode;
+ }
+
+ public ThingsboardException(String message, Throwable cause, ThingsboardErrorCode errorCode) {
+ super(message, cause);
+ this.errorCode = errorCode;
+ }
+
+ public ThingsboardException(Throwable cause, ThingsboardErrorCode errorCode) {
+ super(cause);
+ this.errorCode = errorCode;
+ }
+
+ public ThingsboardErrorCode getErrorCode() {
+ return errorCode;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/environment/EnvironmentLogService.java b/application/src/main/java/org/thingsboard/server/service/environment/EnvironmentLogService.java
new file mode 100644
index 0000000..aae2d8d
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/environment/EnvironmentLogService.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.environment;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.zookeeper.Environment;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PostConstruct;
+
+/**
+ * Created by igor on 11/24/16.
+ */
+
+@Service("environmentLogService")
+@ConditionalOnProperty(prefix = "zk", value = "enabled", havingValue = "false", matchIfMissing = true)
+@Slf4j
+public class EnvironmentLogService {
+
+ @PostConstruct
+ public void init() {
+ Environment.logEnv("Thingsboard server environment: ", log);
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
new file mode 100644
index 0000000..b11517c
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
@@ -0,0 +1,208 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.mail;
+
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+
+import javax.annotation.PostConstruct;
+import javax.mail.internet.MimeMessage;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.velocity.app.VelocityEngine;
+import org.thingsboard.server.exception.ThingsboardErrorCode;
+import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.common.data.AdminSettings;
+import org.thingsboard.server.dao.settings.AdminSettingsService;
+import org.thingsboard.server.dao.exception.IncorrectParameterException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.MessageSource;
+import org.springframework.core.NestedRuntimeException;
+import org.springframework.mail.javamail.JavaMailSenderImpl;
+import org.springframework.mail.javamail.MimeMessageHelper;
+import org.springframework.stereotype.Service;
+import org.springframework.ui.velocity.VelocityEngineUtils;
+
+import com.fasterxml.jackson.databind.JsonNode;
+@Service
+@Slf4j
+public class DefaultMailService implements MailService {
+
+ @Autowired
+ private MessageSource messages;
+
+ @Autowired
+ private VelocityEngine engine;
+
+ private JavaMailSenderImpl mailSender;
+
+ private String mailFrom;
+
+ @Autowired
+ private AdminSettingsService adminSettingsService;
+
+ @PostConstruct
+ private void init() {
+ updateMailConfiguration();
+ }
+
+ @Override
+ public void updateMailConfiguration() {
+ AdminSettings settings = adminSettingsService.findAdminSettingsByKey("mail");
+ JsonNode jsonConfig = settings.getJsonValue();
+ mailSender = createMailSender(jsonConfig);
+ mailFrom = jsonConfig.get("mailFrom").asText();
+ }
+
+ private JavaMailSenderImpl createMailSender(JsonNode jsonConfig) {
+ JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
+ mailSender.setHost(jsonConfig.get("smtpHost").asText());
+ mailSender.setPort(parsePort(jsonConfig.get("smtpPort").asText()));
+ mailSender.setUsername(jsonConfig.get("username").asText());
+ mailSender.setPassword(jsonConfig.get("password").asText());
+ mailSender.setJavaMailProperties(createJavaMailProperties(jsonConfig));
+ return mailSender;
+ }
+
+ private Properties createJavaMailProperties(JsonNode jsonConfig) {
+ Properties javaMailProperties = new Properties();
+ String protocol = jsonConfig.get("smtpProtocol").asText();
+ javaMailProperties.put("mail.transport.protocol", protocol);
+ javaMailProperties.put("mail." + protocol + ".host", jsonConfig.get("smtpHost").asText());
+ javaMailProperties.put("mail." + protocol + ".port", jsonConfig.get("smtpPort").asText());
+ javaMailProperties.put("mail." + protocol + ".timeout", jsonConfig.get("timeout").asText());
+ javaMailProperties.put("mail." + protocol + ".auth", String.valueOf(StringUtils.isNotEmpty(jsonConfig.get("username").asText())));
+ javaMailProperties.put("mail." + protocol + ".starttls.enable", jsonConfig.get("enableTls"));
+ return javaMailProperties;
+ }
+
+ private int parsePort(String strPort) {
+ try {
+ return Integer.valueOf(strPort);
+ } catch (NumberFormatException e) {
+ throw new IncorrectParameterException(String.format("Invalid smtp port value: %s", strPort));
+ }
+ }
+
+ @Override
+ public void sendTestMail(JsonNode jsonConfig, String email) throws ThingsboardException {
+ JavaMailSenderImpl testMailSender = createMailSender(jsonConfig);
+ String mailFrom = jsonConfig.get("mailFrom").asText();
+ String subject = messages.getMessage("test.message.subject", null, Locale.US);
+
+ Map<String, Object> model = new HashMap<String, Object>();
+ model.put("targetEmail", email);
+
+ String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine,
+ "test.vm", "UTF-8", model);
+
+ sendMail(testMailSender, mailFrom, email, subject, message);
+ }
+
+ @Override
+ public void sendActivationEmail(String activationLink, String email) throws ThingsboardException {
+
+ String subject = messages.getMessage("activation.subject", null, Locale.US);
+
+ Map<String, Object> model = new HashMap<String, Object>();
+ model.put("activationLink", activationLink);
+ model.put("targetEmail", email);
+
+ String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine,
+ "activation.vm", "UTF-8", model);
+
+ sendMail(mailSender, mailFrom, email, subject, message);
+ }
+
+ @Override
+ public void sendAccountActivatedEmail(String loginLink, String email) throws ThingsboardException {
+
+ String subject = messages.getMessage("account.activated.subject", null, Locale.US);
+
+ Map<String, Object> model = new HashMap<String, Object>();
+ model.put("loginLink", loginLink);
+ model.put("targetEmail", email);
+
+ String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine,
+ "account.activated.vm", "UTF-8", model);
+
+ sendMail(mailSender, mailFrom, email, subject, message);
+ }
+
+ @Override
+ public void sendResetPasswordEmail(String passwordResetLink, String email) throws ThingsboardException {
+
+ String subject = messages.getMessage("reset.password.subject", null, Locale.US);
+
+ Map<String, Object> model = new HashMap<String, Object>();
+ model.put("passwordResetLink", passwordResetLink);
+ model.put("targetEmail", email);
+
+ String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine,
+ "reset.password.vm", "UTF-8", model);
+
+ sendMail(mailSender, mailFrom, email, subject, message);
+ }
+
+ @Override
+ public void sendPasswordWasResetEmail(String loginLink, String email) throws ThingsboardException {
+
+ String subject = messages.getMessage("password.was.reset.subject", null, Locale.US);
+
+ Map<String, Object> model = new HashMap<String, Object>();
+ model.put("loginLink", loginLink);
+ model.put("targetEmail", email);
+
+ String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine,
+ "password.was.reset.vm", "UTF-8", model);
+
+ sendMail(mailSender, mailFrom, email, subject, message);
+ }
+
+
+ private void sendMail(JavaMailSenderImpl mailSender,
+ String mailFrom, String email,
+ String subject, String message) throws ThingsboardException {
+ try {
+ MimeMessage mimeMsg = mailSender.createMimeMessage();
+ MimeMessageHelper helper = new MimeMessageHelper(mimeMsg, "UTF-8");
+ helper.setFrom(mailFrom);
+ helper.setTo(email);
+ helper.setSubject(subject);
+ helper.setText(message, true);
+ mailSender.send(helper.getMimeMessage());
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ protected ThingsboardException handleException(Exception exception) {
+ String message;
+ if (exception instanceof NestedRuntimeException) {
+ message = ((NestedRuntimeException)exception).getMostSpecificCause().getMessage();
+ } else {
+ message = exception.getMessage();
+ }
+ return new ThingsboardException(String.format("Unable to send mail: %s", message),
+ ThingsboardErrorCode.GENERAL);
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/mail/MailService.java b/application/src/main/java/org/thingsboard/server/service/mail/MailService.java
new file mode 100644
index 0000000..e135253
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/mail/MailService.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.mail;
+
+import org.thingsboard.server.exception.ThingsboardException;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+public interface MailService {
+
+ void updateMailConfiguration();
+
+ void sendTestMail(JsonNode config, String email) throws ThingsboardException;
+
+ void sendActivationEmail(String activationLink, String email) throws ThingsboardException;
+
+ void sendAccountActivatedEmail(String loginLink, String email) throws ThingsboardException;
+
+ void sendResetPasswordEmail(String passwordResetLink, String email) throws ThingsboardException;
+
+ void sendPasswordWasResetEmail(String loginLink, String email) throws ThingsboardException;
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/AbstractJwtAuthenticationToken.java b/application/src/main/java/org/thingsboard/server/service/security/auth/AbstractJwtAuthenticationToken.java
new file mode 100644
index 0000000..eb6541f
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/AbstractJwtAuthenticationToken.java
@@ -0,0 +1,66 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.security.auth;
+
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.security.model.token.RawAccessJwtToken;
+
+public abstract class AbstractJwtAuthenticationToken extends AbstractAuthenticationToken {
+
+ private static final long serialVersionUID = -6212297506742428406L;
+
+ private RawAccessJwtToken rawAccessToken;
+ private SecurityUser securityUser;
+
+ public AbstractJwtAuthenticationToken(RawAccessJwtToken unsafeToken) {
+ super(null);
+ this.rawAccessToken = unsafeToken;
+ this.setAuthenticated(false);
+ }
+
+ public AbstractJwtAuthenticationToken(SecurityUser securityUser) {
+ super(securityUser.getAuthorities());
+ this.eraseCredentials();
+ this.securityUser = securityUser;
+ super.setAuthenticated(true);
+ }
+
+ @Override
+ public void setAuthenticated(boolean authenticated) {
+ if (authenticated) {
+ throw new IllegalArgumentException(
+ "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
+ }
+ super.setAuthenticated(false);
+ }
+
+ @Override
+ public Object getCredentials() {
+ return rawAccessToken;
+ }
+
+ @Override
+ public Object getPrincipal() {
+ return this.securityUser;
+ }
+
+ @Override
+ public void eraseCredentials() {
+ super.eraseCredentials();
+ this.rawAccessToken = null;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtHeaderTokenExtractor.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtHeaderTokenExtractor.java
new file mode 100644
index 0000000..9b60882
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtHeaderTokenExtractor.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.security.auth.jwt.extractor;
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.config.ThingsboardSecurityConfiguration;
+
+import javax.servlet.http.HttpServletRequest;
+
+@Component(value="jwtHeaderTokenExtractor")
+public class JwtHeaderTokenExtractor implements TokenExtractor {
+ public static String HEADER_PREFIX = "Bearer ";
+
+ @Override
+ public String extract(HttpServletRequest request) {
+ String header = request.getHeader(ThingsboardSecurityConfiguration.JWT_TOKEN_HEADER_PARAM);
+ if (StringUtils.isBlank(header)) {
+ throw new AuthenticationServiceException("Authorization header cannot be blank!");
+ }
+
+ if (header.length() < HEADER_PREFIX.length()) {
+ throw new AuthenticationServiceException("Invalid authorization header size.");
+ }
+
+ return header.substring(HEADER_PREFIX.length(), header.length());
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtQueryTokenExtractor.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtQueryTokenExtractor.java
new file mode 100644
index 0000000..d8a8370
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtQueryTokenExtractor.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.security.auth.jwt.extractor;
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.config.ThingsboardSecurityConfiguration;
+
+import javax.servlet.http.HttpServletRequest;
+
+@Component(value="jwtQueryTokenExtractor")
+public class JwtQueryTokenExtractor implements TokenExtractor {
+
+ @Override
+ public String extract(HttpServletRequest request) {
+ String token = null;
+ if (request.getParameterMap() != null && !request.getParameterMap().isEmpty()) {
+ String[] tokenParamValue = request.getParameterMap().get(ThingsboardSecurityConfiguration.JWT_TOKEN_QUERY_PARAM);
+ if (tokenParamValue != null && tokenParamValue.length == 1) {
+ token = tokenParamValue[0];
+ }
+ }
+ if (StringUtils.isBlank(token)) {
+ throw new AuthenticationServiceException("Authorization query parameter cannot be blank!");
+ }
+
+ return token;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/TokenExtractor.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/TokenExtractor.java
new file mode 100644
index 0000000..74c6dea
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/TokenExtractor.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.security.auth.jwt.extractor;
+
+import javax.servlet.http.HttpServletRequest;
+
+public interface TokenExtractor {
+ public String extract(HttpServletRequest request);
+}
\ No newline at end of file
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java
new file mode 100644
index 0000000..d02019c
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java
@@ -0,0 +1,58 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.security.auth.jwt;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jws;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.config.JwtSettings;
+import org.thingsboard.server.service.security.auth.JwtAuthenticationToken;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
+import org.thingsboard.server.service.security.model.token.RawAccessJwtToken;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Component
+@SuppressWarnings("unchecked")
+public class JwtAuthenticationProvider implements AuthenticationProvider {
+
+ private final JwtTokenFactory tokenFactory;
+
+ @Autowired
+ public JwtAuthenticationProvider(JwtTokenFactory tokenFactory) {
+ this.tokenFactory = tokenFactory;
+ }
+
+ @Override
+ public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+ RawAccessJwtToken rawAccessToken = (RawAccessJwtToken) authentication.getCredentials();
+ SecurityUser securityUser = tokenFactory.parseAccessJwtToken(rawAccessToken);
+ return new JwtAuthenticationToken(securityUser);
+ }
+
+ @Override
+ public boolean supports(Class<?> authentication) {
+ return (JwtAuthenticationToken.class.isAssignableFrom(authentication));
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtTokenAuthenticationProcessingFilter.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtTokenAuthenticationProcessingFilter.java
new file mode 100644
index 0000000..523a670
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtTokenAuthenticationProcessingFilter.java
@@ -0,0 +1,71 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.security.auth.jwt;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.thingsboard.server.config.ThingsboardSecurityConfiguration;
+import org.thingsboard.server.service.security.auth.JwtAuthenticationToken;
+import org.thingsboard.server.service.security.auth.jwt.extractor.TokenExtractor;
+import org.thingsboard.server.service.security.model.token.RawAccessJwtToken;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+public class JwtTokenAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {
+ private final AuthenticationFailureHandler failureHandler;
+ private final TokenExtractor tokenExtractor;
+
+ @Autowired
+ public JwtTokenAuthenticationProcessingFilter(AuthenticationFailureHandler failureHandler,
+ TokenExtractor tokenExtractor, RequestMatcher matcher) {
+ super(matcher);
+ this.failureHandler = failureHandler;
+ this.tokenExtractor = tokenExtractor;
+ }
+
+ @Override
+ public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
+ throws AuthenticationException, IOException, ServletException {
+ RawAccessJwtToken token = new RawAccessJwtToken(tokenExtractor.extract(request));
+ return getAuthenticationManager().authenticate(new JwtAuthenticationToken(token));
+ }
+
+ @Override
+ protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
+ Authentication authResult) throws IOException, ServletException {
+ SecurityContext context = SecurityContextHolder.createEmptyContext();
+ context.setAuthentication(authResult);
+ SecurityContextHolder.setContext(context);
+ chain.doFilter(request, response);
+ }
+
+ @Override
+ protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
+ AuthenticationException failed) throws IOException, ServletException {
+ SecurityContextHolder.clearContext();
+ failureHandler.onAuthenticationFailure(request, response, failed);
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java
new file mode 100644
index 0000000..e044ef4
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java
@@ -0,0 +1,79 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.security.auth.jwt;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.DisabledException;
+import org.springframework.security.authentication.InsufficientAuthenticationException;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Component;
+import org.springframework.util.Assert;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.security.UserCredentials;
+import org.thingsboard.server.dao.user.UserService;
+import org.thingsboard.server.service.security.auth.RefreshAuthenticationToken;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
+import org.thingsboard.server.service.security.model.token.RawAccessJwtToken;
+
+@Component
+public class RefreshTokenAuthenticationProvider implements AuthenticationProvider {
+
+ private final JwtTokenFactory tokenFactory;
+ private final UserService userService;
+
+ @Autowired
+ public RefreshTokenAuthenticationProvider(final UserService userService, final JwtTokenFactory tokenFactory) {
+ this.userService = userService;
+ this.tokenFactory = tokenFactory;
+ }
+
+ @Override
+ public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+ Assert.notNull(authentication, "No authentication data provided");
+ RawAccessJwtToken rawAccessToken = (RawAccessJwtToken) authentication.getCredentials();
+ SecurityUser unsafeUser = tokenFactory.parseRefreshToken(rawAccessToken);
+
+ User user = userService.findUserById(unsafeUser.getId());
+ if (user == null) {
+ throw new UsernameNotFoundException("User not found by refresh token");
+ }
+
+ UserCredentials userCredentials = userService.findUserCredentialsByUserId(user.getId());
+ if (userCredentials == null) {
+ throw new UsernameNotFoundException("User credentials not found");
+ }
+
+ if (!userCredentials.isEnabled()) {
+ throw new DisabledException("User is not active");
+ }
+
+ if (user.getAuthority() == null) throw new InsufficientAuthenticationException("User has no authority assigned");
+
+ SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled());
+
+ return new RefreshAuthenticationToken(securityUser);
+ }
+
+ @Override
+ public boolean supports(Class<?> authentication) {
+ return (RefreshAuthenticationToken.class.isAssignableFrom(authentication));
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenProcessingFilter.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenProcessingFilter.java
new file mode 100644
index 0000000..354b68e
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenProcessingFilter.java
@@ -0,0 +1,94 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.security.auth.jwt;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.thingsboard.server.service.security.auth.RefreshAuthenticationToken;
+import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException;
+import org.thingsboard.server.service.security.model.token.RawAccessJwtToken;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+public class RefreshTokenProcessingFilter extends AbstractAuthenticationProcessingFilter {
+ private static Logger logger = LoggerFactory.getLogger(RefreshTokenProcessingFilter.class);
+
+ private final AuthenticationSuccessHandler successHandler;
+ private final AuthenticationFailureHandler failureHandler;
+
+ private final ObjectMapper objectMapper;
+
+ public RefreshTokenProcessingFilter(String defaultProcessUrl, AuthenticationSuccessHandler successHandler,
+ AuthenticationFailureHandler failureHandler, ObjectMapper mapper) {
+ super(defaultProcessUrl);
+ this.successHandler = successHandler;
+ this.failureHandler = failureHandler;
+ this.objectMapper = mapper;
+ }
+
+ @Override
+ public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
+ throws AuthenticationException, IOException, ServletException {
+ if (!HttpMethod.POST.name().equals(request.getMethod())) {
+ if(logger.isDebugEnabled()) {
+ logger.debug("Authentication method not supported. Request method: " + request.getMethod());
+ }
+ throw new AuthMethodNotSupportedException("Authentication method not supported");
+ }
+
+ RefreshTokenRequest refreshTokenRequest;
+ try {
+ refreshTokenRequest = objectMapper.readValue(request.getReader(), RefreshTokenRequest.class);
+ } catch (Exception e) {
+ throw new AuthenticationServiceException("Invalid refresh token request payload");
+ }
+
+ if (StringUtils.isBlank(refreshTokenRequest.getRefreshToken())) {
+ throw new AuthenticationServiceException("Refresh token is not provided");
+ }
+
+ RawAccessJwtToken token = new RawAccessJwtToken(refreshTokenRequest.getRefreshToken());
+
+ return this.getAuthenticationManager().authenticate(new RefreshAuthenticationToken(token));
+ }
+
+ @Override
+ protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
+ Authentication authResult) throws IOException, ServletException {
+ successHandler.onAuthenticationSuccess(request, response, authResult);
+ }
+
+ @Override
+ protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
+ AuthenticationException failed) throws IOException, ServletException {
+ SecurityContextHolder.clearContext();
+ failureHandler.onAuthenticationFailure(request, response, failed);
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenRepository.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenRepository.java
new file mode 100644
index 0000000..06237d8
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenRepository.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.security.auth.jwt;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.security.model.token.JwtToken;
+import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
+
+@Component
+public class RefreshTokenRepository {
+
+ private final JwtTokenFactory tokenFactory;
+
+ @Autowired
+ public RefreshTokenRepository(final JwtTokenFactory tokenFactory) {
+ this.tokenFactory = tokenFactory;
+ }
+
+ public JwtToken requestRefreshToken(SecurityUser user) {
+ return tokenFactory.createRefreshToken(user);
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenRequest.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenRequest.java
new file mode 100644
index 0000000..4a14972
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenRequest.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.security.auth.jwt;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class RefreshTokenRequest {
+ private String refreshToken;
+
+ @JsonCreator
+ public RefreshTokenRequest(@JsonProperty("refreshToken") String refreshToken) {
+ this.refreshToken = refreshToken;
+ }
+
+ public String getRefreshToken() {
+ return refreshToken;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/SkipPathRequestMatcher.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/SkipPathRequestMatcher.java
new file mode 100644
index 0000000..9710cf6
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/SkipPathRequestMatcher.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.security.auth.jwt;
+
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.security.web.util.matcher.OrRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.Assert;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class SkipPathRequestMatcher implements RequestMatcher {
+ private OrRequestMatcher matchers;
+ private RequestMatcher processingMatcher;
+
+ public SkipPathRequestMatcher(List<String> pathsToSkip, String processingPath) {
+ Assert.notNull(pathsToSkip);
+ List<RequestMatcher> m = pathsToSkip.stream().map(path -> new AntPathRequestMatcher(path)).collect(Collectors.toList());
+ matchers = new OrRequestMatcher(m);
+ processingMatcher = new AntPathRequestMatcher(processingPath);
+ }
+
+ @Override
+ public boolean matches(HttpServletRequest request) {
+ if (matchers.matches(request)) {
+ return false;
+ }
+ return processingMatcher.matches(request) ? true : false;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/JwtAuthenticationToken.java b/application/src/main/java/org/thingsboard/server/service/security/auth/JwtAuthenticationToken.java
new file mode 100644
index 0000000..688ca19
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/JwtAuthenticationToken.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.security.auth;
+
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.security.model.token.RawAccessJwtToken;
+
+public class JwtAuthenticationToken extends AbstractJwtAuthenticationToken {
+
+ private static final long serialVersionUID = -8487219769037942225L;
+
+ public JwtAuthenticationToken(RawAccessJwtToken unsafeToken) {
+ super(unsafeToken);
+ }
+
+ public JwtAuthenticationToken(SecurityUser securityUser) {
+ super(securityUser);
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/RefreshAuthenticationToken.java b/application/src/main/java/org/thingsboard/server/service/security/auth/RefreshAuthenticationToken.java
new file mode 100644
index 0000000..6f927c1
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/RefreshAuthenticationToken.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.security.auth;
+
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.security.model.token.RawAccessJwtToken;
+
+public class RefreshAuthenticationToken extends AbstractJwtAuthenticationToken {
+
+ private static final long serialVersionUID = -1311042791508924523L;
+
+ public RefreshAuthenticationToken(RawAccessJwtToken unsafeToken) {
+ super(unsafeToken);
+ }
+
+ public RefreshAuthenticationToken(SecurityUser securityUser) {
+ super(securityUser);
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginRequest.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginRequest.java
new file mode 100644
index 0000000..b00506f
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginRequest.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.security.auth.rest;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class LoginRequest {
+ private String username;
+ private String password;
+
+ @JsonCreator
+ public LoginRequest(@JsonProperty("username") String username, @JsonProperty("password") String password) {
+ this.username = username;
+ this.password = password;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java
new file mode 100644
index 0000000..bcc9588
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java
@@ -0,0 +1,79 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.security.auth.rest;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.authentication.*;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.stereotype.Component;
+import org.springframework.util.Assert;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.security.UserCredentials;
+import org.thingsboard.server.dao.user.UserService;
+import org.thingsboard.server.service.security.model.SecurityUser;
+
+@Component
+public class RestAuthenticationProvider implements AuthenticationProvider {
+
+ private final BCryptPasswordEncoder encoder;
+ private final UserService userService;
+
+ @Autowired
+ public RestAuthenticationProvider(final UserService userService, final BCryptPasswordEncoder encoder) {
+ this.userService = userService;
+ this.encoder = encoder;
+ }
+
+ @Override
+ public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+ Assert.notNull(authentication, "No authentication data provided");
+
+ String username = (String) authentication.getPrincipal();
+ String password = (String) authentication.getCredentials();
+
+ User user = userService.findUserByEmail(username);
+ if (user == null) {
+ throw new UsernameNotFoundException("User not found: " + username);
+ }
+
+ UserCredentials userCredentials = userService.findUserCredentialsByUserId(user.getId());
+ if (userCredentials == null) {
+ throw new UsernameNotFoundException("User credentials not found");
+ }
+
+ if (!userCredentials.isEnabled()) {
+ throw new DisabledException("User is not active");
+ }
+
+ if (!encoder.matches(password, userCredentials.getPassword())) {
+ throw new BadCredentialsException("Authentication Failed. Username or Password not valid.");
+ }
+
+ if (user.getAuthority() == null) throw new InsufficientAuthenticationException("User has no authority assigned");
+
+ SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled());
+
+ return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());
+ }
+
+ @Override
+ public boolean supports(Class<?> authentication) {
+ return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationFailureHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationFailureHandler.java
new file mode 100644
index 0000000..879a17c
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationFailureHandler.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.security.auth.rest;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+@Component
+public class RestAwareAuthenticationFailureHandler implements AuthenticationFailureHandler {
+
+ private final ThingsboardErrorResponseHandler errorResponseHandler;
+
+ @Autowired
+ public RestAwareAuthenticationFailureHandler(ThingsboardErrorResponseHandler errorResponseHandler) {
+ this.errorResponseHandler = errorResponseHandler;
+ }
+
+ @Override
+ public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
+ AuthenticationException e) throws IOException, ServletException {
+ errorResponseHandler.handle(e, response);
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java
new file mode 100644
index 0000000..72c7719
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java
@@ -0,0 +1,85 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.security.auth.rest;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.WebAttributes;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.security.model.token.JwtToken;
+import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+@Component
+public class RestAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
+ private final ObjectMapper mapper;
+ private final JwtTokenFactory tokenFactory;
+ private final RefreshTokenRepository refreshTokenRepository;
+
+ @Autowired
+ public RestAwareAuthenticationSuccessHandler(final ObjectMapper mapper, final JwtTokenFactory tokenFactory, final RefreshTokenRepository refreshTokenRepository) {
+ this.mapper = mapper;
+ this.tokenFactory = tokenFactory;
+ this.refreshTokenRepository = refreshTokenRepository;
+ }
+
+ @Override
+ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
+ Authentication authentication) throws IOException, ServletException {
+ SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
+
+ JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);
+ JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser);
+
+ Map<String, String> tokenMap = new HashMap<String, String>();
+ tokenMap.put("token", accessToken.getToken());
+ tokenMap.put("refreshToken", refreshToken.getToken());
+
+ response.setStatus(HttpStatus.OK.value());
+ response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+ mapper.writeValue(response.getWriter(), tokenMap);
+
+ clearAuthenticationAttributes(request);
+ }
+
+ /**
+ * Removes temporary authentication-related data which may have been stored
+ * in the session during the authentication process..
+ *
+ */
+ protected final void clearAuthenticationAttributes(HttpServletRequest request) {
+ HttpSession session = request.getSession(false);
+
+ if (session == null) {
+ return;
+ }
+
+ session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java
new file mode 100644
index 0000000..ba6431e
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java
@@ -0,0 +1,93 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.security.auth.rest;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+public class RestLoginProcessingFilter extends AbstractAuthenticationProcessingFilter {
+ private static Logger logger = LoggerFactory.getLogger(RestLoginProcessingFilter.class);
+
+ private final AuthenticationSuccessHandler successHandler;
+ private final AuthenticationFailureHandler failureHandler;
+
+ private final ObjectMapper objectMapper;
+
+ public RestLoginProcessingFilter(String defaultProcessUrl, AuthenticationSuccessHandler successHandler,
+ AuthenticationFailureHandler failureHandler, ObjectMapper mapper) {
+ super(defaultProcessUrl);
+ this.successHandler = successHandler;
+ this.failureHandler = failureHandler;
+ this.objectMapper = mapper;
+ }
+
+ @Override
+ public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
+ throws AuthenticationException, IOException, ServletException {
+ if (!HttpMethod.POST.name().equals(request.getMethod())) {
+ if(logger.isDebugEnabled()) {
+ logger.debug("Authentication method not supported. Request method: " + request.getMethod());
+ }
+ throw new AuthMethodNotSupportedException("Authentication method not supported");
+ }
+
+ LoginRequest loginRequest;
+ try {
+ loginRequest = objectMapper.readValue(request.getReader(), LoginRequest.class);
+ } catch (Exception e) {
+ throw new AuthenticationServiceException("Invalid login request payload");
+ }
+
+ if (StringUtils.isBlank(loginRequest.getUsername()) || StringUtils.isBlank(loginRequest.getPassword())) {
+ throw new AuthenticationServiceException("Username or Password not provided");
+ }
+
+ UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword());
+
+ return this.getAuthenticationManager().authenticate(token);
+ }
+
+ @Override
+ protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
+ Authentication authResult) throws IOException, ServletException {
+ successHandler.onAuthenticationSuccess(request, response, authResult);
+ }
+
+ @Override
+ protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
+ AuthenticationException failed) throws IOException, ServletException {
+ SecurityContextHolder.clearContext();
+ failureHandler.onAuthenticationFailure(request, response, failed);
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/device/DefaultDeviceAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/device/DefaultDeviceAuthService.java
new file mode 100644
index 0000000..cef73fe
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/device/DefaultDeviceAuthService.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.security.device;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.security.DeviceCredentials;
+import org.thingsboard.server.common.data.security.DeviceCredentialsFilter;
+import org.thingsboard.server.common.transport.auth.DeviceAuthResult;
+import org.thingsboard.server.common.transport.auth.DeviceAuthService;
+import org.thingsboard.server.dao.device.DeviceCredentialsService;
+import org.thingsboard.server.dao.device.DeviceService;
+
+import java.util.Optional;
+
+@Service
+@Slf4j
+public class DefaultDeviceAuthService implements DeviceAuthService {
+
+ @Autowired
+ DeviceService deviceService;
+
+ @Autowired
+ DeviceCredentialsService deviceCredentialsService;
+
+ @Override
+ public DeviceAuthResult process(DeviceCredentialsFilter credentialsFilter) {
+ log.trace("Lookup device credentials using filter {}", credentialsFilter);
+ DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(credentialsFilter.getCredentialsId());
+ if (credentials != null) {
+ log.trace("Credentials found {}", credentials);
+ if (credentials.getCredentialsType() == credentialsFilter.getCredentialsType()) {
+ switch (credentials.getCredentialsType()) {
+ case ACCESS_TOKEN:
+ // Credentials ID matches Credentials value in this
+ // primitive case;
+ return DeviceAuthResult.of(credentials.getDeviceId());
+ default:
+ return DeviceAuthResult.of("Credentials Type is not supported yet!");
+ }
+ } else {
+ return DeviceAuthResult.of("Credentials Type mismatch!");
+ }
+ } else {
+ log.trace("Credentials not found!");
+ return DeviceAuthResult.of("Credentials Not Found!");
+ }
+ }
+
+ @Override
+ public Optional<Device> findDeviceById(DeviceId deviceId) {
+ return Optional.ofNullable(deviceService.findDeviceById(deviceId));
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/exception/AuthMethodNotSupportedException.java b/application/src/main/java/org/thingsboard/server/service/security/exception/AuthMethodNotSupportedException.java
new file mode 100644
index 0000000..4444870
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/exception/AuthMethodNotSupportedException.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.security.exception;
+
+import org.springframework.security.authentication.AuthenticationServiceException;
+
+public class AuthMethodNotSupportedException extends AuthenticationServiceException {
+ private static final long serialVersionUID = 3705043083010304496L;
+
+ public AuthMethodNotSupportedException(String msg) {
+ super(msg);
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/exception/JwtExpiredTokenException.java b/application/src/main/java/org/thingsboard/server/service/security/exception/JwtExpiredTokenException.java
new file mode 100644
index 0000000..a3c522c
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/exception/JwtExpiredTokenException.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.security.exception;
+
+import org.springframework.security.core.AuthenticationException;
+import org.thingsboard.server.service.security.model.token.JwtToken;
+
+public class JwtExpiredTokenException extends AuthenticationException {
+ private static final long serialVersionUID = -5959543783324224864L;
+
+ private JwtToken token;
+
+ public JwtExpiredTokenException(String msg) {
+ super(msg);
+ }
+
+ public JwtExpiredTokenException(JwtToken token, String msg, Throwable t) {
+ super(msg, t);
+ this.token = token;
+ }
+
+ public String token() {
+ return this.token.getToken();
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java b/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java
new file mode 100644
index 0000000..83b87ab
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java
@@ -0,0 +1,64 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.security.model;
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.id.UserId;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.stream.Collectors;
+
+public class SecurityUser extends User {
+
+ private static final long serialVersionUID = -797397440703066079L;
+
+ private Collection<GrantedAuthority> authorities;
+ private boolean enabled;
+
+ public SecurityUser() {
+ super();
+ }
+
+ public SecurityUser(UserId id) {
+ super(id);
+ }
+
+ public SecurityUser(User user, boolean enabled) {
+ super(user);
+ this.enabled = enabled;
+ }
+
+ public Collection<? extends GrantedAuthority> getAuthorities() {
+ if (authorities == null) {
+ authorities = Arrays.asList(SecurityUser.this.getAuthority()).stream()
+ .map(authority -> new SimpleGrantedAuthority(authority.name()))
+ .collect(Collectors.toList());
+ }
+ return authorities;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/AccessJwtToken.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/AccessJwtToken.java
new file mode 100644
index 0000000..9145937
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/AccessJwtToken.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.security.model.token;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import io.jsonwebtoken.Claims;
+
+public final class AccessJwtToken implements JwtToken {
+ private final String rawToken;
+ @JsonIgnore
+ private Claims claims;
+
+ protected AccessJwtToken(final String token, Claims claims) {
+ this.rawToken = token;
+ this.claims = claims;
+ }
+
+ public String getToken() {
+ return this.rawToken;
+ }
+
+ public Claims getClaims() {
+ return claims;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtToken.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtToken.java
new file mode 100644
index 0000000..0e43fdf
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtToken.java
@@ -0,0 +1,20 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.security.model.token;
+
+public interface JwtToken {
+ String getToken();
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java
new file mode 100644
index 0000000..0ce6181
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java
@@ -0,0 +1,158 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.security.model.token;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jws;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import org.apache.commons.lang3.StringUtils;
+import org.joda.time.DateTime;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UserId;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.config.JwtSettings;
+import org.thingsboard.server.service.security.model.SecurityUser;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+@Component
+public class JwtTokenFactory {
+
+ private static final String SCOPES = "scopes";
+ private static final String USER_ID = "userId";
+ private static final String FIRST_NAME = "firstName";
+ private static final String LAST_NAME = "lastName";
+ private static final String ENABLED = "enabled";
+ private static final String TENANT_ID = "tenantId";
+ private static final String CUSTOMER_ID = "customerId";
+
+ private final JwtSettings settings;
+
+ @Autowired
+ public JwtTokenFactory(JwtSettings settings) {
+ this.settings = settings;
+ }
+
+ /**
+ * Factory method for issuing new JWT Tokens.
+ */
+ public AccessJwtToken createAccessJwtToken(SecurityUser securityUser) {
+ if (StringUtils.isBlank(securityUser.getEmail()))
+ throw new IllegalArgumentException("Cannot create JWT Token without username/email");
+
+ if (securityUser.getAuthority() == null)
+ throw new IllegalArgumentException("User doesn't have any privileges");
+
+ Claims claims = Jwts.claims().setSubject(securityUser.getEmail());
+ claims.put(SCOPES, securityUser.getAuthorities().stream().map(s -> s.getAuthority()).collect(Collectors.toList()));
+ claims.put(USER_ID, securityUser.getId().getId().toString());
+ claims.put(FIRST_NAME, securityUser.getFirstName());
+ claims.put(LAST_NAME, securityUser.getLastName());
+ claims.put(ENABLED, securityUser.isEnabled());
+ if (securityUser.getTenantId() != null) {
+ claims.put(TENANT_ID, securityUser.getTenantId().getId().toString());
+ }
+ if (securityUser.getCustomerId() != null) {
+ claims.put(CUSTOMER_ID, securityUser.getCustomerId().getId().toString());
+ }
+
+ DateTime currentTime = new DateTime();
+
+ String token = Jwts.builder()
+ .setClaims(claims)
+ .setIssuer(settings.getTokenIssuer())
+ .setIssuedAt(currentTime.toDate())
+ .setExpiration(currentTime.plusSeconds(settings.getTokenExpirationTime()).toDate())
+ .signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey())
+ .compact();
+
+ return new AccessJwtToken(token, claims);
+ }
+
+ public SecurityUser parseAccessJwtToken(RawAccessJwtToken rawAccessToken) {
+ Jws<Claims> jwsClaims = rawAccessToken.parseClaims(settings.getTokenSigningKey());
+ Claims claims = jwsClaims.getBody();
+ String subject = claims.getSubject();
+ List<String> scopes = claims.get(SCOPES, List.class);
+ if (scopes == null || scopes.isEmpty()) {
+ throw new IllegalArgumentException("JWT Token doesn't have any scopes");
+ }
+
+ SecurityUser securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class))));
+ securityUser.setEmail(subject);
+ securityUser.setAuthority(Authority.parse(scopes.get(0)));
+ securityUser.setFirstName(claims.get(FIRST_NAME, String.class));
+ securityUser.setLastName(claims.get(LAST_NAME, String.class));
+ securityUser.setEnabled(claims.get(ENABLED, Boolean.class));
+ String tenantId = claims.get(TENANT_ID, String.class);
+ if (tenantId != null) {
+ securityUser.setTenantId(new TenantId(UUID.fromString(tenantId)));
+ }
+ String customerId = claims.get(CUSTOMER_ID, String.class);
+ if (customerId != null) {
+ securityUser.setCustomerId(new CustomerId(UUID.fromString(customerId)));
+ }
+
+ return securityUser;
+ }
+
+ public JwtToken createRefreshToken(SecurityUser securityUser) {
+ if (StringUtils.isBlank(securityUser.getEmail())) {
+ throw new IllegalArgumentException("Cannot create JWT Token without username/email");
+ }
+
+ DateTime currentTime = new DateTime();
+
+ Claims claims = Jwts.claims().setSubject(securityUser.getEmail());
+ claims.put(SCOPES, Arrays.asList(Authority.REFRESH_TOKEN.name()));
+ claims.put(USER_ID, securityUser.getId().getId().toString());
+
+ String token = Jwts.builder()
+ .setClaims(claims)
+ .setIssuer(settings.getTokenIssuer())
+ .setId(UUID.randomUUID().toString())
+ .setIssuedAt(currentTime.toDate())
+ .setExpiration(currentTime.plusSeconds(settings.getRefreshTokenExpTime()).toDate())
+ .signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey())
+ .compact();
+
+ return new AccessJwtToken(token, claims);
+ }
+
+ public SecurityUser parseRefreshToken(RawAccessJwtToken rawAccessToken) {
+ Jws<Claims> jwsClaims = rawAccessToken.parseClaims(settings.getTokenSigningKey());
+ Claims claims = jwsClaims.getBody();
+ String subject = claims.getSubject();
+ List<String> scopes = claims.get(SCOPES, List.class);
+ if (scopes == null || scopes.isEmpty()) {
+ throw new IllegalArgumentException("Refresh Token doesn't have any scopes");
+ }
+ if (!scopes.get(0).equals(Authority.REFRESH_TOKEN.name())) {
+ throw new IllegalArgumentException("Invalid Refresh Token scope");
+ }
+ SecurityUser securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class))));
+ securityUser.setEmail(subject);
+ return securityUser;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/RawAccessJwtToken.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/RawAccessJwtToken.java
new file mode 100644
index 0000000..cf32374
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/RawAccessJwtToken.java
@@ -0,0 +1,56 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.security.model.token;
+
+import io.jsonwebtoken.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.thingsboard.server.service.security.exception.JwtExpiredTokenException;
+
+public class RawAccessJwtToken implements JwtToken {
+ private static Logger logger = LoggerFactory.getLogger(RawAccessJwtToken.class);
+
+ private String token;
+
+ public RawAccessJwtToken(String token) {
+ this.token = token;
+ }
+
+ /**
+ * Parses and validates JWT Token signature.
+ *
+ * @throws BadCredentialsException
+ * @throws JwtExpiredTokenException
+ *
+ */
+ public Jws<Claims> parseClaims(String signingKey) {
+ try {
+ return Jwts.parser().setSigningKey(signingKey).parseClaimsJws(this.token);
+ } catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException | SignatureException ex) {
+ logger.error("Invalid JWT Token", ex);
+ throw new BadCredentialsException("Invalid JWT token: ", ex);
+ } catch (ExpiredJwtException expiredEx) {
+ logger.info("JWT Token is expired", expiredEx);
+ throw new JwtExpiredTokenException(this, "JWT Token expired", expiredEx);
+ }
+ }
+
+ @Override
+ public String getToken() {
+ return token;
+ }
+}
dao/pom.xml 176(+176 -0)
diff --git a/dao/pom.xml b/dao/pom.xml
new file mode 100644
index 0000000..f9f0d56
--- /dev/null
+++ b/dao/pom.xml
@@ -0,0 +1,176 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.thingsboard</groupId>
+ <version>0.0.1-SNAPSHOT</version>
+ <artifactId>server</artifactId>
+ </parent>
+ <groupId>org.thingsboard.server</groupId>
+ <artifactId>dao</artifactId>
+ <packaging>jar</packaging>
+
+ <name>Thingsboard Server DAO Layer</name>
+ <url>http://thingsboard.org</url>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <main.dir>${basedir}/..</main.dir>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.thingsboard.server.common</groupId>
+ <artifactId>data</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>log4j-over-slf4j</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-classic</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-test</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-all</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-lang3</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>commons-validator</groupId>
+ <artifactId>commons-validator</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.github.fge</groupId>
+ <artifactId>json-schema-validator</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-context</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-tx</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.datastax.cassandra</groupId>
+ <artifactId>cassandra-driver-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.datastax.cassandra</groupId>
+ <artifactId>cassandra-driver-mapping</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.datastax.cassandra</groupId>
+ <artifactId>cassandra-driver-extras</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.takari.junit</groupId>
+ <artifactId>takari-cpsuite</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.cassandraunit</groupId>
+ <artifactId>cassandra-unit</artifactId>
+ <exclusions>
+ <exclusion>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-log4j12</artifactId>
+ </exclusion>
+ </exclusions>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.curator</groupId>
+ <artifactId>curator-x-discovery</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.hazelcast</groupId>
+ <artifactId>hazelcast-zookeeper</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.hazelcast</groupId>
+ <artifactId>hazelcast</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.hazelcast</groupId>
+ <artifactId>hazelcast-spring</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-autoconfigure</artifactId>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <version>${surfire.version}</version>
+ <configuration>
+ <includes>
+ <include>**/*TestSuite.java</include>
+ </includes>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <version>${jar-plugin.version}</version>
+ <executions>
+ <execution>
+ <goals>
+ <goal>test-jar</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/dao/src/main/java/org/thingsboard/server/dao/AbstractDao.java b/dao/src/main/java/org/thingsboard/server/dao/AbstractDao.java
new file mode 100644
index 0000000..b927f14
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/AbstractDao.java
@@ -0,0 +1,93 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao;
+
+import com.datastax.driver.core.*;
+import com.datastax.driver.core.exceptions.CodecNotFoundException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.thingsboard.server.dao.cassandra.CassandraCluster;
+import org.thingsboard.server.dao.model.type.*;
+
+@Slf4j
+public abstract class AbstractDao {
+
+ @Autowired
+ protected CassandraCluster cluster;
+
+ private Session session;
+
+ private ConsistencyLevel defaultReadLevel;
+ private ConsistencyLevel defaultWriteLevel;
+
+ protected Session getSession() {
+ if (session == null) {
+ session = cluster.getSession();
+ defaultReadLevel = cluster.getDefaultReadConsistencyLevel();
+ defaultWriteLevel = cluster.getDefaultWriteConsistencyLevel();
+ CodecRegistry registry = session.getCluster().getConfiguration().getCodecRegistry();
+ registerCodecIfNotFound(registry, new JsonCodec());
+ registerCodecIfNotFound(registry, new DeviceCredentialsTypeCodec());
+ registerCodecIfNotFound(registry, new AuthorityCodec());
+ registerCodecIfNotFound(registry, new ComponentLifecycleStateCodec());
+ registerCodecIfNotFound(registry, new ComponentTypeCodec());
+ registerCodecIfNotFound(registry, new ComponentScopeCodec());
+ registerCodecIfNotFound(registry, new EntityTypeCodec());
+ }
+ return session;
+ }
+
+ private void registerCodecIfNotFound(CodecRegistry registry, TypeCodec<?> codec) {
+ try {
+ registry.codecFor(codec.getCqlType(), codec.getJavaType());
+ } catch (CodecNotFoundException e) {
+ registry.register(codec);
+ }
+ }
+
+ protected ResultSet executeRead(Statement statement) {
+ return execute(statement, defaultReadLevel);
+ }
+
+ protected ResultSet executeWrite(Statement statement) {
+ return execute(statement, defaultWriteLevel);
+ }
+
+
+ protected ResultSetFuture executeAsyncRead(Statement statement) {
+ return executeAsync(statement, defaultReadLevel);
+ }
+
+ protected ResultSetFuture executeAsyncWrite(Statement statement) {
+ return executeAsync(statement, defaultWriteLevel);
+ }
+
+ private ResultSet execute(Statement statement, ConsistencyLevel level) {
+ log.debug("Execute cassandra statement {}", statement);
+ if (statement.getConsistencyLevel() == null) {
+ statement.setConsistencyLevel(level);
+ }
+ return getSession().execute(statement);
+ }
+
+ private ResultSetFuture executeAsync(Statement statement, ConsistencyLevel level) {
+ log.debug("Execute cassandra async statement {}", statement);
+ if (statement.getConsistencyLevel() == null) {
+ statement.setConsistencyLevel(level);
+ }
+ return getSession().executeAsync(statement);
+ }
+}
\ No newline at end of file
diff --git a/dao/src/main/java/org/thingsboard/server/dao/AbstractModelDao.java b/dao/src/main/java/org/thingsboard/server/dao/AbstractModelDao.java
new file mode 100644
index 0000000..229cd03
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/AbstractModelDao.java
@@ -0,0 +1,114 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao;
+
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.Statement;
+import com.datastax.driver.core.querybuilder.QueryBuilder;
+import com.datastax.driver.core.querybuilder.Select;
+import com.datastax.driver.core.utils.UUIDs;
+import com.datastax.driver.mapping.Mapper;
+import com.datastax.driver.mapping.Result;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.dao.model.BaseEntity;
+import org.thingsboard.server.dao.model.wrapper.EntityResultSet;
+import org.thingsboard.server.dao.model.ModelConstants;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+
+import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.lt;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
+
+@Slf4j
+public abstract class AbstractModelDao<T extends BaseEntity<?>> extends AbstractDao implements Dao<T> {
+
+ protected abstract Class<T> getColumnFamilyClass();
+
+ protected abstract String getColumnFamilyName();
+
+ protected Mapper<T> getMapper() {
+ return cluster.getMapper(getColumnFamilyClass());
+ }
+
+ protected List<T> findListByStatement(Statement statement) {
+ List<T> list = Collections.emptyList();
+ if (statement != null) {
+ statement.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
+ ResultSet resultSet = getSession().execute(statement);
+ Result<T> result = getMapper().map(resultSet);
+ if (result != null) {
+ list = result.all();
+ }
+ }
+ return list;
+ }
+
+ protected T findOneByStatement(Statement statement) {
+ T object = null;
+ if (statement != null) {
+ statement.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
+ ResultSet resultSet = getSession().execute(statement);
+ Result<T> result = getMapper().map(resultSet);
+ if (result != null) {
+ object = result.one();
+ }
+ }
+ return object;
+ }
+
+ protected Statement getSaveQuery(T dto) {
+ return getMapper().saveQuery(dto);
+ }
+
+ protected EntityResultSet<T> saveWithResult(T entity) {
+ log.debug("Save entity {}", entity);
+ if (entity.getId() == null) {
+ entity.setId(UUIDs.timeBased());
+ } else {
+ removeById(entity.getId());
+ }
+ Statement saveStatement = getSaveQuery(entity);
+ saveStatement.setConsistencyLevel(cluster.getDefaultWriteConsistencyLevel());
+ ResultSet resultSet = executeWrite(saveStatement);
+ return new EntityResultSet<>(resultSet, entity);
+ }
+
+ public T save(T entity) {
+ return saveWithResult(entity).getEntity();
+ }
+
+ public T findById(UUID key) {
+ log.debug("Get entity by key {}", key);
+ Select.Where query = select().from(getColumnFamilyName()).where(eq(ModelConstants.ID_PROPERTY, key));
+ log.trace("Execute query {}", query);
+ return findOneByStatement(query);
+ }
+
+ public ResultSet removeById(UUID key) {
+ Statement delete = QueryBuilder.delete().all().from(getColumnFamilyName()).where(eq(ModelConstants.ID_PROPERTY, key));
+ log.debug("Remove request: {}", delete.toString());
+ return getSession().execute(delete);
+ }
+
+
+ public List<T> find() {
+ log.debug("Get all entities from column family {}", getColumnFamilyName());
+ return findListByStatement(QueryBuilder.select().all().from(getColumnFamilyName()).setConsistencyLevel(cluster.getDefaultReadConsistencyLevel()));
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/AbstractSearchTextDao.java b/dao/src/main/java/org/thingsboard/server/dao/AbstractSearchTextDao.java
new file mode 100644
index 0000000..d2a93e3
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/AbstractSearchTextDao.java
@@ -0,0 +1,78 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao;
+
+import com.datastax.driver.core.querybuilder.Clause;
+import com.datastax.driver.core.querybuilder.QueryBuilder;
+import com.datastax.driver.core.querybuilder.Select;
+import com.datastax.driver.core.querybuilder.Select.Where;
+import org.apache.commons.lang3.StringUtils;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.dao.model.SearchTextEntity;
+import org.thingsboard.server.dao.model.ModelConstants;
+
+import java.util.List;
+
+import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.gt;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.gte;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.lt;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
+
+public abstract class AbstractSearchTextDao<T extends SearchTextEntity<?>> extends AbstractModelDao<T> {
+
+ public T save(T entity) {
+ entity.setSearchText(entity.getSearchTextSource().toLowerCase());
+ return super.save(entity);
+ }
+
+ protected List<T> findPageWithTextSearch(String searchView, List<Clause> clauses, TextPageLink pageLink) {
+ Select select = select().from(searchView);
+ Where query = select.where();
+ for (Clause clause : clauses) {
+ query.and(clause);
+ }
+ query.limit(pageLink.getLimit());
+ if (!StringUtils.isEmpty(pageLink.getTextOffset())) {
+ query.and(eq(ModelConstants.SEARCH_TEXT_PROPERTY, pageLink.getTextOffset()));
+ query.and(QueryBuilder.lt(ModelConstants.ID_PROPERTY, pageLink.getIdOffset()));
+ List<T> result = findListByStatement(query);
+ if (result.size() < pageLink.getLimit()) {
+ select = select().from(searchView);
+ query = select.where();
+ for (Clause clause : clauses) {
+ query.and(clause);
+ }
+ query.and(QueryBuilder.gt(ModelConstants.SEARCH_TEXT_PROPERTY, pageLink.getTextOffset()));
+ if (!StringUtils.isEmpty(pageLink.getTextSearch())) {
+ query.and(QueryBuilder.lt(ModelConstants.SEARCH_TEXT_PROPERTY, pageLink.getTextSearchBound()));
+ }
+ int limit = pageLink.getLimit() - result.size();
+ query.limit(limit);
+ result.addAll(findListByStatement(query));
+ }
+ return result;
+ } else if (!StringUtils.isEmpty(pageLink.getTextSearch())) {
+ query.and(QueryBuilder.gte(ModelConstants.SEARCH_TEXT_PROPERTY, pageLink.getTextSearch()));
+ query.and(QueryBuilder.lt(ModelConstants.SEARCH_TEXT_PROPERTY, pageLink.getTextSearchBound()));
+ return findListByStatement(query);
+ } else {
+ return findListByStatement(query);
+ }
+ }
+
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/AbstractSearchTimeDao.java b/dao/src/main/java/org/thingsboard/server/dao/AbstractSearchTimeDao.java
new file mode 100644
index 0000000..dcdbbbd
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/AbstractSearchTimeDao.java
@@ -0,0 +1,90 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao;
+
+import com.datastax.driver.core.querybuilder.Clause;
+import com.datastax.driver.core.querybuilder.Ordering;
+import com.datastax.driver.core.querybuilder.QueryBuilder;
+import com.datastax.driver.core.querybuilder.Select;
+import com.datastax.driver.core.querybuilder.Select.Where;
+import com.datastax.driver.core.utils.UUIDs;
+import org.apache.commons.lang3.StringUtils;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.dao.model.BaseEntity;
+import org.thingsboard.server.dao.model.ModelConstants;
+import org.thingsboard.server.dao.model.SearchTextEntity;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+
+import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
+
+public abstract class AbstractSearchTimeDao<T extends BaseEntity<?>> extends AbstractModelDao<T> {
+
+
+ protected List<T> findPageWithTimeSearch(String searchView, List<Clause> clauses, TimePageLink pageLink) {
+ return findPageWithTimeSearch(searchView, clauses, Collections.emptyList(), pageLink);
+ }
+
+ protected List<T> findPageWithTimeSearch(String searchView, List<Clause> clauses, Ordering ordering, TimePageLink pageLink) {
+ return findPageWithTimeSearch(searchView, clauses, Collections.singletonList(ordering), pageLink);
+ }
+
+
+ protected List<T> findPageWithTimeSearch(String searchView, List<Clause> clauses, List<Ordering> topLevelOrderings, TimePageLink pageLink) {
+ Select select = select().from(searchView);
+ Where query = select.where();
+ for (Clause clause : clauses) {
+ query.and(clause);
+ }
+ query.limit(pageLink.getLimit());
+ if (pageLink.isAscOrder()) {
+ if (pageLink.getIdOffset() != null) {
+ query.and(QueryBuilder.gt(ModelConstants.ID_PROPERTY, pageLink.getIdOffset()));
+ } else if (pageLink.getStartTime() != null) {
+ final UUID startOf = UUIDs.startOf(pageLink.getStartTime());
+ query.and(QueryBuilder.gte(ModelConstants.ID_PROPERTY, startOf));
+ }
+ if (pageLink.getEndTime() != null) {
+ final UUID endOf = UUIDs.endOf(pageLink.getEndTime());
+ query.and(QueryBuilder.lte(ModelConstants.ID_PROPERTY, endOf));
+ }
+ } else {
+ if (pageLink.getIdOffset() != null) {
+ query.and(QueryBuilder.lt(ModelConstants.ID_PROPERTY, pageLink.getIdOffset()));
+ } else if (pageLink.getEndTime() != null) {
+ final UUID endOf = UUIDs.endOf(pageLink.getEndTime());
+ query.and(QueryBuilder.lte(ModelConstants.ID_PROPERTY, endOf));
+ }
+ if (pageLink.getStartTime() != null) {
+ final UUID startOf = UUIDs.startOf(pageLink.getStartTime());
+ query.and(QueryBuilder.gte(ModelConstants.ID_PROPERTY, startOf));
+ }
+ }
+ List<Ordering> orderings = new ArrayList<>(topLevelOrderings);
+ if (pageLink.isAscOrder()) {
+ orderings.add(QueryBuilder.asc(ModelConstants.ID_PROPERTY));
+ } else {
+ orderings.add(QueryBuilder.desc(ModelConstants.ID_PROPERTY));
+ }
+ query.orderBy(orderings.toArray(new Ordering[orderings.size()]));
+ return findListByStatement(query);
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraCluster.java b/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraCluster.java
new file mode 100644
index 0000000..3af7748
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraCluster.java
@@ -0,0 +1,159 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.cassandra;
+
+
+import com.datastax.driver.core.Cluster;
+import com.datastax.driver.core.ConsistencyLevel;
+import com.datastax.driver.core.ProtocolOptions.Compression;
+import com.datastax.driver.core.Session;
+import com.datastax.driver.core.exceptions.NoHostAvailableException;
+import com.datastax.driver.mapping.Mapper;
+import com.datastax.driver.mapping.MappingManager;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.dao.exception.DatabaseException;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.io.Closeable;
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.StringTokenizer;
+
+@Component
+@Slf4j
+@Data
+public class CassandraCluster {
+
+ private static final String COMMA = ",";
+ private static final String COLON = ":";
+
+ @Value("${cassandra.cluster_name}")
+ private String clusterName;
+ @Value("${cassandra.keyspace_name}")
+ private String keyspaceName;
+ @Value("${cassandra.url}")
+ private String url;
+ @Value("${cassandra.compression}")
+ private String compression;
+ @Value("${cassandra.ssl}")
+ private Boolean ssl;
+ @Value("${cassandra.jmx}")
+ private Boolean jmx;
+ @Value("${cassandra.metrics}")
+ private Boolean metrics;
+ @Value("${cassandra.credentials}")
+ private Boolean credentials;
+ @Value("${cassandra.username}")
+ private String username;
+ @Value("${cassandra.password}")
+ private String password;
+ @Value("${cassandra.init_timeout_ms}")
+ private long initTimeout;
+ @Value("${cassandra.init_retry_interval_ms}")
+ private long initRetryInterval;
+
+ @Autowired
+ private CassandraSocketOptions socketOpts;
+
+ @Autowired
+ private CassandraQueryOptions queryOpts;
+
+ private Cluster cluster;
+ private Session session;
+ private MappingManager mappingManager;
+
+ public <T> Mapper<T> getMapper(Class<T> clazz) {
+ return mappingManager.mapper(clazz);
+ }
+
+ @PostConstruct
+ public void init() {
+ long endTime = System.currentTimeMillis() + initTimeout;
+ while (System.currentTimeMillis() < endTime) {
+ try {
+ Cluster.Builder builder = Cluster.builder()
+ .addContactPointsWithPorts(getContactPoints(url))
+ .withClusterName(clusterName)
+ .withSocketOptions(socketOpts.getOpts());
+ builder.withQueryOptions(queryOpts.getOpts());
+ builder.withCompression(StringUtils.isEmpty(compression) ? Compression.NONE : Compression.valueOf(compression.toUpperCase()));
+ if (ssl) {
+ builder.withSSL();
+ }
+ if (!jmx) {
+ builder.withoutJMXReporting();
+ }
+ if (!metrics) {
+ builder.withoutMetrics();
+ }
+ if (credentials) {
+ builder.withCredentials(username, password);
+ }
+ cluster = builder.build();
+ cluster.init();
+ session = cluster.connect(keyspaceName);
+ mappingManager = new MappingManager(session);
+ break;
+ } catch (Exception e) {
+ log.warn("Failed to initialize cassandra cluster due to {}. Will retry in {} ms", e.getMessage(), initRetryInterval);
+ try {
+ Thread.sleep(initRetryInterval);
+ } catch (InterruptedException ie) {
+ log.warn("Failed to wait until retry", ie);
+ }
+ }
+ }
+ }
+
+ @PreDestroy
+ public void close() {
+ if (cluster != null) {
+ cluster.close();
+ }
+ }
+
+ private List<InetSocketAddress> getContactPoints(String url) {
+ List<InetSocketAddress> result;
+ if (StringUtils.isBlank(url)) {
+ result = Collections.emptyList();
+ } else {
+ result = new ArrayList<>();
+ for (String hostPort : url.split(COMMA)) {
+ String host = hostPort.split(COLON)[0];
+ Integer port = Integer.valueOf(hostPort.split(COLON)[1]);
+ result.add(new InetSocketAddress(host, port));
+ }
+ }
+ return result;
+ }
+
+ public ConsistencyLevel getDefaultReadConsistencyLevel() {
+ return queryOpts.getDefaultReadConsistencyLevel();
+ }
+
+ public ConsistencyLevel getDefaultWriteConsistencyLevel() {
+ return queryOpts.getDefaultWriteConsistencyLevel();
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraQueryOptions.java b/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraQueryOptions.java
new file mode 100644
index 0000000..291f2da
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraQueryOptions.java
@@ -0,0 +1,74 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.cassandra;
+
+import com.datastax.driver.core.ConsistencyLevel;
+import com.datastax.driver.core.QueryOptions;
+import lombok.Data;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import javax.annotation.PostConstruct;
+
+import static org.apache.commons.lang3.StringUtils.isNotBlank;
+
+@Component
+@Configuration
+@Data
+public class CassandraQueryOptions {
+
+ @Value("${cassandra.query.default_fetch_size}")
+ private Integer defaultFetchSize;
+ @Value("${cassandra.query.read_consistency_level}")
+ private String readConsistencyLevel;
+ @Value("${cassandra.query.write_consistency_level}")
+ private String writeConsistencyLevel;
+
+ private QueryOptions opts;
+
+ private ConsistencyLevel defaultReadConsistencyLevel;
+ private ConsistencyLevel defaultWriteConsistencyLevel;
+
+ @PostConstruct
+ public void initOpts(){
+ opts = new QueryOptions();
+ opts.setFetchSize(defaultFetchSize);
+ }
+
+ protected ConsistencyLevel getDefaultReadConsistencyLevel() {
+ if (defaultReadConsistencyLevel == null) {
+ if (readConsistencyLevel != null) {
+ defaultReadConsistencyLevel = ConsistencyLevel.valueOf(readConsistencyLevel.toUpperCase());
+ } else {
+ defaultReadConsistencyLevel = ConsistencyLevel.ONE;
+ }
+ }
+ return defaultReadConsistencyLevel;
+ }
+
+ protected ConsistencyLevel getDefaultWriteConsistencyLevel() {
+ if (defaultWriteConsistencyLevel == null) {
+ if (writeConsistencyLevel != null) {
+ defaultWriteConsistencyLevel = ConsistencyLevel.valueOf(writeConsistencyLevel.toUpperCase());
+ } else {
+ defaultWriteConsistencyLevel = ConsistencyLevel.ONE;
+ }
+ }
+ return defaultWriteConsistencyLevel;
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraSocketOptions.java b/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraSocketOptions.java
new file mode 100644
index 0000000..b2cf918
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraSocketOptions.java
@@ -0,0 +1,75 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.cassandra;
+
+import lombok.Data;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.stereotype.Component;
+
+import com.datastax.driver.core.SocketOptions;
+
+import javax.annotation.PostConstruct;
+
+@Component
+@Configuration
+@Data
+public class CassandraSocketOptions {
+
+ @Value("${cassandra.socket.connect_timeout}")
+ private int connectTimeoutMillis;
+ @Value("${cassandra.socket.read_timeout}")
+ private int readTimeoutMillis;
+ @Value("${cassandra.socket.keep_alive}")
+ private Boolean keepAlive;
+ @Value("${cassandra.socket.reuse_address}")
+ private Boolean reuseAddress;
+ @Value("${cassandra.socket.so_linger}")
+ private Integer soLinger;
+ @Value("${cassandra.socket.tcp_no_delay}")
+ private Boolean tcpNoDelay;
+ @Value("${cassandra.socket.receive_buffer_size}")
+ private Integer receiveBufferSize;
+ @Value("${cassandra.socket.send_buffer_size}")
+ private Integer sendBufferSize;
+
+ private SocketOptions opts;
+
+ @PostConstruct
+ public void initOpts() {
+ opts = new SocketOptions();
+ opts.setConnectTimeoutMillis(connectTimeoutMillis);
+ opts.setReadTimeoutMillis(readTimeoutMillis);
+ if (keepAlive != null) {
+ opts.setKeepAlive(keepAlive);
+ }
+ if (reuseAddress != null) {
+ opts.setReuseAddress(reuseAddress);
+ }
+ if (soLinger != null) {
+ opts.setSoLinger(soLinger);
+ }
+ if (tcpNoDelay != null) {
+ opts.setTcpNoDelay(tcpNoDelay);
+ }
+ if (receiveBufferSize != null) {
+ opts.setReceiveBufferSize(receiveBufferSize);
+ }
+ if (sendBufferSize != null) {
+ opts.setSendBufferSize(sendBufferSize);
+ }
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorDao.java b/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorDao.java
new file mode 100644
index 0000000..8e07bbf
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorDao.java
@@ -0,0 +1,169 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.component;
+
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.Statement;
+import com.datastax.driver.core.querybuilder.QueryBuilder;
+import com.datastax.driver.core.querybuilder.Select;
+import com.datastax.driver.core.utils.UUIDs;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.id.ComponentDescriptorId;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
+import org.thingsboard.server.common.data.plugin.ComponentScope;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.dao.AbstractSearchTextDao;
+import org.thingsboard.server.dao.model.ModelConstants;
+import org.thingsboard.server.dao.model.ComponentDescriptorEntity;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Component
+@Slf4j
+public class BaseComponentDescriptorDao extends AbstractSearchTextDao<ComponentDescriptorEntity> implements ComponentDescriptorDao {
+
+ @Override
+ protected Class<ComponentDescriptorEntity> getColumnFamilyClass() {
+ return ComponentDescriptorEntity.class;
+ }
+
+ @Override
+ protected String getColumnFamilyName() {
+ return ModelConstants.COMPONENT_DESCRIPTOR_COLUMN_FAMILY_NAME;
+ }
+
+ @Override
+ public Optional<ComponentDescriptorEntity> save(ComponentDescriptor component) {
+ ComponentDescriptorEntity entity = new ComponentDescriptorEntity(component);
+ log.debug("Save component entity [{}]", entity);
+ Optional<ComponentDescriptorEntity> result = saveIfNotExist(entity);
+ if (log.isTraceEnabled()) {
+ log.trace("Saved result: [{}] for component entity [{}]", result.isPresent(), result.orElse(null));
+ } else {
+ log.debug("Saved result: [{}]", result.isPresent());
+ }
+ return result;
+ }
+
+ @Override
+ public ComponentDescriptorEntity findById(ComponentDescriptorId componentId) {
+ log.debug("Search component entity by id [{}]", componentId);
+ ComponentDescriptorEntity entity = super.findById(componentId.getId());
+ if (log.isTraceEnabled()) {
+ log.trace("Search result: [{}] for component entity [{}]", entity != null, entity);
+ } else {
+ log.debug("Search result: [{}]", entity != null);
+ }
+ return entity;
+ }
+
+ @Override
+ public ComponentDescriptorEntity findByClazz(String clazz) {
+ log.debug("Search component entity by clazz [{}]", clazz);
+ Select.Where query = select().from(getColumnFamilyName()).where(eq(ModelConstants.COMPONENT_DESCRIPTOR_CLASS_PROPERTY, clazz));
+ log.trace("Execute query [{}]", query);
+ ComponentDescriptorEntity entity = findOneByStatement(query);
+ if (log.isTraceEnabled()) {
+ log.trace("Search result: [{}] for component entity [{}]", entity != null, entity);
+ } else {
+ log.debug("Search result: [{}]", entity != null);
+ }
+ return entity;
+ }
+
+ @Override
+ public List<ComponentDescriptorEntity> findByTypeAndPageLink(ComponentType type, TextPageLink pageLink) {
+ log.debug("Try to find component by type [{}] and pageLink [{}]", type, pageLink);
+ List<ComponentDescriptorEntity> entities = findPageWithTextSearch(ModelConstants.COMPONENT_DESCRIPTOR_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
+ Arrays.asList(eq(ModelConstants.COMPONENT_DESCRIPTOR_TYPE_PROPERTY, type.name())), pageLink);
+ if (log.isTraceEnabled()) {
+ log.trace("Search result: [{}]", Arrays.toString(entities.toArray()));
+ } else {
+ log.debug("Search result: [{}]", entities.size());
+ }
+ return entities;
+ }
+
+ @Override
+ public List<ComponentDescriptorEntity> findByScopeAndTypeAndPageLink(ComponentScope scope, ComponentType type, TextPageLink pageLink) {
+ log.debug("Try to find component by scope [{}] and type [{}] and pageLink [{}]", scope, type, pageLink);
+ List<ComponentDescriptorEntity> entities = findPageWithTextSearch(ModelConstants.COMPONENT_DESCRIPTOR_BY_SCOPE_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
+ Arrays.asList(eq(ModelConstants.COMPONENT_DESCRIPTOR_TYPE_PROPERTY, type.name()),
+ eq(ModelConstants.COMPONENT_DESCRIPTOR_SCOPE_PROPERTY, scope.name())), pageLink);
+ if (log.isTraceEnabled()) {
+ log.trace("Search result: [{}]", Arrays.toString(entities.toArray()));
+ } else {
+ log.debug("Search result: [{}]", entities.size());
+ }
+ return entities;
+ }
+
+ public ResultSet removeById(UUID key) {
+ Statement delete = QueryBuilder.delete().all().from(ModelConstants.COMPONENT_DESCRIPTOR_BY_ID).where(eq(ModelConstants.ID_PROPERTY, key));
+ log.debug("Remove request: {}", delete.toString());
+ return getSession().execute(delete);
+ }
+
+ @Override
+ public void deleteById(ComponentDescriptorId id) {
+ log.debug("Delete plugin meta-data entity by id [{}]", id);
+ ResultSet resultSet = removeById(id.getId());
+ log.debug("Delete result: [{}]", resultSet.wasApplied());
+ }
+
+ @Override
+ public void deleteByClazz(String clazz) {
+ log.debug("Delete plugin meta-data entity by id [{}]", clazz);
+ Statement delete = QueryBuilder.delete().all().from(getColumnFamilyName()).where(eq(ModelConstants.COMPONENT_DESCRIPTOR_CLASS_PROPERTY, clazz));
+ log.debug("Remove request: {}", delete.toString());
+ ResultSet resultSet = getSession().execute(delete);
+ log.debug("Delete result: [{}]", resultSet.wasApplied());
+ }
+
+ private Optional<ComponentDescriptorEntity> saveIfNotExist(ComponentDescriptorEntity entity) {
+ if (entity.getId() == null) {
+ entity.setId(UUIDs.timeBased());
+ }
+
+ ResultSet rs = executeRead(QueryBuilder.insertInto(getColumnFamilyName())
+ .value(ModelConstants.ID_PROPERTY, entity.getId())
+ .value(ModelConstants.COMPONENT_DESCRIPTOR_NAME_PROPERTY, entity.getName())
+ .value(ModelConstants.COMPONENT_DESCRIPTOR_CLASS_PROPERTY, entity.getClazz())
+ .value(ModelConstants.COMPONENT_DESCRIPTOR_TYPE_PROPERTY, entity.getType())
+ .value(ModelConstants.COMPONENT_DESCRIPTOR_SCOPE_PROPERTY, entity.getScope())
+ .value(ModelConstants.COMPONENT_DESCRIPTOR_CONFIGURATION_DESCRIPTOR_PROPERTY, entity.getConfigurationDescriptor())
+ .value(ModelConstants.COMPONENT_DESCRIPTOR_ACTIONS_PROPERTY, entity.getActions())
+ .value(ModelConstants.SEARCH_TEXT_PROPERTY, entity.getSearchText())
+ .ifNotExists()
+ );
+ if (rs.wasApplied()) {
+ return Optional.of(entity);
+ } else {
+ return Optional.empty();
+ }
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java b/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java
new file mode 100644
index 0000000..23a53cf
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java
@@ -0,0 +1,133 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.component;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.github.fge.jsonschema.core.exceptions.ProcessingException;
+import com.github.fge.jsonschema.core.report.ProcessingReport;
+import com.github.fge.jsonschema.main.JsonSchemaFactory;
+import com.github.fge.jsonschema.main.JsonValidator;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.id.ComponentDescriptorId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
+import org.thingsboard.server.common.data.plugin.ComponentScope;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.exception.IncorrectParameterException;
+import org.thingsboard.server.dao.model.ComponentDescriptorEntity;
+import org.thingsboard.server.dao.service.DataValidator;
+import org.thingsboard.server.dao.service.Validator;
+
+import java.util.List;
+import java.util.Optional;
+
+import static org.thingsboard.server.dao.DaoUtil.convertDataList;
+import static org.thingsboard.server.dao.DaoUtil.getData;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Service
+@Slf4j
+public class BaseComponentDescriptorService implements ComponentDescriptorService {
+
+ @Autowired
+ private ComponentDescriptorDao componentDescriptorDao;
+
+ @Override
+ public ComponentDescriptor saveComponent(ComponentDescriptor component) {
+ componentValidator.validate(component);
+ Optional<ComponentDescriptorEntity> result = componentDescriptorDao.save(component);
+ if (result.isPresent()) {
+ return getData(result.get());
+ } else {
+ return getData(componentDescriptorDao.findByClazz(component.getClazz()));
+ }
+ }
+
+ @Override
+ public ComponentDescriptor findById(ComponentDescriptorId componentId) {
+ Validator.validateId(componentId, "Incorrect component id for search request.");
+ return getData(componentDescriptorDao.findById(componentId));
+ }
+
+ @Override
+ public ComponentDescriptor findByClazz(String clazz) {
+ Validator.validateString(clazz, "Incorrect clazz for search request.");
+ return getData(componentDescriptorDao.findByClazz(clazz));
+ }
+
+ @Override
+ public TextPageData<ComponentDescriptor> findByTypeAndPageLink(ComponentType type, TextPageLink pageLink) {
+ Validator.validatePageLink(pageLink, "Incorrect PageLink object for search plugin components request.");
+ List<ComponentDescriptorEntity> pluginEntities = componentDescriptorDao.findByTypeAndPageLink(type, pageLink);
+ List<ComponentDescriptor> components = convertDataList(pluginEntities);
+ return new TextPageData<>(components, pageLink);
+ }
+
+ @Override
+ public TextPageData<ComponentDescriptor> findByScopeAndTypeAndPageLink(ComponentScope scope, ComponentType type, TextPageLink pageLink) {
+ Validator.validatePageLink(pageLink, "Incorrect PageLink object for search plugin components request.");
+ List<ComponentDescriptorEntity> pluginEntities = componentDescriptorDao.findByScopeAndTypeAndPageLink(scope, type, pageLink);
+ List<ComponentDescriptor> components = convertDataList(pluginEntities);
+ return new TextPageData<>(components, pageLink);
+ }
+
+ @Override
+ public void deleteByClazz(String clazz) {
+ Validator.validateString(clazz, "Incorrect clazz for delete request.");
+ componentDescriptorDao.deleteByClazz(clazz);
+ }
+
+ @Override
+ public boolean validate(ComponentDescriptor component, JsonNode configuration) {
+ JsonValidator validator = JsonSchemaFactory.byDefault().getValidator();
+ try {
+ if (!component.getConfigurationDescriptor().has("schema")) {
+ throw new DataValidationException("Configuration descriptor doesn't contain schema property!");
+ }
+ JsonNode configurationSchema = component.getConfigurationDescriptor().get("schema");
+ ProcessingReport report = validator.validate(configurationSchema, configuration);
+ return report.isSuccess();
+ } catch (ProcessingException e) {
+ throw new IncorrectParameterException(e.getMessage(), e);
+ }
+ }
+
+ private DataValidator<ComponentDescriptor> componentValidator =
+ new DataValidator<ComponentDescriptor>() {
+ @Override
+ protected void validateDataImpl(ComponentDescriptor plugin) {
+ if (plugin.getType() == null) {
+ throw new DataValidationException("Component type should be specified!.");
+ }
+ if (plugin.getScope() == null) {
+ throw new DataValidationException("Component scope should be specified!.");
+ }
+ if (StringUtils.isEmpty(plugin.getName())) {
+ throw new DataValidationException("Component name should be specified!.");
+ }
+ if (StringUtils.isEmpty(plugin.getClazz())) {
+ throw new DataValidationException("Component clazz should be specified!.");
+ }
+ }
+ };
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/component/ComponentDescriptorDao.java b/dao/src/main/java/org/thingsboard/server/dao/component/ComponentDescriptorDao.java
new file mode 100644
index 0000000..c78103c
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/component/ComponentDescriptorDao.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.component;
+
+import org.thingsboard.server.common.data.id.ComponentDescriptorId;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
+import org.thingsboard.server.common.data.plugin.ComponentScope;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.dao.Dao;
+import org.thingsboard.server.dao.model.ComponentDescriptorEntity;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface ComponentDescriptorDao extends Dao<ComponentDescriptorEntity> {
+
+ Optional<ComponentDescriptorEntity> save(ComponentDescriptor component);
+
+ ComponentDescriptorEntity findById(ComponentDescriptorId componentId);
+
+ ComponentDescriptorEntity findByClazz(String clazz);
+
+ List<ComponentDescriptorEntity> findByTypeAndPageLink(ComponentType type, TextPageLink pageLink);
+
+ List<ComponentDescriptorEntity> findByScopeAndTypeAndPageLink(ComponentScope scope, ComponentType type, TextPageLink pageLink);
+
+ void deleteById(ComponentDescriptorId componentId);
+
+ void deleteByClazz(String clazz);
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/component/ComponentDescriptorService.java b/dao/src/main/java/org/thingsboard/server/dao/component/ComponentDescriptorService.java
new file mode 100644
index 0000000..9ec81e7
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/component/ComponentDescriptorService.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.component;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.thingsboard.server.common.data.id.ComponentDescriptorId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
+import org.thingsboard.server.common.data.plugin.ComponentScope;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface ComponentDescriptorService {
+
+ ComponentDescriptor saveComponent(ComponentDescriptor component);
+
+ ComponentDescriptor findById(ComponentDescriptorId componentId);
+
+ ComponentDescriptor findByClazz(String clazz);
+
+ TextPageData<ComponentDescriptor> findByTypeAndPageLink(ComponentType type, TextPageLink pageLink);
+
+ TextPageData<ComponentDescriptor> findByScopeAndTypeAndPageLink(ComponentScope scope, ComponentType type, TextPageLink pageLink);
+
+ boolean validate(ComponentDescriptor component, JsonNode configuration);
+
+ void deleteByClazz(String clazz);
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java
new file mode 100644
index 0000000..de950a2
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.customer;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.dao.Dao;
+import org.thingsboard.server.dao.model.CustomerEntity;
+
+/**
+ * The Interface CustomerDao.
+ */
+public interface CustomerDao extends Dao<CustomerEntity> {
+
+ /**
+ * Save or update customer object
+ *
+ * @param customer the customer object
+ * @return saved customer object
+ */
+ CustomerEntity save(Customer customer);
+
+ /**
+ * Find customers by tenant id and page link.
+ *
+ * @param tenantId the tenant id
+ * @param pageLink the page link
+ * @return the list of customer objects
+ */
+ List<CustomerEntity> findCustomersByTenantId(UUID tenantId, TextPageLink pageLink);
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDaoImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDaoImpl.java
new file mode 100644
index 0000000..719be97
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDaoImpl.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.customer;
+
+import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.dao.AbstractSearchTextDao;
+import org.thingsboard.server.dao.model.CustomerEntity;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.thingsboard.server.dao.model.ModelConstants;
+@Component
+@Slf4j
+public class CustomerDaoImpl extends AbstractSearchTextDao<CustomerEntity> implements CustomerDao {
+
+ @Override
+ protected Class<CustomerEntity> getColumnFamilyClass() {
+ return CustomerEntity.class;
+ }
+
+ @Override
+ protected String getColumnFamilyName() {
+ return ModelConstants.CUSTOMER_COLUMN_FAMILY_NAME;
+ }
+
+ @Override
+ public CustomerEntity save(Customer customer) {
+ log.debug("Save customer [{}] ", customer);
+ return save(new CustomerEntity(customer));
+ }
+
+ @Override
+ public List<CustomerEntity> findCustomersByTenantId(UUID tenantId, TextPageLink pageLink) {
+ log.debug("Try to find customers by tenantId [{}] and pageLink [{}]", tenantId, pageLink);
+ List<CustomerEntity> customerEntities = findPageWithTextSearch(ModelConstants.CUSTOMER_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
+ Arrays.asList(eq(ModelConstants.CUSTOMER_TENANT_ID_PROPERTY, tenantId)),
+ pageLink);
+ log.trace("Found customers [{}] by tenantId [{}] and pageLink [{}]", customerEntities, tenantId, pageLink);
+ return customerEntities;
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java
new file mode 100644
index 0000000..bf5ff6e
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.customer;
+
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+
+public interface CustomerService {
+
+ public Customer findCustomerById(CustomerId customerId);
+
+ public Customer saveCustomer(Customer customer);
+
+ public void deleteCustomer(CustomerId customerId);
+
+ public TextPageData<Customer> findCustomersByTenantId(TenantId tenantId, TextPageLink pageLink);
+
+ public void deleteCustomersByTenantId(TenantId tenantId);
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java
new file mode 100644
index 0000000..ffb985c
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java
@@ -0,0 +1,145 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.customer;
+
+import static org.thingsboard.server.dao.DaoUtil.convertDataList;
+import static org.thingsboard.server.dao.DaoUtil.getData;
+
+import java.util.List;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.dao.dashboard.DashboardService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.exception.IncorrectParameterException;
+import org.thingsboard.server.dao.model.CustomerEntity;
+import org.thingsboard.server.dao.model.TenantEntity;
+import org.thingsboard.server.dao.service.DataValidator;
+import org.thingsboard.server.dao.service.PaginatedRemover;
+import org.thingsboard.server.dao.tenant.TenantDao;
+import org.thingsboard.server.dao.user.UserService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.dao.service.Validator;
+@Service
+@Slf4j
+public class CustomerServiceImpl implements CustomerService {
+
+ @Autowired
+ private CustomerDao customerDao;
+
+ @Autowired
+ private UserService userService;
+
+ @Autowired
+ private TenantDao tenantDao;
+
+ @Autowired
+ private DeviceService deviceService;
+
+ @Autowired
+ private DashboardService dashboardService;
+
+ @Override
+ public Customer findCustomerById(CustomerId customerId) {
+ log.trace("Executing findCustomerById [{}]", customerId);
+ Validator.validateId(customerId, "Incorrect customerId " + customerId);
+ CustomerEntity customerEntity = customerDao.findById(customerId.getId());
+ return getData(customerEntity);
+ }
+
+ @Override
+ public Customer saveCustomer(Customer customer) {
+ log.trace("Executing saveCustomer [{}]", customer);
+ customerValidator.validate(customer);
+ CustomerEntity customerEntity = customerDao.save(customer);
+ return getData(customerEntity);
+ }
+
+ @Override
+ public void deleteCustomer(CustomerId customerId) {
+ log.trace("Executing deleteCustomer [{}]", customerId);
+ Validator.validateId(customerId, "Incorrect tenantId " + customerId);
+ Customer customer = findCustomerById(customerId);
+ if (customer == null) {
+ throw new IncorrectParameterException("Unable to delete non-existent customer.");
+ }
+ dashboardService.unassignCustomerDashboards(customer.getTenantId(), customerId);
+ deviceService.unassignCustomerDevices(customer.getTenantId(), customerId);
+ userService.deleteCustomerUsers(customer.getTenantId(), customerId);
+ customerDao.removeById(customerId.getId());
+ }
+
+ @Override
+ public TextPageData<Customer> findCustomersByTenantId(TenantId tenantId, TextPageLink pageLink) {
+ log.trace("Executing findCustomersByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink);
+ Validator.validateId(tenantId, "Incorrect tenantId " + tenantId);
+ Validator.validatePageLink(pageLink, "Incorrect page link " + pageLink);
+ List<CustomerEntity> customerEntities = customerDao.findCustomersByTenantId(tenantId.getId(), pageLink);
+ List<Customer> customers = convertDataList(customerEntities);
+ return new TextPageData<Customer>(customers, pageLink);
+ }
+
+ @Override
+ public void deleteCustomersByTenantId(TenantId tenantId) {
+ log.trace("Executing deleteCustomersByTenantId, tenantId [{}]", tenantId);
+ Validator.validateId(tenantId, "Incorrect tenantId " + tenantId);
+ customersByTenantRemover.removeEntitites(tenantId);
+ }
+
+ private DataValidator<Customer> customerValidator =
+ new DataValidator<Customer>() {
+ @Override
+ protected void validateDataImpl(Customer customer) {
+ if (StringUtils.isEmpty(customer.getTitle())) {
+ throw new DataValidationException("Customer title should be specified!");
+ }
+ if (!StringUtils.isEmpty(customer.getEmail())) {
+ validateEmail(customer.getEmail());
+ }
+ if (customer.getTenantId() == null) {
+ throw new DataValidationException("Customer should be assigned to tenant!");
+ } else {
+ TenantEntity tenant = tenantDao.findById(customer.getTenantId().getId());
+ if (tenant == null) {
+ throw new DataValidationException("Customer is referencing to non-existent tenant!");
+ }
+ }
+ }
+ };
+
+ private PaginatedRemover<TenantId, CustomerEntity> customersByTenantRemover =
+ new PaginatedRemover<TenantId, CustomerEntity>() {
+
+ @Override
+ protected List<CustomerEntity> findEntities(TenantId id, TextPageLink pageLink) {
+ return customerDao.findCustomersByTenantId(id.getId(), pageLink);
+ }
+
+ @Override
+ protected void removeEntity(CustomerEntity entity) {
+ deleteCustomer(new CustomerId(entity.getId()));
+ }
+ };
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/Dao.java b/dao/src/main/java/org/thingsboard/server/dao/Dao.java
new file mode 100644
index 0000000..c34472b
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/Dao.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao;
+
+import com.datastax.driver.core.ResultSet;
+
+import java.util.List;
+import java.util.UUID;
+
+public interface Dao<T> {
+
+ List<T> find();
+
+ T findById(UUID id);
+
+ T save(T t);
+
+ ResultSet removeById(UUID id);
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java
new file mode 100644
index 0000000..da1a42f
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java
@@ -0,0 +1,61 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+
+import org.thingsboard.server.common.data.id.UUIDBased;
+import org.thingsboard.server.dao.model.ToData;
+
+public abstract class DaoUtil {
+
+ private DaoUtil() {
+ }
+
+ public static <T> List<T> convertDataList(Collection<? extends ToData<T>> toDataList) {
+ List<T> list = Collections.emptyList();
+ if (toDataList != null && !toDataList.isEmpty()) {
+ list = new ArrayList<>();
+ for (ToData<T> object : toDataList) {
+ list.add(object.toData());
+ }
+ }
+ return list;
+ }
+
+ public static <T> T getData(ToData<T> data) {
+ T object = null;
+ if (data != null) {
+ object = data.toData();
+ }
+ return object;
+ }
+
+ public static UUID getId(UUIDBased idBased) {
+ UUID id = null;
+ if (idBased != null) {
+ id = idBased.getId();
+ }
+ return id;
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardDao.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardDao.java
new file mode 100644
index 0000000..056e901
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardDao.java
@@ -0,0 +1,60 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.dashboard;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.thingsboard.server.common.data.Dashboard;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.dao.Dao;
+import org.thingsboard.server.dao.model.DashboardEntity;
+
+/**
+ * The Interface DashboardDao.
+ *
+ * @param <T> the generic type
+ */
+public interface DashboardDao extends Dao<DashboardEntity> {
+
+ /**
+ * Save or update dashboard object
+ *
+ * @param dashboard the dashboard object
+ * @return saved dashboard object
+ */
+ DashboardEntity save(Dashboard dashboard);
+
+ /**
+ * Find dashboards by tenantId and page link.
+ *
+ * @param tenantId the tenantId
+ * @param pageLink the page link
+ * @return the list of dashboard objects
+ */
+ List<DashboardEntity> findDashboardsByTenantId(UUID tenantId, TextPageLink pageLink);
+
+ /**
+ * Find dashboards by tenantId, customerId and page link.
+ *
+ * @param tenantId the tenantId
+ * @param customerId the customerId
+ * @param pageLink the page link
+ * @return the list of dashboard objects
+ */
+ List<DashboardEntity> findDashboardsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink);
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardDaoImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardDaoImpl.java
new file mode 100644
index 0000000..54a32ea
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardDaoImpl.java
@@ -0,0 +1,83 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.dashboard;
+
+import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
+import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_CUSTOMER_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.DASHBOARD_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.Dashboard;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.dao.AbstractSearchTextDao;
+import org.thingsboard.server.dao.model.DashboardEntity;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Component
+@Slf4j
+public class DashboardDaoImpl extends AbstractSearchTextDao<DashboardEntity> implements DashboardDao {
+
+ @Override
+ protected Class<DashboardEntity> getColumnFamilyClass() {
+ return DashboardEntity.class;
+ }
+
+ @Override
+ protected String getColumnFamilyName() {
+ return DASHBOARD_COLUMN_FAMILY_NAME;
+ }
+
+ @Override
+ public DashboardEntity save(Dashboard dashboard) {
+ log.debug("Save dashboard [{}] ", dashboard);
+ return save(new DashboardEntity(dashboard));
+ }
+
+ @Override
+ public List<DashboardEntity> findDashboardsByTenantId(UUID tenantId, TextPageLink pageLink) {
+ log.debug("Try to find dashboards by tenantId [{}] and pageLink [{}]", tenantId, pageLink);
+ List<DashboardEntity> dashboardEntities = findPageWithTextSearch(DASHBOARD_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
+ Arrays.asList(eq(DASHBOARD_TENANT_ID_PROPERTY, tenantId),
+ eq(DASHBOARD_CUSTOMER_ID_PROPERTY, NULL_UUID)),
+ pageLink);
+
+ log.trace("Found dashboards [{}] by tenantId [{}] and pageLink [{}]", dashboardEntities, tenantId, pageLink);
+ return dashboardEntities;
+ }
+
+ @Override
+ public List<DashboardEntity> findDashboardsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink) {
+ log.debug("Try to find dashboards by tenantId [{}], customerId[{}] and pageLink [{}]", tenantId, customerId, pageLink);
+ List<DashboardEntity> dashboardEntities = findPageWithTextSearch(DASHBOARD_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
+ Arrays.asList(eq(DASHBOARD_CUSTOMER_ID_PROPERTY, customerId),
+ eq(DASHBOARD_TENANT_ID_PROPERTY, tenantId)),
+ pageLink);
+
+ log.trace("Found dashboards [{}] by tenantId [{}], customerId [{}] and pageLink [{}]", dashboardEntities, tenantId, customerId, pageLink);
+ return dashboardEntities;
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java
new file mode 100644
index 0000000..773f39f
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.dashboard;
+
+import org.thingsboard.server.common.data.Dashboard;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DashboardId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+
+public interface DashboardService {
+
+ public Dashboard findDashboardById(DashboardId dashboardId);
+
+ public Dashboard saveDashboard(Dashboard dashboard);
+
+ public Dashboard assignDashboardToCustomer(DashboardId dashboardId, CustomerId customerId);
+
+ public Dashboard unassignDashboardFromCustomer(DashboardId dashboardId);
+
+ public void deleteDashboard(DashboardId dashboardId);
+
+ public TextPageData<Dashboard> findDashboardsByTenantId(TenantId tenantId, TextPageLink pageLink);
+
+ public void deleteDashboardsByTenantId(TenantId tenantId);
+
+ public TextPageData<Dashboard> findDashboardsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TextPageLink pageLink);
+
+ public void unassignCustomerDashboards(TenantId tenantId, CustomerId customerId);
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
new file mode 100644
index 0000000..33d61dc
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
@@ -0,0 +1,195 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.dashboard;
+
+import static org.thingsboard.server.dao.DaoUtil.convertDataList;
+import static org.thingsboard.server.dao.DaoUtil.getData;
+
+import java.util.List;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.thingsboard.server.common.data.Dashboard;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DashboardId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.dao.customer.CustomerDao;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.model.CustomerEntity;
+import org.thingsboard.server.dao.model.DashboardEntity;
+import org.thingsboard.server.dao.model.TenantEntity;
+import org.thingsboard.server.dao.service.DataValidator;
+import org.thingsboard.server.dao.service.PaginatedRemover;
+import org.thingsboard.server.dao.tenant.TenantDao;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.dao.model.ModelConstants;
+import org.thingsboard.server.dao.service.Validator;
+
+@Service
+@Slf4j
+public class DashboardServiceImpl implements DashboardService {
+
+ @Autowired
+ private DashboardDao dashboardDao;
+
+ @Autowired
+ private TenantDao tenantDao;
+
+ @Autowired
+ private CustomerDao customerDao;
+
+ @Override
+ public Dashboard findDashboardById(DashboardId dashboardId) {
+ log.trace("Executing findDashboardById [{}]", dashboardId);
+ Validator.validateId(dashboardId, "Incorrect dashboardId " + dashboardId);
+ DashboardEntity dashboardEntity = dashboardDao.findById(dashboardId.getId());
+ return getData(dashboardEntity);
+ }
+
+ @Override
+ public Dashboard saveDashboard(Dashboard dashboard) {
+ log.trace("Executing saveDashboard [{}]", dashboard);
+ dashboardValidator.validate(dashboard);
+ DashboardEntity dashboardEntity = dashboardDao.save(dashboard);
+ return getData(dashboardEntity);
+ }
+
+ @Override
+ public Dashboard assignDashboardToCustomer(DashboardId dashboardId, CustomerId customerId) {
+ Dashboard dashboard = findDashboardById(dashboardId);
+ dashboard.setCustomerId(customerId);
+ return saveDashboard(dashboard);
+ }
+
+ @Override
+ public Dashboard unassignDashboardFromCustomer(DashboardId dashboardId) {
+ Dashboard dashboard = findDashboardById(dashboardId);
+ dashboard.setCustomerId(null);
+ return saveDashboard(dashboard);
+ }
+
+ @Override
+ public void deleteDashboard(DashboardId dashboardId) {
+ log.trace("Executing deleteDashboard [{}]", dashboardId);
+ Validator.validateId(dashboardId, "Incorrect dashboardId " + dashboardId);
+ dashboardDao.removeById(dashboardId.getId());
+ }
+
+ @Override
+ public TextPageData<Dashboard> findDashboardsByTenantId(TenantId tenantId, TextPageLink pageLink) {
+ log.trace("Executing findDashboardsByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink);
+ Validator.validateId(tenantId, "Incorrect tenantId " + tenantId);
+ Validator.validatePageLink(pageLink, "Incorrect page link " + pageLink);
+ List<DashboardEntity> dashboardEntities = dashboardDao.findDashboardsByTenantId(tenantId.getId(), pageLink);
+ List<Dashboard> dashboards = convertDataList(dashboardEntities);
+ return new TextPageData<Dashboard>(dashboards, pageLink);
+ }
+
+ @Override
+ public void deleteDashboardsByTenantId(TenantId tenantId) {
+ log.trace("Executing deleteDashboardsByTenantId, tenantId [{}]", tenantId);
+ Validator.validateId(tenantId, "Incorrect tenantId " + tenantId);
+ tenantDashboardsRemover.removeEntitites(tenantId);
+ }
+
+ @Override
+ public TextPageData<Dashboard> findDashboardsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TextPageLink pageLink) {
+ log.trace("Executing findDashboardsByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink);
+ Validator.validateId(tenantId, "Incorrect tenantId " + tenantId);
+ Validator.validateId(customerId, "Incorrect customerId " + customerId);
+ Validator.validatePageLink(pageLink, "Incorrect page link " + pageLink);
+ List<DashboardEntity> dashboardEntities = dashboardDao.findDashboardsByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink);
+ List<Dashboard> dashboards = convertDataList(dashboardEntities);
+ return new TextPageData<Dashboard>(dashboards, pageLink);
+ }
+
+ @Override
+ public void unassignCustomerDashboards(TenantId tenantId, CustomerId customerId) {
+ log.trace("Executing unassignCustomerDashboards, tenantId [{}], customerId [{}]", tenantId, customerId);
+ Validator.validateId(tenantId, "Incorrect tenantId " + tenantId);
+ Validator.validateId(customerId, "Incorrect customerId " + customerId);
+ new CustomerDashboardsUnassigner(tenantId).removeEntitites(customerId);
+ }
+
+ private DataValidator<Dashboard> dashboardValidator =
+ new DataValidator<Dashboard>() {
+ @Override
+ protected void validateDataImpl(Dashboard dashboard) {
+ if (StringUtils.isEmpty(dashboard.getTitle())) {
+ throw new DataValidationException("Dashboard title should be specified!");
+ }
+ if (dashboard.getTenantId() == null) {
+ throw new DataValidationException("Dashboard should be assigned to tenant!");
+ } else {
+ TenantEntity tenant = tenantDao.findById(dashboard.getTenantId().getId());
+ if (tenant == null) {
+ throw new DataValidationException("Dashboard is referencing to non-existent tenant!");
+ }
+ }
+ if (dashboard.getCustomerId() == null) {
+ dashboard.setCustomerId(new CustomerId(ModelConstants.NULL_UUID));
+ } else if (!dashboard.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
+ CustomerEntity customer = customerDao.findById(dashboard.getCustomerId().getId());
+ if (customer == null) {
+ throw new DataValidationException("Can't assign dashboard to non-existent customer!");
+ }
+ if (!customer.getTenantId().equals(dashboard.getTenantId().getId())) {
+ throw new DataValidationException("Can't assign dashboard to customer from different tenant!");
+ }
+ }
+ }
+ };
+
+ private PaginatedRemover<TenantId, DashboardEntity> tenantDashboardsRemover =
+ new PaginatedRemover<TenantId, DashboardEntity>() {
+
+ @Override
+ protected List<DashboardEntity> findEntities(TenantId id, TextPageLink pageLink) {
+ return dashboardDao.findDashboardsByTenantId(id.getId(), pageLink);
+ }
+
+ @Override
+ protected void removeEntity(DashboardEntity entity) {
+ deleteDashboard(new DashboardId(entity.getId()));
+ }
+ };
+
+ class CustomerDashboardsUnassigner extends PaginatedRemover<CustomerId, DashboardEntity> {
+
+ private TenantId tenantId;
+
+ CustomerDashboardsUnassigner(TenantId tenantId) {
+ this.tenantId = tenantId;
+ }
+
+ @Override
+ protected List<DashboardEntity> findEntities(CustomerId id, TextPageLink pageLink) {
+ return dashboardDao.findDashboardsByTenantIdAndCustomerId(tenantId.getId(), id.getId(), pageLink);
+ }
+
+ @Override
+ protected void removeEntity(DashboardEntity entity) {
+ unassignDashboardFromCustomer(new DashboardId(entity.getId()));
+ }
+
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsDao.java
new file mode 100644
index 0000000..5390e95
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsDao.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.device;
+
+import java.util.UUID;
+
+import org.thingsboard.server.common.data.security.DeviceCredentials;
+import org.thingsboard.server.dao.Dao;
+import org.thingsboard.server.dao.model.DeviceCredentialsEntity;
+
+/**
+ * The Interface DeviceCredentialsDao.
+ *
+ * @param <T> the generic type
+ */
+public interface DeviceCredentialsDao extends Dao<DeviceCredentialsEntity> {
+
+ /**
+ * Save or update device credentials object
+ *
+ * @param deviceCredentials the device credentials object
+ * @return saved device credentials object
+ */
+ DeviceCredentialsEntity save(DeviceCredentials deviceCredentials);
+
+ /**
+ * Find device credentials by device id.
+ *
+ * @param deviceId the device id
+ * @return the device credentials object
+ */
+ DeviceCredentialsEntity findByDeviceId(UUID deviceId);
+
+ /**
+ * Find device credentials by credentials id.
+ *
+ * @param credentialsId the credentials id
+ * @return the device credentials object
+ */
+ DeviceCredentialsEntity findByCredentialsId(String credentialsId);
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsDaoImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsDaoImpl.java
new file mode 100644
index 0000000..c38ea8c
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsDaoImpl.java
@@ -0,0 +1,77 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.device;
+
+import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
+
+import java.util.UUID;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.security.DeviceCredentials;
+import org.thingsboard.server.dao.AbstractModelDao;
+import org.thingsboard.server.dao.model.DeviceCredentialsEntity;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Repository;
+
+import com.datastax.driver.core.querybuilder.Select.Where;
+import org.thingsboard.server.dao.model.ModelConstants;
+
+@Component
+@Slf4j
+public class DeviceCredentialsDaoImpl extends AbstractModelDao<DeviceCredentialsEntity> implements DeviceCredentialsDao {
+
+ @Override
+ protected Class<DeviceCredentialsEntity> getColumnFamilyClass() {
+ return DeviceCredentialsEntity.class;
+ }
+
+ @Override
+ protected String getColumnFamilyName() {
+ return ModelConstants.DEVICE_CREDENTIALS_COLUMN_FAMILY_NAME;
+ }
+
+ @Override
+ public DeviceCredentialsEntity findByDeviceId(UUID deviceId) {
+ log.debug("Try to find device credentials by deviceId [{}] ", deviceId);
+ Where query = select().from(ModelConstants.DEVICE_CREDENTIALS_BY_DEVICE_COLUMN_FAMILY_NAME)
+ .where(eq(ModelConstants.DEVICE_CREDENTIALS_DEVICE_ID_PROPERTY, deviceId));
+ log.trace("Execute query {}", query);
+ DeviceCredentialsEntity deviceCredentialsEntity = findOneByStatement(query);
+ log.trace("Found device credentials [{}] by deviceId [{}]", deviceCredentialsEntity, deviceId);
+ return deviceCredentialsEntity;
+ }
+
+ @Override
+ public DeviceCredentialsEntity findByCredentialsId(String credentialsId) {
+ log.debug("Try to find device credentials by credentialsId [{}] ", credentialsId);
+ Where query = select().from(ModelConstants.DEVICE_CREDENTIALS_BY_CREDENTIALS_ID_COLUMN_FAMILY_NAME)
+ .where(eq(ModelConstants.DEVICE_CREDENTIALS_CREDENTIALS_ID_PROPERTY, credentialsId));
+ log.trace("Execute query {}", query);
+ DeviceCredentialsEntity deviceCredentialsEntity = findOneByStatement(query);
+ log.trace("Found device credentials [{}] by credentialsId [{}]", deviceCredentialsEntity, credentialsId);
+ return deviceCredentialsEntity;
+ }
+
+ @Override
+ public DeviceCredentialsEntity save(DeviceCredentials deviceCredentials) {
+ log.debug("Save device credentials [{}] ", deviceCredentials);
+ return save(new DeviceCredentialsEntity(deviceCredentials));
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java
new file mode 100644
index 0000000..43d2442
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.device;
+
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.security.DeviceCredentials;
+
+import static org.thingsboard.server.common.data.CacheConstants.DEVICE_CREDENTIALS_CACHE;
+
+public interface DeviceCredentialsService {
+
+ DeviceCredentials findDeviceCredentialsByDeviceId(DeviceId deviceId);
+
+ @Cacheable(cacheNames = DEVICE_CREDENTIALS_CACHE, unless="#result == null")
+ DeviceCredentials findDeviceCredentialsByCredentialsId(String credentialsId);
+
+ @CacheEvict(cacheNames = DEVICE_CREDENTIALS_CACHE, keyGenerator="previousDeviceCredentialsId", beforeInvocation = true)
+ DeviceCredentials updateDeviceCredentials(DeviceCredentials deviceCredentials);
+
+ DeviceCredentials createDeviceCredentials(DeviceCredentials deviceCredentials);
+
+ @CacheEvict(cacheNames = DEVICE_CREDENTIALS_CACHE, key="#deviceCredentials.credentialsId")
+ void deleteDeviceCredentials(DeviceCredentials deviceCredentials);
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java
new file mode 100644
index 0000000..19ad2d1
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java
@@ -0,0 +1,134 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.device;
+
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.security.DeviceCredentials;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.model.DeviceCredentialsEntity;
+import org.thingsboard.server.dao.service.DataValidator;
+
+import java.util.Optional;
+
+import static org.thingsboard.server.dao.DaoUtil.getData;
+import static org.thingsboard.server.dao.service.Validator.validateId;
+import static org.thingsboard.server.dao.service.Validator.validateString;
+
+@Service
+@Slf4j
+public class DeviceCredentialsServiceImpl implements DeviceCredentialsService {
+
+ @Autowired
+ private DeviceCredentialsDao deviceCredentialsDao;
+
+ @Autowired
+ private DeviceService deviceService;
+
+ @Override
+ public DeviceCredentials findDeviceCredentialsByDeviceId(DeviceId deviceId) {
+ log.trace("Executing findDeviceCredentialsByDeviceId [{}]", deviceId);
+ validateId(deviceId, "Incorrect deviceId " + deviceId);
+ DeviceCredentialsEntity deviceCredentialsEntity = deviceCredentialsDao.findByDeviceId(deviceId.getId());
+ return getData(deviceCredentialsEntity);
+ }
+
+ @Override
+ public DeviceCredentials findDeviceCredentialsByCredentialsId(String credentialsId) {
+ log.trace("Executing findDeviceCredentialsByCredentialsId [{}]", credentialsId);
+ validateString(credentialsId, "Incorrect credentialsId " + credentialsId);
+ DeviceCredentialsEntity deviceCredentialsEntity = deviceCredentialsDao.findByCredentialsId(credentialsId);
+ return getData(deviceCredentialsEntity);
+ }
+
+ @Override
+ public DeviceCredentials updateDeviceCredentials(DeviceCredentials deviceCredentials) {
+ return saveOrUpdare(deviceCredentials);
+ }
+
+ @Override
+ public DeviceCredentials createDeviceCredentials(DeviceCredentials deviceCredentials) {
+ return saveOrUpdare(deviceCredentials);
+ }
+
+ private DeviceCredentials saveOrUpdare(DeviceCredentials deviceCredentials) {
+ log.trace("Executing updateDeviceCredentials [{}]", deviceCredentials);
+ credentialsValidator.validate(deviceCredentials);
+ return getData(deviceCredentialsDao.save(deviceCredentials));
+ }
+
+ @Override
+ public void deleteDeviceCredentials(DeviceCredentials deviceCredentials) {
+ log.trace("Executing deleteDeviceCredentials [{}]", deviceCredentials);
+ deviceCredentialsDao.removeById(deviceCredentials.getUuidId());
+ }
+
+ private DataValidator<DeviceCredentials> credentialsValidator =
+ new DataValidator<DeviceCredentials>() {
+
+ @Override
+ protected void validateCreate(DeviceCredentials deviceCredentials) {
+ DeviceCredentialsEntity existingCredentialsEntity = deviceCredentialsDao.findByCredentialsId(deviceCredentials.getCredentialsId());
+ if (existingCredentialsEntity != null) {
+ throw new DataValidationException("Create of existent device credentials!");
+ }
+ }
+
+ @Override
+ protected void validateUpdate(DeviceCredentials deviceCredentials) {
+ DeviceCredentialsEntity existingCredentialsEntity = deviceCredentialsDao.findById(deviceCredentials.getUuidId());
+ if (existingCredentialsEntity == null) {
+ throw new DataValidationException("Unable to update non-existent device credentials!");
+ }
+ DeviceCredentialsEntity sameCredentialsIdEntity = deviceCredentialsDao.findByCredentialsId(deviceCredentials.getCredentialsId());
+ if (sameCredentialsIdEntity != null && !sameCredentialsIdEntity.getId().equals(deviceCredentials.getUuidId())) {
+ throw new DataValidationException("Specified credentials are already registered!");
+ }
+ }
+
+ @Override
+ protected void validateDataImpl(DeviceCredentials deviceCredentials) {
+ if (deviceCredentials.getDeviceId() == null) {
+ throw new DataValidationException("Device credentials should be assigned to device!");
+ }
+ if (deviceCredentials.getCredentialsType() == null) {
+ throw new DataValidationException("Device credentials type should be specified!");
+ }
+ if (StringUtils.isEmpty(deviceCredentials.getCredentialsId())) {
+ throw new DataValidationException("Device credentials id should be specified!");
+ }
+ switch (deviceCredentials.getCredentialsType()) {
+ case ACCESS_TOKEN:
+ if (deviceCredentials.getCredentialsId().length() < 1 || deviceCredentials.getCredentialsId().length() > 20) {
+ throw new DataValidationException("Incorrect access token length [" + deviceCredentials.getCredentialsId().length() + "]!");
+ }
+ break;
+ default:
+ break;
+ }
+ Device device = deviceService.findDeviceById(deviceCredentials.getDeviceId());
+ if (device == null) {
+ throw new DataValidationException("Can't assign device credentials to non-existent device!");
+ }
+ }
+ };
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java
new file mode 100644
index 0000000..0f00340
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java
@@ -0,0 +1,68 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.device;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.dao.Dao;
+import org.thingsboard.server.dao.model.DeviceEntity;
+
+/**
+ * The Interface DeviceDao.
+ *
+ */
+public interface DeviceDao extends Dao<DeviceEntity> {
+
+ /**
+ * Save or update device object
+ *
+ * @param device the device object
+ * @return saved device object
+ */
+ DeviceEntity save(Device device);
+
+ /**
+ * Find devices by tenantId and page link.
+ *
+ * @param tenantId the tenantId
+ * @param pageLink the page link
+ * @return the list of device objects
+ */
+ List<DeviceEntity> findDevicesByTenantId(UUID tenantId, TextPageLink pageLink);
+
+ /**
+ * Find devices by tenantId, customerId and page link.
+ *
+ * @param tenantId the tenantId
+ * @param customerId the customerId
+ * @param pageLink the page link
+ * @return the list of device objects
+ */
+ List<DeviceEntity> findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink);
+
+ /**
+ * Find devices by tenantId and device name.
+ *
+ * @param tenantId the tenantId
+ * @param name the device name
+ * @return the optional device object
+ */
+ Optional<DeviceEntity> findDevicesByTenantIdAndName(UUID tenantId, String name);
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDaoImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDaoImpl.java
new file mode 100644
index 0000000..719ad37
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDaoImpl.java
@@ -0,0 +1,85 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.device;
+
+import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
+import static org.thingsboard.server.dao.model.ModelConstants.*;
+
+import java.util.*;
+
+import com.datastax.driver.core.querybuilder.Select;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.dao.AbstractSearchTextDao;
+import org.thingsboard.server.dao.model.DeviceEntity;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Component
+@Slf4j
+public class DeviceDaoImpl extends AbstractSearchTextDao<DeviceEntity> implements DeviceDao {
+
+ @Override
+ protected Class<DeviceEntity> getColumnFamilyClass() {
+ return DeviceEntity.class;
+ }
+
+ @Override
+ protected String getColumnFamilyName() {
+ return DEVICE_COLUMN_FAMILY_NAME;
+ }
+
+ @Override
+ public DeviceEntity save(Device device) {
+ log.debug("Save device [{}] ", device);
+ return save(new DeviceEntity(device));
+ }
+
+ @Override
+ public List<DeviceEntity> findDevicesByTenantId(UUID tenantId, TextPageLink pageLink) {
+ log.debug("Try to find devices by tenantId [{}] and pageLink [{}]", tenantId, pageLink);
+ List<DeviceEntity> deviceEntities = findPageWithTextSearch(DEVICE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
+ Collections.singletonList(eq(DEVICE_TENANT_ID_PROPERTY, tenantId)), pageLink);
+
+ log.trace("Found devices [{}] by tenantId [{}] and pageLink [{}]", deviceEntities, tenantId, pageLink);
+ return deviceEntities;
+ }
+
+ @Override
+ public List<DeviceEntity> findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink) {
+ log.debug("Try to find devices by tenantId [{}], customerId[{}] and pageLink [{}]", tenantId, customerId, pageLink);
+ List<DeviceEntity> deviceEntities = findPageWithTextSearch(DEVICE_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
+ Arrays.asList(eq(DEVICE_CUSTOMER_ID_PROPERTY, customerId),
+ eq(DEVICE_TENANT_ID_PROPERTY, tenantId)),
+ pageLink);
+
+ log.trace("Found devices [{}] by tenantId [{}], customerId [{}] and pageLink [{}]", deviceEntities, tenantId, customerId, pageLink);
+ return deviceEntities;
+ }
+
+ @Override
+ public Optional<DeviceEntity> findDevicesByTenantIdAndName(UUID tenantId, String deviceName) {
+ Select select = select().from(DEVICE_BY_TENANT_AND_NAME_VIEW_NAME);
+ Select.Where query = select.where();
+ query.and(eq(DEVICE_TENANT_ID_PROPERTY, tenantId));
+ query.and(eq(DEVICE_NAME_PROPERTY, deviceName));
+ return Optional.ofNullable(findOneByStatement(query));
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
new file mode 100644
index 0000000..eb3cb28
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.device;
+
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+
+public interface DeviceService {
+
+ Device findDeviceById(DeviceId deviceId);
+
+ Device saveDevice(Device device);
+
+ Device assignDeviceToCustomer(DeviceId deviceId, CustomerId customerId);
+
+ Device unassignDeviceFromCustomer(DeviceId deviceId);
+
+ void deleteDevice(DeviceId deviceId);
+
+ TextPageData<Device> findDevicesByTenantId(TenantId tenantId, TextPageLink pageLink);
+
+ void deleteDevicesByTenantId(TenantId tenantId);
+
+ TextPageData<Device> findDevicesByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TextPageLink pageLink);
+
+ void unassignCustomerDevices(TenantId tenantId, CustomerId customerId);
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
new file mode 100644
index 0000000..d321bf4
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
@@ -0,0 +1,231 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.device;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.security.DeviceCredentials;
+import org.thingsboard.server.common.data.security.DeviceCredentialsType;
+import org.thingsboard.server.dao.customer.CustomerDao;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.model.CustomerEntity;
+import org.thingsboard.server.dao.model.DeviceEntity;
+import org.thingsboard.server.dao.model.TenantEntity;
+import org.thingsboard.server.dao.service.DataValidator;
+import org.thingsboard.server.dao.service.PaginatedRemover;
+import org.thingsboard.server.dao.tenant.TenantDao;
+
+import java.util.List;
+
+import static org.thingsboard.server.dao.DaoUtil.convertDataList;
+import static org.thingsboard.server.dao.DaoUtil.getData;
+import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
+import static org.thingsboard.server.dao.service.Validator.validateId;
+import static org.thingsboard.server.dao.service.Validator.validatePageLink;
+
+@Service
+@Slf4j
+public class DeviceServiceImpl implements DeviceService {
+
+ @Autowired
+ private DeviceDao deviceDao;
+
+ @Autowired
+ private TenantDao tenantDao;
+
+ @Autowired
+ private CustomerDao customerDao;
+
+ @Autowired
+ private DeviceCredentialsService deviceCredentialsService;
+
+ @Override
+ public Device findDeviceById(DeviceId deviceId) {
+ log.trace("Executing findDeviceById [{}]", deviceId);
+ validateId(deviceId, "Incorrect deviceId " + deviceId);
+ DeviceEntity deviceEntity = deviceDao.findById(deviceId.getId());
+ return getData(deviceEntity);
+ }
+
+ @Override
+ public Device saveDevice(Device device) {
+ log.trace("Executing saveDevice [{}]", device);
+ deviceValidator.validate(device);
+ DeviceEntity deviceEntity = deviceDao.save(device);
+ if (device.getId() == null) {
+ DeviceCredentials deviceCredentials = new DeviceCredentials();
+ deviceCredentials.setDeviceId(new DeviceId(deviceEntity.getId()));
+ deviceCredentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN);
+ deviceCredentials.setCredentialsId(RandomStringUtils.randomAlphanumeric(20));
+ deviceCredentialsService.createDeviceCredentials(deviceCredentials);
+ }
+ return getData(deviceEntity);
+ }
+
+ @Override
+ public Device assignDeviceToCustomer(DeviceId deviceId, CustomerId customerId) {
+ Device device = findDeviceById(deviceId);
+ device.setCustomerId(customerId);
+ return saveDevice(device);
+ }
+
+ @Override
+ public Device unassignDeviceFromCustomer(DeviceId deviceId) {
+ Device device = findDeviceById(deviceId);
+ device.setCustomerId(null);
+ return saveDevice(device);
+ }
+
+ @Override
+ public void deleteDevice(DeviceId deviceId) {
+ log.trace("Executing deleteDevice [{}]", deviceId);
+ validateId(deviceId, "Incorrect deviceId " + deviceId);
+ DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(deviceId);
+ if (deviceCredentials != null) {
+ deviceCredentialsService.deleteDeviceCredentials(deviceCredentials);
+ }
+ deviceDao.removeById(deviceId.getId());
+ }
+
+ @Override
+ public TextPageData<Device> findDevicesByTenantId(TenantId tenantId, TextPageLink pageLink) {
+ log.trace("Executing findDevicesByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink);
+ validateId(tenantId, "Incorrect tenantId " + tenantId);
+ validatePageLink(pageLink, "Incorrect page link " + pageLink);
+ List<DeviceEntity> deviceEntities = deviceDao.findDevicesByTenantId(tenantId.getId(), pageLink);
+ List<Device> devices = convertDataList(deviceEntities);
+ return new TextPageData<Device>(devices, pageLink);
+ }
+
+ @Override
+ public void deleteDevicesByTenantId(TenantId tenantId) {
+ log.trace("Executing deleteDevicesByTenantId, tenantId [{}]", tenantId);
+ validateId(tenantId, "Incorrect tenantId " + tenantId);
+ tenantDevicesRemover.removeEntitites(tenantId);
+ }
+
+ @Override
+ public TextPageData<Device> findDevicesByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TextPageLink pageLink) {
+ log.trace("Executing findDevicesByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink);
+ validateId(tenantId, "Incorrect tenantId " + tenantId);
+ validateId(customerId, "Incorrect customerId " + customerId);
+ validatePageLink(pageLink, "Incorrect page link " + pageLink);
+ List<DeviceEntity> deviceEntities = deviceDao.findDevicesByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink);
+ List<Device> devices = convertDataList(deviceEntities);
+ return new TextPageData<Device>(devices, pageLink);
+ }
+
+ @Override
+ public void unassignCustomerDevices(TenantId tenantId, CustomerId customerId) {
+ log.trace("Executing unassignCustomerDevices, tenantId [{}], customerId [{}]", tenantId, customerId);
+ validateId(tenantId, "Incorrect tenantId " + tenantId);
+ validateId(customerId, "Incorrect customerId " + customerId);
+ new CustomerDevicesUnassigner(tenantId).removeEntitites(customerId);
+ }
+
+ private DataValidator<Device> deviceValidator =
+ new DataValidator<Device>() {
+
+ @Override
+ protected void validateCreate(Device device) {
+ deviceDao.findDevicesByTenantIdAndName(device.getTenantId().getId(), device.getName()).ifPresent(
+ d -> {
+ throw new DataValidationException("Device with such name already exists!");
+ }
+ );
+ }
+
+ @Override
+ protected void validateUpdate(Device device) {
+ deviceDao.findDevicesByTenantIdAndName(device.getTenantId().getId(), device.getName()).ifPresent(
+ d -> {
+ if (!d.getId().equals(device.getUuidId())) {
+ throw new DataValidationException("Device with such name already exists!");
+ }
+ }
+ );
+ }
+
+ @Override
+ protected void validateDataImpl(Device device) {
+ if (StringUtils.isEmpty(device.getName())) {
+ throw new DataValidationException("Device name should be specified!");
+ }
+ if (device.getTenantId() == null) {
+ throw new DataValidationException("Device should be assigned to tenant!");
+ } else {
+ TenantEntity tenant = tenantDao.findById(device.getTenantId().getId());
+ if (tenant == null) {
+ throw new DataValidationException("Device is referencing to non-existent tenant!");
+ }
+ }
+ if (device.getCustomerId() == null) {
+ device.setCustomerId(new CustomerId(NULL_UUID));
+ } else if (!device.getCustomerId().getId().equals(NULL_UUID)) {
+ CustomerEntity customer = customerDao.findById(device.getCustomerId().getId());
+ if (customer == null) {
+ throw new DataValidationException("Can't assign device to non-existent customer!");
+ }
+ if (!customer.getTenantId().equals(device.getTenantId().getId())) {
+ throw new DataValidationException("Can't assign device to customer from different tenant!");
+ }
+ }
+ }
+ };
+
+ private PaginatedRemover<TenantId, DeviceEntity> tenantDevicesRemover =
+ new PaginatedRemover<TenantId, DeviceEntity>() {
+
+ @Override
+ protected List<DeviceEntity> findEntities(TenantId id, TextPageLink pageLink) {
+ return deviceDao.findDevicesByTenantId(id.getId(), pageLink);
+ }
+
+ @Override
+ protected void removeEntity(DeviceEntity entity) {
+ deleteDevice(new DeviceId(entity.getId()));
+ }
+ };
+
+ class CustomerDevicesUnassigner extends PaginatedRemover<CustomerId, DeviceEntity> {
+
+ private TenantId tenantId;
+
+ CustomerDevicesUnassigner(TenantId tenantId) {
+ this.tenantId = tenantId;
+ }
+
+ @Override
+ protected List<DeviceEntity> findEntities(CustomerId id, TextPageLink pageLink) {
+ return deviceDao.findDevicesByTenantIdAndCustomerId(tenantId.getId(), id.getId(), pageLink);
+ }
+
+ @Override
+ protected void removeEntity(DeviceEntity entity) {
+ unassignDeviceFromCustomer(new DeviceId(entity.getId()));
+ }
+
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventDao.java
new file mode 100644
index 0000000..736b825
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventDao.java
@@ -0,0 +1,136 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.event;
+
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.querybuilder.Insert;
+import com.datastax.driver.core.querybuilder.QueryBuilder;
+import com.datastax.driver.core.querybuilder.Select;
+import com.datastax.driver.core.utils.UUIDs;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.dao.AbstractSearchTimeDao;
+import org.thingsboard.server.dao.model.EventEntity;
+import org.thingsboard.server.dao.model.ModelConstants;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
+import static org.thingsboard.server.dao.model.ModelConstants.EVENT_BY_ID_VIEW_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.EVENT_BY_TYPE_AND_ID_VIEW_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.EVENT_COLUMN_FAMILY_NAME;
+
+@Component
+@Slf4j
+public class BaseEventDao extends AbstractSearchTimeDao<EventEntity> implements EventDao {
+
+ @Override
+ protected Class<EventEntity> getColumnFamilyClass() {
+ return EventEntity.class;
+ }
+
+ @Override
+ protected String getColumnFamilyName() {
+ return EVENT_COLUMN_FAMILY_NAME;
+ }
+
+ @Override
+ public EventEntity save(Event event) {
+ log.debug("Save event [{}] ", event);
+ return save(new EventEntity(event), false).orElse(null);
+ }
+
+ @Override
+ public Optional<EventEntity> saveIfNotExists(Event event) {
+ return save(new EventEntity(event), true);
+ }
+
+ @Override
+ public EventEntity findEvent(UUID tenantId, EntityId entityId, String eventType, String eventUid) {
+ log.debug("Search event entity by [{}][{}][{}][{}]", tenantId, entityId, eventType, eventUid);
+ Select.Where query = select().from(getColumnFamilyName()).where(
+ eq(ModelConstants.EVENT_TENANT_ID_PROPERTY, tenantId))
+ .and(eq(ModelConstants.EVENT_ENTITY_TYPE_PROPERTY, entityId.getEntityType()))
+ .and(eq(ModelConstants.EVENT_ENTITY_ID_PROPERTY, entityId.getId()))
+ .and(eq(ModelConstants.EVENT_TYPE_PROPERTY, eventType))
+ .and(eq(ModelConstants.EVENT_UID_PROPERTY, eventUid));
+ log.trace("Execute query [{}]", query);
+ EventEntity entity = findOneByStatement(query);
+ if (log.isTraceEnabled()) {
+ log.trace("Search result: [{}] for event entity [{}]", entity != null, entity);
+ } else {
+ log.debug("Search result: [{}]", entity != null);
+ }
+ return entity;
+ }
+
+ @Override
+ public List<EventEntity> findEvents(UUID tenantId, EntityId entityId, TimePageLink pageLink) {
+ log.trace("Try to find events by tenant [{}], entity [{}]and pageLink [{}]", tenantId, entityId, pageLink);
+ List<EventEntity> entities = findPageWithTimeSearch(EVENT_BY_ID_VIEW_NAME,
+ Arrays.asList(eq(ModelConstants.EVENT_TENANT_ID_PROPERTY, tenantId),
+ eq(ModelConstants.EVENT_ENTITY_TYPE_PROPERTY, entityId.getEntityType()),
+ eq(ModelConstants.EVENT_ENTITY_ID_PROPERTY, entityId.getId())),
+ pageLink);
+ log.trace("Found events by tenant [{}], entity [{}] and pageLink [{}]", tenantId, entityId, pageLink);
+ return entities;
+ }
+
+ @Override
+ public List<EventEntity> findEvents(UUID tenantId, EntityId entityId, String eventType, TimePageLink pageLink) {
+ log.trace("Try to find events by tenant [{}], entity [{}], type [{}] and pageLink [{}]", tenantId, entityId, eventType, pageLink);
+ List<EventEntity> entities = findPageWithTimeSearch(EVENT_BY_TYPE_AND_ID_VIEW_NAME,
+ Arrays.asList(eq(ModelConstants.EVENT_TENANT_ID_PROPERTY, tenantId),
+ eq(ModelConstants.EVENT_ENTITY_TYPE_PROPERTY, entityId.getEntityType()),
+ eq(ModelConstants.EVENT_ENTITY_ID_PROPERTY, entityId.getId()),
+ eq(ModelConstants.EVENT_TYPE_PROPERTY, eventType)),
+ pageLink.isAscOrder() ? QueryBuilder.asc(ModelConstants.EVENT_TYPE_PROPERTY) :
+ QueryBuilder.desc(ModelConstants.EVENT_TYPE_PROPERTY),
+ pageLink);
+ log.trace("Found events by tenant [{}], entity [{}], type [{}] and pageLink [{}]", tenantId, entityId, eventType, pageLink);
+ return entities;
+ }
+
+ private Optional<EventEntity> save(EventEntity entity, boolean ifNotExists) {
+ if (entity.getId() == null) {
+ entity.setId(UUIDs.timeBased());
+ }
+ Insert insert = QueryBuilder.insertInto(getColumnFamilyName())
+ .value(ModelConstants.ID_PROPERTY, entity.getId())
+ .value(ModelConstants.EVENT_TENANT_ID_PROPERTY, entity.getTenantId())
+ .value(ModelConstants.EVENT_ENTITY_TYPE_PROPERTY, entity.getEntityType())
+ .value(ModelConstants.EVENT_ENTITY_ID_PROPERTY, entity.getEntityId())
+ .value(ModelConstants.EVENT_TYPE_PROPERTY, entity.getEventType())
+ .value(ModelConstants.EVENT_UID_PROPERTY, entity.getEventUId())
+ .value(ModelConstants.EVENT_BODY_PROPERTY, entity.getBody());
+ if (ifNotExists) {
+ insert = insert.ifNotExists();
+ }
+ ResultSet rs = executeWrite(insert);
+ if (rs.wasApplied()) {
+ return Optional.of(entity);
+ } else {
+ return Optional.empty();
+ }
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java b/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java
new file mode 100644
index 0000000..9e86a94
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java
@@ -0,0 +1,130 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.event;
+
+import com.datastax.driver.core.utils.UUIDs;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EventId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.model.EventEntity;
+import org.thingsboard.server.dao.service.DataValidator;
+
+import java.util.List;
+import java.util.Optional;
+
+import static org.thingsboard.server.dao.DaoUtil.convertDataList;
+import static org.thingsboard.server.dao.DaoUtil.getData;
+import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
+
+@Service
+@Slf4j
+public class BaseEventService implements EventService {
+
+ private final TenantId systemTenantId = new TenantId(NULL_UUID);
+
+ @Autowired
+ public EventDao eventDao;
+
+ @Override
+ public Event save(Event event) {
+ eventValidator.validate(event);
+ if (event.getTenantId() == null) {
+ log.trace("Save system event with predefined id {}", systemTenantId);
+ event.setTenantId(systemTenantId);
+ }
+ if (event.getId() == null) {
+ event.setId(new EventId(UUIDs.timeBased()));
+ }
+ if (StringUtils.isEmpty(event.getUid())) {
+ event.setUid(event.getId().toString());
+ }
+ return getData(eventDao.save(event));
+ }
+
+ @Override
+ public Optional<Event> saveIfNotExists(Event event) {
+ eventValidator.validate(event);
+ if (StringUtils.isEmpty(event.getUid())) {
+ throw new DataValidationException("Event uid should be specified!.");
+ }
+ if (event.getTenantId() == null) {
+ log.trace("Save system event with predefined id {}", systemTenantId);
+ event.setTenantId(systemTenantId);
+ }
+ if (event.getId() == null) {
+ event.setId(new EventId(UUIDs.timeBased()));
+ }
+ Optional<EventEntity> result = eventDao.saveIfNotExists(event);
+ return result.isPresent() ? Optional.of(getData(result.get())) : Optional.empty();
+ }
+
+ @Override
+ public Optional<Event> findEvent(TenantId tenantId, EntityId entityId, String eventType, String eventUid) {
+ if (tenantId == null) {
+ throw new DataValidationException("Tenant id should be specified!.");
+ }
+ if (entityId == null) {
+ throw new DataValidationException("Entity id should be specified!.");
+ }
+ if (StringUtils.isEmpty(eventType)) {
+ throw new DataValidationException("Event type should be specified!.");
+ }
+ if (StringUtils.isEmpty(eventUid)) {
+ throw new DataValidationException("Event uid should be specified!.");
+ }
+ EventEntity entity = eventDao.findEvent(tenantId.getId(), entityId, eventType, eventUid);
+ return entity != null ? Optional.of(getData(entity)) : Optional.empty();
+ }
+
+ @Override
+ public TimePageData<Event> findEvents(TenantId tenantId, EntityId entityId, TimePageLink pageLink) {
+ List<EventEntity> entities = eventDao.findEvents(tenantId.getId(), entityId, pageLink);
+ List<Event> events = convertDataList(entities);
+ return new TimePageData<Event>(events, pageLink);
+ }
+
+
+ @Override
+ public TimePageData<Event> findEvents(TenantId tenantId, EntityId entityId, String eventType, TimePageLink pageLink) {
+ List<EventEntity> entities = eventDao.findEvents(tenantId.getId(), entityId, eventType, pageLink);
+ List<Event> events = convertDataList(entities);
+ return new TimePageData<Event>(events, pageLink);
+ }
+
+ private DataValidator<Event> eventValidator =
+ new DataValidator<Event>() {
+ @Override
+ protected void validateDataImpl(Event event) {
+ if (event.getEntityId() == null) {
+ throw new DataValidationException("Entity id should be specified!.");
+ }
+ if (StringUtils.isEmpty(event.getType())) {
+ throw new DataValidationException("Event type should be specified!.");
+ }
+ if (event.getBody() == null) {
+ throw new DataValidationException("Event body should be specified!.");
+ }
+ }
+ };
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java b/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java
new file mode 100644
index 0000000..4d7b1f6
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java
@@ -0,0 +1,82 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.event;
+
+import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.dao.Dao;
+import org.thingsboard.server.dao.model.EventEntity;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+/**
+ * The Interface DeviceDao.
+ *
+ * @param <T> the generic type
+ */
+public interface EventDao extends Dao<EventEntity> {
+
+ /**
+ * Save or update event object
+ *
+ * @param event the event object
+ * @return saved event object
+ */
+ EventEntity save(Event event);
+
+ /**
+ * Save event object if it is not yet saved
+ *
+ * @param event the event object
+ * @return saved event object
+ */
+ Optional<EventEntity> saveIfNotExists(Event event);
+
+ /**
+ * Find event by tenantId, entityId and eventUid.
+ *
+ * @param tenantId the tenantId
+ * @param entityId the entityId
+ * @param eventType the eventType
+ * @param eventUid the eventUid
+ * @return the event
+ */
+ EventEntity findEvent(UUID tenantId, EntityId entityId, String eventType, String eventUid);
+
+ /**
+ * Find events by tenantId, entityId and pageLink.
+ *
+ * @param tenantId the tenantId
+ * @param entityId the entityId
+ * @param pageLink the pageLink
+ * @return the event list
+ */
+ List<EventEntity> findEvents(UUID tenantId, EntityId entityId, TimePageLink pageLink);
+
+ /**
+ * Find events by tenantId, entityId, eventType and pageLink.
+ *
+ * @param tenantId the tenantId
+ * @param entityId the entityId
+ * @param eventType the eventType
+ * @param pageLink the pageLink
+ * @return the event list
+ */
+ List<EventEntity> findEvents(UUID tenantId, EntityId entityId, String eventType, TimePageLink pageLink);
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/EventService.java b/dao/src/main/java/org/thingsboard/server/dao/event/EventService.java
new file mode 100644
index 0000000..386f894
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/event/EventService.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.event;
+
+import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.page.TimePageLink;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface EventService {
+
+ Event save(Event event);
+
+ Optional<Event> saveIfNotExists(Event event);
+
+ Optional<Event> findEvent(TenantId tenantId, EntityId entityId, String eventType, String eventUid);
+
+ TimePageData<Event> findEvents(TenantId tenantId, EntityId entityId, TimePageLink pageLink);
+
+ TimePageData<Event> findEvents(TenantId tenantId, EntityId entityId, String eventType, TimePageLink pageLink);
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/exception/DatabaseException.java b/dao/src/main/java/org/thingsboard/server/dao/exception/DatabaseException.java
new file mode 100644
index 0000000..9cd6b04
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/exception/DatabaseException.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.exception;
+
+public class DatabaseException extends RuntimeException {
+
+ private static final long serialVersionUID = 3463762014441887103L;
+
+ public DatabaseException() {
+ super();
+ }
+
+ public DatabaseException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public DatabaseException(String message) {
+ super(message);
+ }
+
+ public DatabaseException(Throwable cause) {
+ super(cause);
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/exception/DataValidationException.java b/dao/src/main/java/org/thingsboard/server/dao/exception/DataValidationException.java
new file mode 100644
index 0000000..4a69ec7
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/exception/DataValidationException.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.exception;
+
+public class DataValidationException extends RuntimeException {
+
+ private static final long serialVersionUID = 7659985660312721830L;
+
+ public DataValidationException(String message) {
+ super(message);
+ }
+
+ public DataValidationException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/exception/IncorrectParameterException.java b/dao/src/main/java/org/thingsboard/server/dao/exception/IncorrectParameterException.java
new file mode 100644
index 0000000..7ee8fb1
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/exception/IncorrectParameterException.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.exception;
+
+
+public class IncorrectParameterException extends RuntimeException {
+
+ private static final long serialVersionUID = 601995650578985289L;
+
+ public IncorrectParameterException(String message) {
+ super(message);
+ }
+
+ public IncorrectParameterException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/AdminSettingsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/AdminSettingsEntity.java
new file mode 100644
index 0000000..3e72fcb
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/AdminSettingsEntity.java
@@ -0,0 +1,147 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model;
+
+import static org.thingsboard.server.dao.model.ModelConstants.ADMIN_SETTINGS_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ADMIN_SETTINGS_JSON_VALUE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ADMIN_SETTINGS_KEY_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
+
+import java.util.UUID;
+
+import org.thingsboard.server.common.data.AdminSettings;
+import org.thingsboard.server.common.data.id.AdminSettingsId;
+import org.thingsboard.server.dao.model.type.JsonCodec;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.datastax.driver.mapping.annotations.Column;
+import com.datastax.driver.mapping.annotations.PartitionKey;
+import com.datastax.driver.mapping.annotations.Table;
+import com.datastax.driver.mapping.annotations.Transient;
+import com.fasterxml.jackson.databind.JsonNode;
+
+@Table(name = ADMIN_SETTINGS_COLUMN_FAMILY_NAME)
+public final class AdminSettingsEntity implements BaseEntity<AdminSettings> {
+
+ @Transient
+ private static final long serialVersionUID = 899117723388310403L;
+
+ @PartitionKey(value = 0)
+ @Column(name = ID_PROPERTY)
+ private UUID id;
+
+ @Column(name = ADMIN_SETTINGS_KEY_PROPERTY)
+ private String key;
+
+ @Column(name = ADMIN_SETTINGS_JSON_VALUE_PROPERTY, codec = JsonCodec.class)
+ private JsonNode jsonValue;
+
+ public AdminSettingsEntity() {
+ super();
+ }
+
+ public AdminSettingsEntity(AdminSettings adminSettings) {
+ if (adminSettings.getId() != null) {
+ this.id = adminSettings.getId().getId();
+ }
+ this.key = adminSettings.getKey();
+ this.jsonValue = adminSettings.getJsonValue();
+ }
+
+ public UUID getId() {
+ return id;
+ }
+
+ public void setId(UUID id) {
+ this.id = id;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public void setKey(String key) {
+ this.key = key;
+ }
+
+ public JsonNode getJsonValue() {
+ return jsonValue;
+ }
+
+ public void setJsonValue(JsonNode jsonValue) {
+ this.jsonValue = jsonValue;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((id == null) ? 0 : id.hashCode());
+ result = prime * result + ((jsonValue == null) ? 0 : jsonValue.hashCode());
+ result = prime * result + ((key == null) ? 0 : key.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ AdminSettingsEntity other = (AdminSettingsEntity) obj;
+ if (id == null) {
+ if (other.id != null)
+ return false;
+ } else if (!id.equals(other.id))
+ return false;
+ if (jsonValue == null) {
+ if (other.jsonValue != null)
+ return false;
+ } else if (!jsonValue.equals(other.jsonValue))
+ return false;
+ if (key == null) {
+ if (other.key != null)
+ return false;
+ } else if (!key.equals(other.key))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("AdminSettingsEntity [id=");
+ builder.append(id);
+ builder.append(", key=");
+ builder.append(key);
+ builder.append(", jsonValue=");
+ builder.append(jsonValue);
+ builder.append("]");
+ return builder.toString();
+ }
+
+ @Override
+ public AdminSettings toData() {
+ AdminSettings adminSettings = new AdminSettings(new AdminSettingsId(id));
+ adminSettings.setCreatedTime(UUIDs.unixTimestamp(id));
+ adminSettings.setKey(key);
+ adminSettings.setJsonValue(jsonValue);
+ return adminSettings;
+ }
+
+}
\ No newline at end of file
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/BaseEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/BaseEntity.java
new file mode 100644
index 0000000..02828ec
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/BaseEntity.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model;
+
+import java.io.Serializable;
+import java.util.UUID;
+
+public interface BaseEntity<D> extends ToData<D>, Serializable {
+
+ UUID getId();
+
+ void setId(UUID id);
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ComponentDescriptorEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/ComponentDescriptorEntity.java
new file mode 100644
index 0000000..99851de
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/ComponentDescriptorEntity.java
@@ -0,0 +1,162 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model;
+
+import com.datastax.driver.mapping.annotations.Column;
+import com.datastax.driver.mapping.annotations.PartitionKey;
+import com.datastax.driver.mapping.annotations.Table;
+import com.fasterxml.jackson.databind.JsonNode;
+import org.thingsboard.server.common.data.id.ComponentDescriptorId;
+import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
+import org.thingsboard.server.common.data.plugin.ComponentScope;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.dao.model.type.JsonCodec;
+
+import java.util.UUID;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Table(name = ModelConstants.COMPONENT_DESCRIPTOR_COLUMN_FAMILY_NAME)
+public class ComponentDescriptorEntity implements SearchTextEntity<ComponentDescriptor> {
+
+ private static final long serialVersionUID = 1L;
+
+ @PartitionKey
+ @Column(name = ModelConstants.ID_PROPERTY)
+ private UUID id;
+
+ @Column(name = ModelConstants.COMPONENT_DESCRIPTOR_TYPE_PROPERTY)
+ private ComponentType type;
+
+ @Column(name = ModelConstants.COMPONENT_DESCRIPTOR_SCOPE_PROPERTY)
+ private ComponentScope scope;
+
+ @Column(name = ModelConstants.COMPONENT_DESCRIPTOR_NAME_PROPERTY)
+ private String name;
+
+ @Column(name = ModelConstants.COMPONENT_DESCRIPTOR_CLASS_PROPERTY)
+ private String clazz;
+
+ @Column(name = ModelConstants.COMPONENT_DESCRIPTOR_CONFIGURATION_DESCRIPTOR_PROPERTY, codec = JsonCodec.class)
+ private JsonNode configurationDescriptor;
+
+ @Column(name = ModelConstants.COMPONENT_DESCRIPTOR_ACTIONS_PROPERTY)
+ private String actions;
+
+ @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY)
+ private String searchText;
+
+ public ComponentDescriptorEntity() {
+ }
+
+ public ComponentDescriptorEntity(ComponentDescriptor component) {
+ if (component.getId() != null) {
+ this.id = component.getId().getId();
+ }
+ this.actions = component.getActions();
+ this.type = component.getType();
+ this.scope = component.getScope();
+ this.name = component.getName();
+ this.clazz = component.getClazz();
+ this.configurationDescriptor = component.getConfigurationDescriptor();
+ this.searchText = component.getName();
+ }
+
+ @Override
+ public ComponentDescriptor toData() {
+ ComponentDescriptor data = new ComponentDescriptor(new ComponentDescriptorId(id));
+ data.setType(type);
+ data.setScope(scope);
+ data.setName(this.getName());
+ data.setClazz(this.getClazz());
+ data.setActions(this.getActions());
+ data.setConfigurationDescriptor(this.getConfigurationDescriptor());
+ return data;
+ }
+
+ @Override
+ public UUID getId() {
+ return id;
+ }
+
+ @Override
+ public void setId(UUID id) {
+ this.id = id;
+ }
+
+ public String getActions() {
+ return actions;
+ }
+
+ public void setActions(String actions) {
+ this.actions = actions;
+ }
+
+ public ComponentType getType() {
+ return type;
+ }
+
+ public void setType(ComponentType type) {
+ this.type = type;
+ }
+
+ public ComponentScope getScope() {
+ return scope;
+ }
+
+ public void setScope(ComponentScope scope) {
+ this.scope = scope;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getClazz() {
+ return clazz;
+ }
+
+ public void setClazz(String clazz) {
+ this.clazz = clazz;
+ }
+
+ public JsonNode getConfigurationDescriptor() {
+ return configurationDescriptor;
+ }
+
+ public void setConfigurationDescriptor(JsonNode configurationDescriptor) {
+ this.configurationDescriptor = configurationDescriptor;
+ }
+
+ public String getSearchText() {
+ return searchText;
+ }
+
+ @Override
+ public void setSearchText(String searchText) {
+ this.searchText = searchText;
+ }
+
+ @Override
+ public String getSearchTextSource() {
+ return searchText;
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/CustomerEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/CustomerEntity.java
new file mode 100644
index 0000000..5c26189
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/CustomerEntity.java
@@ -0,0 +1,365 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model;
+
+import static org.thingsboard.server.dao.model.ModelConstants.ADDRESS2_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ADDRESS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.CITY_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.COUNTRY_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_ADDITIONAL_INFO_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_TENANT_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_TITLE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.EMAIL_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.PHONE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.STATE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ZIP_PROPERTY;
+
+import java.util.UUID;
+
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.dao.model.type.JsonCodec;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.datastax.driver.mapping.annotations.Column;
+import com.datastax.driver.mapping.annotations.PartitionKey;
+import com.datastax.driver.mapping.annotations.Table;
+import com.datastax.driver.mapping.annotations.Transient;
+import com.fasterxml.jackson.databind.JsonNode;
+
+@Table(name = CUSTOMER_COLUMN_FAMILY_NAME)
+public final class CustomerEntity implements SearchTextEntity<Customer> {
+
+ @Transient
+ private static final long serialVersionUID = -7732527103760948490L;
+
+ @PartitionKey(value = 0)
+ @Column(name = ID_PROPERTY)
+ private UUID id;
+
+ @PartitionKey(value = 1)
+ @Column(name = CUSTOMER_TENANT_ID_PROPERTY)
+ private UUID tenantId;
+
+ @Column(name = CUSTOMER_TITLE_PROPERTY)
+ private String title;
+
+ @Column(name = SEARCH_TEXT_PROPERTY)
+ private String searchText;
+
+ @Column(name = COUNTRY_PROPERTY)
+ private String country;
+
+ @Column(name = STATE_PROPERTY)
+ private String state;
+
+ @Column(name = CITY_PROPERTY)
+ private String city;
+
+ @Column(name = ADDRESS_PROPERTY)
+ private String address;
+
+ @Column(name = ADDRESS2_PROPERTY)
+ private String address2;
+
+ @Column(name = ZIP_PROPERTY)
+ private String zip;
+
+ @Column(name = PHONE_PROPERTY)
+ private String phone;
+
+ @Column(name = EMAIL_PROPERTY)
+ private String email;
+
+ @Column(name = CUSTOMER_ADDITIONAL_INFO_PROPERTY, codec = JsonCodec.class)
+ private JsonNode additionalInfo;
+
+ public CustomerEntity() {
+ super();
+ }
+
+ public CustomerEntity(Customer customer) {
+ if (customer.getId() != null) {
+ this.id = customer.getId().getId();
+ }
+ this.tenantId = customer.getTenantId().getId();
+ this.title = customer.getTitle();
+ this.country = customer.getCountry();
+ this.state = customer.getState();
+ this.city = customer.getCity();
+ this.address = customer.getAddress();
+ this.address2 = customer.getAddress2();
+ this.zip = customer.getZip();
+ this.phone = customer.getPhone();
+ this.email = customer.getEmail();
+ this.additionalInfo = customer.getAdditionalInfo();
+ }
+
+ public UUID getId() {
+ return id;
+ }
+
+ public void setId(UUID id) {
+ this.id = id;
+ }
+
+ public UUID getTenantId() {
+ return tenantId;
+ }
+
+ public void setTenantId(UUID tenantId) {
+ this.tenantId = tenantId;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getCountry() {
+ return country;
+ }
+
+ public void setCountry(String country) {
+ this.country = country;
+ }
+
+ public String getState() {
+ return state;
+ }
+
+ public void setState(String state) {
+ this.state = state;
+ }
+
+ public String getCity() {
+ return city;
+ }
+
+ public void setCity(String city) {
+ this.city = city;
+ }
+
+ public String getAddress() {
+ return address;
+ }
+
+ public void setAddress(String address) {
+ this.address = address;
+ }
+
+ public String getAddress2() {
+ return address2;
+ }
+
+ public void setAddress2(String address2) {
+ this.address2 = address2;
+ }
+
+ public String getZip() {
+ return zip;
+ }
+
+ public void setZip(String zip) {
+ this.zip = zip;
+ }
+
+ public String getPhone() {
+ return phone;
+ }
+
+ public void setPhone(String phone) {
+ this.phone = phone;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public JsonNode getAdditionalInfo() {
+ return additionalInfo;
+ }
+
+ public void setAdditionalInfo(JsonNode additionalInfo) {
+ this.additionalInfo = additionalInfo;
+ }
+
+ @Override
+ public String getSearchTextSource() {
+ return title;
+ }
+
+ @Override
+ public void setSearchText(String searchText) {
+ this.searchText = searchText;
+ }
+
+ public String getSearchText() {
+ return searchText;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((additionalInfo == null) ? 0 : additionalInfo.hashCode());
+ result = prime * result + ((address == null) ? 0 : address.hashCode());
+ result = prime * result + ((address2 == null) ? 0 : address2.hashCode());
+ result = prime * result + ((city == null) ? 0 : city.hashCode());
+ result = prime * result + ((country == null) ? 0 : country.hashCode());
+ result = prime * result + ((email == null) ? 0 : email.hashCode());
+ result = prime * result + ((id == null) ? 0 : id.hashCode());
+ result = prime * result + ((phone == null) ? 0 : phone.hashCode());
+ result = prime * result + ((state == null) ? 0 : state.hashCode());
+ result = prime * result + ((tenantId == null) ? 0 : tenantId.hashCode());
+ result = prime * result + ((title == null) ? 0 : title.hashCode());
+ result = prime * result + ((zip == null) ? 0 : zip.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ CustomerEntity other = (CustomerEntity) obj;
+ if (additionalInfo == null) {
+ if (other.additionalInfo != null)
+ return false;
+ } else if (!additionalInfo.equals(other.additionalInfo))
+ return false;
+ if (address == null) {
+ if (other.address != null)
+ return false;
+ } else if (!address.equals(other.address))
+ return false;
+ if (address2 == null) {
+ if (other.address2 != null)
+ return false;
+ } else if (!address2.equals(other.address2))
+ return false;
+ if (city == null) {
+ if (other.city != null)
+ return false;
+ } else if (!city.equals(other.city))
+ return false;
+ if (country == null) {
+ if (other.country != null)
+ return false;
+ } else if (!country.equals(other.country))
+ return false;
+ if (email == null) {
+ if (other.email != null)
+ return false;
+ } else if (!email.equals(other.email))
+ return false;
+ if (id == null) {
+ if (other.id != null)
+ return false;
+ } else if (!id.equals(other.id))
+ return false;
+ if (phone == null) {
+ if (other.phone != null)
+ return false;
+ } else if (!phone.equals(other.phone))
+ return false;
+ if (state == null) {
+ if (other.state != null)
+ return false;
+ } else if (!state.equals(other.state))
+ return false;
+ if (tenantId == null) {
+ if (other.tenantId != null)
+ return false;
+ } else if (!tenantId.equals(other.tenantId))
+ return false;
+ if (title == null) {
+ if (other.title != null)
+ return false;
+ } else if (!title.equals(other.title))
+ return false;
+ if (zip == null) {
+ if (other.zip != null)
+ return false;
+ } else if (!zip.equals(other.zip))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("CustomerEntity [id=");
+ builder.append(id);
+ builder.append(", tenantId=");
+ builder.append(tenantId);
+ builder.append(", title=");
+ builder.append(title);
+ builder.append(", country=");
+ builder.append(country);
+ builder.append(", state=");
+ builder.append(state);
+ builder.append(", city=");
+ builder.append(city);
+ builder.append(", address=");
+ builder.append(address);
+ builder.append(", address2=");
+ builder.append(address2);
+ builder.append(", zip=");
+ builder.append(zip);
+ builder.append(", phone=");
+ builder.append(phone);
+ builder.append(", email=");
+ builder.append(email);
+ builder.append(", additionalInfo=");
+ builder.append(additionalInfo);
+ builder.append("]");
+ return builder.toString();
+ }
+
+ @Override
+ public Customer toData() {
+ Customer customer = new Customer(new CustomerId(id));
+ customer.setCreatedTime(UUIDs.unixTimestamp(id));
+ customer.setTenantId(new TenantId(tenantId));
+ customer.setTitle(title);
+ customer.setCountry(country);
+ customer.setState(state);
+ customer.setCity(city);
+ customer.setAddress(address);
+ customer.setAddress2(address2);
+ customer.setZip(zip);
+ customer.setPhone(phone);
+ customer.setEmail(email);
+ customer.setAdditionalInfo(additionalInfo);
+ return customer;
+ }
+
+}
\ No newline at end of file
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/DashboardEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/DashboardEntity.java
new file mode 100644
index 0000000..cf74d82
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/DashboardEntity.java
@@ -0,0 +1,221 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model;
+
+import java.util.UUID;
+
+import org.thingsboard.server.common.data.Dashboard;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DashboardId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.dao.model.type.JsonCodec;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.datastax.driver.mapping.annotations.Column;
+import com.datastax.driver.mapping.annotations.PartitionKey;
+import com.datastax.driver.mapping.annotations.Table;
+import com.datastax.driver.mapping.annotations.Transient;
+import com.fasterxml.jackson.databind.JsonNode;
+
+@Table(name = ModelConstants.DASHBOARD_COLUMN_FAMILY_NAME)
+public final class DashboardEntity implements SearchTextEntity<Dashboard> {
+
+ @Transient
+ private static final long serialVersionUID = 2998395951247446191L;
+
+ @PartitionKey(value = 0)
+ @Column(name = ModelConstants.ID_PROPERTY)
+ private UUID id;
+
+ @PartitionKey(value = 1)
+ @Column(name = ModelConstants.DASHBOARD_TENANT_ID_PROPERTY)
+ private UUID tenantId;
+
+ @PartitionKey(value = 2)
+ @Column(name = ModelConstants.DASHBOARD_CUSTOMER_ID_PROPERTY)
+ private UUID customerId;
+
+ @Column(name = ModelConstants.DASHBOARD_TITLE_PROPERTY)
+ private String title;
+
+ @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY)
+ private String searchText;
+
+ @Column(name = ModelConstants.DASHBOARD_CONFIGURATION_PROPERTY, codec = JsonCodec.class)
+ private JsonNode configuration;
+
+ public DashboardEntity() {
+ super();
+ }
+
+ public DashboardEntity(Dashboard dashboard) {
+ if (dashboard.getId() != null) {
+ this.id = dashboard.getId().getId();
+ }
+ if (dashboard.getTenantId() != null) {
+ this.tenantId = dashboard.getTenantId().getId();
+ }
+ if (dashboard.getCustomerId() != null) {
+ this.customerId = dashboard.getCustomerId().getId();
+ }
+ this.title = dashboard.getTitle();
+ this.configuration = dashboard.getConfiguration();
+ }
+
+ public UUID getId() {
+ return id;
+ }
+
+ public void setId(UUID id) {
+ this.id = id;
+ }
+
+ public UUID getTenantId() {
+ return tenantId;
+ }
+
+ public void setTenantId(UUID tenantId) {
+ this.tenantId = tenantId;
+ }
+
+ public UUID getCustomerId() {
+ return customerId;
+ }
+
+ public void setCustomerId(UUID customerId) {
+ this.customerId = customerId;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public JsonNode getConfiguration() {
+ return configuration;
+ }
+
+ public void setConfiguration(JsonNode configuration) {
+ this.configuration = configuration;
+ }
+
+ @Override
+ public String getSearchTextSource() {
+ return title;
+ }
+
+ @Override
+ public void setSearchText(String searchText) {
+ this.searchText = searchText;
+ }
+
+ public String getSearchText() {
+ return searchText;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((configuration == null) ? 0 : configuration.hashCode());
+ result = prime * result + ((customerId == null) ? 0 : customerId.hashCode());
+ result = prime * result + ((id == null) ? 0 : id.hashCode());
+ result = prime * result + ((searchText == null) ? 0 : searchText.hashCode());
+ result = prime * result + ((tenantId == null) ? 0 : tenantId.hashCode());
+ result = prime * result + ((title == null) ? 0 : title.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ DashboardEntity other = (DashboardEntity) obj;
+ if (configuration == null) {
+ if (other.configuration != null)
+ return false;
+ } else if (!configuration.equals(other.configuration))
+ return false;
+ if (customerId == null) {
+ if (other.customerId != null)
+ return false;
+ } else if (!customerId.equals(other.customerId))
+ return false;
+ if (id == null) {
+ if (other.id != null)
+ return false;
+ } else if (!id.equals(other.id))
+ return false;
+ if (searchText == null) {
+ if (other.searchText != null)
+ return false;
+ } else if (!searchText.equals(other.searchText))
+ return false;
+ if (tenantId == null) {
+ if (other.tenantId != null)
+ return false;
+ } else if (!tenantId.equals(other.tenantId))
+ return false;
+ if (title == null) {
+ if (other.title != null)
+ return false;
+ } else if (!title.equals(other.title))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("DashboardEntity [id=");
+ builder.append(id);
+ builder.append(", tenantId=");
+ builder.append(tenantId);
+ builder.append(", customerId=");
+ builder.append(customerId);
+ builder.append(", title=");
+ builder.append(title);
+ builder.append(", searchText=");
+ builder.append(searchText);
+ builder.append(", configuration=");
+ builder.append(configuration);
+ builder.append("]");
+ return builder.toString();
+ }
+
+ @Override
+ public Dashboard toData() {
+ Dashboard dashboard = new Dashboard(new DashboardId(id));
+ dashboard.setCreatedTime(UUIDs.unixTimestamp(id));
+ if (tenantId != null) {
+ dashboard.setTenantId(new TenantId(tenantId));
+ }
+ if (customerId != null) {
+ dashboard.setCustomerId(new CustomerId(customerId));
+ }
+ dashboard.setTitle(title);
+ dashboard.setConfiguration(configuration);
+ return dashboard;
+ }
+
+}
\ No newline at end of file
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/DeviceCredentialsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/DeviceCredentialsEntity.java
new file mode 100644
index 0000000..5a1fd28
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/DeviceCredentialsEntity.java
@@ -0,0 +1,186 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model;
+
+import java.util.UUID;
+
+import org.thingsboard.server.common.data.id.DeviceCredentialsId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.security.DeviceCredentials;
+import org.thingsboard.server.common.data.security.DeviceCredentialsType;
+import org.thingsboard.server.dao.model.type.DeviceCredentialsTypeCodec;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.datastax.driver.mapping.annotations.Column;
+import com.datastax.driver.mapping.annotations.PartitionKey;
+import com.datastax.driver.mapping.annotations.Table;
+import com.datastax.driver.mapping.annotations.Transient;
+
+@Table(name = ModelConstants.DEVICE_CREDENTIALS_COLUMN_FAMILY_NAME)
+public final class DeviceCredentialsEntity implements BaseEntity<DeviceCredentials> {
+
+ @Transient
+ private static final long serialVersionUID = -2667310560260623272L;
+
+ @PartitionKey(value = 0)
+ @Column(name = ModelConstants.ID_PROPERTY)
+ private UUID id;
+
+ @Column(name = ModelConstants.DEVICE_CREDENTIALS_DEVICE_ID_PROPERTY)
+ private UUID deviceId;
+
+ @Column(name = ModelConstants.DEVICE_CREDENTIALS_CREDENTIALS_TYPE_PROPERTY, codec = DeviceCredentialsTypeCodec.class)
+ private DeviceCredentialsType credentialsType;
+
+ @Column(name = ModelConstants.DEVICE_CREDENTIALS_CREDENTIALS_ID_PROPERTY)
+ private String credentialsId;
+
+ @Column(name = ModelConstants.DEVICE_CREDENTIALS_CREDENTIALS_VALUE_PROPERTY)
+ private String credentialsValue;
+
+ public DeviceCredentialsEntity() {
+ super();
+ }
+
+ public DeviceCredentialsEntity(DeviceCredentials deviceCredentials) {
+ if (deviceCredentials.getId() != null) {
+ this.id = deviceCredentials.getId().getId();
+ }
+ if (deviceCredentials.getDeviceId() != null) {
+ this.deviceId = deviceCredentials.getDeviceId().getId();
+ }
+ this.credentialsType = deviceCredentials.getCredentialsType();
+ this.credentialsId = deviceCredentials.getCredentialsId();
+ this.credentialsValue = deviceCredentials.getCredentialsValue();
+ }
+
+ public UUID getId() {
+ return id;
+ }
+
+ public void setId(UUID id) {
+ this.id = id;
+ }
+
+ public UUID getDeviceId() {
+ return deviceId;
+ }
+
+ public void setDeviceId(UUID deviceId) {
+ this.deviceId = deviceId;
+ }
+
+ public DeviceCredentialsType getCredentialsType() {
+ return credentialsType;
+ }
+
+ public void setCredentialsType(DeviceCredentialsType credentialsType) {
+ this.credentialsType = credentialsType;
+ }
+
+ public String getCredentialsId() {
+ return credentialsId;
+ }
+
+ public void setCredentialsId(String credentialsId) {
+ this.credentialsId = credentialsId;
+ }
+
+ public String getCredentialsValue() {
+ return credentialsValue;
+ }
+
+ public void setCredentialsValue(String credentialsValue) {
+ this.credentialsValue = credentialsValue;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((credentialsId == null) ? 0 : credentialsId.hashCode());
+ result = prime * result + ((credentialsType == null) ? 0 : credentialsType.hashCode());
+ result = prime * result + ((credentialsValue == null) ? 0 : credentialsValue.hashCode());
+ result = prime * result + ((deviceId == null) ? 0 : deviceId.hashCode());
+ result = prime * result + ((id == null) ? 0 : id.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ DeviceCredentialsEntity other = (DeviceCredentialsEntity) obj;
+ if (credentialsId == null) {
+ if (other.credentialsId != null)
+ return false;
+ } else if (!credentialsId.equals(other.credentialsId))
+ return false;
+ if (credentialsType != other.credentialsType)
+ return false;
+ if (credentialsValue == null) {
+ if (other.credentialsValue != null)
+ return false;
+ } else if (!credentialsValue.equals(other.credentialsValue))
+ return false;
+ if (deviceId == null) {
+ if (other.deviceId != null)
+ return false;
+ } else if (!deviceId.equals(other.deviceId))
+ return false;
+ if (id == null) {
+ if (other.id != null)
+ return false;
+ } else if (!id.equals(other.id))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("DeviceCredentialsEntity [id=");
+ builder.append(id);
+ builder.append(", deviceId=");
+ builder.append(deviceId);
+ builder.append(", credentialsType=");
+ builder.append(credentialsType);
+ builder.append(", credentialsId=");
+ builder.append(credentialsId);
+ builder.append(", credentialsValue=");
+ builder.append(credentialsValue);
+ builder.append("]");
+ return builder.toString();
+ }
+
+ @Override
+ public DeviceCredentials toData() {
+ DeviceCredentials deviceCredentials = new DeviceCredentials(new DeviceCredentialsId(id));
+ deviceCredentials.setCreatedTime(UUIDs.unixTimestamp(id));
+ if (deviceId != null) {
+ deviceCredentials.setDeviceId(new DeviceId(deviceId));
+ }
+ deviceCredentials.setCredentialsType(credentialsType);
+ deviceCredentials.setCredentialsId(credentialsId);
+ deviceCredentials.setCredentialsValue(credentialsValue);
+ return deviceCredentials;
+ }
+
+}
\ No newline at end of file
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/DeviceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/DeviceEntity.java
new file mode 100644
index 0000000..4fc1ea2
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/DeviceEntity.java
@@ -0,0 +1,214 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.datastax.driver.mapping.annotations.Column;
+import com.datastax.driver.mapping.annotations.PartitionKey;
+import com.datastax.driver.mapping.annotations.Table;
+import com.datastax.driver.mapping.annotations.Transient;
+import com.fasterxml.jackson.databind.JsonNode;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.dao.model.type.JsonCodec;
+
+import java.util.UUID;
+
+import static org.thingsboard.server.dao.model.ModelConstants.*;
+
+@Table(name = DEVICE_COLUMN_FAMILY_NAME)
+public final class DeviceEntity implements SearchTextEntity<Device> {
+
+ @Transient
+ private static final long serialVersionUID = -1265181166886910152L;
+
+ @PartitionKey(value = 0)
+ @Column(name = ID_PROPERTY)
+ private UUID id;
+
+ @PartitionKey(value = 1)
+ @Column(name = DEVICE_TENANT_ID_PROPERTY)
+ private UUID tenantId;
+
+ @PartitionKey(value = 2)
+ @Column(name = DEVICE_CUSTOMER_ID_PROPERTY)
+ private UUID customerId;
+
+ @Column(name = DEVICE_NAME_PROPERTY)
+ private String name;
+
+ @Column(name = SEARCH_TEXT_PROPERTY)
+ private String searchText;
+
+ @Column(name = DEVICE_ADDITIONAL_INFO_PROPERTY, codec = JsonCodec.class)
+ private JsonNode additionalInfo;
+
+ public DeviceEntity() {
+ super();
+ }
+
+ public DeviceEntity(Device device) {
+ if (device.getId() != null) {
+ this.id = device.getId().getId();
+ }
+ if (device.getTenantId() != null) {
+ this.tenantId = device.getTenantId().getId();
+ }
+ if (device.getCustomerId() != null) {
+ this.customerId = device.getCustomerId().getId();
+ }
+ this.name = device.getName();
+ this.additionalInfo = device.getAdditionalInfo();
+ }
+
+ public UUID getId() {
+ return id;
+ }
+
+ public void setId(UUID id) {
+ this.id = id;
+ }
+
+ public UUID getTenantId() {
+ return tenantId;
+ }
+
+ public void setTenantId(UUID tenantId) {
+ this.tenantId = tenantId;
+ }
+
+ public UUID getCustomerId() {
+ return customerId;
+ }
+
+ public void setCustomerId(UUID customerId) {
+ this.customerId = customerId;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public JsonNode getAdditionalInfo() {
+ return additionalInfo;
+ }
+
+ public void setAdditionalInfo(JsonNode additionalInfo) {
+ this.additionalInfo = additionalInfo;
+ }
+
+ @Override
+ public String getSearchTextSource() {
+ return name;
+ }
+
+ @Override
+ public void setSearchText(String searchText) {
+ this.searchText = searchText;
+ }
+
+ public String getSearchText() {
+ return searchText;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((additionalInfo == null) ? 0 : additionalInfo.hashCode());
+ result = prime * result + ((customerId == null) ? 0 : customerId.hashCode());
+ result = prime * result + ((id == null) ? 0 : id.hashCode());
+ result = prime * result + ((name == null) ? 0 : name.hashCode());
+ result = prime * result + ((tenantId == null) ? 0 : tenantId.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ DeviceEntity other = (DeviceEntity) obj;
+ if (additionalInfo == null) {
+ if (other.additionalInfo != null)
+ return false;
+ } else if (!additionalInfo.equals(other.additionalInfo))
+ return false;
+ if (customerId == null) {
+ if (other.customerId != null)
+ return false;
+ } else if (!customerId.equals(other.customerId))
+ return false;
+ if (id == null) {
+ if (other.id != null)
+ return false;
+ } else if (!id.equals(other.id))
+ return false;
+ if (name == null) {
+ if (other.name != null)
+ return false;
+ } else if (!name.equals(other.name))
+ return false;
+ if (tenantId == null) {
+ if (other.tenantId != null)
+ return false;
+ } else if (!tenantId.equals(other.tenantId))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("DeviceEntity [id=");
+ builder.append(id);
+ builder.append(", tenantId=");
+ builder.append(tenantId);
+ builder.append(", customerId=");
+ builder.append(customerId);
+ builder.append(", name=");
+ builder.append(name);
+ builder.append(", additionalInfo=");
+ builder.append(additionalInfo);
+ builder.append("]");
+ return builder.toString();
+ }
+
+ @Override
+ public Device toData() {
+ Device device = new Device(new DeviceId(id));
+ device.setCreatedTime(UUIDs.unixTimestamp(id));
+ if (tenantId != null) {
+ device.setTenantId(new TenantId(tenantId));
+ }
+ if (customerId != null) {
+ device.setCustomerId(new CustomerId(customerId));
+ }
+ device.setName(name);
+ device.setAdditionalInfo(additionalInfo);
+ return device;
+ }
+
+}
\ No newline at end of file
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/EventEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/EventEntity.java
new file mode 100644
index 0000000..a655daf
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/EventEntity.java
@@ -0,0 +1,123 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.datastax.driver.mapping.annotations.*;
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.data.id.*;
+import org.thingsboard.server.dao.model.type.EntityTypeCodec;
+import org.thingsboard.server.dao.model.type.JsonCodec;
+
+import java.util.UUID;
+
+import static org.thingsboard.server.dao.model.ModelConstants.*;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+@NoArgsConstructor
+@Table(name = DEVICE_COLUMN_FAMILY_NAME)
+public class EventEntity implements BaseEntity<Event> {
+
+ @Transient
+ private static final long serialVersionUID = -1265181166886910153L;
+
+ @Column(name = ID_PROPERTY)
+ private UUID id;
+
+ @PartitionKey()
+ @Column(name = EVENT_TENANT_ID_PROPERTY)
+ private UUID tenantId;
+
+ @PartitionKey(value = 1)
+ @Column(name = EVENT_ENTITY_TYPE_PROPERTY, codec = EntityTypeCodec.class)
+ private EntityType entityType;
+
+ @PartitionKey(value = 2)
+ @Column(name = EVENT_ENTITY_ID_PROPERTY)
+ private UUID entityId;
+
+ @ClusteringColumn()
+ @Column(name = EVENT_TYPE_PROPERTY)
+ private String eventType;
+
+ @ClusteringColumn(value = 1)
+ @Column(name = EVENT_UID_PROPERTY)
+ private String eventUId;
+
+ @Column(name = EVENT_BODY_PROPERTY, codec = JsonCodec.class)
+ private JsonNode body;
+
+ public EventEntity(Event event) {
+ if (event.getId() != null) {
+ this.id = event.getId().getId();
+ }
+ if (event.getTenantId() != null) {
+ this.tenantId = event.getTenantId().getId();
+ }
+ if (event.getEntityId() != null) {
+ this.entityType = event.getEntityId().getEntityType();
+ this.entityId = event.getEntityId().getId();
+ }
+ this.eventType = event.getType();
+ this.eventUId = event.getUid();
+ this.body = event.getBody();
+ }
+
+ @Override
+ public UUID getId() {
+ return id;
+ }
+
+ @Override
+ public void setId(UUID id) {
+ this.id = id;
+ }
+
+ @Override
+ public Event toData() {
+ Event event = new Event(new EventId(id));
+ event.setCreatedTime(UUIDs.unixTimestamp(id));
+ event.setTenantId(new TenantId(tenantId));
+ switch (entityType) {
+ case TENANT:
+ event.setEntityId(new TenantId(entityId));
+ break;
+ case DEVICE:
+ event.setEntityId(new DeviceId(entityId));
+ break;
+ case CUSTOMER:
+ event.setEntityId(new CustomerId(entityId));
+ break;
+ case RULE:
+ event.setEntityId(new RuleId(entityId));
+ break;
+ case PLUGIN:
+ event.setEntityId(new PluginId(entityId));
+ break;
+ }
+ event.setBody(body);
+ event.setType(eventType);
+ event.setUid(eventUId);
+ return event;
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
new file mode 100644
index 0000000..b14e99f
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
@@ -0,0 +1,262 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model;
+
+import java.util.UUID;
+
+import com.datastax.driver.core.utils.UUIDs;
+
+public class ModelConstants {
+
+ private ModelConstants() {
+ }
+
+ public static UUID NULL_UUID = UUIDs.startOf(0);
+
+ /**
+ * Generic constants.
+ */
+ public static final String ID_PROPERTY = "id";
+ public static final String USER_ID_PROPERTY = "user_id";
+ public static final String TENTANT_ID_PROPERTY = "tenant_id";
+ public static final String CUSTOMER_ID_PROPERTY = "customer_id";
+ public static final String DEVICE_ID_PROPERTY = "device_id";
+ public static final String TITLE_PROPERTY = "title";
+ public static final String ALIAS_PROPERTY = "alias";
+ public static final String SEARCH_TEXT_PROPERTY = "search_text";
+ public static final String ADDITIONAL_INFO_PROPERTY = "additional_info";
+
+ /**
+ * Cassandra user constants.
+ */
+ public static final String USER_COLUMN_FAMILY_NAME = "user";
+ public static final String USER_TENANT_ID_PROPERTY = TENTANT_ID_PROPERTY;
+ public static final String USER_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY;
+ public static final String USER_EMAIL_PROPERTY = "email";
+ public static final String USER_AUTHORITY_PROPERTY = "authority";
+ public static final String USER_FIRST_NAME_PROPERTY = "first_name";
+ public static final String USER_LAST_NAME_PROPERTY = "last_name";
+ public static final String USER_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY;
+
+ public static final String USER_BY_EMAIL_COLUMN_FAMILY_NAME = "user_by_email";
+ public static final String USER_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "user_by_tenant_and_search_text";
+ public static final String USER_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "user_by_customer_and_search_text";
+
+ /**
+ * Cassandra user_credentials constants.
+ */
+ public static final String USER_CREDENTIALS_COLUMN_FAMILY_NAME = "user_credentials";
+ public static final String USER_CREDENTIALS_USER_ID_PROPERTY = USER_ID_PROPERTY;
+ public static final String USER_CREDENTIALS_ENABLED_PROPERTY = "enabled";
+ public static final String USER_CREDENTIALS_PASSWORD_PROPERTY = "password";
+ public static final String USER_CREDENTIALS_ACTIVATE_TOKEN_PROPERTY = "activate_token";
+ public static final String USER_CREDENTIALS_RESET_TOKEN_PROPERTY = "reset_token";
+
+ public static final String USER_CREDENTIALS_BY_USER_COLUMN_FAMILY_NAME = "user_credentials_by_user";
+ public static final String USER_CREDENTIALS_BY_ACTIVATE_TOKEN_COLUMN_FAMILY_NAME = "user_credentials_by_activate_token";
+ public static final String USER_CREDENTIALS_BY_RESET_TOKEN_COLUMN_FAMILY_NAME = "user_credentials_by_reset_token";
+
+ /**
+ * Cassandra admin_settings constants.
+ */
+ public static final String ADMIN_SETTINGS_COLUMN_FAMILY_NAME = "admin_settings";
+ public static final String ADMIN_SETTINGS_KEY_PROPERTY = "key";
+ public static final String ADMIN_SETTINGS_JSON_VALUE_PROPERTY = "json_value";
+
+ public static final String ADMIN_SETTINGS_BY_KEY_COLUMN_FAMILY_NAME = "admin_settings_by_key";
+
+ /**
+ * Cassandra contact constants.
+ */
+ public static final String COUNTRY_PROPERTY = "country";
+ public static final String STATE_PROPERTY = "state";
+ public static final String CITY_PROPERTY = "city";
+ public static final String ADDRESS_PROPERTY = "address";
+ public static final String ADDRESS2_PROPERTY = "address2";
+ public static final String ZIP_PROPERTY = "zip";
+ public static final String PHONE_PROPERTY = "phone";
+ public static final String EMAIL_PROPERTY = "email";
+
+ /**
+ * Cassandra tenant constants.
+ */
+ public static final String TENANT_COLUMN_FAMILY_NAME = "tenant";
+ public static final String TENANT_TITLE_PROPERTY = TITLE_PROPERTY;
+ public static final String TENANT_REGION_PROPERTY = "region";
+ public static final String TENANT_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY;
+
+ public static final String TENANT_BY_REGION_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "tenant_by_region_and_search_text";
+
+ /**
+ * Cassandra customer constants.
+ */
+ public static final String CUSTOMER_COLUMN_FAMILY_NAME = "customer";
+ public static final String CUSTOMER_TENANT_ID_PROPERTY = TENTANT_ID_PROPERTY;
+ public static final String CUSTOMER_TITLE_PROPERTY = TITLE_PROPERTY;
+ public static final String CUSTOMER_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY;
+
+ public static final String CUSTOMER_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "customer_by_tenant_and_search_text";
+
+ /**
+ * Cassandra device constants.
+ */
+ public static final String DEVICE_COLUMN_FAMILY_NAME = "device";
+ public static final String DEVICE_TENANT_ID_PROPERTY = TENTANT_ID_PROPERTY;
+ public static final String DEVICE_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY;
+ public static final String DEVICE_NAME_PROPERTY = "name";
+ public static final String DEVICE_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY;
+
+ public static final String DEVICE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_and_search_text";
+ public static final String DEVICE_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_customer_and_search_text";
+ public static final String DEVICE_BY_TENANT_AND_NAME_VIEW_NAME = "device_by_tenant_and_name";
+
+
+ /**
+ * Cassandra device_credentials constants.
+ */
+ public static final String DEVICE_CREDENTIALS_COLUMN_FAMILY_NAME = "device_credentials";
+ public static final String DEVICE_CREDENTIALS_DEVICE_ID_PROPERTY = DEVICE_ID_PROPERTY;
+ public static final String DEVICE_CREDENTIALS_CREDENTIALS_TYPE_PROPERTY = "credentials_type";
+ public static final String DEVICE_CREDENTIALS_CREDENTIALS_ID_PROPERTY = "credentials_id";
+ public static final String DEVICE_CREDENTIALS_CREDENTIALS_VALUE_PROPERTY = "credentials_value";
+
+ public static final String DEVICE_CREDENTIALS_BY_DEVICE_COLUMN_FAMILY_NAME = "device_credentials_by_device";
+ public static final String DEVICE_CREDENTIALS_BY_CREDENTIALS_ID_COLUMN_FAMILY_NAME = "device_credentials_by_credentials_id";
+
+ /**
+ * Cassandra widgets_bundle constants.
+ */
+ public static final String WIDGETS_BUNDLE_COLUMN_FAMILY_NAME = "widgets_bundle";
+ public static final String WIDGETS_BUNDLE_TENANT_ID_PROPERTY = TENTANT_ID_PROPERTY;
+ public static final String WIDGETS_BUNDLE_ALIAS_PROPERTY = ALIAS_PROPERTY;
+ public static final String WIDGETS_BUNDLE_TITLE_PROPERTY = TITLE_PROPERTY;
+ public static final String WIDGETS_BUNDLE_IMAGE_PROPERTY = "image";
+
+ public static final String WIDGETS_BUNDLE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "widgets_bundle_by_tenant_and_search_text";
+ public static final String WIDGETS_BUNDLE_BY_TENANT_AND_ALIAS_COLUMN_FAMILY_NAME = "widgets_bundle_by_tenant_and_alias";
+
+ /**
+ * Cassandra widget_type constants.
+ */
+ public static final String WIDGET_TYPE_COLUMN_FAMILY_NAME = "widget_type";
+ public static final String WIDGET_TYPE_TENANT_ID_PROPERTY = TENTANT_ID_PROPERTY;
+ public static final String WIDGET_TYPE_BUNDLE_ALIAS_PROPERTY = "bundle_alias";
+ public static final String WIDGET_TYPE_ALIAS_PROPERTY = ALIAS_PROPERTY;
+ public static final String WIDGET_TYPE_NAME_PROPERTY = "name";
+ public static final String WIDGET_TYPE_DESCRIPTOR_PROPERTY = "descriptor";
+
+ public static final String WIDGET_TYPE_BY_TENANT_AND_ALIASES_COLUMN_FAMILY_NAME = "widget_type_by_tenant_and_aliases";
+
+ /**
+ * Cassandra dashboard constants.
+ */
+ public static final String DASHBOARD_COLUMN_FAMILY_NAME = "dashboard";
+ public static final String DASHBOARD_TENANT_ID_PROPERTY = TENTANT_ID_PROPERTY;
+ public static final String DASHBOARD_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY;
+ public static final String DASHBOARD_TITLE_PROPERTY = TITLE_PROPERTY;
+ public static final String DASHBOARD_CONFIGURATION_PROPERTY = "configuration";
+
+ public static final String DASHBOARD_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "dashboard_by_tenant_and_search_text";
+ public static final String DASHBOARD_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "dashboard_by_customer_and_search_text";
+
+
+ /**
+ * Cassandra plugin metadata constants.
+ */
+ public static final String PLUGIN_COLUMN_FAMILY_NAME = "plugin";
+ public static final String PLUGIN_TENANT_ID_PROPERTY = TENTANT_ID_PROPERTY;
+ public static final String PLUGIN_NAME_PROPERTY = "name";
+ public static final String PLUGIN_API_TOKEN_PROPERTY = "api_token";
+ public static final String PLUGIN_CLASS_PROPERTY = "plugin_class";
+ public static final String PLUGIN_ACCESS_PROPERTY = "public_access";
+ public static final String PLUGIN_STATE_PROPERTY = "state";
+ public static final String PLUGIN_CONFIGURATION_PROPERTY = "configuration";
+
+ public static final String PLUGIN_BY_API_TOKEN_COLUMN_FAMILY_NAME = "plugin_by_api_token";
+ public static final String PLUGIN_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "plugin_by_tenant_and_search_text";
+
+ /**
+ * Cassandra plugin component metadata constants.
+ */
+ public static final String COMPONENT_DESCRIPTOR_COLUMN_FAMILY_NAME = "component_descriptor";
+ public static final String COMPONENT_DESCRIPTOR_TYPE_PROPERTY = "type";
+ public static final String COMPONENT_DESCRIPTOR_SCOPE_PROPERTY = "scope";
+ public static final String COMPONENT_DESCRIPTOR_NAME_PROPERTY = "name";
+ public static final String COMPONENT_DESCRIPTOR_CLASS_PROPERTY = "clazz";
+ public static final String COMPONENT_DESCRIPTOR_CONFIGURATION_DESCRIPTOR_PROPERTY = "configuration_descriptor";
+ public static final String COMPONENT_DESCRIPTOR_ACTIONS_PROPERTY = "actions";
+
+ public static final String COMPONENT_DESCRIPTOR_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "component_desc_by_type_search_text";
+ public static final String COMPONENT_DESCRIPTOR_BY_SCOPE_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "component_desc_by_scope_type_search_text";
+ public static final String COMPONENT_DESCRIPTOR_BY_ID = "component_desc_by_id";
+
+ /**
+ * Cassandra rule metadata constants.
+ */
+ public static final String RULE_COLUMN_FAMILY_NAME = "rule";
+ public static final String RULE_TENANT_ID_PROPERTY = TENTANT_ID_PROPERTY;
+ public static final String RULE_NAME_PROPERTY = "name";
+ public static final String RULE_STATE_PROPERTY = "state";
+ public static final String RULE_WEIGHT_PROPERTY = "weight";
+ public static final String RULE_PLUGIN_TOKEN_PROPERTY = "plugin_token";
+ public static final String RULE_FILTERS = "filters";
+ public static final String RULE_PROCESSOR = "processor";
+ public static final String RULE_ACTION = "action";
+
+ public static final String RULE_BY_PLUGIN_TOKEN = "rule_by_plugin_token";
+ public static final String RULE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "rule_by_tenant_and_search_text";
+
+ /**
+ * Cassandra event constants.
+ */
+ public static final String EVENT_COLUMN_FAMILY_NAME = "event";
+ public static final String EVENT_TENANT_ID_PROPERTY = TENTANT_ID_PROPERTY;
+ public static final String EVENT_TYPE_PROPERTY = "event_type";
+ public static final String EVENT_UID_PROPERTY = "event_uid";
+ public static final String EVENT_ENTITY_TYPE_PROPERTY = "entity_type";
+ public static final String EVENT_ENTITY_ID_PROPERTY = "entity_id";
+ public static final String EVENT_BODY_PROPERTY = "body";
+
+ public static final String EVENT_BY_TYPE_AND_ID_VIEW_NAME = "event_by_type_and_id";
+ public static final String EVENT_BY_ID_VIEW_NAME = "event_by_id";
+
+ /**
+ * Cassandra attributes and timeseries constants.
+ */
+ public static final String ATTRIBUTES_KV_CF = "attributes_kv_cf";
+ public static final String TS_KV_CF = "ts_kv_cf";
+ public static final String TS_KV_PARTITIONS_CF = "ts_kv_partitions_cf";
+ public static final String TS_KV_LATEST_CF = "ts_kv_latest_cf";
+
+
+ public static final String ENTITY_TYPE_COLUMN = "entity_type";
+ public static final String ENTITY_ID_COLUMN = "entity_id";
+ public static final String ATTRIBUTE_TYPE_COLUMN = "attribute_type";
+ public static final String ATTRIBUTE_KEY_COLUMN = "attribute_key";
+ public static final String LAST_UPDATE_TS_COLUMN = "last_update_ts";
+
+ public static final String PARTITION_COLUMN = "partition";
+ public static final String KEY_COLUMN = "key";
+ public static final String TS_COLUMN = "ts";
+
+ /**
+ * Main names of cassandra key-value columns storage.
+ */
+ public static final String BOOLEAN_VALUE_COLUMN = "bool_v";
+ public static final String STRING_VALUE_COLUMN = "str_v";
+ public static final String LONG_VALUE_COLUMN = "long_v";
+ public static final String DOUBLE_VALUE_COLUMN = "dbl_v";
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/PluginMetaDataEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/PluginMetaDataEntity.java
new file mode 100644
index 0000000..c36bc28
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/PluginMetaDataEntity.java
@@ -0,0 +1,218 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.datastax.driver.mapping.annotations.ClusteringColumn;
+import com.datastax.driver.mapping.annotations.Column;
+import com.datastax.driver.mapping.annotations.PartitionKey;
+import com.datastax.driver.mapping.annotations.Table;
+import com.fasterxml.jackson.databind.JsonNode;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
+import org.thingsboard.server.common.data.plugin.PluginMetaData;
+import org.thingsboard.server.dao.model.type.ComponentLifecycleStateCodec;
+import org.thingsboard.server.dao.model.type.JsonCodec;
+
+import java.util.Objects;
+import java.util.UUID;
+
+import static org.thingsboard.server.dao.model.ModelConstants.ADDITIONAL_INFO_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.RULE_ACTION;
+
+@Table(name = ModelConstants.PLUGIN_COLUMN_FAMILY_NAME)
+public class PluginMetaDataEntity implements SearchTextEntity<PluginMetaData> {
+
+ private static final long serialVersionUID = 1L;
+
+ @PartitionKey
+ @Column(name = ModelConstants.ID_PROPERTY)
+ private UUID id;
+
+ @Column(name = ModelConstants.PLUGIN_API_TOKEN_PROPERTY)
+ private String apiToken;
+
+ @ClusteringColumn
+ @Column(name = ModelConstants.PLUGIN_TENANT_ID_PROPERTY)
+ private UUID tenantId;
+
+ @Column(name = ModelConstants.PLUGIN_NAME_PROPERTY)
+ private String name;
+
+ @Column(name = ModelConstants.PLUGIN_CLASS_PROPERTY)
+ private String clazz;
+
+ @Column(name = ModelConstants.PLUGIN_ACCESS_PROPERTY)
+ private boolean publicAccess;
+
+ @Column(name = ModelConstants.PLUGIN_STATE_PROPERTY, codec = ComponentLifecycleStateCodec.class)
+ private ComponentLifecycleState state;
+
+ @Column(name = ModelConstants.PLUGIN_CONFIGURATION_PROPERTY, codec = JsonCodec.class)
+ private JsonNode configuration;
+
+ @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY)
+ private String searchText;
+
+ @Column(name = ADDITIONAL_INFO_PROPERTY, codec = JsonCodec.class)
+ private JsonNode additionalInfo;
+
+ public PluginMetaDataEntity() {
+ }
+
+ public PluginMetaDataEntity(PluginMetaData pluginMetaData) {
+ if (pluginMetaData.getId() != null) {
+ this.id = pluginMetaData.getId().getId();
+ }
+ this.tenantId = pluginMetaData.getTenantId().getId();
+ this.apiToken = pluginMetaData.getApiToken();
+ this.clazz = pluginMetaData.getClazz();
+ this.name = pluginMetaData.getName();
+ this.publicAccess = pluginMetaData.isPublicAccess();
+ this.state = pluginMetaData.getState();
+ this.configuration = pluginMetaData.getConfiguration();
+ this.searchText = pluginMetaData.getName();
+ this.additionalInfo = pluginMetaData.getAdditionalInfo();
+ }
+
+ @Override
+ public String getSearchTextSource() {
+ return searchText;
+ }
+
+ @Override
+ public void setSearchText(String searchText) {
+ this.searchText = searchText;
+ }
+
+ @Override
+ public UUID getId() {
+ return id;
+ }
+
+ @Override
+ public void setId(UUID id) {
+ this.id = id;
+ }
+
+ public String getApiToken() {
+ return apiToken;
+ }
+
+ public void setApiToken(String apiToken) {
+ this.apiToken = apiToken;
+ }
+
+ public UUID getTenantId() {
+ return tenantId;
+ }
+
+ public void setTenantId(UUID tenantId) {
+ this.tenantId = tenantId;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getClazz() {
+ return clazz;
+ }
+
+ public void setClazz(String clazz) {
+ this.clazz = clazz;
+ }
+
+ public JsonNode getConfiguration() {
+ return configuration;
+ }
+
+ public void setConfiguration(JsonNode configuration) {
+ this.configuration = configuration;
+ }
+
+ public boolean isPublicAccess() {
+ return publicAccess;
+ }
+
+ public void setPublicAccess(boolean publicAccess) {
+ this.publicAccess = publicAccess;
+ }
+
+ public ComponentLifecycleState getState() {
+ return state;
+ }
+
+ public void setState(ComponentLifecycleState state) {
+ this.state = state;
+ }
+
+ public String getSearchText() {
+ return searchText;
+ }
+
+ public JsonNode getAdditionalInfo() {
+ return additionalInfo;
+ }
+
+ public void setAdditionalInfo(JsonNode additionalInfo) {
+ this.additionalInfo = additionalInfo;
+ }
+
+ @Override
+ public PluginMetaData toData() {
+ PluginMetaData data = new PluginMetaData(new PluginId(id));
+ data.setTenantId(new TenantId(tenantId));
+ data.setCreatedTime(UUIDs.unixTimestamp(id));
+ data.setName(name);
+ data.setConfiguration(configuration);
+ data.setClazz(clazz);
+ data.setPublicAccess(publicAccess);
+ data.setState(state);
+ data.setApiToken(apiToken);
+ data.setAdditionalInfo(additionalInfo);
+ return data;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+ PluginMetaDataEntity entity = (PluginMetaDataEntity) o;
+ return Objects.equals(id, entity.id) && Objects.equals(apiToken, entity.apiToken) && Objects.equals(tenantId, entity.tenantId)
+ && Objects.equals(name, entity.name) && Objects.equals(clazz, entity.clazz) && Objects.equals(state, entity.state)
+ && Objects.equals(configuration, entity.configuration)
+ && Objects.equals(searchText, entity.searchText);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, apiToken, tenantId, name, clazz, state, configuration, searchText);
+ }
+
+ @Override
+ public String toString() {
+ return "PluginMetaDataEntity{" + "id=" + id + ", apiToken='" + apiToken + '\'' + ", tenantId=" + tenantId + ", name='" + name + '\'' + ", clazz='"
+ + clazz + '\'' + ", state=" + state + ", configuration=" + configuration + ", searchText='" + searchText + '\'' + '}';
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/RuleMetaDataEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/RuleMetaDataEntity.java
new file mode 100644
index 0000000..dd2c55e
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/RuleMetaDataEntity.java
@@ -0,0 +1,227 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.datastax.driver.mapping.annotations.ClusteringColumn;
+import com.datastax.driver.mapping.annotations.Column;
+import com.datastax.driver.mapping.annotations.PartitionKey;
+import com.datastax.driver.mapping.annotations.Table;
+import com.fasterxml.jackson.databind.JsonNode;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
+import org.thingsboard.server.common.data.rule.RuleMetaData;
+import org.thingsboard.server.dao.DaoUtil;
+import org.thingsboard.server.dao.model.type.ComponentLifecycleStateCodec;
+import org.thingsboard.server.dao.model.type.JsonCodec;
+
+import java.util.Objects;
+import java.util.UUID;
+
+import static org.thingsboard.server.dao.model.ModelConstants.*;
+
+@Table(name = RULE_COLUMN_FAMILY_NAME)
+public class RuleMetaDataEntity implements SearchTextEntity<RuleMetaData> {
+
+ @PartitionKey
+ @Column(name = ID_PROPERTY)
+ private UUID id;
+ @ClusteringColumn
+ @Column(name = RULE_TENANT_ID_PROPERTY)
+ private UUID tenantId;
+ @Column(name = RULE_NAME_PROPERTY)
+ private String name;
+ @Column(name = ModelConstants.RULE_STATE_PROPERTY, codec = ComponentLifecycleStateCodec.class)
+ private ComponentLifecycleState state;
+ @Column(name = RULE_WEIGHT_PROPERTY)
+ private int weight;
+ @Column(name = SEARCH_TEXT_PROPERTY)
+ private String searchText;
+ @Column(name = RULE_PLUGIN_TOKEN_PROPERTY)
+ private String pluginToken;
+ @Column(name = RULE_FILTERS, codec = JsonCodec.class)
+ private JsonNode filters;
+ @Column(name = RULE_PROCESSOR, codec = JsonCodec.class)
+ private JsonNode processor;
+ @Column(name = RULE_ACTION, codec = JsonCodec.class)
+ private JsonNode action;
+ @Column(name = ADDITIONAL_INFO_PROPERTY, codec = JsonCodec.class)
+ private JsonNode additionalInfo;
+
+ public RuleMetaDataEntity() {
+ }
+
+ public RuleMetaDataEntity(RuleMetaData rule) {
+ if (rule.getId() != null) {
+ this.id = rule.getUuidId();
+ }
+ this.tenantId = DaoUtil.getId(rule.getTenantId());
+ this.name = rule.getName();
+ this.pluginToken = rule.getPluginToken();
+ this.state = rule.getState();
+ this.weight = rule.getWeight();
+ this.searchText = rule.getName();
+ this.filters = rule.getFilters();
+ this.processor = rule.getProcessor();
+ this.action = rule.getAction();
+ this.additionalInfo = rule.getAdditionalInfo();
+ }
+
+ @Override
+ public String getSearchTextSource() {
+ return searchText;
+ }
+
+ @Override
+ public void setSearchText(String searchText) {
+ this.searchText = searchText;
+ }
+
+ @Override
+ public UUID getId() {
+ return id;
+ }
+
+ @Override
+ public void setId(UUID id) {
+ this.id = id;
+ }
+
+ public UUID getTenantId() {
+ return tenantId;
+ }
+
+ public void setTenantId(UUID tenantId) {
+ this.tenantId = tenantId;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public ComponentLifecycleState getState() {
+ return state;
+ }
+
+ public void setState(ComponentLifecycleState state) {
+ this.state = state;
+ }
+
+ public int getWeight() {
+ return weight;
+ }
+
+ public void setWeight(int weight) {
+ this.weight = weight;
+ }
+
+ public String getPluginToken() {
+ return pluginToken;
+ }
+
+ public void setPluginToken(String pluginToken) {
+ this.pluginToken = pluginToken;
+ }
+
+ public String getSearchText() {
+ return searchText;
+ }
+
+ public JsonNode getFilters() {
+ return filters;
+ }
+
+ public void setFilters(JsonNode filters) {
+ this.filters = filters;
+ }
+
+ public JsonNode getProcessor() {
+ return processor;
+ }
+
+ public void setProcessor(JsonNode processor) {
+ this.processor = processor;
+ }
+
+ public JsonNode getAction() {
+ return action;
+ }
+
+ public void setAction(JsonNode action) {
+ this.action = action;
+ }
+
+ public JsonNode getAdditionalInfo() {
+ return additionalInfo;
+ }
+
+ public void setAdditionalInfo(JsonNode additionalInfo) {
+ this.additionalInfo = additionalInfo;
+ }
+
+ @Override
+ public RuleMetaData toData() {
+ RuleMetaData rule = new RuleMetaData(new RuleId(id));
+ rule.setTenantId(new TenantId(tenantId));
+ rule.setName(name);
+ rule.setState(state);
+ rule.setWeight(weight);
+ rule.setCreatedTime(UUIDs.unixTimestamp(id));
+ rule.setPluginToken(pluginToken);
+ rule.setFilters(filters);
+ rule.setProcessor(processor);
+ rule.setAction(action);
+ rule.setAdditionalInfo(additionalInfo);
+ return rule;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ RuleMetaDataEntity that = (RuleMetaDataEntity) o;
+ return weight == that.weight &&
+ Objects.equals(id, that.id) &&
+ Objects.equals(tenantId, that.tenantId) &&
+ Objects.equals(name, that.name) &&
+ Objects.equals(pluginToken, that.pluginToken) &&
+ Objects.equals(state, that.state) &&
+ Objects.equals(searchText, that.searchText);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, tenantId, name, pluginToken, state, weight, searchText);
+ }
+
+ @Override
+ public String toString() {
+ return "RuleMetaDataEntity{" +
+ "id=" + id +
+ ", tenantId=" + tenantId +
+ ", name='" + name + '\'' +
+ ", pluginToken='" + pluginToken + '\'' +
+ ", state='" + state + '\'' +
+ ", weight=" + weight +
+ ", searchText='" + searchText + '\'' +
+ '}';
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/SearchTextEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/SearchTextEntity.java
new file mode 100644
index 0000000..9233dd3
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/SearchTextEntity.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model;
+
+public interface SearchTextEntity<D> extends BaseEntity<D> {
+
+ String getSearchTextSource();
+
+ void setSearchText(String searchText);
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/TenantEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/TenantEntity.java
new file mode 100644
index 0000000..7069cca
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/TenantEntity.java
@@ -0,0 +1,364 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model;
+
+import static org.thingsboard.server.dao.model.ModelConstants.ADDRESS2_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ADDRESS_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.CITY_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.COUNTRY_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.EMAIL_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.PHONE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.STATE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.TENANT_ADDITIONAL_INFO_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.TENANT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.TENANT_REGION_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.TENANT_TITLE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ZIP_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY;
+
+import java.util.UUID;
+
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.dao.model.type.JsonCodec;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.datastax.driver.mapping.annotations.Column;
+import com.datastax.driver.mapping.annotations.PartitionKey;
+import com.datastax.driver.mapping.annotations.Table;
+import com.datastax.driver.mapping.annotations.Transient;
+import com.fasterxml.jackson.databind.JsonNode;
+
+@Table(name = TENANT_COLUMN_FAMILY_NAME)
+public final class TenantEntity implements SearchTextEntity<Tenant> {
+
+ @Transient
+ private static final long serialVersionUID = -6198635547142409206L;
+
+ @PartitionKey(value = 0)
+ @Column(name = ID_PROPERTY)
+ private UUID id;
+
+ @Column(name = TENANT_TITLE_PROPERTY)
+ private String title;
+
+ @Column(name = SEARCH_TEXT_PROPERTY)
+ private String searchText;
+
+ @Column(name = TENANT_REGION_PROPERTY)
+ private String region;
+
+ @Column(name = COUNTRY_PROPERTY)
+ private String country;
+
+ @Column(name = STATE_PROPERTY)
+ private String state;
+
+ @Column(name = CITY_PROPERTY)
+ private String city;
+
+ @Column(name = ADDRESS_PROPERTY)
+ private String address;
+
+ @Column(name = ADDRESS2_PROPERTY)
+ private String address2;
+
+ @Column(name = ZIP_PROPERTY)
+ private String zip;
+
+ @Column(name = PHONE_PROPERTY)
+ private String phone;
+
+ @Column(name = EMAIL_PROPERTY)
+ private String email;
+
+ @Column(name = TENANT_ADDITIONAL_INFO_PROPERTY, codec = JsonCodec.class)
+ private JsonNode additionalInfo;
+
+ public TenantEntity() {
+ super();
+ }
+
+ public TenantEntity(Tenant tenant) {
+ if (tenant.getId() != null) {
+ this.id = tenant.getId().getId();
+ }
+ this.title = tenant.getTitle();
+ this.region = tenant.getRegion();
+ this.country = tenant.getCountry();
+ this.state = tenant.getState();
+ this.city = tenant.getCity();
+ this.address = tenant.getAddress();
+ this.address2 = tenant.getAddress2();
+ this.zip = tenant.getZip();
+ this.phone = tenant.getPhone();
+ this.email = tenant.getEmail();
+ this.additionalInfo = tenant.getAdditionalInfo();
+ }
+
+ public UUID getId() {
+ return id;
+ }
+
+ public void setId(UUID id) {
+ this.id = id;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getRegion() {
+ return region;
+ }
+
+ public void setRegion(String region) {
+ this.region = region;
+ }
+
+ public String getCountry() {
+ return country;
+ }
+
+ public void setCountry(String country) {
+ this.country = country;
+ }
+
+ public String getState() {
+ return state;
+ }
+
+ public void setState(String state) {
+ this.state = state;
+ }
+
+ public String getCity() {
+ return city;
+ }
+
+ public void setCity(String city) {
+ this.city = city;
+ }
+
+ public String getAddress() {
+ return address;
+ }
+
+ public void setAddress(String address) {
+ this.address = address;
+ }
+
+ public String getAddress2() {
+ return address2;
+ }
+
+ public void setAddress2(String address2) {
+ this.address2 = address2;
+ }
+
+ public String getZip() {
+ return zip;
+ }
+
+ public void setZip(String zip) {
+ this.zip = zip;
+ }
+
+ public String getPhone() {
+ return phone;
+ }
+
+ public void setPhone(String phone) {
+ this.phone = phone;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public JsonNode getAdditionalInfo() {
+ return additionalInfo;
+ }
+
+ public void setAdditionalInfo(JsonNode additionalInfo) {
+ this.additionalInfo = additionalInfo;
+ }
+
+ @Override
+ public String getSearchTextSource() {
+ return title;
+ }
+
+ @Override
+ public void setSearchText(String searchText) {
+ this.searchText = searchText;
+ }
+
+ public String getSearchText() {
+ return searchText;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((additionalInfo == null) ? 0 : additionalInfo.hashCode());
+ result = prime * result + ((address == null) ? 0 : address.hashCode());
+ result = prime * result + ((address2 == null) ? 0 : address2.hashCode());
+ result = prime * result + ((city == null) ? 0 : city.hashCode());
+ result = prime * result + ((country == null) ? 0 : country.hashCode());
+ result = prime * result + ((email == null) ? 0 : email.hashCode());
+ result = prime * result + ((id == null) ? 0 : id.hashCode());
+ result = prime * result + ((phone == null) ? 0 : phone.hashCode());
+ result = prime * result + ((region == null) ? 0 : region.hashCode());
+ result = prime * result + ((state == null) ? 0 : state.hashCode());
+ result = prime * result + ((title == null) ? 0 : title.hashCode());
+ result = prime * result + ((zip == null) ? 0 : zip.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ TenantEntity other = (TenantEntity) obj;
+ if (additionalInfo == null) {
+ if (other.additionalInfo != null)
+ return false;
+ } else if (!additionalInfo.equals(other.additionalInfo))
+ return false;
+ if (address == null) {
+ if (other.address != null)
+ return false;
+ } else if (!address.equals(other.address))
+ return false;
+ if (address2 == null) {
+ if (other.address2 != null)
+ return false;
+ } else if (!address2.equals(other.address2))
+ return false;
+ if (city == null) {
+ if (other.city != null)
+ return false;
+ } else if (!city.equals(other.city))
+ return false;
+ if (country == null) {
+ if (other.country != null)
+ return false;
+ } else if (!country.equals(other.country))
+ return false;
+ if (email == null) {
+ if (other.email != null)
+ return false;
+ } else if (!email.equals(other.email))
+ return false;
+ if (id == null) {
+ if (other.id != null)
+ return false;
+ } else if (!id.equals(other.id))
+ return false;
+ if (phone == null) {
+ if (other.phone != null)
+ return false;
+ } else if (!phone.equals(other.phone))
+ return false;
+ if (region == null) {
+ if (other.region != null)
+ return false;
+ } else if (!region.equals(other.region))
+ return false;
+ if (state == null) {
+ if (other.state != null)
+ return false;
+ } else if (!state.equals(other.state))
+ return false;
+ if (title == null) {
+ if (other.title != null)
+ return false;
+ } else if (!title.equals(other.title))
+ return false;
+ if (zip == null) {
+ if (other.zip != null)
+ return false;
+ } else if (!zip.equals(other.zip))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("TenantEntity [id=");
+ builder.append(id);
+ builder.append(", title=");
+ builder.append(title);
+ builder.append(", region=");
+ builder.append(region);
+ builder.append(", country=");
+ builder.append(country);
+ builder.append(", state=");
+ builder.append(state);
+ builder.append(", city=");
+ builder.append(city);
+ builder.append(", address=");
+ builder.append(address);
+ builder.append(", address2=");
+ builder.append(address2);
+ builder.append(", zip=");
+ builder.append(zip);
+ builder.append(", phone=");
+ builder.append(phone);
+ builder.append(", email=");
+ builder.append(email);
+ builder.append(", additionalInfo=");
+ builder.append(additionalInfo);
+ builder.append("]");
+ return builder.toString();
+ }
+
+ @Override
+ public Tenant toData() {
+ Tenant tenant = new Tenant(new TenantId(id));
+ tenant.setCreatedTime(UUIDs.unixTimestamp(id));
+ tenant.setTitle(title);
+ tenant.setRegion(region);
+ tenant.setCountry(country);
+ tenant.setState(state);
+ tenant.setCity(city);
+ tenant.setAddress(address);
+ tenant.setAddress2(address2);
+ tenant.setZip(zip);
+ tenant.setPhone(phone);
+ tenant.setEmail(email);
+ tenant.setAdditionalInfo(additionalInfo);
+ return tenant;
+ }
+
+
+}
\ No newline at end of file
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ToData.java b/dao/src/main/java/org/thingsboard/server/dao/model/ToData.java
new file mode 100644
index 0000000..53dd9c9
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/ToData.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model;
+
+/**
+ * The interface To dto.
+ *
+ * @param <T> the type parameter
+ */
+public interface ToData<T> {
+
+ /**
+ * This method convert domain model object to data transfer object.
+ *
+ * @return the dto object
+ */
+ T toData();
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/type/AuthorityCodec.java b/dao/src/main/java/org/thingsboard/server/dao/model/type/AuthorityCodec.java
new file mode 100644
index 0000000..35f708d
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/type/AuthorityCodec.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model.type;
+
+import org.thingsboard.server.common.data.security.Authority;
+
+import com.datastax.driver.extras.codecs.enums.EnumNameCodec;
+
+public class AuthorityCodec extends EnumNameCodec<Authority> {
+
+ public AuthorityCodec() {
+ super(Authority.class);
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/type/ComponentLifecycleStateCodec.java b/dao/src/main/java/org/thingsboard/server/dao/model/type/ComponentLifecycleStateCodec.java
new file mode 100644
index 0000000..fb6d309
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/type/ComponentLifecycleStateCodec.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model.type;
+
+import com.datastax.driver.extras.codecs.enums.EnumNameCodec;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
+import org.thingsboard.server.common.data.security.Authority;
+
+public class ComponentLifecycleStateCodec extends EnumNameCodec<ComponentLifecycleState> {
+
+ public ComponentLifecycleStateCodec() {
+ super(ComponentLifecycleState.class);
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/type/ComponentScopeCodec.java b/dao/src/main/java/org/thingsboard/server/dao/model/type/ComponentScopeCodec.java
new file mode 100644
index 0000000..7a3d4db
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/type/ComponentScopeCodec.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model.type;
+
+import com.datastax.driver.extras.codecs.enums.EnumNameCodec;
+import org.thingsboard.server.common.data.plugin.ComponentScope;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+
+public class ComponentScopeCodec extends EnumNameCodec<ComponentScope> {
+
+ public ComponentScopeCodec() {
+ super(ComponentScope.class);
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/type/ComponentTypeCodec.java b/dao/src/main/java/org/thingsboard/server/dao/model/type/ComponentTypeCodec.java
new file mode 100644
index 0000000..9953e8b
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/type/ComponentTypeCodec.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model.type;
+
+import com.datastax.driver.extras.codecs.enums.EnumNameCodec;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+
+public class ComponentTypeCodec extends EnumNameCodec<ComponentType> {
+
+ public ComponentTypeCodec() {
+ super(ComponentType.class);
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/type/DeviceCredentialsTypeCodec.java b/dao/src/main/java/org/thingsboard/server/dao/model/type/DeviceCredentialsTypeCodec.java
new file mode 100644
index 0000000..25a8a27
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/type/DeviceCredentialsTypeCodec.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model.type;
+
+import org.thingsboard.server.common.data.security.DeviceCredentialsType;
+
+import com.datastax.driver.extras.codecs.enums.EnumNameCodec;
+
+public class DeviceCredentialsTypeCodec extends EnumNameCodec<DeviceCredentialsType> {
+
+ public DeviceCredentialsTypeCodec() {
+ super(DeviceCredentialsType.class);
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/type/EntityTypeCodec.java b/dao/src/main/java/org/thingsboard/server/dao/model/type/EntityTypeCodec.java
new file mode 100644
index 0000000..5f19d72
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/type/EntityTypeCodec.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model.type;
+
+import com.datastax.driver.extras.codecs.enums.EnumNameCodec;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+
+public class EntityTypeCodec extends EnumNameCodec<EntityType> {
+
+ public EntityTypeCodec() {
+ super(EntityType.class);
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/type/JsonCodec.java b/dao/src/main/java/org/thingsboard/server/dao/model/type/JsonCodec.java
new file mode 100644
index 0000000..5dc9d10
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/type/JsonCodec.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model.type;
+
+import com.datastax.driver.extras.codecs.json.JacksonJsonCodec;
+import com.fasterxml.jackson.databind.JsonNode;
+
+public class JsonCodec extends JacksonJsonCodec<JsonNode> {
+
+ public JsonCodec() {
+ super(JsonNode.class);
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/UserCredentialsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/UserCredentialsEntity.java
new file mode 100644
index 0000000..ebf914d
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/UserCredentialsEntity.java
@@ -0,0 +1,194 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model;
+
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_CREDENTIALS_ACTIVATE_TOKEN_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_CREDENTIALS_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_CREDENTIALS_PASSWORD_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_CREDENTIALS_USER_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_CREDENTIALS_ENABLED_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_CREDENTIALS_RESET_TOKEN_PROPERTY;
+
+import java.util.UUID;
+
+import org.thingsboard.server.common.data.id.UserCredentialsId;
+import org.thingsboard.server.common.data.id.UserId;
+import org.thingsboard.server.common.data.security.UserCredentials;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.datastax.driver.mapping.annotations.Column;
+import com.datastax.driver.mapping.annotations.PartitionKey;
+import com.datastax.driver.mapping.annotations.Table;
+import com.datastax.driver.mapping.annotations.Transient;
+
+@Table(name = USER_CREDENTIALS_COLUMN_FAMILY_NAME)
+public final class UserCredentialsEntity implements BaseEntity<UserCredentials> {
+
+ @Transient
+ private static final long serialVersionUID = 1348221414123438374L;
+
+ @PartitionKey(value = 0)
+ @Column(name = ID_PROPERTY)
+ private UUID id;
+
+ @Column(name = USER_CREDENTIALS_USER_ID_PROPERTY)
+ private UUID userId;
+
+ @Column(name = USER_CREDENTIALS_ENABLED_PROPERTY)
+ private boolean enabled;
+
+ @Column(name = USER_CREDENTIALS_PASSWORD_PROPERTY)
+ private String password;
+
+ @Column(name = USER_CREDENTIALS_ACTIVATE_TOKEN_PROPERTY)
+ private String activateToken;
+
+ @Column(name = USER_CREDENTIALS_RESET_TOKEN_PROPERTY)
+ private String resetToken;
+
+ public UserCredentialsEntity() {
+ super();
+ }
+
+ public UserCredentialsEntity(UserCredentials userCredentials) {
+ if (userCredentials.getId() != null) {
+ this.id = userCredentials.getId().getId();
+ }
+ if (userCredentials.getUserId() != null) {
+ this.userId = userCredentials.getUserId().getId();
+ }
+ this.enabled = userCredentials.isEnabled();
+ this.password = userCredentials.getPassword();
+ this.activateToken = userCredentials.getActivateToken();
+ this.resetToken = userCredentials.getResetToken();
+ }
+
+ public UUID getId() {
+ return id;
+ }
+
+ public void setId(UUID id) {
+ this.id = id;
+ }
+
+ public UUID getUserId() {
+ return userId;
+ }
+
+ public void setUserId(UUID userId) {
+ this.userId = userId;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public String getActivateToken() {
+ return activateToken;
+ }
+
+ public void setActivateToken(String activateToken) {
+ this.activateToken = activateToken;
+ }
+
+ public String getResetToken() {
+ return resetToken;
+ }
+
+ public void setResetToken(String resetToken) {
+ this.resetToken = resetToken;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((activateToken == null) ? 0 : activateToken.hashCode());
+ result = prime * result + (enabled ? 1231 : 1237);
+ result = prime * result + ((id == null) ? 0 : id.hashCode());
+ result = prime * result + ((password == null) ? 0 : password.hashCode());
+ result = prime * result + ((resetToken == null) ? 0 : resetToken.hashCode());
+ result = prime * result + ((userId == null) ? 0 : userId.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ UserCredentialsEntity other = (UserCredentialsEntity) obj;
+ if (activateToken == null) {
+ if (other.activateToken != null)
+ return false;
+ } else if (!activateToken.equals(other.activateToken))
+ return false;
+ if (enabled != other.enabled)
+ return false;
+ if (id == null) {
+ if (other.id != null)
+ return false;
+ } else if (!id.equals(other.id))
+ return false;
+ if (password == null) {
+ if (other.password != null)
+ return false;
+ } else if (!password.equals(other.password))
+ return false;
+ if (resetToken == null) {
+ if (other.resetToken != null)
+ return false;
+ } else if (!resetToken.equals(other.resetToken))
+ return false;
+ if (userId == null) {
+ if (other.userId != null)
+ return false;
+ } else if (!userId.equals(other.userId))
+ return false;
+ return true;
+ }
+
+ @Override
+ public UserCredentials toData() {
+ UserCredentials userCredentials = new UserCredentials(new UserCredentialsId(id));
+ userCredentials.setCreatedTime(UUIDs.unixTimestamp(id));
+ if (userId != null) {
+ userCredentials.setUserId(new UserId(userId));
+ }
+ userCredentials.setEnabled(enabled);
+ userCredentials.setPassword(password);
+ userCredentials.setActivateToken(activateToken);
+ userCredentials.setResetToken(resetToken);
+ return userCredentials;
+ }
+
+}
\ No newline at end of file
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/UserEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/UserEntity.java
new file mode 100644
index 0000000..12dec6d
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/UserEntity.java
@@ -0,0 +1,287 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model;
+
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_ADDITIONAL_INFO_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_AUTHORITY_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_CUSTOMER_ID_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_EMAIL_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_FIRST_NAME_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_LAST_NAME_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.USER_TENANT_ID_PROPERTY;
+
+import java.util.UUID;
+
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UserId;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.dao.model.type.AuthorityCodec;
+import org.thingsboard.server.dao.model.type.JsonCodec;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.datastax.driver.mapping.annotations.Column;
+import com.datastax.driver.mapping.annotations.PartitionKey;
+import com.datastax.driver.mapping.annotations.Table;
+import com.datastax.driver.mapping.annotations.Transient;
+import com.fasterxml.jackson.databind.JsonNode;
+
+@Table(name = USER_COLUMN_FAMILY_NAME)
+public final class UserEntity implements SearchTextEntity<User> {
+
+ @Transient
+ private static final long serialVersionUID = -7740338274987723489L;
+
+ @PartitionKey(value = 0)
+ @Column(name = ID_PROPERTY)
+ private UUID id;
+
+ @PartitionKey(value = 1)
+ @Column(name = USER_TENANT_ID_PROPERTY)
+ private UUID tenantId;
+
+ @PartitionKey(value = 2)
+ @Column(name = USER_CUSTOMER_ID_PROPERTY)
+ private UUID customerId;
+
+ @PartitionKey(value = 3)
+ @Column(name = USER_AUTHORITY_PROPERTY, codec = AuthorityCodec.class)
+ private Authority authority;
+
+ @Column(name = USER_EMAIL_PROPERTY)
+ private String email;
+
+ @Column(name = SEARCH_TEXT_PROPERTY)
+ private String searchText;
+
+ @Column(name = USER_FIRST_NAME_PROPERTY)
+ private String firstName;
+
+ @Column(name = USER_LAST_NAME_PROPERTY)
+ private String lastName;
+
+ @Column(name = USER_ADDITIONAL_INFO_PROPERTY, codec = JsonCodec.class)
+ private JsonNode additionalInfo;
+
+ public UserEntity() {
+ super();
+ }
+
+ public UserEntity(User user) {
+ if (user.getId() != null) {
+ this.id = user.getId().getId();
+ }
+ this.authority = user.getAuthority();
+ if (user.getTenantId() != null) {
+ this.tenantId = user.getTenantId().getId();
+ }
+ if (user.getCustomerId() != null) {
+ this.customerId = user.getCustomerId().getId();
+ }
+ this.email = user.getEmail();
+ this.firstName = user.getFirstName();
+ this.lastName = user.getLastName();
+ this.additionalInfo = user.getAdditionalInfo();
+ }
+
+ public UUID getId() {
+ return id;
+ }
+
+ public void setId(UUID id) {
+ this.id = id;
+ }
+
+ public Authority getAuthority() {
+ return authority;
+ }
+
+ public void setAuthority(Authority authority) {
+ this.authority = authority;
+ }
+
+ public UUID getTenantId() {
+ return tenantId;
+ }
+
+ public void setTenantId(UUID tenantId) {
+ this.tenantId = tenantId;
+ }
+
+ public UUID getCustomerId() {
+ return customerId;
+ }
+
+ public void setCustomerId(UUID customerId) {
+ this.customerId = customerId;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public JsonNode getAdditionalInfo() {
+ return additionalInfo;
+ }
+
+ public void setAdditionalInfo(JsonNode additionalInfo) {
+ this.additionalInfo = additionalInfo;
+ }
+
+ @Override
+ public String getSearchTextSource() {
+ return email;
+ }
+
+ @Override
+ public void setSearchText(String searchText) {
+ this.searchText = searchText;
+ }
+
+ public String getSearchText() {
+ return searchText;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((additionalInfo == null) ? 0 : additionalInfo.hashCode());
+ result = prime * result + ((authority == null) ? 0 : authority.hashCode());
+ result = prime * result + ((customerId == null) ? 0 : customerId.hashCode());
+ result = prime * result + ((email == null) ? 0 : email.hashCode());
+ result = prime * result + ((firstName == null) ? 0 : firstName.hashCode());
+ result = prime * result + ((id == null) ? 0 : id.hashCode());
+ result = prime * result + ((lastName == null) ? 0 : lastName.hashCode());
+ result = prime * result + ((tenantId == null) ? 0 : tenantId.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ UserEntity other = (UserEntity) obj;
+ if (additionalInfo == null) {
+ if (other.additionalInfo != null)
+ return false;
+ } else if (!additionalInfo.equals(other.additionalInfo))
+ return false;
+ if (authority != other.authority)
+ return false;
+ if (customerId == null) {
+ if (other.customerId != null)
+ return false;
+ } else if (!customerId.equals(other.customerId))
+ return false;
+ if (email == null) {
+ if (other.email != null)
+ return false;
+ } else if (!email.equals(other.email))
+ return false;
+ if (firstName == null) {
+ if (other.firstName != null)
+ return false;
+ } else if (!firstName.equals(other.firstName))
+ return false;
+ if (id == null) {
+ if (other.id != null)
+ return false;
+ } else if (!id.equals(other.id))
+ return false;
+ if (lastName == null) {
+ if (other.lastName != null)
+ return false;
+ } else if (!lastName.equals(other.lastName))
+ return false;
+ if (tenantId == null) {
+ if (other.tenantId != null)
+ return false;
+ } else if (!tenantId.equals(other.tenantId))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("UserEntity [id=");
+ builder.append(id);
+ builder.append(", authority=");
+ builder.append(authority);
+ builder.append(", tenantId=");
+ builder.append(tenantId);
+ builder.append(", customerId=");
+ builder.append(customerId);
+ builder.append(", email=");
+ builder.append(email);
+ builder.append(", firstName=");
+ builder.append(firstName);
+ builder.append(", lastName=");
+ builder.append(lastName);
+ builder.append(", additionalInfo=");
+ builder.append(additionalInfo);
+ builder.append("]");
+ return builder.toString();
+ }
+
+ @Override
+ public User toData() {
+ User user = new User(new UserId(id));
+ user.setCreatedTime(UUIDs.unixTimestamp(id));
+ user.setAuthority(authority);
+ if (tenantId != null) {
+ user.setTenantId(new TenantId(tenantId));
+ }
+ if (customerId != null) {
+ user.setCustomerId(new CustomerId(customerId));
+ }
+ user.setEmail(email);
+ user.setFirstName(firstName);
+ user.setLastName(lastName);
+ user.setAdditionalInfo(additionalInfo);
+ return user;
+ }
+
+}
\ No newline at end of file
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/WidgetsBundleEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/WidgetsBundleEntity.java
new file mode 100644
index 0000000..60889b6
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/WidgetsBundleEntity.java
@@ -0,0 +1,187 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model;
+
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.datastax.driver.mapping.annotations.Column;
+import com.datastax.driver.mapping.annotations.PartitionKey;
+import com.datastax.driver.mapping.annotations.Table;
+import com.datastax.driver.mapping.annotations.Transient;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.WidgetsBundleId;
+import org.thingsboard.server.common.data.widget.WidgetsBundle;
+
+import java.nio.ByteBuffer;
+import java.util.UUID;
+
+@Table(name = ModelConstants.WIDGETS_BUNDLE_COLUMN_FAMILY_NAME)
+public final class WidgetsBundleEntity implements SearchTextEntity<WidgetsBundle> {
+
+ @Transient
+ private static final long serialVersionUID = -8842195928585650849L;
+
+ @PartitionKey(value = 0)
+ @Column(name = ModelConstants.ID_PROPERTY)
+ private UUID id;
+
+ @PartitionKey(value = 1)
+ @Column(name = ModelConstants.WIDGETS_BUNDLE_TENANT_ID_PROPERTY)
+ private UUID tenantId;
+
+ @Column(name = ModelConstants.WIDGETS_BUNDLE_ALIAS_PROPERTY)
+ private String alias;
+
+ @Column(name = ModelConstants.WIDGETS_BUNDLE_TITLE_PROPERTY)
+ private String title;
+
+ @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY)
+ private String searchText;
+
+ @Column(name = ModelConstants.WIDGETS_BUNDLE_IMAGE_PROPERTY)
+ private ByteBuffer image;
+
+ public WidgetsBundleEntity() {
+ super();
+ }
+
+ public WidgetsBundleEntity(WidgetsBundle widgetsBundle) {
+ if (widgetsBundle.getId() != null) {
+ this.id = widgetsBundle.getId().getId();
+ }
+ if (widgetsBundle.getTenantId() != null) {
+ this.tenantId = widgetsBundle.getTenantId().getId();
+ }
+ this.alias = widgetsBundle.getAlias();
+ this.title = widgetsBundle.getTitle();
+ if (widgetsBundle.getImage() != null) {
+ this.image = ByteBuffer.wrap(widgetsBundle.getImage());
+ }
+ }
+
+ @Override
+ public UUID getId() {
+ return id;
+ }
+
+ @Override
+ public void setId(UUID id) {
+ this.id = id;
+ }
+
+ public UUID getTenantId() {
+ return tenantId;
+ }
+
+ public void setTenantId(UUID tenantId) {
+ this.tenantId = tenantId;
+ }
+
+ public String getAlias() {
+ return alias;
+ }
+
+ public void setAlias(String alias) {
+ this.alias = alias;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public ByteBuffer getImage() {
+ return image;
+ }
+
+ public void setImage(ByteBuffer image) {
+ this.image = image;
+ }
+
+ @Override
+ public String getSearchTextSource() {
+ return title;
+ }
+
+ @Override
+ public void setSearchText(String searchText) {
+ this.searchText = searchText;
+ }
+
+ public String getSearchText() {
+ return searchText;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = id != null ? id.hashCode() : 0;
+ result = 31 * result + (tenantId != null ? tenantId.hashCode() : 0);
+ result = 31 * result + (alias != null ? alias.hashCode() : 0);
+ result = 31 * result + (title != null ? title.hashCode() : 0);
+ result = 31 * result + (searchText != null ? searchText.hashCode() : 0);
+ result = 31 * result + (image != null ? image.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ WidgetsBundleEntity that = (WidgetsBundleEntity) o;
+
+ if (id != null ? !id.equals(that.id) : that.id != null) return false;
+ if (tenantId != null ? !tenantId.equals(that.tenantId) : that.tenantId != null) return false;
+ if (alias != null ? !alias.equals(that.alias) : that.alias != null) return false;
+ if (title != null ? !title.equals(that.title) : that.title != null) return false;
+ if (searchText != null ? !searchText.equals(that.searchText) : that.searchText != null) return false;
+ return image != null ? image.equals(that.image) : that.image == null;
+
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("WidgetsBundleEntity{");
+ sb.append("id=").append(id);
+ sb.append(", tenantId=").append(tenantId);
+ sb.append(", alias='").append(alias).append('\'');
+ sb.append(", title='").append(title).append('\'');
+ sb.append(", searchText='").append(searchText).append('\'');
+ sb.append(", image=").append(image);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public WidgetsBundle toData() {
+ WidgetsBundle widgetsBundle = new WidgetsBundle(new WidgetsBundleId(id));
+ widgetsBundle.setCreatedTime(UUIDs.unixTimestamp(id));
+ if (tenantId != null) {
+ widgetsBundle.setTenantId(new TenantId(tenantId));
+ }
+ widgetsBundle.setAlias(alias);
+ widgetsBundle.setTitle(title);
+ if (image != null) {
+ byte[] imageByteArray = new byte[image.remaining()];
+ image.get(imageByteArray);
+ widgetsBundle.setImage(imageByteArray);
+ }
+ return widgetsBundle;
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/WidgetTypeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/WidgetTypeEntity.java
new file mode 100644
index 0000000..df33948
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/WidgetTypeEntity.java
@@ -0,0 +1,178 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.datastax.driver.mapping.annotations.Column;
+import com.datastax.driver.mapping.annotations.PartitionKey;
+import com.datastax.driver.mapping.annotations.Table;
+import com.datastax.driver.mapping.annotations.Transient;
+import com.fasterxml.jackson.databind.JsonNode;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.WidgetTypeId;
+import org.thingsboard.server.common.data.widget.WidgetType;
+import org.thingsboard.server.dao.model.type.JsonCodec;
+
+import java.util.UUID;
+
+@Table(name = ModelConstants.WIDGET_TYPE_COLUMN_FAMILY_NAME)
+public final class WidgetTypeEntity implements BaseEntity<WidgetType> {
+
+ @Transient
+ private static final long serialVersionUID = 3591054897680176342L;
+
+ @PartitionKey(value = 0)
+ @Column(name = ModelConstants.ID_PROPERTY)
+ private UUID id;
+
+ @PartitionKey(value = 1)
+ @Column(name = ModelConstants.WIDGET_TYPE_TENANT_ID_PROPERTY)
+ private UUID tenantId;
+
+ @PartitionKey(value = 2)
+ @Column(name = ModelConstants.WIDGET_TYPE_BUNDLE_ALIAS_PROPERTY)
+ private String bundleAlias;
+
+ @Column(name = ModelConstants.WIDGET_TYPE_ALIAS_PROPERTY)
+ private String alias;
+
+ @Column(name = ModelConstants.WIDGET_TYPE_NAME_PROPERTY)
+ private String name;
+
+ @Column(name = ModelConstants.WIDGET_TYPE_DESCRIPTOR_PROPERTY, codec = JsonCodec.class)
+ private JsonNode descriptor;
+
+ public WidgetTypeEntity() {
+ super();
+ }
+
+ public WidgetTypeEntity(WidgetType widgetType) {
+ if (widgetType.getId() != null) {
+ this.id = widgetType.getId().getId();
+ }
+ if (widgetType.getTenantId() != null) {
+ this.tenantId = widgetType.getTenantId().getId();
+ }
+ this.bundleAlias = widgetType.getBundleAlias();
+ this.alias = widgetType.getAlias();
+ this.name = widgetType.getName();
+ this.descriptor = widgetType.getDescriptor();
+ }
+
+ @Override
+ public UUID getId() {
+ return id;
+ }
+
+ @Override
+ public void setId(UUID id) {
+ this.id = id;
+ }
+
+ public UUID getTenantId() {
+ return tenantId;
+ }
+
+ public void setTenantId(UUID tenantId) {
+ this.tenantId = tenantId;
+ }
+
+ public String getBundleAlias() {
+ return bundleAlias;
+ }
+
+ public void setBundleAlias(String bundleAlias) {
+ this.bundleAlias = bundleAlias;
+ }
+
+ public String getAlias() {
+ return alias;
+ }
+
+ public void setAlias(String alias) {
+ this.alias = alias;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public JsonNode getDescriptor() {
+ return descriptor;
+ }
+
+ public void setDescriptor(JsonNode descriptor) {
+ this.descriptor = descriptor;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = id != null ? id.hashCode() : 0;
+ result = 31 * result + (tenantId != null ? tenantId.hashCode() : 0);
+ result = 31 * result + (bundleAlias != null ? bundleAlias.hashCode() : 0);
+ result = 31 * result + (alias != null ? alias.hashCode() : 0);
+ result = 31 * result + (name != null ? name.hashCode() : 0);
+ result = 31 * result + (descriptor != null ? descriptor.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ WidgetTypeEntity that = (WidgetTypeEntity) o;
+
+ if (id != null ? !id.equals(that.id) : that.id != null) return false;
+ if (tenantId != null ? !tenantId.equals(that.tenantId) : that.tenantId != null) return false;
+ if (bundleAlias != null ? !bundleAlias.equals(that.bundleAlias) : that.bundleAlias != null) return false;
+ if (alias != null ? !alias.equals(that.alias) : that.alias != null) return false;
+ if (name != null ? !name.equals(that.name) : that.name != null) return false;
+ return descriptor != null ? descriptor.equals(that.descriptor) : that.descriptor == null;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("WidgetTypeEntity{");
+ sb.append("id=").append(id);
+ sb.append(", tenantId=").append(tenantId);
+ sb.append(", bundleAlias='").append(bundleAlias).append('\'');
+ sb.append(", alias='").append(alias).append('\'');
+ sb.append(", name='").append(name).append('\'');
+ sb.append(", descriptor=").append(descriptor);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public WidgetType toData() {
+ WidgetType widgetType = new WidgetType(new WidgetTypeId(id));
+ widgetType.setCreatedTime(UUIDs.unixTimestamp(id));
+ if (tenantId != null) {
+ widgetType.setTenantId(new TenantId(tenantId));
+ }
+ widgetType.setBundleAlias(bundleAlias);
+ widgetType.setAlias(alias);
+ widgetType.setName(name);
+ widgetType.setDescriptor(descriptor);
+ return widgetType;
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/wrapper/EntityResultSet.java b/dao/src/main/java/org/thingsboard/server/dao/model/wrapper/EntityResultSet.java
new file mode 100644
index 0000000..be02f72
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/wrapper/EntityResultSet.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model.wrapper;
+
+
+import com.datastax.driver.core.ResultSet;
+
+public class EntityResultSet<T> {
+
+ private ResultSet resultSet;
+ private T entity;
+
+ public EntityResultSet(ResultSet resultSet, T entity) {
+ this.resultSet = resultSet;
+ this.entity = entity;
+ }
+
+ public T getEntity() {
+ return entity;
+ }
+
+ public boolean wasApplied() {
+ return resultSet.wasApplied();
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/plugin/BasePluginDao.java b/dao/src/main/java/org/thingsboard/server/dao/plugin/BasePluginDao.java
new file mode 100644
index 0000000..2bed72c
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/plugin/BasePluginDao.java
@@ -0,0 +1,123 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.plugin;
+
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.querybuilder.Select;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.plugin.PluginMetaData;
+import org.thingsboard.server.dao.AbstractSearchTextDao;
+import org.thingsboard.server.dao.model.ModelConstants;
+import org.thingsboard.server.dao.model.PluginMetaDataEntity;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+
+import static com.datastax.driver.core.querybuilder.QueryBuilder.*;
+import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
+
+@Component
+@Slf4j
+public class BasePluginDao extends AbstractSearchTextDao<PluginMetaDataEntity> implements PluginDao {
+
+ @Override
+ protected Class<PluginMetaDataEntity> getColumnFamilyClass() {
+ return PluginMetaDataEntity.class;
+ }
+
+ @Override
+ protected String getColumnFamilyName() {
+ return ModelConstants.PLUGIN_COLUMN_FAMILY_NAME;
+ }
+
+ @Override
+ public PluginMetaDataEntity save(PluginMetaData plugin) {
+ return save(new PluginMetaDataEntity(plugin));
+ }
+
+ @Override
+ public PluginMetaDataEntity findById(PluginId pluginId) {
+ log.debug("Search plugin meta-data entity by id [{}]", pluginId);
+ PluginMetaDataEntity entity = super.findById(pluginId.getId());
+ if (log.isTraceEnabled()) {
+ log.trace("Search result: [{}] for plugin entity [{}]", entity != null, entity);
+ } else {
+ log.debug("Search result: [{}]", entity != null);
+ }
+ return entity;
+ }
+
+ @Override
+ public PluginMetaDataEntity findByApiToken(String apiToken) {
+ log.debug("Search plugin meta-data entity by api token [{}]", apiToken);
+ Select.Where query = select().from(ModelConstants.PLUGIN_BY_API_TOKEN_COLUMN_FAMILY_NAME).where(eq(ModelConstants.PLUGIN_API_TOKEN_PROPERTY, apiToken));
+ log.trace("Execute query [{}]", query);
+ PluginMetaDataEntity entity = findOneByStatement(query);
+ if (log.isTraceEnabled()) {
+ log.trace("Search result: [{}] for plugin entity [{}]", entity != null, entity);
+ } else {
+ log.debug("Search result: [{}]", entity != null);
+ }
+ return entity;
+ }
+
+ @Override
+ public void deleteById(UUID id) {
+ log.debug("Delete plugin meta-data entity by id [{}]", id);
+ ResultSet resultSet = removeById(id);
+ log.debug("Delete result: [{}]", resultSet.wasApplied());
+ }
+
+ @Override
+ public void deleteById(PluginId pluginId) {
+ deleteById(pluginId.getId());
+ }
+
+ @Override
+ public List<PluginMetaDataEntity> findByTenantIdAndPageLink(TenantId tenantId, TextPageLink pageLink) {
+ log.debug("Try to find plugins by tenantId [{}] and pageLink [{}]", tenantId, pageLink);
+ List<PluginMetaDataEntity> entities = findPageWithTextSearch(ModelConstants.PLUGIN_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
+ Arrays.asList(eq(ModelConstants.PLUGIN_TENANT_ID_PROPERTY, tenantId.getId())), pageLink);
+ if (log.isTraceEnabled()) {
+ log.trace("Search result: [{}]", Arrays.toString(entities.toArray()));
+ } else {
+ log.debug("Search result: [{}]", entities.size());
+ }
+ return entities;
+ }
+
+ @Override
+ public List<PluginMetaDataEntity> findAllTenantPluginsByTenantId(UUID tenantId, TextPageLink pageLink) {
+ log.debug("Try to find all tenant plugins by tenantId [{}] and pageLink [{}]", tenantId, pageLink);
+ List<PluginMetaDataEntity> pluginEntities = findPageWithTextSearch(ModelConstants.PLUGIN_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
+ Arrays.asList(in(ModelConstants.PLUGIN_TENANT_ID_PROPERTY, Arrays.asList(NULL_UUID, tenantId))),
+ pageLink);
+ if (log.isTraceEnabled()) {
+ log.trace("Search result: [{}]", Arrays.toString(pluginEntities.toArray()));
+ } else {
+ log.debug("Search result: [{}]", pluginEntities.size());
+ }
+ return pluginEntities;
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/plugin/BasePluginService.java b/dao/src/main/java/org/thingsboard/server/dao/plugin/BasePluginService.java
new file mode 100644
index 0000000..732d957
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/plugin/BasePluginService.java
@@ -0,0 +1,260 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.plugin;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.data.plugin.PluginMetaData;
+import org.thingsboard.server.dao.component.ComponentDescriptorService;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.exception.DatabaseException;
+import org.thingsboard.server.dao.exception.IncorrectParameterException;
+import org.thingsboard.server.dao.model.ModelConstants;
+import org.thingsboard.server.dao.model.PluginMetaDataEntity;
+import org.thingsboard.server.dao.model.RuleMetaDataEntity;
+import org.thingsboard.server.dao.rule.RuleDao;
+import org.thingsboard.server.dao.service.DataValidator;
+import org.thingsboard.server.dao.service.PaginatedRemover;
+import org.thingsboard.server.dao.service.Validator;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import static org.thingsboard.server.dao.DaoUtil.convertDataList;
+import static org.thingsboard.server.dao.DaoUtil.getData;
+
+@Service
+@Slf4j
+public class BasePluginService implements PluginService {
+
+ //TODO: move to a better place.
+ public static final TenantId SYSTEM_TENANT = new TenantId(ModelConstants.NULL_UUID);
+
+ @Autowired
+ private PluginDao pluginDao;
+
+ @Autowired
+ private RuleDao ruleDao;
+
+ @Autowired
+ private ComponentDescriptorService componentDescriptorService;
+
+ @Override
+ public PluginMetaData savePlugin(PluginMetaData plugin) {
+ pluginValidator.validate(plugin);
+ if (plugin.getTenantId() == null) {
+ log.trace("Save system plugin metadata with predefined id {}", SYSTEM_TENANT);
+ plugin.setTenantId(SYSTEM_TENANT);
+ }
+ if (plugin.getId() != null) {
+ PluginMetaData oldVersion = getData(pluginDao.findById(plugin.getId()));
+ if (plugin.getState() == null) {
+ plugin.setState(oldVersion.getState());
+ } else if (plugin.getState() != oldVersion.getState()) {
+ throw new IncorrectParameterException("Use Activate/Suspend method to control state of the plugin!");
+ }
+ } else {
+ if (plugin.getState() == null) {
+ plugin.setState(ComponentLifecycleState.SUSPENDED);
+ } else if (plugin.getState() != ComponentLifecycleState.SUSPENDED) {
+ throw new IncorrectParameterException("Use Activate/Suspend method to control state of the plugin!");
+ }
+ }
+ ComponentDescriptor descriptor = componentDescriptorService.findByClazz(plugin.getClazz());
+ if (descriptor == null) {
+ throw new IncorrectParameterException("Plugin descriptor not found!");
+ } else if (!ComponentType.PLUGIN.equals(descriptor.getType())) {
+ throw new IncorrectParameterException("Plugin class is actually " + descriptor.getType() + "!");
+ }
+ PluginMetaDataEntity entity = pluginDao.findByApiToken(plugin.getApiToken());
+ if (entity != null && (plugin.getId() == null || !entity.getId().equals(plugin.getId().getId()))) {
+ throw new IncorrectParameterException("API token is already reserved!");
+ }
+ if (!componentDescriptorService.validate(descriptor, plugin.getConfiguration())) {
+ throw new IncorrectParameterException("Filters configuration is not valid!");
+ }
+ return getData(pluginDao.save(plugin));
+ }
+
+ @Override
+ public PluginMetaData findPluginById(PluginId pluginId) {
+ Validator.validateId(pluginId, "Incorrect plugin id for search request.");
+ return getData(pluginDao.findById(pluginId));
+ }
+
+ @Override
+ public PluginMetaData findPluginByApiToken(String apiToken) {
+ Validator.validateString(apiToken, "Incorrect plugin apiToken for search request.");
+ return getData(pluginDao.findByApiToken(apiToken));
+ }
+
+ @Override
+ public TextPageData<PluginMetaData> findSystemPlugins(TextPageLink pageLink) {
+ Validator.validatePageLink(pageLink, "Incorrect PageLink object for search system plugin request.");
+ List<PluginMetaDataEntity> pluginEntities = pluginDao.findByTenantIdAndPageLink(SYSTEM_TENANT, pageLink);
+ List<PluginMetaData> plugins = convertDataList(pluginEntities);
+ return new TextPageData<>(plugins, pageLink);
+ }
+
+ @Override
+ public TextPageData<PluginMetaData> findTenantPlugins(TenantId tenantId, TextPageLink pageLink) {
+ Validator.validateId(tenantId, "Incorrect tenant id for search plugins request.");
+ Validator.validatePageLink(pageLink, "Incorrect PageLink object for search plugin request.");
+ List<PluginMetaDataEntity> pluginEntities = pluginDao.findByTenantIdAndPageLink(tenantId, pageLink);
+ List<PluginMetaData> plugins = convertDataList(pluginEntities);
+ return new TextPageData<>(plugins, pageLink);
+ }
+
+ @Override
+ public List<PluginMetaData> findSystemPlugins() {
+ log.trace("Executing findSystemPlugins");
+ List<PluginMetaData> plugins = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(300);
+ TextPageData<PluginMetaData> pageData = null;
+ do {
+ pageData = findSystemPlugins(pageLink);
+ plugins.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+ return plugins;
+ }
+
+ @Override
+ public TextPageData<PluginMetaData> findAllTenantPluginsByTenantIdAndPageLink(TenantId tenantId, TextPageLink pageLink) {
+ log.trace("Executing findAllTenantPluginsByTenantIdAndPageLink, tenantId [{}], pageLink [{}]", tenantId, pageLink);
+ Validator.validateId(tenantId, "Incorrect tenantId " + tenantId);
+ Validator.validatePageLink(pageLink, "Incorrect page link " + pageLink);
+ List<PluginMetaDataEntity> pluginsEntities = pluginDao.findAllTenantPluginsByTenantId(tenantId.getId(), pageLink);
+ List<PluginMetaData> plugins = convertDataList(pluginsEntities);
+ return new TextPageData<>(plugins, pageLink);
+ }
+
+ @Override
+ public List<PluginMetaData> findAllTenantPluginsByTenantId(TenantId tenantId) {
+ log.trace("Executing findAllTenantPluginsByTenantId, tenantId [{}]", tenantId);
+ Validator.validateId(tenantId, "Incorrect tenantId " + tenantId);
+ List<PluginMetaData> plugins = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(300);
+ TextPageData<PluginMetaData> pageData = null;
+ do {
+ pageData = findAllTenantPluginsByTenantIdAndPageLink(tenantId, pageLink);
+ plugins.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+ return plugins;
+ }
+
+ @Override
+ public void activatePluginById(PluginId pluginId) {
+ updateLifeCycleState(pluginId, ComponentLifecycleState.ACTIVE);
+ }
+
+ @Override
+ public void suspendPluginById(PluginId pluginId) {
+ PluginMetaDataEntity plugin = pluginDao.findById(pluginId);
+ List<RuleMetaDataEntity> affectedRules = ruleDao.findRulesByPlugin(plugin.getApiToken())
+ .stream().filter(rule -> rule.getState() == ComponentLifecycleState.ACTIVE).collect(Collectors.toList());
+ if (affectedRules.isEmpty()) {
+ updateLifeCycleState(pluginId, ComponentLifecycleState.SUSPENDED);
+ } else {
+ throw new DataValidationException("Can't suspend plugin that has active rules!");
+ }
+ }
+
+ private void updateLifeCycleState(PluginId pluginId, ComponentLifecycleState state) {
+ Validator.validateId(pluginId, "Incorrect plugin id for state change request.");
+ PluginMetaDataEntity plugin = pluginDao.findById(pluginId);
+ if (plugin != null) {
+ plugin.setState(state);
+ pluginDao.save(plugin);
+ } else {
+ throw new DatabaseException("Plugin not found!");
+ }
+ }
+
+ @Override
+ public void deletePluginById(PluginId pluginId) {
+ Validator.validateId(pluginId, "Incorrect plugin id for delete request.");
+ checkRulesAndDelete(pluginId.getId());
+ }
+
+ private void checkRulesAndDelete(UUID pluginId) {
+ PluginMetaDataEntity plugin = pluginDao.findById(pluginId);
+ List<RuleMetaDataEntity> affectedRules = ruleDao.findRulesByPlugin(plugin.getApiToken());
+ if (affectedRules.isEmpty()) {
+ pluginDao.deleteById(pluginId);
+ } else {
+ throw new DataValidationException("Plugin deletion will affect existing rules!");
+ }
+ }
+
+ @Override
+ public void deletePluginsByTenantId(TenantId tenantId) {
+ Validator.validateId(tenantId, "Incorrect tenant id for delete plugins request.");
+ tenantPluginRemover.removeEntitites(tenantId);
+ }
+
+
+ private DataValidator<PluginMetaData> pluginValidator =
+ new DataValidator<PluginMetaData>() {
+ @Override
+ protected void validateDataImpl(PluginMetaData plugin) {
+ if (StringUtils.isEmpty(plugin.getName())) {
+ throw new DataValidationException("Plugin name should be specified!.");
+ }
+ if (StringUtils.isEmpty(plugin.getClazz())) {
+ throw new DataValidationException("Plugin clazz should be specified!.");
+ }
+ if (StringUtils.isEmpty(plugin.getApiToken())) {
+ throw new DataValidationException("Plugin api token is not set!");
+ }
+ if (plugin.getConfiguration() == null) {
+ throw new DataValidationException("Plugin configuration is not set!");
+ }
+ }
+ };
+
+ private PaginatedRemover<TenantId, PluginMetaDataEntity> tenantPluginRemover =
+ new PaginatedRemover<TenantId, PluginMetaDataEntity>() {
+
+ @Override
+ protected List<PluginMetaDataEntity> findEntities(TenantId id, TextPageLink pageLink) {
+ return pluginDao.findByTenantIdAndPageLink(id, pageLink);
+ }
+
+ @Override
+ protected void removeEntity(PluginMetaDataEntity entity) {
+ checkRulesAndDelete(entity.getId());
+ }
+ };
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/plugin/PluginDao.java b/dao/src/main/java/org/thingsboard/server/dao/plugin/PluginDao.java
new file mode 100644
index 0000000..d6b7434
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/plugin/PluginDao.java
@@ -0,0 +1,51 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.plugin;
+
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.plugin.PluginMetaData;
+import org.thingsboard.server.dao.Dao;
+import org.thingsboard.server.dao.model.PluginMetaDataEntity;
+
+import java.util.List;
+import java.util.UUID;
+
+public interface PluginDao extends Dao<PluginMetaDataEntity> {
+
+ PluginMetaDataEntity save(PluginMetaData plugin);
+
+ PluginMetaDataEntity findById(PluginId pluginId);
+
+ PluginMetaDataEntity findByApiToken(String apiToken);
+
+ void deleteById(UUID id);
+
+ void deleteById(PluginId pluginId);
+
+ List<PluginMetaDataEntity> findByTenantIdAndPageLink(TenantId tenantId, TextPageLink pageLink);
+
+ /**
+ * Find all tenant plugins (including system) by tenantId and page link.
+ *
+ * @param tenantId the tenantId
+ * @param pageLink the page link
+ * @return the list of plugins objects
+ */
+ List<PluginMetaDataEntity> findAllTenantPluginsByTenantId(UUID tenantId, TextPageLink pageLink);
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/plugin/PluginService.java b/dao/src/main/java/org/thingsboard/server/dao/plugin/PluginService.java
new file mode 100644
index 0000000..7c16bd4
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/plugin/PluginService.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.plugin;
+
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.plugin.PluginMetaData;
+
+import java.util.List;
+
+public interface PluginService {
+
+ PluginMetaData savePlugin(PluginMetaData plugin);
+
+ PluginMetaData findPluginById(PluginId pluginId);
+
+ PluginMetaData findPluginByApiToken(String apiToken);
+
+ TextPageData<PluginMetaData> findSystemPlugins(TextPageLink pageLink);
+
+ TextPageData<PluginMetaData> findTenantPlugins(TenantId tenantId, TextPageLink pageLink);
+
+ List<PluginMetaData> findSystemPlugins();
+
+ TextPageData<PluginMetaData> findAllTenantPluginsByTenantIdAndPageLink(TenantId tenantId, TextPageLink pageLink);
+
+ List<PluginMetaData> findAllTenantPluginsByTenantId(TenantId tenantId);
+
+ void activatePluginById(PluginId pluginId);
+
+ void suspendPluginById(PluginId pluginId);
+
+ void deletePluginById(PluginId pluginId);
+
+ void deletePluginsByTenantId(TenantId tenantId);
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleDao.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleDao.java
new file mode 100644
index 0000000..0070b79
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleDao.java
@@ -0,0 +1,108 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.rule;
+
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.querybuilder.Select;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.rule.RuleMetaData;
+import org.thingsboard.server.dao.AbstractSearchTextDao;
+import org.thingsboard.server.dao.model.ModelConstants;
+import org.thingsboard.server.dao.model.RuleMetaDataEntity;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+
+import static com.datastax.driver.core.querybuilder.QueryBuilder.*;
+import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
+
+@Component
+@Slf4j
+public class BaseRuleDao extends AbstractSearchTextDao<RuleMetaDataEntity> implements RuleDao {
+
+ @Override
+ protected Class<RuleMetaDataEntity> getColumnFamilyClass() {
+ return RuleMetaDataEntity.class;
+ }
+
+ @Override
+ protected String getColumnFamilyName() {
+ return ModelConstants.RULE_COLUMN_FAMILY_NAME;
+ }
+
+ @Override
+ public RuleMetaDataEntity findById(RuleId ruleId) {
+ return findById(ruleId.getId());
+ }
+
+ @Override
+ public RuleMetaDataEntity save(RuleMetaData rule) {
+ return save(new RuleMetaDataEntity(rule));
+ }
+
+ @Override
+ public List<RuleMetaDataEntity> findByTenantIdAndPageLink(TenantId tenantId, TextPageLink pageLink) {
+ log.debug("Try to find rules by tenantId [{}] and pageLink [{}]", tenantId, pageLink);
+ List<RuleMetaDataEntity> entities = findPageWithTextSearch(ModelConstants.RULE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
+ Arrays.asList(eq(ModelConstants.RULE_TENANT_ID_PROPERTY, tenantId.getId())), pageLink);
+ if (log.isTraceEnabled()) {
+ log.trace("Search result: [{}]", Arrays.toString(entities.toArray()));
+ } else {
+ log.debug("Search result: [{}]", entities.size());
+ }
+ return entities;
+ }
+
+ @Override
+ public List<RuleMetaDataEntity> findAllTenantRulesByTenantId(UUID tenantId, TextPageLink pageLink) {
+ log.debug("Try to find all tenant rules by tenantId [{}] and pageLink [{}]", tenantId, pageLink);
+ List<RuleMetaDataEntity> entities = findPageWithTextSearch(ModelConstants.RULE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
+ Arrays.asList(in(ModelConstants.RULE_TENANT_ID_PROPERTY, Arrays.asList(NULL_UUID, tenantId))),
+ pageLink);
+ if (log.isTraceEnabled()) {
+ log.trace("Search result: [{}]", Arrays.toString(entities.toArray()));
+ } else {
+ log.debug("Search result: [{}]", entities.size());
+ }
+ return entities;
+ }
+
+ @Override
+ public List<RuleMetaDataEntity> findRulesByPlugin(String pluginToken) {
+ log.debug("Search rules by api token [{}]", pluginToken);
+ Select select = select().from(ModelConstants.RULE_BY_PLUGIN_TOKEN);
+ Select.Where query = select.where();
+ query.and(eq(ModelConstants.RULE_PLUGIN_TOKEN_PROPERTY, pluginToken));
+ return findListByStatement(query);
+ }
+
+ @Override
+ public void deleteById(UUID id) {
+ log.debug("Delete rule meta-data entity by id [{}]", id);
+ ResultSet resultSet = removeById(id);
+ log.debug("Delete result: [{}]", resultSet.wasApplied());
+ }
+
+ @Override
+ public void deleteById(RuleId ruleId) {
+ deleteById(ruleId.getId());
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleService.java
new file mode 100644
index 0000000..b0755e7
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleService.java
@@ -0,0 +1,294 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.rule;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.data.plugin.PluginMetaData;
+import org.thingsboard.server.common.data.rule.RuleMetaData;
+import org.thingsboard.server.dao.component.ComponentDescriptorService;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.exception.DatabaseException;
+import org.thingsboard.server.dao.exception.IncorrectParameterException;
+import org.thingsboard.server.dao.model.RuleMetaDataEntity;
+import org.thingsboard.server.dao.plugin.PluginService;
+import org.thingsboard.server.dao.service.DataValidator;
+import org.thingsboard.server.dao.service.PaginatedRemover;
+import org.thingsboard.server.dao.service.Validator;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Function;
+
+import static org.thingsboard.server.dao.DaoUtil.convertDataList;
+import static org.thingsboard.server.dao.DaoUtil.getData;
+import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
+import static org.thingsboard.server.dao.service.Validator.validateId;
+import static org.thingsboard.server.dao.service.Validator.validatePageLink;
+
+@Service
+@Slf4j
+public class BaseRuleService implements RuleService {
+
+ private final TenantId systemTenantId = new TenantId(NULL_UUID);
+
+ @Autowired
+ public RuleDao ruleDao;
+
+ @Autowired
+ public PluginService pluginService;
+
+ @Autowired
+ private ComponentDescriptorService componentDescriptorService;
+
+ @Override
+ public RuleMetaData saveRule(RuleMetaData rule) {
+ ruleValidator.validate(rule);
+ if (rule.getTenantId() == null) {
+ log.trace("Save system rule metadata with predefined id {}", systemTenantId);
+ rule.setTenantId(systemTenantId);
+ }
+ if (rule.getId() != null) {
+ RuleMetaData oldVersion = getData(ruleDao.findById(rule.getId()));
+ if (rule.getState() == null) {
+ rule.setState(oldVersion.getState());
+ } else if (rule.getState() != oldVersion.getState()) {
+ throw new IncorrectParameterException("Use Activate/Suspend method to control state of the rule!");
+ }
+ } else {
+ if (rule.getState() == null) {
+ rule.setState(ComponentLifecycleState.SUSPENDED);
+ } else if (rule.getState() != ComponentLifecycleState.SUSPENDED) {
+ throw new IncorrectParameterException("Use Activate/Suspend method to control state of the rule!");
+ }
+ }
+
+ validateFilters(rule.getFilters());
+ if (rule.getProcessor() != null && !rule.getProcessor().isNull()) {
+ validateComponentJson(rule.getProcessor(), ComponentType.PROCESSOR);
+ }
+ validateComponentJson(rule.getAction(), ComponentType.ACTION);
+ validateRuleAndPluginState(rule);
+ return getData(ruleDao.save(rule));
+ }
+
+ private void validateFilters(JsonNode filtersJson) {
+ if (filtersJson == null || filtersJson.isNull()) {
+ throw new IncorrectParameterException("Rule filters are required!");
+ }
+ if (!filtersJson.isArray()) {
+ throw new IncorrectParameterException("Filters json is not an array!");
+ }
+ ArrayNode filtersArray = (ArrayNode) filtersJson;
+ for (int i = 0; i < filtersArray.size(); i++) {
+ validateComponentJson(filtersArray.get(i), ComponentType.FILTER);
+ }
+ }
+
+ private void validateComponentJson(JsonNode json, ComponentType type) {
+ if (json == null || json.isNull()) {
+ throw new IncorrectParameterException(type.name() + " is required!");
+ }
+ String clazz = getIfValid(type.name(), json, "clazz", JsonNode::isTextual, JsonNode::asText);
+ String name = getIfValid(type.name(), json, "name", JsonNode::isTextual, JsonNode::asText);
+ JsonNode configuration = getIfValid(type.name(), json, "configuration", JsonNode::isObject, node -> node);
+ ComponentDescriptor descriptor = componentDescriptorService.findByClazz(clazz);
+ if (descriptor == null) {
+ throw new IncorrectParameterException(type.name() + " clazz " + clazz + " is not a valid component!");
+ }
+ if (descriptor.getType() != type) {
+ throw new IncorrectParameterException("Clazz " + clazz + " is not a valid " + type.name() + " component!");
+ }
+ if (!componentDescriptorService.validate(descriptor, configuration)) {
+ throw new IncorrectParameterException(type.name() + " configuration is not valid!");
+ }
+ }
+
+ private void validateRuleAndPluginState(RuleMetaData rule) {
+ PluginMetaData pluginMd = pluginService.findPluginByApiToken(rule.getPluginToken());
+ if (pluginMd == null) {
+ throw new IncorrectParameterException("Rule points to non-existent plugin!");
+ }
+ if (!pluginMd.getTenantId().equals(systemTenantId) && !pluginMd.getTenantId().equals(rule.getTenantId())) {
+ throw new IncorrectParameterException("Rule access plugin that belongs to different tenant!");
+ }
+ if (rule.getState() == ComponentLifecycleState.ACTIVE && pluginMd.getState() != ComponentLifecycleState.ACTIVE) {
+ throw new IncorrectParameterException("Can't save active rule that points to inactive plugin!");
+ }
+ ComponentDescriptor pluginDescriptor = componentDescriptorService.findByClazz(pluginMd.getClazz());
+ String actionClazz = getIfValid(ComponentType.ACTION.name(), rule.getAction(), "clazz", JsonNode::isTextual, JsonNode::asText);
+ if (!Arrays.asList(pluginDescriptor.getActions().split(",")).contains(actionClazz)) {
+ throw new IncorrectParameterException("Rule's action is not supported by plugin with token " + rule.getPluginToken() + "!");
+ }
+ }
+
+ private static <T> T getIfValid(String parentName, JsonNode node, String name, Function<JsonNode, Boolean> validator, Function<JsonNode, T> extractor) {
+ if (!node.has(name)) {
+ throw new IncorrectParameterException(parentName + "'s " + name + " is not set!");
+ } else {
+ JsonNode value = node.get(name);
+ if (validator.apply(value)) {
+ return extractor.apply(value);
+ } else {
+ throw new IncorrectParameterException(parentName + "'s " + name + " is not valid!");
+ }
+ }
+ }
+
+ @Override
+ public RuleMetaData findRuleById(RuleId ruleId) {
+ validateId(ruleId, "Incorrect rule id for search rule request.");
+ return getData(ruleDao.findById(ruleId.getId()));
+ }
+
+ @Override
+ public List<RuleMetaData> findPluginRules(String pluginToken) {
+ List<RuleMetaDataEntity> ruleEntities = ruleDao.findRulesByPlugin(pluginToken);
+ return convertDataList(ruleEntities);
+ }
+
+ @Override
+ public TextPageData<RuleMetaData> findSystemRules(TextPageLink pageLink) {
+ validatePageLink(pageLink, "Incorrect PageLink object for search rule request.");
+ List<RuleMetaDataEntity> ruleEntities = ruleDao.findByTenantIdAndPageLink(systemTenantId, pageLink);
+ List<RuleMetaData> plugins = convertDataList(ruleEntities);
+ return new TextPageData<>(plugins, pageLink);
+ }
+
+ @Override
+ public TextPageData<RuleMetaData> findTenantRules(TenantId tenantId, TextPageLink pageLink) {
+ validateId(tenantId, "Incorrect tenant id for search rule request.");
+ validatePageLink(pageLink, "Incorrect PageLink object for search rule request.");
+ List<RuleMetaDataEntity> ruleEntities = ruleDao.findByTenantIdAndPageLink(tenantId, pageLink);
+ List<RuleMetaData> plugins = convertDataList(ruleEntities);
+ return new TextPageData<>(plugins, pageLink);
+ }
+
+ @Override
+ public List<RuleMetaData> findSystemRules() {
+ log.trace("Executing findSystemRules");
+ List<RuleMetaData> rules = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(300);
+ TextPageData<RuleMetaData> pageData = null;
+ do {
+ pageData = findSystemRules(pageLink);
+ rules.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+ return rules;
+ }
+
+ @Override
+ public TextPageData<RuleMetaData> findAllTenantRulesByTenantIdAndPageLink(TenantId tenantId, TextPageLink pageLink) {
+ log.trace("Executing findAllTenantRulesByTenantIdAndPageLink, tenantId [{}], pageLink [{}]", tenantId, pageLink);
+ Validator.validateId(tenantId, "Incorrect tenantId " + tenantId);
+ Validator.validatePageLink(pageLink, "Incorrect page link " + pageLink);
+ List<RuleMetaDataEntity> rulesEntities = ruleDao.findAllTenantRulesByTenantId(tenantId.getId(), pageLink);
+ List<RuleMetaData> rules = convertDataList(rulesEntities);
+ return new TextPageData<>(rules, pageLink);
+ }
+
+ @Override
+ public List<RuleMetaData> findAllTenantRulesByTenantId(TenantId tenantId) {
+ log.trace("Executing findAllTenantRulesByTenantId, tenantId [{}]", tenantId);
+ Validator.validateId(tenantId, "Incorrect tenantId " + tenantId);
+ List<RuleMetaData> rules = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(300);
+ TextPageData<RuleMetaData> pageData = null;
+ do {
+ pageData = findAllTenantRulesByTenantIdAndPageLink(tenantId, pageLink);
+ rules.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+ return rules;
+ }
+
+ @Override
+ public void deleteRuleById(RuleId ruleId) {
+ validateId(ruleId, "Incorrect rule id for delete rule request.");
+ ruleDao.deleteById(ruleId);
+ }
+
+ @Override
+ public void activateRuleById(RuleId ruleId) {
+ updateLifeCycleState(ruleId, ComponentLifecycleState.ACTIVE);
+
+ }
+
+ @Override
+ public void suspendRuleById(RuleId ruleId) {
+ updateLifeCycleState(ruleId, ComponentLifecycleState.SUSPENDED);
+ }
+
+ private void updateLifeCycleState(RuleId ruleId, ComponentLifecycleState state) {
+ Validator.validateId(ruleId, "Incorrect rule id for state change request.");
+ RuleMetaDataEntity rule = ruleDao.findById(ruleId);
+ if (rule != null) {
+ rule.setState(state);
+ validateRuleAndPluginState(getData(rule));
+ ruleDao.save(rule);
+ } else {
+ throw new DatabaseException("Plugin not found!");
+ }
+ }
+
+ @Override
+ public void deleteRulesByTenantId(TenantId tenantId) {
+ validateId(tenantId, "Incorrect tenant id for delete rules request.");
+ tenantRulesRemover.removeEntitites(tenantId);
+ }
+
+ private DataValidator<RuleMetaData> ruleValidator =
+ new DataValidator<RuleMetaData>() {
+ @Override
+ protected void validateDataImpl(RuleMetaData rule) {
+ if (StringUtils.isEmpty(rule.getName())) {
+ throw new DataValidationException("Rule name should be specified!.");
+ }
+ }
+ };
+
+ private PaginatedRemover<TenantId, RuleMetaDataEntity> tenantRulesRemover =
+ new PaginatedRemover<TenantId, RuleMetaDataEntity>() {
+
+ @Override
+ protected List<RuleMetaDataEntity> findEntities(TenantId id, TextPageLink pageLink) {
+ return ruleDao.findByTenantIdAndPageLink(id, pageLink);
+ }
+
+ @Override
+ protected void removeEntity(RuleMetaDataEntity entity) {
+ ruleDao.deleteById(entity.getId());
+ }
+ };
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleDao.java b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleDao.java
new file mode 100644
index 0000000..cdca0ed
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleDao.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.rule;
+
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.rule.RuleMetaData;
+import org.thingsboard.server.dao.Dao;
+import org.thingsboard.server.dao.model.RuleMetaDataEntity;
+
+import java.util.List;
+import java.util.UUID;
+
+public interface RuleDao extends Dao<RuleMetaDataEntity> {
+
+ RuleMetaDataEntity save(RuleMetaData rule);
+
+ RuleMetaDataEntity findById(RuleId ruleId);
+
+ List<RuleMetaDataEntity> findRulesByPlugin(String pluginToken);
+
+ List<RuleMetaDataEntity> findByTenantIdAndPageLink(TenantId tenantId, TextPageLink pageLink);
+
+ /**
+ * Find all tenant rules (including system) by tenantId and page link.
+ *
+ * @param tenantId the tenantId
+ * @param pageLink the page link
+ * @return the list of rules objects
+ */
+ List<RuleMetaDataEntity> findAllTenantRulesByTenantId(UUID tenantId, TextPageLink pageLink);
+
+ void deleteById(UUID id);
+
+ void deleteById(RuleId ruleId);
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleService.java
new file mode 100644
index 0000000..e11fa93
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleService.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.rule;
+
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.rule.RuleMetaData;
+
+import java.util.List;
+
+public interface RuleService {
+
+ RuleMetaData saveRule(RuleMetaData device);
+
+ RuleMetaData findRuleById(RuleId ruleId);
+
+ List<RuleMetaData> findPluginRules(String pluginToken);
+
+ TextPageData<RuleMetaData> findSystemRules(TextPageLink pageLink);
+
+ TextPageData<RuleMetaData> findTenantRules(TenantId tenantId, TextPageLink pageLink);
+
+ List<RuleMetaData> findSystemRules();
+
+ TextPageData<RuleMetaData> findAllTenantRulesByTenantIdAndPageLink(TenantId tenantId, TextPageLink pageLink);
+
+ List<RuleMetaData> findAllTenantRulesByTenantId(TenantId tenantId);
+
+ void activateRuleById(RuleId ruleId);
+
+ void suspendRuleById(RuleId ruleId);
+
+ void deleteRuleById(RuleId ruleId);
+
+ void deleteRulesByTenantId(TenantId tenantId);
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java
new file mode 100644
index 0000000..87101c7
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java
@@ -0,0 +1,100 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.service;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.validator.routines.EmailValidator;
+import org.thingsboard.server.common.data.BaseData;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+@Slf4j
+public abstract class DataValidator<D extends BaseData<?>> {
+
+ private static EmailValidator emailValidator = EmailValidator.getInstance();
+
+ public void validate(D data) {
+ try {
+ if (data == null) {
+ throw new DataValidationException("Data object can't be null!");
+ }
+ validateDataImpl(data);
+ if (data.getId() == null) {
+ validateCreate(data);
+ } else {
+ validateUpdate(data);
+ }
+ } catch (DataValidationException e) {
+ log.error("Data object is invalid: [{}]", e.getMessage());
+ throw e;
+ }
+ }
+
+ protected void validateDataImpl(D data) {
+ }
+
+ protected void validateCreate(D data) {
+ }
+
+ protected void validateUpdate(D data) {
+ }
+
+ protected boolean isSameData(D existentData, D actualData) {
+ if (actualData.getId() == null) {
+ return false;
+ } else if (!existentData.getId().equals(actualData.getId())) {
+ return false;
+ }
+ return true;
+ }
+
+ protected static void validateEmail(String email) {
+ if (!emailValidator.isValid(email)) {
+ throw new DataValidationException("Invalid email address format '" + email + "'!");
+ }
+ }
+
+ protected static void validateJsonStructure(JsonNode expectedNode, JsonNode actualNode) {
+ Set<String> expectedFields = new HashSet<>();
+ Iterator<String> fieldsIterator = expectedNode.fieldNames();
+ while (fieldsIterator.hasNext()) {
+ expectedFields.add(fieldsIterator.next());
+ }
+
+ Set<String> actualFields = new HashSet<>();
+ fieldsIterator = actualNode.fieldNames();
+ while (fieldsIterator.hasNext()) {
+ actualFields.add(fieldsIterator.next());
+ }
+
+ if (!expectedFields.containsAll(actualFields) || !actualFields.containsAll(expectedFields)) {
+ throw new DataValidationException("Provided json structure is different from stored one '" + actualNode + "'!");
+ }
+
+ for (String field : actualFields) {
+ if (!actualNode.get(field).isTextual()) {
+ throw new DataValidationException("Provided json structure can't contain non-text values '" + actualNode + "'!");
+ }
+ }
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/PaginatedRemover.java b/dao/src/main/java/org/thingsboard/server/dao/service/PaginatedRemover.java
new file mode 100644
index 0000000..66914d5
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/service/PaginatedRemover.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.service;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.dao.model.BaseEntity;
+
+public abstract class PaginatedRemover<I, E extends BaseEntity<?>> {
+
+ public void removeEntitites(I id) {
+ TextPageLink pageLink = new TextPageLink(100);
+ boolean hasNext = true;
+ while (hasNext) {
+ List<E> entities = findEntities(id, pageLink);
+ for (E entity : entities) {
+ removeEntity(entity);
+ }
+ hasNext = entities.size() == pageLink.getLimit();
+ if (hasNext) {
+ int index = entities.size()-1;
+ UUID idOffset = entities.get(index).getId();
+ pageLink.setIdOffset(idOffset);
+ }
+ }
+ }
+
+ protected abstract List<E> findEntities(I id, TextPageLink pageLink);
+
+ protected abstract void removeEntity(E entity);
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java b/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java
new file mode 100644
index 0000000..7b6fbbd
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java
@@ -0,0 +1,97 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.service;
+
+import org.thingsboard.server.common.data.id.UUIDBased;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.dao.exception.IncorrectParameterException;
+
+import java.util.UUID;
+
+public class Validator {
+
+ /**
+ * This method validate <code>String</code> string. If string is invalid than throw
+ * <code>IncorrectParameterException</code> exception
+ *
+ * @param val the val
+ * @param errorMessage the error message for exception
+ */
+ public static void validateString(String val, String errorMessage) {
+ if (val == null || val.isEmpty()) {
+ throw new IncorrectParameterException(errorMessage);
+ }
+ }
+
+
+ /**
+ * This method validate <code>String</code> string. If string is invalid than throw
+ * <code>IncorrectParameterException</code> exception
+ *
+ * @param val the val
+ * @param errorMessage the error message for exception
+ */
+ public static void validatePositiveNumber(long val, String errorMessage) {
+ if (val <= 0) {
+ throw new IncorrectParameterException(errorMessage);
+ }
+ }
+
+ /**
+ * This method validate <code>UUID</code> id. If id is null than throw
+ * <code>IncorrectParameterException</code> exception
+ *
+ * @param id the id
+ * @param errorMessage the error message for exception
+ */
+ public static void validateId(UUID id, String errorMessage) {
+ if (id == null) {
+ throw new IncorrectParameterException(errorMessage);
+ }
+ }
+
+
+ /**
+ * This method validate <code>UUIDBased</code> id. If id is null than throw
+ * <code>IncorrectParameterException</code> exception
+ *
+ * @param id the id
+ * @param errorMessage the error message for exception
+ */
+ public static void validateId(UUIDBased id, String errorMessage) {
+ if (id == null || id.getId() == null) {
+ throw new IncorrectParameterException(errorMessage);
+ }
+ }
+
+ /**
+ * This method validate <code>PageLink</code> page link. If pageLink is invalid than throw
+ * <code>IncorrectParameterException</code> exception
+ *
+ * @param pageLink the page link
+ * @param errorMessage the error message for exception
+ */
+ public static void validatePageLink(TextPageLink pageLink, String errorMessage) {
+ if (pageLink == null) {
+ throw new IncorrectParameterException(errorMessage);
+ } else if (pageLink.getLimit() < 1) {
+ throw new IncorrectParameterException(errorMessage);
+ } else if (pageLink.getIdOffset() != null && pageLink.getIdOffset().version() != 1) {
+ throw new IncorrectParameterException(errorMessage);
+ }
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsDao.java
new file mode 100644
index 0000000..c8a6e90
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsDao.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.settings;
+
+import org.thingsboard.server.common.data.AdminSettings;
+import org.thingsboard.server.dao.Dao;
+import org.thingsboard.server.dao.model.AdminSettingsEntity;
+
+public interface AdminSettingsDao extends Dao<AdminSettingsEntity> {
+
+ /**
+ * Save or update admin settings object
+ *
+ * @param adminSettings the admin settings object
+ * @return saved admin settings object
+ */
+ AdminSettingsEntity save(AdminSettings adminSettings);
+
+ /**
+ * Find admin settings by key.
+ *
+ * @param key the key
+ * @return the admin settings object
+ */
+ AdminSettingsEntity findByKey(String key);
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsDaoImpl.java b/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsDaoImpl.java
new file mode 100644
index 0000000..0a99b02
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsDaoImpl.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.settings;
+
+import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
+import static org.thingsboard.server.dao.model.ModelConstants.ADMIN_SETTINGS_BY_KEY_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ADMIN_SETTINGS_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.ADMIN_SETTINGS_KEY_PROPERTY;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.AdminSettings;
+import org.thingsboard.server.dao.AbstractModelDao;
+import org.thingsboard.server.dao.model.AdminSettingsEntity;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Repository;
+
+import com.datastax.driver.core.querybuilder.Select.Where;
+
+@Component
+@Slf4j
+public class AdminSettingsDaoImpl extends AbstractModelDao<AdminSettingsEntity> implements AdminSettingsDao {
+
+ @Override
+ protected Class<AdminSettingsEntity> getColumnFamilyClass() {
+ return AdminSettingsEntity.class;
+ }
+
+ @Override
+ protected String getColumnFamilyName() {
+ return ADMIN_SETTINGS_COLUMN_FAMILY_NAME;
+ }
+
+ @Override
+ public AdminSettingsEntity save(AdminSettings adminSettings) {
+ log.debug("Save admin settings [{}] ", adminSettings);
+ return save(new AdminSettingsEntity(adminSettings));
+ }
+
+ @Override
+ public AdminSettingsEntity findByKey(String key) {
+ log.debug("Try to find admin settings by key [{}] ", key);
+ Where query = select().from(ADMIN_SETTINGS_BY_KEY_COLUMN_FAMILY_NAME).where(eq(ADMIN_SETTINGS_KEY_PROPERTY, key));
+ log.trace("Execute query {}", query);
+ AdminSettingsEntity adminSettingsEntity = findOneByStatement(query);
+ log.trace("Found admin settings [{}] by key [{}]", adminSettingsEntity, key);
+ return adminSettingsEntity;
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsService.java b/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsService.java
new file mode 100644
index 0000000..72492a8
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsService.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.settings;
+
+import org.thingsboard.server.common.data.AdminSettings;
+import org.thingsboard.server.common.data.id.AdminSettingsId;
+
+public interface AdminSettingsService {
+
+ public AdminSettings findAdminSettingsById(AdminSettingsId adminSettingsId);
+
+ public AdminSettings findAdminSettingsByKey(String key);
+
+ public AdminSettings saveAdminSettings(AdminSettings adminSettings);
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java
new file mode 100644
index 0000000..cb4ed01
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java
@@ -0,0 +1,88 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.settings;
+
+import static org.thingsboard.server.dao.DaoUtil.getData;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.thingsboard.server.common.data.AdminSettings;
+import org.thingsboard.server.common.data.id.AdminSettingsId;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.model.AdminSettingsEntity;
+import org.thingsboard.server.dao.service.DataValidator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.dao.service.Validator;
+
+@Service
+@Slf4j
+public class AdminSettingsServiceImpl implements AdminSettingsService {
+
+ @Autowired
+ private AdminSettingsDao adminSettingsDao;
+
+ @Override
+ public AdminSettings findAdminSettingsById(AdminSettingsId adminSettingsId) {
+ log.trace("Executing findAdminSettingsById [{}]", adminSettingsId);
+ Validator.validateId(adminSettingsId, "Incorrect adminSettingsId " + adminSettingsId);
+ AdminSettingsEntity adminSettingsEntity = adminSettingsDao.findById(adminSettingsId.getId());
+ return getData(adminSettingsEntity);
+ }
+
+ @Override
+ public AdminSettings findAdminSettingsByKey(String key) {
+ log.trace("Executing findAdminSettingsByKey [{}]", key);
+ Validator.validateString(key, "Incorrect key " + key);
+ AdminSettingsEntity adminSettingsEntity = adminSettingsDao.findByKey(key);
+ return getData(adminSettingsEntity);
+ }
+
+ @Override
+ public AdminSettings saveAdminSettings(AdminSettings adminSettings) {
+ log.trace("Executing saveAdminSettings [{}]", adminSettings);
+ adminSettingsValidator.validate(adminSettings);
+ AdminSettingsEntity adminSettingsEntity = adminSettingsDao.save(adminSettings);
+ return getData(adminSettingsEntity);
+ }
+
+ private DataValidator<AdminSettings> adminSettingsValidator =
+ new DataValidator<AdminSettings>() {
+
+ @Override
+ protected void validateCreate(AdminSettings adminSettings) {
+ throw new DataValidationException("Creation of new admin settings entry is prohibited!");
+ }
+
+ @Override
+ protected void validateDataImpl(AdminSettings adminSettings) {
+ if (StringUtils.isEmpty(adminSettings.getKey())) {
+ throw new DataValidationException("Key should be specified!");
+ }
+ if (adminSettings.getJsonValue() == null) {
+ throw new DataValidationException("Json value should be specified!");
+ }
+ AdminSettings existentAdminSettingsWithKey = findAdminSettingsByKey(adminSettings.getKey());
+ if (existentAdminSettingsWithKey == null || !isSameData(existentAdminSettingsWithKey, adminSettings)) {
+ throw new DataValidationException("Changing key of admin settings entry is prohibited!");
+ }
+ validateJsonStructure(existentAdminSettingsWithKey.getJsonValue(), adminSettings.getJsonValue());
+ }
+ };
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantDao.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantDao.java
new file mode 100644
index 0000000..98894d2
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantDao.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.tenant;
+
+import java.util.List;
+
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.dao.Dao;
+import org.thingsboard.server.dao.model.TenantEntity;
+
+public interface TenantDao extends Dao<TenantEntity> {
+
+ /**
+ * Save or update tenant object
+ *
+ * @param tenant the tenant object
+ * @return saved tenant object
+ */
+ TenantEntity save(Tenant tenant);
+
+ /**
+ * Find tenants by region and page link.
+ *
+ * @param region the region
+ * @param pageLink the page link
+ * @return the list of tenant objects
+ */
+ List<TenantEntity> findTenantsByRegion(String region, TextPageLink pageLink);
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantDaoImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantDaoImpl.java
new file mode 100644
index 0000000..18af130
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantDaoImpl.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.tenant;
+
+import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
+import static org.thingsboard.server.dao.model.ModelConstants.TENANT_BY_REGION_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.TENANT_COLUMN_FAMILY_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.TENANT_REGION_PROPERTY;
+
+import java.util.Arrays;
+import java.util.List;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.dao.AbstractSearchTextDao;
+import org.thingsboard.server.dao.model.TenantEntity;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Component
+@Slf4j
+public class TenantDaoImpl extends AbstractSearchTextDao<TenantEntity> implements TenantDao {
+
+ @Override
+ protected Class<TenantEntity> getColumnFamilyClass() {
+ return TenantEntity.class;
+ }
+
+ @Override
+ protected String getColumnFamilyName() {
+ return TENANT_COLUMN_FAMILY_NAME;
+ }
+
+ @Override
+ public TenantEntity save(Tenant tenant) {
+ log.debug("Save tenant [{}] ", tenant);
+ return save(new TenantEntity(tenant));
+ }
+
+ @Override
+ public List<TenantEntity> findTenantsByRegion(String region, TextPageLink pageLink) {
+ log.debug("Try to find tenants by region [{}] and pageLink [{}]", region, pageLink);
+ List<TenantEntity> tenantEntities = findPageWithTextSearch(TENANT_BY_REGION_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
+ Arrays.asList(eq(TENANT_REGION_PROPERTY, region)),
+ pageLink);
+ log.trace("Found tenants [{}] by region [{}] and pageLink [{}]", tenantEntities, region, pageLink);
+ return tenantEntities;
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java
new file mode 100644
index 0000000..2778415
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.tenant;
+
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+
+public interface TenantService {
+
+ public Tenant findTenantById(TenantId tenantId);
+
+ public Tenant saveTenant(Tenant tenant);
+
+ public void deleteTenant(TenantId tenantId);
+
+ public TextPageData<Tenant> findTenants(TextPageLink pageLink);
+
+ //public TextPageData<Tenant> findTenantsByTitle(String title, PageLink pageLink);
+
+ public void deleteTenants();
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
new file mode 100644
index 0000000..622adf7
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
@@ -0,0 +1,148 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.tenant;
+
+import static org.thingsboard.server.dao.DaoUtil.convertDataList;
+import static org.thingsboard.server.dao.DaoUtil.getData;
+
+import java.util.List;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.dao.customer.CustomerService;
+import org.thingsboard.server.dao.dashboard.DashboardService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.model.TenantEntity;
+import org.thingsboard.server.dao.plugin.PluginService;
+import org.thingsboard.server.dao.rule.RuleService;
+import org.thingsboard.server.dao.service.DataValidator;
+import org.thingsboard.server.dao.service.PaginatedRemover;
+import org.thingsboard.server.dao.user.UserService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.dao.service.Validator;
+import org.thingsboard.server.dao.widget.WidgetsBundleService;
+
+@Service
+@Slf4j
+public class TenantServiceImpl implements TenantService {
+
+ private static final String DEFAULT_TENANT_REGION = "Global";
+
+ @Autowired
+ private TenantDao tenantDao;
+
+ @Autowired
+ private UserService userService;
+
+ @Autowired
+ private CustomerService customerService;
+
+ @Autowired
+ private DeviceService deviceService;
+
+ @Autowired
+ private WidgetsBundleService widgetsBundleService;
+
+ @Autowired
+ private DashboardService dashboardService;
+
+ @Autowired
+ private RuleService ruleService;
+
+ @Autowired
+ private PluginService pluginService;
+
+ @Override
+ public Tenant findTenantById(TenantId tenantId) {
+ log.trace("Executing findTenantById [{}]", tenantId);
+ Validator.validateId(tenantId, "Incorrect tenantId " + tenantId);
+ TenantEntity tenantEntity = tenantDao.findById(tenantId.getId());
+ return getData(tenantEntity);
+ }
+
+ @Override
+ public Tenant saveTenant(Tenant tenant) {
+ log.trace("Executing saveTenant [{}]", tenant);
+ tenant.setRegion(DEFAULT_TENANT_REGION);
+ tenantValidator.validate(tenant);
+ TenantEntity tenantEntity = tenantDao.save(tenant);
+ return getData(tenantEntity);
+ }
+
+ @Override
+ public void deleteTenant(TenantId tenantId) {
+ log.trace("Executing deleteTenant [{}]", tenantId);
+ Validator.validateId(tenantId, "Incorrect tenantId " + tenantId);
+ customerService.deleteCustomersByTenantId(tenantId);
+ widgetsBundleService.deleteWidgetsBundlesByTenantId(tenantId);
+ dashboardService.deleteDashboardsByTenantId(tenantId);
+ deviceService.deleteDevicesByTenantId(tenantId);
+ userService.deleteTenantAdmins(tenantId);
+ ruleService.deleteRulesByTenantId(tenantId);
+ pluginService.deletePluginsByTenantId(tenantId);
+ tenantDao.removeById(tenantId.getId());
+ }
+
+ @Override
+ public TextPageData<Tenant> findTenants(TextPageLink pageLink) {
+ log.trace("Executing findTenants pageLink [{}]", pageLink);
+ Validator.validatePageLink(pageLink, "Incorrect page link " + pageLink);
+ List<TenantEntity> tenantEntities = tenantDao.findTenantsByRegion(DEFAULT_TENANT_REGION, pageLink);
+ List<Tenant> tenants = convertDataList(tenantEntities);
+ return new TextPageData<Tenant>(tenants, pageLink);
+ }
+
+ @Override
+ public void deleteTenants() {
+ log.trace("Executing deleteTenants");
+ tenantsRemover.removeEntitites(DEFAULT_TENANT_REGION);
+ }
+
+ private DataValidator<Tenant> tenantValidator =
+ new DataValidator<Tenant>() {
+ @Override
+ protected void validateDataImpl(Tenant tenant) {
+ if (StringUtils.isEmpty(tenant.getTitle())) {
+ throw new DataValidationException("Tenant title should be specified!");
+ }
+ if (!StringUtils.isEmpty(tenant.getEmail())) {
+ validateEmail(tenant.getEmail());
+ }
+ }
+ };
+
+ private PaginatedRemover<String, TenantEntity> tenantsRemover =
+ new PaginatedRemover<String, TenantEntity>() {
+
+ @Override
+ protected List<TenantEntity> findEntities(String region, TextPageLink pageLink) {
+ return tenantDao.findTenantsByRegion(region, pageLink);
+ }
+
+ @Override
+ protected void removeEntity(TenantEntity entity) {
+ deleteTenant(new TenantId(entity.getId()));
+ }
+ };
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserCredentialsDao.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserCredentialsDao.java
new file mode 100644
index 0000000..9913df8
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserCredentialsDao.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.user;
+
+import java.util.UUID;
+
+import org.thingsboard.server.common.data.security.UserCredentials;
+import org.thingsboard.server.dao.Dao;
+import org.thingsboard.server.dao.model.UserCredentialsEntity;
+
+/**
+ * The Interface UserCredentialsDao.
+ *
+ * @param <T> the generic type
+ */
+public interface UserCredentialsDao extends Dao<UserCredentialsEntity> {
+
+ /**
+ * Save or update user credentials object
+ *
+ * @param userCredentials the user credentials object
+ * @return saved user credentials object
+ */
+ UserCredentialsEntity save(UserCredentials userCredentials);
+
+ /**
+ * Find user credentials by user id.
+ *
+ * @param userId the user id
+ * @return the user credentials object
+ */
+ UserCredentialsEntity findByUserId(UUID userId);
+
+ /**
+ * Find user credentials by activate token.
+ *
+ * @param activateToken the activate token
+ * @return the user credentials object
+ */
+ UserCredentialsEntity findByActivateToken(String activateToken);
+
+ /**
+ * Find user credentials by reset token.
+ *
+ * @param resetToken the reset token
+ * @return the user credentials object
+ */
+ UserCredentialsEntity findByResetToken(String resetToken);
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserCredentialsDaoImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserCredentialsDaoImpl.java
new file mode 100644
index 0000000..65efdf4
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserCredentialsDaoImpl.java
@@ -0,0 +1,87 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.user;
+
+import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
+
+import java.util.UUID;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.security.UserCredentials;
+import org.thingsboard.server.dao.AbstractModelDao;
+import org.thingsboard.server.dao.model.UserCredentialsEntity;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Repository;
+
+import com.datastax.driver.core.querybuilder.Select.Where;
+import org.thingsboard.server.dao.model.ModelConstants;
+
+@Component
+@Slf4j
+public class UserCredentialsDaoImpl extends AbstractModelDao<UserCredentialsEntity> implements UserCredentialsDao {
+
+ @Override
+ protected Class<UserCredentialsEntity> getColumnFamilyClass() {
+ return UserCredentialsEntity.class;
+ }
+
+ @Override
+ protected String getColumnFamilyName() {
+ return ModelConstants.USER_CREDENTIALS_COLUMN_FAMILY_NAME;
+ }
+
+ @Override
+ public UserCredentialsEntity save(UserCredentials userCredentials) {
+ log.debug("Save user credentials [{}] ", userCredentials);
+ return save(new UserCredentialsEntity(userCredentials));
+ }
+
+ @Override
+ public UserCredentialsEntity findByUserId(UUID userId) {
+ log.debug("Try to find user credentials by userId [{}] ", userId);
+ Where query = select().from(ModelConstants.USER_CREDENTIALS_BY_USER_COLUMN_FAMILY_NAME).where(eq(ModelConstants.USER_CREDENTIALS_USER_ID_PROPERTY, userId));
+ log.trace("Execute query {}", query);
+ UserCredentialsEntity userCredentialsEntity = findOneByStatement(query);
+ log.trace("Found user credentials [{}] by userId [{}]", userCredentialsEntity, userId);
+ return userCredentialsEntity;
+ }
+
+ @Override
+ public UserCredentialsEntity findByActivateToken(String activateToken) {
+ log.debug("Try to find user credentials by activateToken [{}] ", activateToken);
+ Where query = select().from(ModelConstants.USER_CREDENTIALS_BY_ACTIVATE_TOKEN_COLUMN_FAMILY_NAME)
+ .where(eq(ModelConstants.USER_CREDENTIALS_ACTIVATE_TOKEN_PROPERTY, activateToken));
+ log.trace("Execute query {}", query);
+ UserCredentialsEntity userCredentialsEntity = findOneByStatement(query);
+ log.trace("Found user credentials [{}] by activateToken [{}]", userCredentialsEntity, activateToken);
+ return userCredentialsEntity;
+ }
+
+ @Override
+ public UserCredentialsEntity findByResetToken(String resetToken) {
+ log.debug("Try to find user credentials by resetToken [{}] ", resetToken);
+ Where query = select().from(ModelConstants.USER_CREDENTIALS_BY_RESET_TOKEN_COLUMN_FAMILY_NAME)
+ .where(eq(ModelConstants.USER_CREDENTIALS_RESET_TOKEN_PROPERTY, resetToken));
+ log.trace("Execute query {}", query);
+ UserCredentialsEntity userCredentialsEntity = findOneByStatement(query);
+ log.trace("Found user credentials [{}] by resetToken [{}]", userCredentialsEntity, resetToken);
+ return userCredentialsEntity;
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java
new file mode 100644
index 0000000..45e2f51
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.user;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.dao.Dao;
+import org.thingsboard.server.dao.model.UserEntity;
+
+public interface UserDao extends Dao<UserEntity> {
+
+ /**
+ * Save or update user object
+ *
+ * @param user the user object
+ * @return saved user entity
+ */
+ UserEntity save(User user);
+
+ /**
+ * Find user by email.
+ *
+ * @param email the email
+ * @return the user entity
+ */
+ UserEntity findByEmail(String email);
+
+ /**
+ * Find tenant admin users by tenantId and page link.
+ *
+ * @param tenantId the tenantId
+ * @param pageLink the page link
+ * @return the list of user entities
+ */
+ List<UserEntity> findTenantAdmins(UUID tenantId, TextPageLink pageLink);
+
+ /**
+ * Find customer users by tenantId, customerId and page link.
+ *
+ * @param tenantId the tenantId
+ * @param customerId the customerId
+ * @param pageLink the page link
+ * @return the list of user entities
+ */
+ List<UserEntity> findCustomerUsers(UUID tenantId, UUID customerId, TextPageLink pageLink);
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserDaoImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserDaoImpl.java
new file mode 100644
index 0000000..16f73fe
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserDaoImpl.java
@@ -0,0 +1,92 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.user;
+
+import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.dao.AbstractSearchTextDao;
+import org.thingsboard.server.dao.model.UserEntity;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.datastax.driver.core.querybuilder.Select.Where;
+import org.thingsboard.server.dao.model.ModelConstants;
+
+@Component
+@Slf4j
+public class UserDaoImpl extends AbstractSearchTextDao<UserEntity> implements UserDao {
+
+ @Override
+ protected Class<UserEntity> getColumnFamilyClass() {
+ return UserEntity.class;
+ }
+
+ @Override
+ protected String getColumnFamilyName() {
+ return ModelConstants.USER_COLUMN_FAMILY_NAME;
+ }
+
+ @Override
+ public UserEntity findByEmail(String email) {
+ log.debug("Try to find user by email [{}] ", email);
+ Where query = select().from(ModelConstants.USER_BY_EMAIL_COLUMN_FAMILY_NAME).where(eq(ModelConstants.USER_EMAIL_PROPERTY, email));
+ log.trace("Execute query {}", query);
+ UserEntity userEntity = findOneByStatement(query);
+ log.trace("Found user [{}] by email [{}]", userEntity, email);
+ return userEntity;
+ }
+
+ @Override
+ public UserEntity save(User user) {
+ log.debug("Save user [{}] ", user);
+ return save(new UserEntity(user));
+ }
+
+ @Override
+ public List<UserEntity> findTenantAdmins(UUID tenantId, TextPageLink pageLink) {
+ log.debug("Try to find tenant admin users by tenantId [{}] and pageLink [{}]", tenantId, pageLink);
+ List<UserEntity> userEntities = findPageWithTextSearch(ModelConstants.USER_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
+ Arrays.asList(eq(ModelConstants.USER_TENANT_ID_PROPERTY, tenantId),
+ eq(ModelConstants.USER_CUSTOMER_ID_PROPERTY, ModelConstants.NULL_UUID),
+ eq(ModelConstants.USER_AUTHORITY_PROPERTY, Authority.TENANT_ADMIN.name())),
+ pageLink);
+ log.trace("Found tenant admin users [{}] by tenantId [{}] and pageLink [{}]", userEntities, tenantId, pageLink);
+ return userEntities;
+ }
+
+ @Override
+ public List<UserEntity> findCustomerUsers(UUID tenantId, UUID customerId, TextPageLink pageLink) {
+ log.debug("Try to find customer users by tenantId [{}], customerId [{}] and pageLink [{}]", tenantId, customerId, pageLink);
+ List<UserEntity> userEntities = findPageWithTextSearch(ModelConstants.USER_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
+ Arrays.asList(eq(ModelConstants.USER_TENANT_ID_PROPERTY, tenantId),
+ eq(ModelConstants.USER_CUSTOMER_ID_PROPERTY, customerId),
+ eq(ModelConstants.USER_AUTHORITY_PROPERTY, Authority.CUSTOMER_USER.name())),
+ pageLink);
+ log.trace("Found customer users [{}] by tenantId [{}], customerId [{}] and pageLink [{}]", userEntities, tenantId, customerId, pageLink);
+ return userEntities;
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserService.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserService.java
new file mode 100644
index 0000000..1ad77c9
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserService.java
@@ -0,0 +1,56 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.user;
+
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UserId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.security.UserCredentials;
+
+public interface UserService {
+
+ public User findUserById(UserId userId);
+
+ public User findUserByEmail(String email);
+
+ public User saveUser(User user);
+
+ public UserCredentials findUserCredentialsByUserId(UserId userId);
+
+ public UserCredentials findUserCredentialsByActivateToken(String activateToken);
+
+ public UserCredentials findUserCredentialsByResetToken(String resetToken);
+
+ public UserCredentials saveUserCredentials(UserCredentials userCredentials);
+
+ public UserCredentials activateUserCredentials(String activateToken, String password);
+
+ public UserCredentials requestPasswordReset(String email);
+
+ public void deleteUser(UserId userId);
+
+ public TextPageData<User> findTenantAdmins(TenantId tenantId, TextPageLink pageLink);
+
+ public void deleteTenantAdmins(TenantId tenantId);
+
+ public TextPageData<User> findCustomerUsers(TenantId tenantId, CustomerId customerId, TextPageLink pageLink);
+
+ public void deleteCustomerUsers(TenantId tenantId, CustomerId customerId);
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
new file mode 100644
index 0000000..17784d1
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
@@ -0,0 +1,352 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.user;
+
+import static org.thingsboard.server.dao.DaoUtil.convertDataList;
+import static org.thingsboard.server.dao.DaoUtil.getData;
+import static org.thingsboard.server.dao.service.Validator.validateId;
+import static org.thingsboard.server.dao.service.Validator.validatePageLink;
+import static org.thingsboard.server.dao.service.Validator.validateString;
+
+import java.util.List;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UserId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.common.data.security.UserCredentials;
+import org.thingsboard.server.dao.customer.CustomerDao;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.exception.IncorrectParameterException;
+import org.thingsboard.server.dao.model.*;
+import org.thingsboard.server.dao.service.DataValidator;
+import org.thingsboard.server.dao.service.PaginatedRemover;
+import org.thingsboard.server.dao.tenant.TenantDao;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+@Slf4j
+public class UserServiceImpl implements UserService {
+
+ @Autowired
+ private UserDao userDao;
+
+ @Autowired
+ private UserCredentialsDao userCredentialsDao;
+
+ @Autowired
+ private TenantDao tenantDao;
+
+ @Autowired
+ private CustomerDao customerDao;
+
+ @Override
+ public User findUserByEmail(String email) {
+ log.trace("Executing findUserByEmail [{}]", email);
+ validateString(email, "Incorrect email " + email);
+ UserEntity userEntity = userDao.findByEmail(email);
+ return getData(userEntity);
+ }
+
+ @Override
+ public User findUserById(UserId userId) {
+ log.trace("Executing findUserById [{}]", userId);
+ validateId(userId, "Incorrect userId " + userId);
+ UserEntity userEntity = userDao.findById(userId.getId());
+ return getData(userEntity);
+ }
+
+ @Override
+ public User saveUser(User user) {
+ log.trace("Executing saveUser [{}]", user);
+ userValidator.validate(user);
+ UserEntity userEntity = userDao.save(user);
+ if (user.getId() == null) {
+ UserCredentials userCredentials = new UserCredentials();
+ userCredentials.setEnabled(false);
+ userCredentials.setActivateToken(RandomStringUtils.randomAlphanumeric(30));
+ userCredentials.setUserId(new UserId(userEntity.getId()));
+ userCredentialsDao.save(userCredentials);
+ }
+ return getData(userEntity);
+ }
+
+ @Override
+ public UserCredentials findUserCredentialsByUserId(UserId userId) {
+ log.trace("Executing findUserCredentialsByUserId [{}]", userId);
+ validateId(userId, "Incorrect userId " + userId);
+ UserCredentialsEntity userCredentialsEntity = userCredentialsDao.findByUserId(userId.getId());
+ return getData(userCredentialsEntity);
+ }
+
+ @Override
+ public UserCredentials findUserCredentialsByActivateToken(String activateToken) {
+ log.trace("Executing findUserCredentialsByActivateToken [{}]", activateToken);
+ validateString(activateToken, "Incorrect activateToken " + activateToken);
+ UserCredentialsEntity userCredentialsEntity = userCredentialsDao.findByActivateToken(activateToken);
+ return getData(userCredentialsEntity);
+ }
+
+ @Override
+ public UserCredentials findUserCredentialsByResetToken(String resetToken) {
+ log.trace("Executing findUserCredentialsByResetToken [{}]", resetToken);
+ validateString(resetToken, "Incorrect resetToken " + resetToken);
+ UserCredentialsEntity userCredentialsEntity = userCredentialsDao.findByResetToken(resetToken);
+ return getData(userCredentialsEntity);
+ }
+
+ @Override
+ public UserCredentials saveUserCredentials(UserCredentials userCredentials) {
+ log.trace("Executing saveUserCredentials [{}]", userCredentials);
+ userCredentialsValidator.validate(userCredentials);
+ UserCredentialsEntity userCredentialsEntity = userCredentialsDao.save(userCredentials);
+ return getData(userCredentialsEntity);
+ }
+
+ @Override
+ public UserCredentials activateUserCredentials(String activateToken, String password) {
+ log.trace("Executing activateUserCredentials activateToken [{}], password [{}]", activateToken, password);
+ validateString(activateToken, "Incorrect activateToken " + activateToken);
+ validateString(password, "Incorrect password " + password);
+ UserCredentialsEntity userCredentialsEntity = userCredentialsDao.findByActivateToken(activateToken);
+ if (userCredentialsEntity == null) {
+ throw new IncorrectParameterException(String.format("Unable to find user credentials by activateToken [%s]", activateToken));
+ }
+ UserCredentials userCredentials = getData(userCredentialsEntity);
+ if (userCredentials.isEnabled()) {
+ throw new IncorrectParameterException("User credentials already activated");
+ }
+ userCredentials.setEnabled(true);
+ userCredentials.setActivateToken(null);
+ userCredentials.setPassword(password);
+
+ return saveUserCredentials(userCredentials);
+ }
+
+ @Override
+ public UserCredentials requestPasswordReset(String email) {
+ log.trace("Executing requestPasswordReset email [{}]", email);
+ validateString(email, "Incorrect email " + email);
+ UserEntity userEntity = userDao.findByEmail(email);
+ if (userEntity == null) {
+ throw new IncorrectParameterException(String.format("Unable to find user by email [%s]", email));
+ }
+ UserCredentialsEntity userCredentialsEntity = userCredentialsDao.findByUserId(userEntity.getId());
+ UserCredentials userCredentials = getData(userCredentialsEntity);
+ if (!userCredentials.isEnabled()) {
+ throw new IncorrectParameterException("Unable to reset password for unactive user");
+ }
+ userCredentials.setResetToken(RandomStringUtils.randomAlphanumeric(30));
+ return saveUserCredentials(userCredentials);
+ }
+
+
+ @Override
+ public void deleteUser(UserId userId) {
+ log.trace("Executing deleteUser [{}]", userId);
+ validateId(userId, "Incorrect userId " + userId);
+ UserCredentialsEntity userCredentialsEntity = userCredentialsDao.findByUserId(userId.getId());
+ userCredentialsDao.removeById(userCredentialsEntity.getId());
+ userDao.removeById(userId.getId());
+ }
+
+ @Override
+ public TextPageData<User> findTenantAdmins(TenantId tenantId, TextPageLink pageLink) {
+ log.trace("Executing findTenantAdmins, tenantId [{}], pageLink [{}]", tenantId, pageLink);
+ validateId(tenantId, "Incorrect tenantId " + tenantId);
+ validatePageLink(pageLink, "Incorrect page link " + pageLink);
+ List<UserEntity> userEntities = userDao.findTenantAdmins(tenantId.getId(), pageLink);
+ List<User> users = convertDataList(userEntities);
+ return new TextPageData<User>(users, pageLink);
+ }
+
+ @Override
+ public void deleteTenantAdmins(TenantId tenantId) {
+ log.trace("Executing deleteTenantAdmins, tenantId [{}]", tenantId);
+ validateId(tenantId, "Incorrect tenantId " + tenantId);
+ tenantAdminsRemover.removeEntitites(tenantId);
+ }
+
+ @Override
+ public TextPageData<User> findCustomerUsers(TenantId tenantId, CustomerId customerId, TextPageLink pageLink) {
+ log.trace("Executing findCustomerUsers, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink);
+ validateId(tenantId, "Incorrect tenantId " + tenantId);
+ validateId(customerId, "Incorrect customerId " + customerId);
+ validatePageLink(pageLink, "Incorrect page link " + pageLink);
+ List<UserEntity> userEntities = userDao.findCustomerUsers(tenantId.getId(), customerId.getId(), pageLink);
+ List<User> users = convertDataList(userEntities);
+ return new TextPageData<User>(users, pageLink);
+ }
+
+ @Override
+ public void deleteCustomerUsers(TenantId tenantId, CustomerId customerId) {
+ log.trace("Executing deleteCustomerUsers, customerId [{}]", customerId);
+ validateId(tenantId, "Incorrect tenantId " + tenantId);
+ validateId(customerId, "Incorrect customerId " + customerId);
+ new CustomerUsersRemover(tenantId).removeEntitites(customerId);
+ }
+
+ private DataValidator<User> userValidator =
+ new DataValidator<User>() {
+ @Override
+ protected void validateDataImpl(User user) {
+ if (StringUtils.isEmpty(user.getEmail())) {
+ throw new DataValidationException("User email should be specified!");
+ }
+
+ validateEmail(user.getEmail());
+
+ Authority authority = user.getAuthority();
+ if (authority == null) {
+ throw new DataValidationException("User authority isn't defined!");
+ }
+ TenantId tenantId = user.getTenantId();
+ if (tenantId == null) {
+ tenantId = new TenantId(ModelConstants.NULL_UUID);
+ user.setTenantId(tenantId);
+ }
+ CustomerId customerId = user.getCustomerId();
+ if (customerId == null) {
+ customerId = new CustomerId(ModelConstants.NULL_UUID);
+ user.setCustomerId(customerId);
+ }
+
+ switch (authority) {
+ case SYS_ADMIN:
+ if (user.getId() == null) {
+ throw new DataValidationException("Creation of system administrator is prohibited!");
+ }
+ if (!tenantId.getId().equals(ModelConstants.NULL_UUID)
+ || !customerId.getId().equals(ModelConstants.NULL_UUID)) {
+ throw new DataValidationException("System administrator can't be assigned neither to tenant nor to customer!");
+ }
+ break;
+ case TENANT_ADMIN:
+ if (tenantId.getId().equals(ModelConstants.NULL_UUID)) {
+ throw new DataValidationException("Tenant administrator should be assigned to tenant!");
+ } else if (!customerId.getId().equals(ModelConstants.NULL_UUID)) {
+ throw new DataValidationException("Tenant administrator can't be assigned to customer!");
+ }
+ break;
+ case CUSTOMER_USER:
+ if (tenantId.getId().equals(ModelConstants.NULL_UUID)
+ || customerId.getId().equals(ModelConstants.NULL_UUID) ) {
+ throw new DataValidationException("Customer user should be assigned to customer!");
+ }
+ break;
+ default:
+ break;
+ }
+
+ User existentUserWithEmail = findUserByEmail(user.getEmail());
+ if (existentUserWithEmail != null && !isSameData(existentUserWithEmail, user)) {
+ throw new DataValidationException("User with email '" + user.getEmail() + "' "
+ + " already present in database!");
+ }
+ if (!tenantId.getId().equals(ModelConstants.NULL_UUID)) {
+ TenantEntity tenant = tenantDao.findById(user.getTenantId().getId());
+ if (tenant == null) {
+ throw new DataValidationException("User is referencing to non-existent tenant!");
+ }
+ }
+ if (!customerId.getId().equals(ModelConstants.NULL_UUID)) {
+ CustomerEntity customer = customerDao.findById(user.getCustomerId().getId());
+ if (customer == null) {
+ throw new DataValidationException("User is referencing to non-existent customer!");
+ } else if (!customer.getTenantId().equals(tenantId.getId())) {
+ throw new DataValidationException("User can't be assigned to customer from different tenant!");
+ }
+ }
+ }
+ };
+
+ private DataValidator<UserCredentials> userCredentialsValidator =
+ new DataValidator<UserCredentials>() {
+
+ @Override
+ protected void validateCreate(UserCredentials userCredentials) {
+ throw new IncorrectParameterException("Creation of new user credentials is prohibited.");
+ }
+
+ @Override
+ protected void validateDataImpl(UserCredentials userCredentials) {
+ if (userCredentials.getUserId() == null) {
+ throw new DataValidationException("User credentials should be assigned to user!");
+ }
+ if (userCredentials.isEnabled()) {
+ if (StringUtils.isEmpty(userCredentials.getPassword())) {
+ throw new DataValidationException("Enabled user credentials should have password!");
+ }
+ if (StringUtils.isNotEmpty(userCredentials.getActivateToken())) {
+ throw new DataValidationException("Enabled user credentials can't have activate token!");
+ }
+ }
+ UserCredentialsEntity existingUserCredentialsEntity = userCredentialsDao.findById(userCredentials.getId().getId());
+ if (existingUserCredentialsEntity == null) {
+ throw new DataValidationException("Unable to update non-existent user credentials!");
+ }
+ User user = findUserById(userCredentials.getUserId());
+ if (user == null) {
+ throw new DataValidationException("Can't assign user credentials to non-existent user!");
+ }
+ }
+ };
+
+ private PaginatedRemover<TenantId, UserEntity> tenantAdminsRemover =
+ new PaginatedRemover<TenantId, UserEntity>() {
+
+ @Override
+ protected List<UserEntity> findEntities(TenantId id, TextPageLink pageLink) {
+ return userDao.findTenantAdmins(id.getId(), pageLink);
+ }
+
+ @Override
+ protected void removeEntity(UserEntity entity) {
+ deleteUser(new UserId(entity.getId()));
+ }
+ };
+
+ class CustomerUsersRemover extends PaginatedRemover<CustomerId, UserEntity> {
+
+ private TenantId tenantId;
+
+ CustomerUsersRemover(TenantId tenantId) {
+ this.tenantId = tenantId;
+ }
+
+ @Override
+ protected List<UserEntity> findEntities(CustomerId id, TextPageLink pageLink) {
+ return userDao.findCustomerUsers(tenantId.getId(), id.getId(), pageLink);
+
+ }
+
+ @Override
+ protected void removeEntity(UserEntity entity) {
+ deleteUser(new UserId(entity.getId()));
+ }
+
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDao.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDao.java
new file mode 100644
index 0000000..d047d7d
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDao.java
@@ -0,0 +1,77 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.widget;
+
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.widget.WidgetsBundle;
+import org.thingsboard.server.dao.Dao;
+import org.thingsboard.server.dao.model.WidgetsBundleEntity;
+
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * The Interface WidgetsBundleDao.
+ *
+ * @param <T> the generic type
+ */
+public interface WidgetsBundleDao extends Dao<WidgetsBundleEntity> {
+
+ /**
+ * Save or update widgets bundle object
+ *
+ * @param widgetsBundle the widgets bundle object
+ * @return saved widgets bundle object
+ */
+ WidgetsBundleEntity save(WidgetsBundle widgetsBundle);
+
+ /**
+ * Find widgets bundle by tenantId and alias.
+ *
+ * @param tenantId the tenantId
+ * @param alias the alias
+ * @return the widgets bundle object
+ */
+ WidgetsBundleEntity findWidgetsBundleByTenantIdAndAlias(UUID tenantId, String alias);
+
+ /**
+ * Find system widgets bundles by page link.
+ *
+ * @param pageLink the page link
+ * @return the list of widgets bundles objects
+ */
+ List<WidgetsBundleEntity> findSystemWidgetsBundles(TextPageLink pageLink);
+
+ /**
+ * Find tenant widgets bundles by tenantId and page link.
+ *
+ * @param tenantId the tenantId
+ * @param pageLink the page link
+ * @return the list of widgets bundles objects
+ */
+ List<WidgetsBundleEntity> findTenantWidgetsBundlesByTenantId(UUID tenantId, TextPageLink pageLink);
+
+ /**
+ * Find all tenant widgets bundles (including system) by tenantId and page link.
+ *
+ * @param tenantId the tenantId
+ * @param pageLink the page link
+ * @return the list of widgets bundles objects
+ */
+ List<WidgetsBundleEntity> findAllTenantWidgetsBundlesByTenantId(UUID tenantId, TextPageLink pageLink);
+
+}
+
diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDaoImpl.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDaoImpl.java
new file mode 100644
index 0000000..3de8bd3
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDaoImpl.java
@@ -0,0 +1,101 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.widget;
+
+import com.datastax.driver.core.querybuilder.Select;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.widget.WidgetsBundle;
+import org.thingsboard.server.dao.AbstractSearchTextDao;
+import org.thingsboard.server.dao.model.WidgetsBundleEntity;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+
+import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.in;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
+import static org.thingsboard.server.dao.model.ModelConstants.*;
+
+@Component
+@Slf4j
+public class WidgetsBundleDaoImpl extends AbstractSearchTextDao<WidgetsBundleEntity> implements WidgetsBundleDao {
+
+ @Override
+ protected Class<WidgetsBundleEntity> getColumnFamilyClass() {
+ return WidgetsBundleEntity.class;
+ }
+
+ @Override
+ protected String getColumnFamilyName() {
+ return WIDGETS_BUNDLE_COLUMN_FAMILY_NAME;
+ }
+
+ @Override
+ public WidgetsBundleEntity save(WidgetsBundle widgetsBundle) {
+ log.debug("Save widgets bundle [{}] ", widgetsBundle);
+ return save(new WidgetsBundleEntity(widgetsBundle));
+ }
+
+ @Override
+ public WidgetsBundleEntity findWidgetsBundleByTenantIdAndAlias(UUID tenantId, String alias) {
+ log.debug("Try to find widgets bundle by tenantId [{}] and alias [{}]", tenantId, alias);
+ Select.Where query = select().from(WIDGETS_BUNDLE_BY_TENANT_AND_ALIAS_COLUMN_FAMILY_NAME)
+ .where()
+ .and(eq(WIDGETS_BUNDLE_TENANT_ID_PROPERTY, tenantId))
+ .and(eq(WIDGETS_BUNDLE_ALIAS_PROPERTY, alias));
+ log.trace("Execute query {}", query);
+ WidgetsBundleEntity widgetsBundleEntity = findOneByStatement(query);
+ log.trace("Found widgets bundle [{}] by tenantId [{}] and alias [{}]",
+ widgetsBundleEntity, tenantId, alias);
+ return widgetsBundleEntity;
+ }
+
+ @Override
+ public List<WidgetsBundleEntity> findSystemWidgetsBundles(TextPageLink pageLink) {
+ log.debug("Try to find system widgets bundles by pageLink [{}]", pageLink);
+ List<WidgetsBundleEntity> widgetsBundlesEntities = findPageWithTextSearch(WIDGETS_BUNDLE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
+ Arrays.asList(eq(WIDGETS_BUNDLE_TENANT_ID_PROPERTY, NULL_UUID)),
+ pageLink);
+ log.trace("Found system widgets bundles [{}] by pageLink [{}]", widgetsBundlesEntities, pageLink);
+ return widgetsBundlesEntities;
+ }
+
+ @Override
+ public List<WidgetsBundleEntity> findTenantWidgetsBundlesByTenantId(UUID tenantId, TextPageLink pageLink) {
+ log.debug("Try to find tenant widgets bundles by tenantId [{}] and pageLink [{}]", tenantId, pageLink);
+ List<WidgetsBundleEntity> widgetsBundlesEntities = findPageWithTextSearch(WIDGETS_BUNDLE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
+ Arrays.asList(eq(WIDGETS_BUNDLE_TENANT_ID_PROPERTY, tenantId)),
+ pageLink);
+ log.trace("Found tenant widgets bundles [{}] by tenantId [{}] and pageLink [{}]", widgetsBundlesEntities, tenantId, pageLink);
+ return widgetsBundlesEntities;
+ }
+
+ @Override
+ public List<WidgetsBundleEntity> findAllTenantWidgetsBundlesByTenantId(UUID tenantId, TextPageLink pageLink) {
+ log.debug("Try to find all tenant widgets bundles by tenantId [{}] and pageLink [{}]", tenantId, pageLink);
+ List<WidgetsBundleEntity> widgetsBundlesEntities = findPageWithTextSearch(WIDGETS_BUNDLE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
+ Arrays.asList(in(WIDGETS_BUNDLE_TENANT_ID_PROPERTY, Arrays.asList(NULL_UUID, tenantId))),
+ pageLink);
+ log.trace("Found all tenant widgets bundles [{}] by tenantId [{}] and pageLink [{}]", widgetsBundlesEntities, tenantId, pageLink);
+ return widgetsBundlesEntities;
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java
new file mode 100644
index 0000000..ac316e9
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.widget;
+
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.WidgetsBundleId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.widget.WidgetsBundle;
+
+import java.util.List;
+
+public interface WidgetsBundleService {
+
+ public WidgetsBundle findWidgetsBundleById(WidgetsBundleId widgetsBundleId);
+
+ public WidgetsBundle saveWidgetsBundle(WidgetsBundle widgetsBundle);
+
+ public void deleteWidgetsBundle(WidgetsBundleId widgetsBundleId);
+
+ public WidgetsBundle findWidgetsBundleByTenantIdAndAlias(TenantId tenantId, String alias);
+
+ public TextPageData<WidgetsBundle> findSystemWidgetsBundlesByPageLink(TextPageLink pageLink);
+
+ public List<WidgetsBundle> findSystemWidgetsBundles();
+
+ public TextPageData<WidgetsBundle> findTenantWidgetsBundlesByTenantId(TenantId tenantId, TextPageLink pageLink);
+
+ public TextPageData<WidgetsBundle> findAllTenantWidgetsBundlesByTenantIdAndPageLink(TenantId tenantId, TextPageLink pageLink);
+
+ public List<WidgetsBundle> findAllTenantWidgetsBundlesByTenantId(TenantId tenantId);
+
+ public void deleteWidgetsBundlesByTenantId(TenantId tenantId);
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java
new file mode 100644
index 0000000..8f1d0ed
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java
@@ -0,0 +1,225 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.widget;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.WidgetsBundleId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.widget.WidgetsBundle;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.exception.IncorrectParameterException;
+import org.thingsboard.server.dao.model.ModelConstants;
+import org.thingsboard.server.dao.model.TenantEntity;
+import org.thingsboard.server.dao.model.WidgetsBundleEntity;
+import org.thingsboard.server.dao.service.DataValidator;
+import org.thingsboard.server.dao.service.PaginatedRemover;
+import org.thingsboard.server.dao.service.Validator;
+import org.thingsboard.server.dao.tenant.TenantDao;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.thingsboard.server.dao.DaoUtil.convertDataList;
+import static org.thingsboard.server.dao.DaoUtil.getData;
+
+@Service
+@Slf4j
+public class WidgetsBundleServiceImpl implements WidgetsBundleService {
+
+ @Autowired
+ private WidgetsBundleDao widgetsBundleDao;
+
+ @Autowired
+ private TenantDao tenantDao;
+
+ @Autowired
+ private WidgetTypeService widgetTypeService;
+
+ @Override
+ public WidgetsBundle findWidgetsBundleById(WidgetsBundleId widgetsBundleId) {
+ log.trace("Executing findWidgetsBundleById [{}]", widgetsBundleId);
+ Validator.validateId(widgetsBundleId, "Incorrect widgetsBundleId " + widgetsBundleId);
+ WidgetsBundleEntity widgetsBundleEntity = widgetsBundleDao.findById(widgetsBundleId.getId());
+ return getData(widgetsBundleEntity);
+ }
+
+ @Override
+ public WidgetsBundle saveWidgetsBundle(WidgetsBundle widgetsBundle) {
+ log.trace("Executing saveWidgetsBundle [{}]", widgetsBundle);
+ widgetsBundleValidator.validate(widgetsBundle);
+ WidgetsBundleEntity widgetsBundleEntity = widgetsBundleDao.save(widgetsBundle);
+ return getData(widgetsBundleEntity);
+ }
+
+ @Override
+ public void deleteWidgetsBundle(WidgetsBundleId widgetsBundleId) {
+ log.trace("Executing deleteWidgetsBundle [{}]", widgetsBundleId);
+ Validator.validateId(widgetsBundleId, "Incorrect widgetsBundleId " + widgetsBundleId);
+ WidgetsBundle widgetsBundle = findWidgetsBundleById(widgetsBundleId);
+ if (widgetsBundle == null) {
+ throw new IncorrectParameterException("Unable to delete non-existent widgets bundle.");
+ }
+ widgetTypeService.deleteWidgetTypesByTenantIdAndBundleAlias(widgetsBundle.getTenantId(), widgetsBundle.getAlias());
+ widgetsBundleDao.removeById(widgetsBundleId.getId());
+ }
+
+ @Override
+ public WidgetsBundle findWidgetsBundleByTenantIdAndAlias(TenantId tenantId, String alias) {
+ log.trace("Executing findWidgetsBundleByTenantIdAndAlias, tenantId [{}], alias [{}]", tenantId, alias);
+ Validator.validateId(tenantId, "Incorrect tenantId " + tenantId);
+ Validator.validateString(alias, "Incorrect alias " + alias);
+ WidgetsBundleEntity widgetsBundleEntity = widgetsBundleDao.findWidgetsBundleByTenantIdAndAlias(tenantId.getId(), alias);
+ return getData(widgetsBundleEntity);
+ }
+
+ @Override
+ public TextPageData<WidgetsBundle> findSystemWidgetsBundlesByPageLink(TextPageLink pageLink) {
+ log.trace("Executing findSystemWidgetsBundles, pageLink [{}]", pageLink);
+ Validator.validatePageLink(pageLink, "Incorrect page link " + pageLink);
+ List<WidgetsBundleEntity> widgetsBundlesEntities = widgetsBundleDao.findSystemWidgetsBundles(pageLink);
+ List<WidgetsBundle> widgetsBundles = convertDataList(widgetsBundlesEntities);
+ return new TextPageData<>(widgetsBundles, pageLink);
+ }
+
+ @Override
+ public List<WidgetsBundle> findSystemWidgetsBundles() {
+ log.trace("Executing findSystemWidgetsBundles");
+ List<WidgetsBundle> widgetsBundles = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(300);
+ TextPageData<WidgetsBundle> pageData = null;
+ do {
+ pageData = findSystemWidgetsBundlesByPageLink(pageLink);
+ widgetsBundles.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+ return widgetsBundles;
+ }
+
+ @Override
+ public TextPageData<WidgetsBundle> findTenantWidgetsBundlesByTenantId(TenantId tenantId, TextPageLink pageLink) {
+ log.trace("Executing findTenantWidgetsBundlesByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink);
+ Validator.validateId(tenantId, "Incorrect tenantId " + tenantId);
+ Validator.validatePageLink(pageLink, "Incorrect page link " + pageLink);
+ List<WidgetsBundleEntity> widgetsBundlesEntities = widgetsBundleDao.findTenantWidgetsBundlesByTenantId(tenantId.getId(), pageLink);
+ List<WidgetsBundle> widgetsBundles = convertDataList(widgetsBundlesEntities);
+ return new TextPageData<>(widgetsBundles, pageLink);
+ }
+
+ @Override
+ public TextPageData<WidgetsBundle> findAllTenantWidgetsBundlesByTenantIdAndPageLink(TenantId tenantId, TextPageLink pageLink) {
+ log.trace("Executing findAllTenantWidgetsBundlesByTenantIdAndPageLink, tenantId [{}], pageLink [{}]", tenantId, pageLink);
+ Validator.validateId(tenantId, "Incorrect tenantId " + tenantId);
+ Validator.validatePageLink(pageLink, "Incorrect page link " + pageLink);
+ List<WidgetsBundleEntity> widgetsBundlesEntities = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId.getId(), pageLink);
+ List<WidgetsBundle> widgetsBundles = convertDataList(widgetsBundlesEntities);
+ return new TextPageData<>(widgetsBundles, pageLink);
+ }
+
+ @Override
+ public List<WidgetsBundle> findAllTenantWidgetsBundlesByTenantId(TenantId tenantId) {
+ log.trace("Executing findAllTenantWidgetsBundlesByTenantId, tenantId [{}]", tenantId);
+ Validator.validateId(tenantId, "Incorrect tenantId " + tenantId);
+ List<WidgetsBundle> widgetsBundles = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(300);
+ TextPageData<WidgetsBundle> pageData = null;
+ do {
+ pageData = findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, pageLink);
+ widgetsBundles.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+ return widgetsBundles;
+ }
+
+ @Override
+ public void deleteWidgetsBundlesByTenantId(TenantId tenantId) {
+ log.trace("Executing deleteWidgetsBundlesByTenantId, tenantId [{}]", tenantId);
+ Validator.validateId(tenantId, "Incorrect tenantId " + tenantId);
+ tenantWidgetsBundleRemover.removeEntitites(tenantId);
+ }
+
+ private DataValidator<WidgetsBundle> widgetsBundleValidator =
+ new DataValidator<WidgetsBundle>() {
+
+ @Override
+ protected void validateDataImpl(WidgetsBundle widgetsBundle) {
+ if (StringUtils.isEmpty(widgetsBundle.getTitle())) {
+ throw new DataValidationException("Widgets bundle title should be specified!");
+ }
+ if (widgetsBundle.getTenantId() == null) {
+ widgetsBundle.setTenantId(new TenantId(ModelConstants.NULL_UUID));
+ }
+ if (!widgetsBundle.getTenantId().getId().equals(ModelConstants.NULL_UUID)) {
+ TenantEntity tenant = tenantDao.findById(widgetsBundle.getTenantId().getId());
+ if (tenant == null) {
+ throw new DataValidationException("Widgets bundle is referencing to non-existent tenant!");
+ }
+ }
+ }
+
+ @Override
+ protected void validateCreate(WidgetsBundle widgetsBundle) {
+ String alias = widgetsBundle.getTitle().toLowerCase().replaceAll("\\W+", "_");
+ String originalAlias = alias;
+ int c = 1;
+ WidgetsBundleEntity withSameAlias;
+ do {
+ withSameAlias = widgetsBundleDao.findWidgetsBundleByTenantIdAndAlias(widgetsBundle.getTenantId().getId(), alias);
+ if (withSameAlias != null) {
+ alias = originalAlias + (++c);
+ }
+ } while(withSameAlias != null);
+ widgetsBundle.setAlias(alias);
+ }
+
+ @Override
+ protected void validateUpdate(WidgetsBundle widgetsBundle) {
+ WidgetsBundleEntity storedWidgetsBundle = widgetsBundleDao.findById(widgetsBundle.getId().getId());
+ if (!storedWidgetsBundle.getTenantId().equals(widgetsBundle.getTenantId().getId())) {
+ throw new DataValidationException("Can't move existing widgets bundle to different tenant!");
+ }
+ if (!storedWidgetsBundle.getAlias().equals(widgetsBundle.getAlias())) {
+ throw new DataValidationException("Update of widgets bundle alias is prohibited!");
+ }
+ }
+
+ };
+
+ private PaginatedRemover<TenantId, WidgetsBundleEntity> tenantWidgetsBundleRemover =
+ new PaginatedRemover<TenantId, WidgetsBundleEntity>() {
+
+ @Override
+ protected List<WidgetsBundleEntity> findEntities(TenantId id, TextPageLink pageLink) {
+ return widgetsBundleDao.findTenantWidgetsBundlesByTenantId(id.getId(), pageLink);
+ }
+
+ @Override
+ protected void removeEntity(WidgetsBundleEntity entity) {
+ deleteWidgetsBundle(new WidgetsBundleId(entity.getId()));
+ }
+ };
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java
new file mode 100644
index 0000000..8f98a64
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.widget;
+
+import org.thingsboard.server.common.data.widget.WidgetType;
+import org.thingsboard.server.dao.Dao;
+import org.thingsboard.server.dao.model.WidgetTypeEntity;
+
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * The Interface WidgetTypeDao.
+ *
+ * @param <T> the generic type
+ */
+public interface WidgetTypeDao extends Dao<WidgetTypeEntity> {
+
+ /**
+ * Save or update widget type object
+ *
+ * @param widgetType the widget type object
+ * @return saved widget type object
+ */
+ WidgetTypeEntity save(WidgetType widgetType);
+
+ /**
+ * Find widget types by tenantId and bundleAlias.
+ *
+ * @param tenantId the tenantId
+ * @param bundleAlias the bundle alias
+ * @return the list of widget types objects
+ */
+ List<WidgetTypeEntity> findWidgetTypesByTenantIdAndBundleAlias(UUID tenantId, String bundleAlias);
+
+ /**
+ * Find widget type by tenantId, bundleAlias and alias.
+ *
+ * @param tenantId the tenantId
+ * @param bundleAlias the bundle alias
+ * @param alias the alias
+ * @return the widget type object
+ */
+ WidgetTypeEntity findByTenantIdBundleAliasAndAlias(UUID tenantId, String bundleAlias, String alias);
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDaoImpl.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDaoImpl.java
new file mode 100644
index 0000000..ced1830
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDaoImpl.java
@@ -0,0 +1,82 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.widget;
+
+import com.datastax.driver.core.querybuilder.Select.Where;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+import org.springframework.stereotype.Repository;
+import org.thingsboard.server.common.data.widget.WidgetType;
+import org.thingsboard.server.dao.AbstractModelDao;
+import org.thingsboard.server.dao.model.WidgetTypeEntity;
+
+import java.util.List;
+import java.util.UUID;
+
+import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
+import static org.thingsboard.server.dao.model.ModelConstants.*;
+
+@Component
+@Slf4j
+public class WidgetTypeDaoImpl extends AbstractModelDao<WidgetTypeEntity> implements WidgetTypeDao {
+
+ @Override
+ protected Class<WidgetTypeEntity> getColumnFamilyClass() {
+ return WidgetTypeEntity.class;
+ }
+
+ @Override
+ protected String getColumnFamilyName() {
+ return WIDGET_TYPE_COLUMN_FAMILY_NAME;
+ }
+
+ @Override
+ public WidgetTypeEntity save(WidgetType widgetType) {
+ log.debug("Save widget type [{}] ", widgetType);
+ return save(new WidgetTypeEntity(widgetType));
+ }
+
+ @Override
+ public List<WidgetTypeEntity> findWidgetTypesByTenantIdAndBundleAlias(UUID tenantId, String bundleAlias) {
+ log.debug("Try to find widget types by tenantId [{}] and bundleAlias [{}]", tenantId, bundleAlias);
+ Where query = select().from(WIDGET_TYPE_BY_TENANT_AND_ALIASES_COLUMN_FAMILY_NAME)
+ .where()
+ .and(eq(WIDGET_TYPE_TENANT_ID_PROPERTY, tenantId))
+ .and(eq(WIDGET_TYPE_BUNDLE_ALIAS_PROPERTY, bundleAlias));
+ List<WidgetTypeEntity> widgetTypesEntities = findListByStatement(query);
+ log.trace("Found widget types [{}] by tenantId [{}] and bundleAlias [{}]", widgetTypesEntities, tenantId, bundleAlias);
+ return widgetTypesEntities;
+ }
+
+ @Override
+ public WidgetTypeEntity findByTenantIdBundleAliasAndAlias(UUID tenantId, String bundleAlias, String alias) {
+ log.debug("Try to find widget type by tenantId [{}], bundleAlias [{}] and alias [{}]", tenantId, bundleAlias, alias);
+ Where query = select().from(WIDGET_TYPE_BY_TENANT_AND_ALIASES_COLUMN_FAMILY_NAME)
+ .where()
+ .and(eq(WIDGET_TYPE_TENANT_ID_PROPERTY, tenantId))
+ .and(eq(WIDGET_TYPE_BUNDLE_ALIAS_PROPERTY, bundleAlias))
+ .and(eq(WIDGET_TYPE_ALIAS_PROPERTY, alias));
+ log.trace("Execute query {}", query);
+ WidgetTypeEntity widgetTypeEntity = findOneByStatement(query);
+ log.trace("Found widget type [{}] by tenantId [{}], bundleAlias [{}] and alias [{}]",
+ widgetTypeEntity, tenantId, bundleAlias, alias);
+ return widgetTypeEntity;
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java
new file mode 100644
index 0000000..a3c8967
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.widget;
+
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.WidgetTypeId;
+import org.thingsboard.server.common.data.widget.WidgetType;
+
+import java.util.List;
+
+public interface WidgetTypeService {
+
+ public WidgetType findWidgetTypeById(WidgetTypeId widgetTypeId);
+
+ public WidgetType saveWidgetType(WidgetType widgetType);
+
+ public void deleteWidgetType(WidgetTypeId widgetTypeId);
+
+ public List<WidgetType> findWidgetTypesByTenantIdAndBundleAlias(TenantId tenantId, String bundleAlias);
+
+ public WidgetType findWidgetTypeByTenantIdBundleAliasAndAlias(TenantId tenantId, String bundleAlias, String alias);
+
+ public void deleteWidgetTypesByTenantIdAndBundleAlias(TenantId tenantId, String bundleAlias);
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java
new file mode 100644
index 0000000..97f5048
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java
@@ -0,0 +1,169 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.widget;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.WidgetTypeId;
+import org.thingsboard.server.common.data.widget.WidgetType;
+import org.thingsboard.server.common.data.widget.WidgetsBundle;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.model.ModelConstants;
+import org.thingsboard.server.dao.model.TenantEntity;
+import org.thingsboard.server.dao.model.WidgetTypeEntity;
+import org.thingsboard.server.dao.model.WidgetsBundleEntity;
+import org.thingsboard.server.dao.service.DataValidator;
+import org.thingsboard.server.dao.service.Validator;
+import org.thingsboard.server.dao.tenant.TenantDao;
+import org.thingsboard.server.dao.tenant.TenantService;
+
+import java.util.List;
+
+import static org.thingsboard.server.dao.DaoUtil.convertDataList;
+import static org.thingsboard.server.dao.DaoUtil.getData;
+
+@Service
+@Slf4j
+public class WidgetTypeServiceImpl implements WidgetTypeService {
+
+ @Autowired
+ private WidgetTypeDao widgetTypeDao;
+
+ @Autowired
+ private TenantDao tenantDao;
+
+ @Autowired
+ private WidgetsBundleDao widgetsBundleService;
+
+ @Override
+ public WidgetType findWidgetTypeById(WidgetTypeId widgetTypeId) {
+ log.trace("Executing findWidgetTypeById [{}]", widgetTypeId);
+ Validator.validateId(widgetTypeId, "Incorrect widgetTypeId " + widgetTypeId);
+ WidgetTypeEntity widgetTypeEntity = widgetTypeDao.findById(widgetTypeId.getId());
+ return getData(widgetTypeEntity);
+ }
+
+ @Override
+ public WidgetType saveWidgetType(WidgetType widgetType) {
+ log.trace("Executing saveWidgetType [{}]", widgetType);
+ widgetTypeValidator.validate(widgetType);
+ WidgetTypeEntity widgetTypeEntity = widgetTypeDao.save(widgetType);
+ return getData(widgetTypeEntity);
+ }
+
+ @Override
+ public void deleteWidgetType(WidgetTypeId widgetTypeId) {
+ log.trace("Executing deleteWidgetType [{}]", widgetTypeId);
+ Validator.validateId(widgetTypeId, "Incorrect widgetTypeId " + widgetTypeId);
+ widgetTypeDao.removeById(widgetTypeId.getId());
+ }
+
+ @Override
+ public List<WidgetType> findWidgetTypesByTenantIdAndBundleAlias(TenantId tenantId, String bundleAlias) {
+ log.trace("Executing findWidgetTypesByTenantIdAndBundleAlias, tenantId [{}], bundleAlias [{}]", tenantId, bundleAlias);
+ Validator.validateId(tenantId, "Incorrect tenantId " + tenantId);
+ Validator.validateString(bundleAlias, "Incorrect bundleAlias " + bundleAlias);
+ List<WidgetTypeEntity> widgetTypesEntities = widgetTypeDao.findWidgetTypesByTenantIdAndBundleAlias(tenantId.getId(), bundleAlias);
+ return convertDataList(widgetTypesEntities);
+ }
+
+ @Override
+ public WidgetType findWidgetTypeByTenantIdBundleAliasAndAlias(TenantId tenantId, String bundleAlias, String alias) {
+ log.trace("Executing findWidgetTypeByTenantIdBundleAliasAndAlias, tenantId [{}], bundleAlias [{}], alias [{}]", tenantId, bundleAlias, alias);
+ Validator.validateId(tenantId, "Incorrect tenantId " + tenantId);
+ Validator.validateString(bundleAlias, "Incorrect bundleAlias " + bundleAlias);
+ Validator.validateString(alias, "Incorrect alias " + alias);
+ WidgetTypeEntity widgetTypeEntity = widgetTypeDao.findByTenantIdBundleAliasAndAlias(tenantId.getId(), bundleAlias, alias);
+ return getData(widgetTypeEntity);
+ }
+
+ @Override
+ public void deleteWidgetTypesByTenantIdAndBundleAlias(TenantId tenantId, String bundleAlias) {
+ log.trace("Executing deleteWidgetTypesByTenantIdAndBundleAlias, tenantId [{}], bundleAlias [{}]", tenantId, bundleAlias);
+ Validator.validateId(tenantId, "Incorrect tenantId " + tenantId);
+ Validator.validateString(bundleAlias, "Incorrect bundleAlias " + bundleAlias);
+ List<WidgetTypeEntity> widgetTypesEntities = widgetTypeDao.findWidgetTypesByTenantIdAndBundleAlias(tenantId.getId(), bundleAlias);
+ for (WidgetTypeEntity widgetTypeEntity : widgetTypesEntities) {
+ deleteWidgetType(new WidgetTypeId(widgetTypeEntity.getId()));
+ }
+ }
+
+ private DataValidator<WidgetType> widgetTypeValidator =
+ new DataValidator<WidgetType>() {
+ @Override
+ protected void validateDataImpl(WidgetType widgetType) {
+ if (StringUtils.isEmpty(widgetType.getName())) {
+ throw new DataValidationException("Widgets type name should be specified!");
+ }
+ if (StringUtils.isEmpty(widgetType.getBundleAlias())) {
+ throw new DataValidationException("Widgets type bundle alias should be specified!");
+ }
+ if (widgetType.getDescriptor() == null || widgetType.getDescriptor().size() == 0) {
+ throw new DataValidationException("Widgets type descriptor can't be empty!");
+ }
+ if (widgetType.getTenantId() == null) {
+ widgetType.setTenantId(new TenantId(ModelConstants.NULL_UUID));
+ }
+ if (!widgetType.getTenantId().getId().equals(ModelConstants.NULL_UUID)) {
+ TenantEntity tenant = tenantDao.findById(widgetType.getTenantId().getId());
+ if (tenant == null) {
+ throw new DataValidationException("Widget type is referencing to non-existent tenant!");
+ }
+ }
+ }
+
+ @Override
+ protected void validateCreate(WidgetType widgetType) {
+
+ WidgetsBundleEntity widgetsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(widgetType.getTenantId().getId(), widgetType.getBundleAlias());
+ if (widgetsBundle == null) {
+ throw new DataValidationException("Widget type is referencing to non-existent widgets bundle!");
+ }
+
+ String alias = widgetType.getName().toLowerCase().replaceAll("\\W+", "_");
+ String originalAlias = alias;
+ int c = 1;
+ WidgetTypeEntity withSameAlias;
+ do {
+ withSameAlias = widgetTypeDao.findByTenantIdBundleAliasAndAlias(widgetType.getTenantId().getId(), widgetType.getBundleAlias(), alias);
+ if (withSameAlias != null) {
+ alias = originalAlias + (++c);
+ }
+ } while(withSameAlias != null);
+ widgetType.setAlias(alias);
+ }
+
+ @Override
+ protected void validateUpdate(WidgetType widgetType) {
+ WidgetTypeEntity storedWidgetType = widgetTypeDao.findById(widgetType.getId().getId());
+ if (!storedWidgetType.getTenantId().equals(widgetType.getTenantId().getId())) {
+ throw new DataValidationException("Can't move existing widget type to different tenant!");
+ }
+ if (!storedWidgetType.getBundleAlias().equals(widgetType.getBundleAlias())) {
+ throw new DataValidationException("Update of widget type bundle alias is prohibited!");
+ }
+ if (!storedWidgetType.getAlias().equals(widgetType.getAlias())) {
+ throw new DataValidationException("Update of widget type alias is prohibited!");
+ }
+ }
+ };
+}
dao/src/main/resources/demo-data.cql 402(+402 -0)
diff --git a/dao/src/main/resources/demo-data.cql b/dao/src/main/resources/demo-data.cql
new file mode 100644
index 0000000..68154cc
--- /dev/null
+++ b/dao/src/main/resources/demo-data.cql
@@ -0,0 +1,402 @@
+--
+-- Copyright © 2016 The Thingsboard Authors
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+/** Demo data **/
+
+/** Demo tenant **/
+
+INSERT INTO thingsboard.tenant ( id, region, title, search_text )
+VALUES (
+ minTimeuuid ( '2016-11-01 01:02:01+0000' ),
+ 'Global',
+ 'Tenant',
+ 'tenant'
+);
+
+/** Demo tenant admin **/
+
+INSERT INTO thingsboard.user ( id, tenant_id, customer_id, email, search_text, authority )
+VALUES (
+ minTimeuuid ( '2016-11-01 01:02:02+0000' ),
+ minTimeuuid ( '2016-11-01 01:02:01+0000' ),
+ minTimeuuid ( 0 ),
+ 'tenant@thingsboard.org',
+ 'tenant@thingsboard.org',
+ 'TENANT_ADMIN'
+);
+
+INSERT INTO thingsboard.user_credentials ( id, user_id, enabled, password )
+VALUES (
+ now ( ),
+ minTimeuuid ( '2016-11-01 01:02:02+0000' ),
+ true,
+ '$2a$10$CUHks/PiEvxSGCKzrHCQGe/MoseIQw6qijIDjSa2sNoIyXkgJGyMO'
+);
+
+/** Demo customers **/
+
+INSERT INTO thingsboard.customer ( id, tenant_id, title, search_text )
+VALUES (
+ minTimeuuid ( '2016-11-01 01:02:03+0000' ),
+ minTimeuuid ( '2016-11-01 01:02:01+0000' ),
+ 'Customer A',
+ 'customer a'
+);
+
+INSERT INTO thingsboard.customer ( id, tenant_id, title, search_text )
+VALUES (
+ minTimeuuid ( '2016-11-01 01:02:03+0001' ),
+ minTimeuuid ( '2016-11-01 01:02:01+0000' ),
+ 'Customer B',
+ 'customer b'
+);
+
+INSERT INTO thingsboard.customer ( id, tenant_id, title, search_text )
+VALUES (
+ minTimeuuid ( '2016-11-01 01:02:03+0002' ),
+ minTimeuuid ( '2016-11-01 01:02:01+0000' ),
+ 'Customer C',
+ 'customer c'
+);
+
+/** Demo customer user **/
+
+INSERT INTO thingsboard.user ( id, tenant_id, customer_id, email, search_text, authority )
+VALUES (
+ minTimeuuid ( '2016-11-01 01:02:04+0000' ),
+ minTimeuuid ( '2016-11-01 01:02:01+0000' ),
+ minTimeuuid ( '2016-11-01 01:02:03+0000' ),
+ 'customer@thingsboard.org',
+ 'customer@thingsboard.org',
+ 'CUSTOMER_USER'
+);
+
+INSERT INTO thingsboard.user ( id, tenant_id, customer_id, email, search_text, authority )
+VALUES (
+ minTimeuuid ( '2016-11-01 01:02:04+0001' ),
+ minTimeuuid ( '2016-11-01 01:02:01+0000' ),
+ minTimeuuid ( '2016-11-01 01:02:03+0000' ),
+ 'customerA@thingsboard.org',
+ 'customera@thingsboard.org',
+ 'CUSTOMER_USER'
+);
+
+INSERT INTO thingsboard.user ( id, tenant_id, customer_id, email, search_text, authority )
+VALUES (
+ minTimeuuid ( '2016-11-01 01:02:04+0002' ),
+ minTimeuuid ( '2016-11-01 01:02:01+0000' ),
+ minTimeuuid ( '2016-11-01 01:02:03+0001' ),
+ 'customerB@thingsboard.org',
+ 'customerb@thingsboard.org',
+ 'CUSTOMER_USER'
+);
+
+INSERT INTO thingsboard.user ( id, tenant_id, customer_id, email, search_text, authority )
+VALUES (
+ minTimeuuid ( '2016-11-01 01:02:04+0003' ),
+ minTimeuuid ( '2016-11-01 01:02:01+0000' ),
+ minTimeuuid ( '2016-11-01 01:02:03+0002' ),
+ 'customerC@thingsboard.org',
+ 'customerc@thingsboard.org',
+ 'CUSTOMER_USER'
+);
+
+
+INSERT INTO thingsboard.user_credentials ( id, user_id, enabled, password )
+VALUES (
+ now ( ),
+ minTimeuuid ( '2016-11-01 01:02:04+0000' ),
+ true,
+ '$2a$10$1Ki3Nl10pagxZncDQZtU.uHttir3HGKzLeovxCNKdSSJa3PU49L1C'
+);
+
+INSERT INTO thingsboard.user_credentials ( id, user_id, enabled, password )
+VALUES (
+ now ( ),
+ minTimeuuid ( '2016-11-01 01:02:04+0001' ),
+ true,
+ '$2a$10$1Ki3Nl10pagxZncDQZtU.uHttir3HGKzLeovxCNKdSSJa3PU49L1C'
+);
+
+INSERT INTO thingsboard.user_credentials ( id, user_id, enabled, password )
+VALUES (
+ now ( ),
+ minTimeuuid ( '2016-11-01 01:02:04+0002' ),
+ true,
+ '$2a$10$1Ki3Nl10pagxZncDQZtU.uHttir3HGKzLeovxCNKdSSJa3PU49L1C'
+);
+
+INSERT INTO thingsboard.user_credentials ( id, user_id, enabled, password )
+VALUES (
+ now ( ),
+ minTimeuuid ( '2016-11-01 01:02:04+0003' ),
+ true,
+ '$2a$10$1Ki3Nl10pagxZncDQZtU.uHttir3HGKzLeovxCNKdSSJa3PU49L1C'
+);
+
+/** Demo device **/
+
+INSERT INTO thingsboard.device ( id, tenant_id, customer_id, name, search_text)
+VALUES (
+ minTimeuuid ( '2016-11-01 01:02:05+0000' ),
+ minTimeuuid ( '2016-11-01 01:02:01+0000' ),
+ minTimeuuid ( '2016-11-01 01:02:03+0000' ),
+ 'Test Device A1',
+ 'test device a1'
+);
+
+INSERT INTO thingsboard.device ( id, tenant_id, customer_id, name, search_text)
+VALUES (
+ minTimeuuid ( '2016-11-01 01:02:05+0001' ),
+ minTimeuuid ( '2016-11-01 01:02:01+0000' ),
+ minTimeuuid ( '2016-11-01 01:02:03+0000' ),
+ 'Test Device A2',
+ 'test device a2'
+);
+
+INSERT INTO thingsboard.device ( id, tenant_id, customer_id, name, search_text)
+VALUES (
+ minTimeuuid ( '2016-11-01 01:02:05+0002' ),
+ minTimeuuid ( '2016-11-01 01:02:01+0000' ),
+ minTimeuuid ( '2016-11-01 01:02:03+0000' ),
+ 'Test Device A3',
+ 'test device a3'
+);
+
+INSERT INTO thingsboard.device ( id, tenant_id, customer_id, name, search_text)
+VALUES (
+ minTimeuuid ( '2016-11-01 01:02:05+0003' ),
+ minTimeuuid ( '2016-11-01 01:02:01+0000' ),
+ minTimeuuid ( '2016-11-01 01:02:03+0001' ),
+ 'Test Device B1',
+ 'test device b1'
+);
+
+INSERT INTO thingsboard.device ( id, tenant_id, customer_id, name, search_text)
+VALUES (
+ minTimeuuid ( '2016-11-01 01:02:05+0004' ),
+ minTimeuuid ( '2016-11-01 01:02:01+0000' ),
+ minTimeuuid ( '2016-11-01 01:02:03+0002' ),
+ 'Test Device C1',
+ 'test device c1'
+);
+
+INSERT INTO thingsboard.device_credentials ( id, device_id, credentials_type, credentials_id)
+VALUES (
+ now(),
+ minTimeuuid ( '2016-11-01 01:02:05+0000' ),
+ 'ACCESS_TOKEN',
+ 'A1_TEST_TOKEN'
+);
+
+INSERT INTO thingsboard.device_credentials ( id, device_id, credentials_type, credentials_id)
+VALUES (
+ now(),
+ minTimeuuid ( '2016-11-01 01:02:05+0001' ),
+ 'ACCESS_TOKEN',
+ 'A2_TEST_TOKEN'
+);
+
+INSERT INTO thingsboard.device_credentials ( id, device_id, credentials_type, credentials_id)
+VALUES (
+ now(),
+ minTimeuuid ( '2016-11-01 01:02:05+0002' ),
+ 'ACCESS_TOKEN',
+ 'A3_TEST_TOKEN'
+);
+
+INSERT INTO thingsboard.device_credentials ( id, device_id, credentials_type, credentials_id)
+VALUES (
+ now(),
+ minTimeuuid ( '2016-11-01 01:02:05+0003' ),
+ 'ACCESS_TOKEN',
+ 'B1_TEST_TOKEN'
+);
+
+INSERT INTO thingsboard.device_credentials ( id, device_id, credentials_type, credentials_id)
+VALUES (
+ now(),
+ minTimeuuid ( '2016-11-01 01:02:05+0004' ),
+ 'ACCESS_TOKEN',
+ 'C1_TEST_TOKEN'
+);
+
+/** Demo data **/
+
+/** Demo plugins & rules **/
+
+/** Email plugin. Please change username and password here or via configuration **/
+
+INSERT INTO thingsboard.plugin ( id, tenant_id, name, state, search_text, api_token, plugin_class, public_access, configuration)
+VALUES (
+ minTimeuuid ( '2016-11-01 01:02:06+0000' ),
+ minTimeuuid ( '2016-11-01 01:02:01+0000' ),
+ 'Demo Email Plugin',
+ 'ACTIVE',
+ 'mail sender plugin',
+ 'mail',
+ 'org.thingsboard.server.extensions.core.plugin.mail.MailPlugin',
+ true,
+ '{
+ "host": "smtp.gmail.com",
+ "port": 587,
+ "username": "username@gmail.com",
+ "password": "password",
+ "otherProperties": [
+ {
+ "key":"mail.smtp.auth",
+ "value":"true"
+ },
+ {
+ "key":"mail.smtp.timeout",
+ "value":"10000"
+ },
+ {
+ "key":"mail.smtp.starttls.enable",
+ "value":"true"
+ },
+ {
+ "key":"mail.smtp.host",
+ "value":"smtp.gmail.com"
+ },
+ {
+ "key":"mail.smtp.port",
+ "value":"587"
+ }
+ ]
+ }'
+);
+
+INSERT INTO thingsboard.plugin ( id, tenant_id, name, state, search_text, api_token, plugin_class, public_access, configuration)
+VALUES (
+ minTimeuuid ( '2016-11-01 01:02:07+0000' ),
+ minTimeuuid ( '2016-11-01 01:02:01+0000' ),
+ 'Demo Time RPC Plugin',
+ 'ACTIVE',
+ 'demo time rpc plugin',
+ 'time',
+ 'org.thingsboard.server.extensions.core.plugin.time.TimePlugin',
+ false,
+ '{"timeFormat":"yyyy MM dd HH:mm:ss.SSS"}'
+);
+
+INSERT INTO thingsboard.plugin ( id, tenant_id, name, state, search_text, api_token, plugin_class, public_access, configuration)
+VALUES (
+ minTimeuuid ( '2016-11-01 01:02:08+0000' ),
+ minTimeuuid ( '2016-11-01 01:02:01+0000' ),
+ 'Demo Device Messaging RPC Plugin',
+ 'ACTIVE',
+ 'demo device messaging rpc plugin',
+ 'messaging',
+ 'org.thingsboard.server.extensions.core.plugin.messaging.DeviceMessagingPlugin',
+ false,
+ '{"maxDeviceCountPerCustomer":1024,"defaultTimeout":20000,"maxTimeout":60000}'
+);
+
+
+INSERT INTO thingsboard.rule ( id, tenant_id, name, plugin_token, state, search_text, weight, filters, processor, action)
+VALUES (
+ minTimeuuid ( '2016-11-01 01:02:09+0000' ),
+ minTimeuuid ( '2016-11-01 01:02:01+0000' ),
+ 'Demo Alarm Rule',
+ 'mail',
+ 'ACTIVE',
+ 'demo alarm rule',
+ 0,
+ '[{"clazz":"org.thingsboard.server.extensions.core.filter.MsgTypeFilter", "name":"MsgTypeFilter", "configuration": {"messageTypes":["POST_TELEMETRY","POST_ATTRIBUTES","GET_ATTRIBUTES"]}}
+ ,
+ {"clazz":"org.thingsboard.server.extensions.core.filter.DeviceTelemetryFilter", "name":"Temperature filter", "configuration": {"filter":"typeof temperature !== ''undefined'' && temperature >= 100"}}
+ ]',
+ '{"clazz":"org.thingsboard.server.extensions.core.processor.AlarmDeduplicationProcessor", "name": "AlarmDeduplicationProcessor", "configuration":{
+ "alarmIdTemplate": "[$date.get(''yyyy-MM-dd HH:mm'')] Device $cs.get(''serialNumber'')($cs.get(''model'')) temperature is high!",
+ "alarmBodyTemplate": "[$date.get(''yyyy-MM-dd HH:mm:ss'')] Device $cs.get(''serialNumber'')($cs.get(''model'')) temperature is $temp.valueAsString!"
+ }}',
+ '{"clazz":"org.thingsboard.server.extensions.core.action.mail.SendMailAction", "name":"Send Mail Action", "configuration":{
+ "sendFlag": "isNewAlarm",
+ "fromTemplate": "thingsboard@gmail.com",
+ "toTemplate": "thingsboard@gmail.com",
+ "subjectTemplate": "$alarmId",
+ "bodyTemplate": "$alarmBody"
+ }}'
+);
+
+INSERT INTO thingsboard.rule ( id, tenant_id, name, plugin_token, state, search_text, weight, filters, processor, action)
+VALUES (
+ minTimeuuid ( '2016-11-01 01:02:10+0000' ),
+ minTimeuuid ( '2016-11-01 01:02:01+0000' ),
+ 'Demo Alarm Rule',
+ 'mail',
+ 'ACTIVE',
+ 'demo alarm rule',
+ 0,
+ '[{"clazz":"org.thingsboard.server.extensions.core.filter.MsgTypeFilter", "name":"MsgTypeFilter", "configuration": {"messageTypes":["POST_TELEMETRY","POST_ATTRIBUTES","GET_ATTRIBUTES"]}}
+ ,
+ {"clazz":"org.thingsboard.server.extensions.core.filter.DeviceTelemetryFilter", "name":"Temperature filter", "configuration": {"filter":"typeof temperature !== ''undefined'' && temperature >= 100"}}
+ ]',
+ '{"clazz":"org.thingsboard.server.extensions.core.processor.AlarmDeduplicationProcessor", "name": "AlarmDeduplicationProcessor", "configuration":{
+ "alarmIdTemplate": "[$date.get(''yyyy-MM-dd HH:mm'')] Device $cs.get(''serialNumber'')($cs.get(''model'')) temperature is high!",
+ "alarmBodyTemplate": "[$date.get(''yyyy-MM-dd HH:mm:ss'')] Device $cs.get(''serialNumber'')($cs.get(''model'')) temperature is $temperature.valueAsString!"
+ }}',
+ '{"clazz":"org.thingsboard.server.extensions.core.action.mail.SendMailAction", "name":"Send Mail Action", "configuration":{
+ "sendFlag": "isNewAlarm",
+ "fromTemplate": "thingsboard@gmail.com",
+ "toTemplate": "thingsboard@gmail.com",
+ "subjectTemplate": "$alarmId",
+ "bodyTemplate": "$alarmBody"
+ }}'
+);
+
+INSERT INTO thingsboard.rule ( id, tenant_id, name, plugin_token, state, search_text, weight, filters, processor, action)
+VALUES (
+ minTimeuuid ( '2016-11-01 01:02:10+0000' ),
+ minTimeuuid ( '2016-11-01 01:02:01+0000' ),
+ 'Demo getTime RPC Rule',
+ 'time',
+ 'ACTIVE',
+ 'demo alarm rule',
+ 0,
+ '[{"configuration":{"messageTypes":["RPC_REQUEST"]},"name":"RPC Request Filter","clazz":"org.thingsboard.server.extensions.core.filter.MsgTypeFilter"},{"configuration":{"methodNames":[{"name":"getTime"}]},"name":"getTime method filter","clazz":"org.thingsboard.server.extensions.core.filter.MethodNameFilter"}]',
+ null,
+ '{"configuration":{},"clazz":"org.thingsboard.server.extensions.core.action.rpc.RpcPluginAction","name":"getTimeAction"}'
+);
+
+INSERT INTO thingsboard.rule ( id, tenant_id, name, plugin_token, state, search_text, weight, filters, processor, action)
+VALUES (
+ minTimeuuid ( '2016-11-01 01:02:11+0000' ),
+ minTimeuuid ( '2016-11-01 01:02:01+0000' ),
+ 'Demo Time RPC Rule',
+ 'time',
+ 'ACTIVE',
+ 'demo time rpc rule',
+ 0,
+ '[{"configuration":{"messageTypes":["RPC_REQUEST"]},"name":"RPC Request Filter","clazz":"org.thingsboard.server.extensions.core.filter.MsgTypeFilter"},{"configuration":{"methodNames":[{"name":"getTime"}]},"name":"getTime method filter","clazz":"org.thingsboard.server.extensions.core.filter.MethodNameFilter"}]',
+ null,
+ '{"configuration":{},"clazz":"org.thingsboard.server.extensions.core.action.rpc.RpcPluginAction","name":"getTimeAction"}'
+);
+
+INSERT INTO thingsboard.rule ( id, tenant_id, name, plugin_token, state, search_text, weight, filters, processor, action)
+VALUES (
+ minTimeuuid ( '2016-11-01 01:02:12+0000' ),
+ minTimeuuid ( '2016-11-01 01:02:01+0000' ),
+ 'Demo Messaging RPC Rule',
+ 'messaging',
+ 'ACTIVE',
+ 'demo messaging rpc rule',
+ 0,
+ '[{"configuration":{"messageTypes":["RPC_REQUEST"]},"name":"RPC Request Filter","clazz":"org.thingsboard.server.extensions.core.filter.MsgTypeFilter"},{"configuration":{"methodNames":[{"name":"getDevices"},{"name":"sendMsg"}]},"name":"Messaging methods filter","clazz":"org.thingsboard.server.extensions.core.filter.MethodNameFilter"}]',
+ null,
+ '{"configuration":{},"clazz":"org.thingsboard.server.extensions.core.action.rpc.RpcPluginAction","name":"Messaging RPC Action"}'
+);
\ No newline at end of file
dao/src/main/resources/schema.cql 425(+425 -0)
diff --git a/dao/src/main/resources/schema.cql b/dao/src/main/resources/schema.cql
new file mode 100644
index 0000000..57bc650
--- /dev/null
+++ b/dao/src/main/resources/schema.cql
@@ -0,0 +1,425 @@
+--
+-- Copyright © 2016 The Thingsboard Authors
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+CREATE KEYSPACE IF NOT EXISTS thingsboard
+WITH replication = {
+ 'class' : 'SimpleStrategy',
+ 'replication_factor' : 1
+};
+
+CREATE TABLE IF NOT EXISTS thingsboard.user (
+ id timeuuid,
+ tenant_id timeuuid,
+ customer_id timeuuid,
+ email text,
+ search_text text,
+ authority text,
+ first_name text,
+ last_name text,
+ additional_info text,
+ PRIMARY KEY (id, tenant_id, customer_id, authority)
+);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.user_by_email AS
+ SELECT *
+ from thingsboard.user
+ WHERE email IS NOT NULL AND tenant_id IS NOT NULL AND customer_id IS NOT NULL AND id IS NOT NULL AND authority IS NOT
+ NULL
+ PRIMARY KEY ( email, tenant_id, customer_id, id, authority );
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.user_by_tenant_and_search_text AS
+ SELECT *
+ from thingsboard.user
+ WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND authority IS NOT NULL AND search_text IS NOT NULL AND id
+ IS NOT NULL
+ PRIMARY KEY ( tenant_id, customer_id, authority, search_text, id )
+ WITH CLUSTERING ORDER BY ( customer_id DESC, authority DESC, search_text ASC, id DESC );
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.user_by_customer_and_search_text AS
+ SELECT *
+ from thingsboard.user
+ WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND authority IS NOT NULL AND search_text IS NOT NULL AND id
+ IS NOT NULL
+ PRIMARY KEY ( customer_id, tenant_id, authority, search_text, id )
+ WITH CLUSTERING ORDER BY ( tenant_id DESC, authority DESC, search_text ASC, id DESC );
+
+CREATE TABLE IF NOT EXISTS thingsboard.user_credentials (
+ id timeuuid PRIMARY KEY,
+ user_id timeuuid,
+ enabled boolean,
+ password text,
+ activate_token text,
+ reset_token text
+);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.user_credentials_by_user AS
+ SELECT *
+ from thingsboard.user_credentials
+ WHERE user_id IS NOT NULL AND id IS NOT NULL
+ PRIMARY KEY ( user_id, id );
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.user_credentials_by_activate_token AS
+ SELECT *
+ from thingsboard.user_credentials
+ WHERE activate_token IS NOT NULL AND id IS NOT NULL
+ PRIMARY KEY ( activate_token, id );
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.user_credentials_by_reset_token AS
+ SELECT *
+ from thingsboard.user_credentials
+ WHERE reset_token IS NOT NULL AND id IS NOT NULL
+ PRIMARY KEY ( reset_token, id );
+
+CREATE TABLE IF NOT EXISTS thingsboard.admin_settings (
+ id timeuuid PRIMARY KEY,
+ key text,
+ json_value text
+);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.admin_settings_by_key AS
+ SELECT *
+ from thingsboard.admin_settings
+ WHERE key IS NOT NULL AND id IS NOT NULL
+ PRIMARY KEY ( key, id )
+ WITH CLUSTERING ORDER BY ( id DESC );
+
+CREATE TABLE IF NOT EXISTS thingsboard.tenant (
+ id timeuuid,
+ title text,
+ search_text text,
+ region text,
+ country text,
+ state text,
+ city text,
+ address text,
+ address2 text,
+ zip text,
+ phone text,
+ email text,
+ additional_info text,
+ PRIMARY KEY (id, region)
+);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.tenant_by_region_and_search_text AS
+ SELECT *
+ from thingsboard.tenant
+ WHERE region IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+ PRIMARY KEY ( region, search_text, id )
+ WITH CLUSTERING ORDER BY ( search_text ASC, id DESC );
+
+CREATE TABLE IF NOT EXISTS thingsboard.customer (
+ id timeuuid,
+ tenant_id timeuuid,
+ title text,
+ search_text text,
+ country text,
+ state text,
+ city text,
+ address text,
+ address2 text,
+ zip text,
+ phone text,
+ email text,
+ additional_info text,
+ PRIMARY KEY (id, tenant_id)
+);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.customer_by_tenant_and_search_text AS
+ SELECT *
+ from thingsboard.customer
+ WHERE tenant_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+ PRIMARY KEY ( tenant_id, search_text, id )
+ WITH CLUSTERING ORDER BY ( search_text ASC, id DESC );
+
+CREATE TABLE IF NOT EXISTS thingsboard.device (
+ id timeuuid,
+ tenant_id timeuuid,
+ customer_id timeuuid,
+ name text,
+ search_text text,
+ additional_info text,
+ PRIMARY KEY (id, tenant_id, customer_id)
+);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_by_tenant_and_name AS
+ SELECT *
+ from thingsboard.device
+ WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND name IS NOT NULL AND id IS NOT NULL
+ PRIMARY KEY ( tenant_id, name, id, customer_id)
+ WITH CLUSTERING ORDER BY ( name ASC, id DESC, customer_id DESC);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_by_tenant_and_search_text AS
+ SELECT *
+ from thingsboard.device
+ WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+ PRIMARY KEY ( tenant_id, search_text, id, customer_id)
+ WITH CLUSTERING ORDER BY ( search_text ASC, id DESC, customer_id DESC);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_by_customer_and_search_text AS
+ SELECT *
+ from thingsboard.device
+ WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+ PRIMARY KEY ( customer_id, tenant_id, search_text, id )
+ WITH CLUSTERING ORDER BY ( tenant_id DESC, search_text ASC, id DESC );
+
+CREATE TABLE IF NOT EXISTS thingsboard.device_credentials (
+ id timeuuid PRIMARY KEY,
+ device_id timeuuid,
+ credentials_type text,
+ credentials_id text,
+ credentials_value text
+);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_credentials_by_device AS
+ SELECT *
+ from thingsboard.device_credentials
+ WHERE device_id IS NOT NULL AND id IS NOT NULL
+ PRIMARY KEY ( device_id, id );
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_credentials_by_credentials_id AS
+ SELECT *
+ from thingsboard.device_credentials
+ WHERE credentials_id IS NOT NULL AND id IS NOT NULL
+ PRIMARY KEY ( credentials_id, id );
+
+CREATE TABLE IF NOT EXISTS thingsboard.widgets_bundle (
+ id timeuuid,
+ tenant_id timeuuid,
+ alias text,
+ title text,
+ search_text text,
+ image blob,
+ PRIMARY KEY (id, tenant_id)
+);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.widgets_bundle_by_tenant_and_search_text AS
+ SELECT *
+ from thingsboard.widgets_bundle
+ WHERE tenant_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+ PRIMARY KEY ( tenant_id, search_text, id )
+ WITH CLUSTERING ORDER BY ( search_text ASC, id DESC );
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.widgets_bundle_by_tenant_and_alias AS
+ SELECT *
+ from thingsboard.widgets_bundle
+ WHERE tenant_id IS NOT NULL AND alias IS NOT NULL AND id IS NOT NULL
+ PRIMARY KEY ( tenant_id, alias, id )
+ WITH CLUSTERING ORDER BY ( alias ASC, id DESC );
+
+CREATE TABLE IF NOT EXISTS thingsboard.widget_type (
+ id timeuuid,
+ tenant_id timeuuid,
+ bundle_alias text,
+ alias text,
+ name text,
+ descriptor text,
+ PRIMARY KEY (id, tenant_id, bundle_alias)
+);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.widget_type_by_tenant_and_aliases AS
+ SELECT *
+ from thingsboard.widget_type
+ WHERE tenant_id IS NOT NULL AND bundle_alias IS NOT NULL AND alias IS NOT NULL AND id IS NOT NULL
+ PRIMARY KEY ( tenant_id, bundle_alias, alias, id )
+ WITH CLUSTERING ORDER BY ( bundle_alias ASC, alias ASC, id DESC );
+
+CREATE TABLE IF NOT EXISTS thingsboard.dashboard (
+ id timeuuid,
+ tenant_id timeuuid,
+ customer_id timeuuid,
+ title text,
+ search_text text,
+ configuration text,
+ PRIMARY KEY (id, tenant_id, customer_id)
+);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.dashboard_by_tenant_and_search_text AS
+ SELECT *
+ from thingsboard.dashboard
+ WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+ PRIMARY KEY ( tenant_id, customer_id, search_text, id )
+ WITH CLUSTERING ORDER BY ( customer_id DESC, search_text ASC, id DESC );
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.dashboard_by_customer_and_search_text AS
+ SELECT *
+ from thingsboard.dashboard
+ WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+ PRIMARY KEY ( customer_id, tenant_id, search_text, id )
+ WITH CLUSTERING ORDER BY ( tenant_id DESC, search_text ASC, id DESC );
+
+CREATE TABLE IF NOT EXISTS thingsboard.ts_kv_cf (
+ entity_type text, // (DEVICE, CUSTOMER, TENANT)
+ entity_id timeuuid,
+ key text,
+ partition bigint,
+ ts bigint,
+ bool_v boolean,
+ str_v text,
+ long_v bigint,
+ dbl_v double,
+ PRIMARY KEY (( entity_type, entity_id, key, partition ), ts)
+);
+
+CREATE TABLE IF NOT EXISTS thingsboard.ts_kv_partitions_cf (
+ entity_type text, // (DEVICE, CUSTOMER, TENANT)
+ entity_id timeuuid,
+ key text,
+ partition bigint,
+ PRIMARY KEY (( entity_type, entity_id, key ), partition)
+) WITH CLUSTERING ORDER BY ( partition ASC )
+ AND compaction = { 'class' : 'LeveledCompactionStrategy' };
+
+CREATE TABLE IF NOT EXISTS thingsboard.ts_kv_latest_cf (
+ entity_type text, // (DEVICE, CUSTOMER, TENANT)
+ entity_id timeuuid,
+ key text,
+ ts bigint,
+ bool_v boolean,
+ str_v text,
+ long_v bigint,
+ dbl_v double,
+ PRIMARY KEY (( entity_type, entity_id ), key)
+) WITH compaction = { 'class' : 'LeveledCompactionStrategy' };
+
+
+CREATE TABLE IF NOT EXISTS thingsboard.attributes_kv_cf (
+ entity_type text, // (DEVICE, CUSTOMER, TENANT)
+ entity_id timeuuid,
+ attribute_type text, // (CLIENT_SIDE, SHARED, SERVER_SIDE)
+ attribute_key text,
+ bool_v boolean,
+ str_v text,
+ long_v bigint,
+ dbl_v double,
+ last_update_ts bigint,
+ PRIMARY KEY ((entity_type, entity_id, attribute_type), attribute_key)
+) WITH compaction = { 'class' : 'LeveledCompactionStrategy' };
+
+CREATE TABLE IF NOT EXISTS thingsboard.component_descriptor (
+ id timeuuid,
+ type text, //("FILTER", "PROCESSOR", "ACTION", "PLUGIN")
+ scope text,
+ name text,
+ search_text text,
+ clazz text,
+ configuration_descriptor text,
+ actions text,
+ PRIMARY KEY (clazz, id, type, scope)
+);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.component_desc_by_type_search_text AS
+ SELECT *
+ from thingsboard.component_descriptor
+ WHERE type IS NOT NULL AND scope IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL AND clazz IS NOT NULL
+ PRIMARY KEY ( type, search_text, id, clazz, scope)
+ WITH CLUSTERING ORDER BY ( search_text DESC);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.component_desc_by_scope_type_search_text AS
+ SELECT *
+ from thingsboard.component_descriptor
+ WHERE type IS NOT NULL AND scope IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL AND clazz IS NOT NULL
+ PRIMARY KEY ( (scope, type), search_text, id, clazz)
+ WITH CLUSTERING ORDER BY ( search_text DESC);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.component_desc_by_id AS
+ SELECT *
+ from thingsboard.component_descriptor
+ WHERE type IS NOT NULL AND scope IS NOT NULL AND id IS NOT NULL AND clazz IS NOT NULL
+ PRIMARY KEY ( id, clazz, scope, type )
+ WITH CLUSTERING ORDER BY ( clazz ASC, scope ASC, type DESC);
+
+CREATE TABLE IF NOT EXISTS thingsboard.rule (
+ id timeuuid,
+ tenant_id timeuuid,
+ name text,
+ state text,
+ search_text text,
+ weight int,
+ plugin_token text,
+ filters text, // Format: {"clazz":"A", "name": "Filter A", "configuration": {"types":["TELEMETRY"]}}
+ processor text, // Format: {"clazz":"A", "name": "Processor A", "configuration": null}
+ action text, // Format: {"clazz":"A", "name": "Action A", "configuration": null}
+ additional_info text,
+ PRIMARY KEY (id, tenant_id)
+);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.rule_by_plugin_token AS
+ SELECT *
+
+ FROM thingsboard.rule
+ WHERE tenant_id IS NOT NULL AND id IS NOT NULL AND plugin_token IS NOT NULL
+ PRIMARY KEY (plugin_token, tenant_id, id) WITH CLUSTERING ORDER BY (tenant_id DESC, id DESC);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.rule_by_tenant_and_search_text AS
+ SELECT *
+ FROM thingsboard.rule
+ WHERE tenant_id IS NOT NULL AND id IS NOT NULL AND search_text IS NOT NULL
+ PRIMARY KEY (tenant_id, search_text, id) WITH CLUSTERING ORDER BY (search_text ASC);
+
+CREATE TABLE IF NOT EXISTS thingsboard.plugin (
+ id uuid,
+ tenant_id uuid,
+ name text,
+ state text,
+ search_text text,
+ api_token text,
+ plugin_class text,
+ public_access boolean,
+ configuration text,
+ additional_info text,
+ PRIMARY KEY (id, tenant_id)
+);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.plugin_by_api_token AS
+ SELECT *
+ FROM thingsboard.plugin
+ WHERE api_token IS NOT NULL AND id IS NOT NULL AND tenant_id IS NOT NULL
+ PRIMARY KEY (api_token, id, tenant_id) WITH CLUSTERING ORDER BY (id DESC);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.plugin_by_tenant_and_search_text AS
+ SELECT *
+ from thingsboard.plugin
+ WHERE tenant_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+ PRIMARY KEY ( tenant_id, search_text, id )
+ WITH CLUSTERING ORDER BY ( search_text ASC, id DESC );
+
+CREATE TABLE IF NOT EXISTS thingsboard.event (
+ tenant_id timeuuid, // tenant or system
+ id timeuuid,
+ event_type text,
+ event_uid text,
+ entity_type text, // (device, customer, rule, plugin)
+ entity_id timeuuid,
+ body text,
+ PRIMARY KEY ((tenant_id, entity_type, entity_id), event_type, event_uid)
+);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.event_by_type_and_id AS
+ SELECT *
+ FROM thingsboard.event
+ WHERE tenant_id IS NOT NULL AND entity_type IS NOT NULL AND entity_id IS NOT NULL AND id IS NOT NULL
+ AND event_type IS NOT NULL AND event_uid IS NOT NULL
+ PRIMARY KEY ((tenant_id, entity_type, entity_id), event_type, id, event_uid)
+ WITH CLUSTERING ORDER BY (event_type ASC, id ASC, event_uid ASC);
+
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.event_by_id AS
+ SELECT *
+ FROM thingsboard.event
+ WHERE tenant_id IS NOT NULL AND entity_type IS NOT NULL AND entity_id IS NOT NULL AND id IS NOT NULL
+ AND event_type IS NOT NULL AND event_uid IS NOT NULL
+ PRIMARY KEY ((tenant_id, entity_type, entity_id), id, event_type, event_uid)
+ WITH CLUSTERING ORDER BY (id ASC, event_type ASC, event_uid ASC);
dao/src/main/resources/system-data.cql 258(+258 -0)
diff --git a/dao/src/main/resources/system-data.cql b/dao/src/main/resources/system-data.cql
new file mode 100644
index 0000000..1358e58
--- /dev/null
+++ b/dao/src/main/resources/system-data.cql
@@ -0,0 +1,258 @@
+--
+-- Copyright © 2016 The Thingsboard Authors
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+/** SYSTEM **/
+
+/** System admin **/
+INSERT INTO thingsboard.user ( id, tenant_id, customer_id, email, search_text, authority )
+VALUES ( minTimeuuid ( '2016-11-01 01:01:01+0000' ), minTimeuuid ( 0 ), minTimeuuid ( 0 ), 'sysadmin@thingsboard.org',
+'sysadmin@thingsboard.org', 'SYS_ADMIN' );
+
+INSERT INTO thingsboard.user_credentials ( id, user_id, enabled, password )
+VALUES ( now ( ), minTimeuuid ( '2016-11-01 01:01:01+0000' ), true,
+'$2a$10$5JTB8/hxWc9WAy62nCGSxeefl3KWmipA9nFpVdDa0/xfIseeBB4Bu' );
+
+/** System settings **/
+INSERT INTO thingsboard.admin_settings ( id, key, json_value )
+VALUES ( now ( ), 'general', '{
+ "baseUrl": "http://localhost:8080"
+}' );
+
+INSERT INTO thingsboard.admin_settings ( id, key, json_value )
+VALUES ( now ( ), 'mail', '{
+ "mailFrom": "Thingsboard <sysadmin@localhost.localdomain>",
+ "smtpProtocol": "smtp",
+ "smtpHost": "localhost",
+ "smtpPort": "25",
+ "timeout": "10000",
+ "enableTls": "false",
+ "username": "",
+ "password": ""
+}' );
+
+/** System widgets library **/
+INSERT INTO "thingsboard"."widgets_bundle" ( "id", "tenant_id", "alias", "search_text", "title" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'gpio_widgets', 'gpio widgets', 'GPIO widgets' );
+
+INSERT INTO "thingsboard"."widgets_bundle" ( "id", "tenant_id", "alias", "search_text", "title" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'maps', 'Maps' );
+
+INSERT INTO "thingsboard"."widgets_bundle" ( "id", "tenant_id", "alias", "search_text", "title" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'digital_gauges', 'digital gauges', 'Digital gauges' );
+
+INSERT INTO "thingsboard"."widgets_bundle" ( "id", "tenant_id", "alias", "search_text", "title" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'charts', 'charts', 'Charts' );
+
+INSERT INTO "thingsboard"."widgets_bundle" ( "id", "tenant_id", "alias", "search_text", "title" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'cards', 'cards', 'Cards' );
+
+INSERT INTO "thingsboard"."widgets_bundle" ( "id", "tenant_id", "alias", "search_text", "title" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'analogue_gauges', 'analogue gauges', 'Analogue gauges' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'cards', 'attributes_card',
+'{"type":"latest","sizeX":7.5,"sizeY":3,"resources":[],"templateHtml":"","templateCss":"#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}","controllerScript":"var datasourceTitleCells = [];\nvar valueCells = [];\nvar labelCells = [];\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n\n var container = $(containerElement);\n\n for (var i in datasources) {\n var tbDatasource = datasources[i];\n\n var datasourceId = ''tbDatasource'' + i;\n container.append(\n \"<div id=''\" + datasourceId +\n \"'' class=''tbDatasource-container''></div>\"\n );\n\n var datasourceContainer = $(''#'' + datasourceId,\n container);\n\n datasourceContainer.append(\n \"<div class=''tbDatasource-title''>\" +\n tbDatasource.name + \"</div>\"\n );\n \n var datasourceTitleCell = $(''.tbDatasource-title'', datasourceContainer);\n datasourceTitleCells.push(datasourceTitleCell);\n \n var tableId = ''table'' + i;\n datasourceContainer.append(\n \"<table id=''\" + tableId +\n \"'' class=''tbDatasource-table''><col width=''30%''><col width=''70%''></table>\"\n );\n var table = $(''#'' + tableId, containerElement);\n\n for (var a in tbDatasource.dataKeys) {\n var dataKey = tbDatasource.dataKeys[a];\n var labelCellId = ''labelCell'' + a;\n var cellId = ''cell'' + a;\n table.append(\"<tr><td id=''\" + labelCellId + \"''>\" + dataKey.label +\n \"</td><td id=''\" + cellId +\n \"''></td></tr>\");\n var labelCell = $(''#'' + labelCellId, table);\n labelCells.push(labelCell);\n var valueCell = $(''#'' + cellId, table);\n valueCells.push(valueCell);\n }\n }\n\n}\n\n\nfns.redraw = function(containerElement, width, height, data, timeWindow, sizeChanged) {\n \n if (sizeChanged) {\n var datasoirceTitleFontSize = height/8;\n if (width/height <= 1.5) {\n datasoirceTitleFontSize = width/12;\n }\n datasoirceTitleFontSize = Math.min(datasoirceTitleFontSize, 20);\n for (var i in datasourceTitleCells) {\n datasourceTitleCells[i].css(''font-size'', datasoirceTitleFontSize+''px'');\n }\n var valueFontSize = height/9;\n var labelFontSize = height/9;\n if (width/height <= 1.5) {\n valueFontSize = width/15;\n labelFontSize = width/15;\n }\n valueFontSize = Math.min(valueFontSize, 18);\n labelFontSize = Math.min(labelFontSize, 18);\n\n for (i in valueCells) {\n valueCells[i].css(''font-size'', valueFontSize+''px'');\n valueCells[i].css(''height'', valueFontSize*2.5+''px'');\n valueCells[i].css(''padding'', ''0px '' + valueFontSize + ''px'');\n labelCells[i].css(''font-size'', labelFontSize+''px'');\n labelCells[i].css(''height'', labelFontSize*2.5+''px'');\n labelCells[i].css(''padding'', ''0px '' + labelFontSize + ''px'');\n }\n }\n \n for (i in valueCells) {\n var cellData = data[i];\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length -\n 1];\n var value = tvPair[1];\n valueCells[i].html(value);\n }\n }\n\n};","settingsSchema":"{}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Attributes card\"}"}',
+'Attributes card' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'cards', 'simple_card',
+'{"type":"latest","sizeX":5,"sizeY":3,"resources":[],"templateHtml":"","templateCss":"#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n\n.tbDatasource-table {\n width: 100%;\n height: 100%;\n border-collapse: collapse;\n white-space: nowrap;\n font-weight: 100;\n text-align: right;\n}\n\n.tbDatasource-table td {\n padding: 12px;\n position: relative;\n box-sizing: border-box;\n}\n\n.tbDatasource-data-key {\n opacity: 0.7;\n font-weight: 400;\n font-size: 3.500rem;\n}\n\n.tbDatasource-value {\n font-size: 5.000rem;\n}","controllerScript":"var labelCell;\nvar valueCell;\nvar valueFontSize;\nvar padding;\nvar datasourceContainer;\nvar units;\nvar valueDec;\nvar labelPosition;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n\n var container = $(containerElement);\n \n units = settings.units || \"\";\n valueDec = (typeof settings.valueDec !== ''undefined'' && settings.valueDec !== null)\n ? settings.valueDec : 2;\n \n labelPosition = settings.labelPosition || ''left'';\n \n if (datasources.length > 0) {\n var tbDatasource = datasources[0];\n var datasourceId = ''tbDatasource'' + 0;\n container.append(\n \"<div id=''\" + datasourceId +\n \"'' class=''tbDatasource-container''></div>\"\n );\n \n datasourceContainer = $(''#'' + datasourceId,\n container);\n \n var tableId = ''table'' + 0;\n datasourceContainer.append(\n \"<table id=''\" + tableId +\n \"'' class=''tbDatasource-table''><col width=''30%''><col width=''70%''></table>\"\n );\n var table = $(''#'' + tableId, containerElement);\n if (labelPosition === ''top'') {\n table.css(''text-align'', ''left'');\n }\n \n if (tbDatasource.dataKeys.length > 0) {\n var dataKey = tbDatasource.dataKeys[0];\n var labelCellId = ''labelCell'' + 0;\n var cellId = ''cell'' + 0;\n if (labelPosition === ''left'') {\n table.append(\n \"<tr><td class=''tbDatasource-data-key'' id=''\" + labelCellId +\"''>\" +\n dataKey.label +\n \"</td><td class=''tbDatasource-value'' id=''\" +\n cellId +\n \"''></td></tr>\");\n } else {\n table.append(\n \"<tr style=''vertical-align: bottom;''><td class=''tbDatasource-data-key'' id=''\" + labelCellId +\"''>\" +\n dataKey.label +\n \"</td></tr><tr><td class=''tbDatasource-value'' id=''\" +\n cellId +\n \"''></td></tr>\");\n }\n labelCell = $(''#'' + labelCellId, table);\n valueCell = $(''#'' + cellId, table);\n valueCell.html(0 + '' '' + units);\n }\n }\n \n $.fn.textWidth = function(){\n var html_org = $(this).html();\n var html_calc = ''<span>'' + html_org + ''</span>'';\n $(this).html(html_calc);\n var width = $(this).find(''span:first'').width();\n $(this).html(html_org);\n return width;\n };\n}\n\n\nfns.redraw = function(containerElement, width, height, data, timeWindow, sizeChanged) {\n \n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n \n function padValue(val, dec, int) {\n var i = 0;\n var s, strVal, n;\n \n val = parseFloat(val);\n n = (val < 0);\n val = Math.abs(val);\n \n if (dec > 0) {\n strVal = val.toFixed(dec).toString().split(''.'');\n s = int - strVal[0].length;\n \n for (; i < s; ++i) {\n strVal[0] = ''0'' + strVal[0];\n }\n \n strVal = (n ? ''-'' : '''') + strVal[0] + ''.'' + strVal[1];\n }\n \n else {\n strVal = Math.round(val).toString();\n s = int - strVal.length;\n \n for (; i < s; ++i) {\n strVal = ''0'' + strVal;\n }\n \n strVal = (n ? ''-'' : '''') + strVal;\n }\n \n return strVal;\n }\n \n if (sizeChanged) {\n var labelFontSize;\n if (labelPosition === ''top'') {\n padding = height/20;\n labelFontSize = height/4;\n valueFontSize = height/2;\n } else {\n padding = width/50;\n labelFontSize = height/2.5;\n valueFontSize = height/2;\n if (width/height <= 2.7) {\n labelFontSize = width/7;\n valueFontSize = width/6;\n }\n }\n padding = Math.min(12, padding);\n \n if (labelCell) {\n labelCell.css(''font-size'', labelFontSize+''px'');\n labelCell.css(''padding'', padding+''px'');\n }\n if (valueCell) {\n valueCell.css(''font-size'', valueFontSize+''px'');\n valueCell.css(''padding'', padding+''px'');\n }\n }\n\n if (valueCell && data.length > 0) {\n var cellData = data[0];\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length -\n 1];\n var value = tvPair[1];\n var txtValue;\n if (isNumber(value)) {\n txtValue = padValue(value, valueDec, 0) + '' '' + units;\n } else {\n txtValue = value;\n }\n valueCell.html(txtValue);\n var targetWidth;\n var minDelta;\n if (labelPosition === ''left'') {\n targetWidth = datasourceContainer.width() - labelCell.width();\n minDelta = width/16 + padding;\n } else {\n targetWidth = datasourceContainer.width();\n minDelta = padding;\n }\n var delta = targetWidth - valueCell.textWidth();\n var fontSize = valueFontSize;\n if (targetWidth > minDelta) {\n while (delta < minDelta && fontSize > 6) {\n fontSize--;\n valueCell.css(''font-size'', fontSize+''px'');\n delta = targetWidth - valueCell.textWidth();\n }\n }\n }\n }\n\n};\n","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"units\": {\n \"title\": \"Units\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"valueDec\": {\n \"title\": \"Digits count for decimal part of value\",\n \"type\": \"number\",\n \"default\": 2\n },\n \"labelPosition\": {\n \"title\": \"Label position\",\n \"type\": \"string\",\n \"default\": \"left\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"units\",\n \"valueDec\",\n {\n \"key\": \"labelPosition\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"left\",\n \"label\": \"Left\"\n },\n {\n \"value\": \"top\",\n \"label\": \"Top\"\n }\n ]\n }\n ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ff5722\",\"color\":\"rgba(255, 255, 255, 0.87)\",\"padding\":\"16px\",\"settings\":{\"units\":\"°C\",\"valueDec\":1,\"labelPosition\":\"top\"},\"title\":\"Simple card\"}"}',
+'Simple card' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'analogue_gauges', 'speed_gauge_canvas_gauges',
+'{"type":"latest","sizeX":7,"sizeY":5,"resources":[],"templateHtml":"<canvas id=\"radialGauge\"></canvas>\n","templateCss":"","controllerScript":"var gauge;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n gauge = new TbAnalogueRadialGauge(containerElement, settings, data, ''radialGauge''); \n\n}\n\n\nfns.redraw = function(containerElement, width, height, data, timeWindow, sizeChanged) {\n gauge.redraw(width, height, data, sizeChanged);\n};\n\nfns.destroy = function() {\n}\n","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"minValue\": {\n \"title\": \"Minimum value\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"maxValue\": {\n \"title\": \"Maximum value\",\n \"type\": \"number\",\n \"default\": 100\n },\n \"unitTitle\": {\n \"title\": \"Unit title\",\n \"type\": \"string\",\n \"default\": null\n },\n \"showUnitTitle\": {\n \"title\": \"Show unit title\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"units\": {\n \"title\": \"Units\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"majorTicksCount\": {\n \"title\": \"Major ticks count\",\n \"type\": \"number\",\n \"default\": null\n },\n \"minorTicks\": {\n \"title\": \"Minor ticks count\",\n \"type\": \"number\",\n \"default\": 2\n },\n \"valueBox\": {\n \"title\": \"Show value box\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"valueInt\": {\n \"title\": \"Digits count for integer part of value\",\n \"type\": \"number\",\n \"default\": 3\n },\n \"valueDec\": {\n \"title\": \"Digits count for decimal part of value\",\n \"type\": \"number\",\n \"default\": 2\n },\n \"defaultColor\": {\n \"title\": \"Default color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"colorPlate\": {\n \"title\": \"Plate color\",\n \"type\": \"string\",\n \"default\": \"#fff\"\n },\n \"colorMajorTicks\": {\n \"title\": \"Major ticks color\",\n \"type\": \"string\",\n \"default\": \"#444\"\n },\n \"colorMinorTicks\": {\n \"title\": \"Minor ticks color\",\n \"type\": \"string\",\n \"default\": \"#666\"\n },\n \"colorNeedle\": {\n \"title\": \"Needle color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"colorNeedleEnd\": {\n \"title\": \"Needle color - end gradient\",\n \"type\": \"string\",\n \"default\": null\n },\n \"colorNeedleShadowUp\": {\n \"title\": \"Upper half of the needle shadow color\",\n \"type\": \"string\",\n \"default\": \"rgba(2,255,255,0.2)\"\n },\n \"colorNeedleShadowDown\": {\n \"title\": \"Drop shadow needle color.\",\n \"type\": \"string\",\n \"default\": \"rgba(188,143,143,0.45)\"\n },\n \"colorValueBoxRect\": {\n \"title\": \"Value box rectangle stroke color\",\n \"type\": \"string\",\n \"default\": \"#888\"\n },\n \"colorValueBoxRectEnd\": {\n \"title\": \"Value box rectangle stroke color - end gradient\",\n \"type\": \"string\",\n \"default\": \"#666\"\n },\n \"colorValueBoxBackground\": {\n \"title\": \"Value box background color\",\n \"type\": \"string\",\n \"default\": \"#babab2\"\n },\n \"colorValueBoxShadow\": {\n \"title\": \"Value box shadow color\",\n \"type\": \"string\",\n \"default\": \"rgba(0,0,0,1)\"\n },\n \"highlights\": {\n \"title\": \"Highlights\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Highlight\",\n \"type\": \"object\",\n \"properties\": {\n \"from\": {\n \"title\": \"From\",\n \"type\": \"number\"\n },\n \"to\": {\n \"title\": \"To\",\n \"type\": \"number\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n }\n }\n }\n },\n \"highlightsWidth\": {\n \"title\": \"Highlights width\",\n \"type\": \"number\",\n \"default\": 15\n },\n \"showBorder\": {\n \"title\": \"Show border\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"numbersFont\": {\n \"title\": \"Tick numbers font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 18\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"titleFont\": {\n \"title\": \"Title text font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 24\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": \"#888\"\n }\n }\n },\n \"unitsFont\": {\n \"title\": \"Units text font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 22\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": \"#888\"\n }\n }\n },\n \"valueFont\": {\n \"title\": \"Value text font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 40\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": \"#444\"\n },\n \"shadowColor\": {\n \"title\": \"Shadow color\",\n \"type\": \"string\",\n \"default\": \"rgba(0,0,0,0.3)\"\n }\n }\n },\n \"animation\": {\n \"title\": \"Enable animation\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"animationDuration\": {\n \"title\": \"Animation duration\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"animationRule\": {\n \"title\": \"Animation rule\",\n \"type\": \"string\",\n \"default\": \"cycle\"\n },\n \"startAngle\": {\n \"title\": \"Start ticks angle\",\n \"type\": \"number\",\n \"default\": 45\n },\n \"ticksAngle\": {\n \"title\": \"Ticks angle\",\n \"type\": \"number\",\n \"default\": 270\n },\n \"needleCircleSize\": {\n \"title\": \"Needle circle size\",\n \"type\": \"number\",\n \"default\": 10\n }\n },\n \"required\": []\n },\n \"form\": [\n \"startAngle\",\n \"ticksAngle\",\n \"needleCircleSize\",\n \"minValue\",\n \"maxValue\",\n \"unitTitle\",\n \"showUnitTitle\",\n \"units\",\n \"majorTicksCount\",\n \"minorTicks\",\n \"valueBox\",\n \"valueInt\",\n \"valueDec\",\n {\n \"key\": \"defaultColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorPlate\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorMajorTicks\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorMinorTicks\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorNeedle\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorNeedleEnd\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorNeedleShadowUp\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorNeedleShadowDown\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorValueBoxRect\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorValueBoxRectEnd\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorValueBoxBackground\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorValueBoxShadow\",\n \"type\": \"color\"\n },\n {\n \"key\": \"highlights\",\n \"items\": [\n \"highlights[].from\",\n \"highlights[].to\",\n {\n \"key\": \"highlights[].color\",\n \"type\": \"color\"\n }\n ]\n },\n \"highlightsWidth\",\n \"showBorder\",\n {\n \"key\": \"numbersFont\",\n \"items\": [\n \"numbersFont.family\",\n \"numbersFont.size\",\n {\n \"key\": \"numbersFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"numbersFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"numbersFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"titleFont\",\n \"items\": [\n \"titleFont.family\",\n \"titleFont.size\",\n {\n \"key\": \"titleFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"titleFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"titleFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"unitsFont\",\n \"items\": [\n \"unitsFont.family\",\n \"unitsFont.size\",\n {\n \"key\": \"unitsFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"unitsFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"unitsFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"valueFont\",\n \"items\": [\n \"valueFont.family\",\n \"valueFont.size\",\n {\n \"key\": \"valueFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"valueFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"valueFont.color\",\n \"type\": \"color\"\n },\n {\n \"key\": \"valueFont.shadowColor\",\n \"type\": \"color\"\n }\n ]\n }, \n \"animation\",\n \"animationDuration\",\n {\n \"key\": \"animationRule\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \"quad\",\n \"label\": \"Quad\"\n },\n {\n \"value\": \"quint\",\n \"label\": \"Quint\"\n },\n {\n \"value\": \"cycle\",\n \"label\": \"Cycle\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n },\n {\n \"value\": \"elastic\",\n \"label\": \"Elastic\"\n },\n {\n \"value\": \"dequad\",\n \"label\": \"Dequad\"\n },\n {\n \"value\": \"dequint\",\n \"label\": \"Dequint\"\n },\n {\n \"value\": \"decycle\",\n \"label\": \"Decycle\"\n },\n {\n \"value\": \"debounce\",\n \"label\": \"Debounce\"\n },\n {\n \"value\": \"delastic\",\n \"label\": \"Delastic\"\n }\n ]\n }\n ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 220) {\\n\\tvalue = 220;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":180,\"startAngle\":45,\"ticksAngle\":270,\"showBorder\":false,\"defaultColor\":\"#e65100\",\"needleCircleSize\":7,\"highlights\":[{\"from\":80,\"to\":120,\"color\":\"#fdd835\"},{\"color\":\"#e57373\",\"from\":120,\"to\":180}],\"showUnitTitle\":false,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":2,\"valueInt\":3,\"minValue\":0,\"valueDec\":0,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":1500,\"animationRule\":\"linear\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188, 143, 143, 0.78)\",\"units\":\"MPH\",\"majorTicksCount\":9,\"numbersFont\":{\"family\":\"RobotoDraft\",\"size\":22,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"titleFont\":{\"family\":\"RobotoDraft\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"unitsFont\":{\"family\":\"RobotoDraft\",\"size\":28,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"size\":32,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\",\"family\":\"Segment7Standard\"},\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\"},\"title\":\"Speed gauge - Canvas Gauges\"}"}',
+'Speed gauge - Canvas Gauges' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'analogue_gauges', 'temperature_gauge_canvas_gauges',
+'{"type":"latest","sizeX":7,"sizeY":3,"resources":[],"templateHtml":"<canvas id=\"linearGauge\"></canvas>\n","templateCss":"","controllerScript":"var gauge;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n gauge = new TbAnalogueLinearGauge(containerElement, settings, data, ''linearGauge''); \n\n}\n\n\nfns.redraw = function(containerElement, width, height, data, timeWindow, sizeChanged) {\n gauge.redraw(width, height, data, sizeChanged);\n};\n\nfns.destroy = function() {\n}\n","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"minValue\": {\n \"title\": \"Minimum value\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"maxValue\": {\n \"title\": \"Maximum value\",\n \"type\": \"number\",\n \"default\": 100\n },\n \"unitTitle\": {\n \"title\": \"Unit title\",\n \"type\": \"string\",\n \"default\": null\n },\n \"showUnitTitle\": {\n \"title\": \"Show unit title\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"units\": {\n \"title\": \"Units\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"majorTicksCount\": {\n \"title\": \"Major ticks count\",\n \"type\": \"number\",\n \"default\": null\n },\n \"minorTicks\": {\n \"title\": \"Minor ticks count\",\n \"type\": \"number\",\n \"default\": 2\n },\n \"valueBox\": {\n \"title\": \"Show value box\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"valueInt\": {\n \"title\": \"Digits count for integer part of value\",\n \"type\": \"number\",\n \"default\": 3\n },\n \"valueDec\": {\n \"title\": \"Digits count for decimal part of value\",\n \"type\": \"number\",\n \"default\": 2\n },\n \"defaultColor\": {\n \"title\": \"Default color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"colorPlate\": {\n \"title\": \"Plate color\",\n \"type\": \"string\",\n \"default\": \"#fff\"\n },\n \"colorMajorTicks\": {\n \"title\": \"Major ticks color\",\n \"type\": \"string\",\n \"default\": \"#444\"\n },\n \"colorMinorTicks\": {\n \"title\": \"Minor ticks color\",\n \"type\": \"string\",\n \"default\": \"#666\"\n },\n \"colorNeedle\": {\n \"title\": \"Needle color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"colorNeedleEnd\": {\n \"title\": \"Needle color - end gradient\",\n \"type\": \"string\",\n \"default\": null\n },\n \"colorNeedleShadowUp\": {\n \"title\": \"Upper half of the needle shadow color\",\n \"type\": \"string\",\n \"default\": \"rgba(2,255,255,0.2)\"\n },\n \"colorNeedleShadowDown\": {\n \"title\": \"Drop shadow needle color.\",\n \"type\": \"string\",\n \"default\": \"rgba(188,143,143,0.45)\"\n },\n \"colorValueBoxRect\": {\n \"title\": \"Value box rectangle stroke color\",\n \"type\": \"string\",\n \"default\": \"#888\"\n },\n \"colorValueBoxRectEnd\": {\n \"title\": \"Value box rectangle stroke color - end gradient\",\n \"type\": \"string\",\n \"default\": \"#666\"\n },\n \"colorValueBoxBackground\": {\n \"title\": \"Value box background color\",\n \"type\": \"string\",\n \"default\": \"#babab2\"\n },\n \"colorValueBoxShadow\": {\n \"title\": \"Value box shadow color\",\n \"type\": \"string\",\n \"default\": \"rgba(0,0,0,1)\"\n },\n \"highlights\": {\n \"title\": \"Highlights\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Highlight\",\n \"type\": \"object\",\n \"properties\": {\n \"from\": {\n \"title\": \"From\",\n \"type\": \"number\"\n },\n \"to\": {\n \"title\": \"To\",\n \"type\": \"number\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n }\n }\n }\n },\n \"highlightsWidth\": {\n \"title\": \"Highlights width\",\n \"type\": \"number\",\n \"default\": 15\n },\n \"showBorder\": {\n \"title\": \"Show border\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"numbersFont\": {\n \"title\": \"Tick numbers font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 18\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"titleFont\": {\n \"title\": \"Title text font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 24\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": \"#888\"\n }\n }\n },\n \"unitsFont\": {\n \"title\": \"Units text font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 22\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": \"#888\"\n }\n }\n },\n \"valueFont\": {\n \"title\": \"Value text font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 40\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": \"#444\"\n },\n \"shadowColor\": {\n \"title\": \"Shadow color\",\n \"type\": \"string\",\n \"default\": \"rgba(0,0,0,0.3)\"\n }\n }\n },\n \"animation\": {\n \"title\": \"Enable animation\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"animationDuration\": {\n \"title\": \"Animation duration\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"animationRule\": {\n \"title\": \"Animation rule\",\n \"type\": \"string\",\n \"default\": \"cycle\"\n },\n \"barStrokeWidth\": {\n \"title\": \"Bar stroke width\",\n \"type\": \"number\",\n \"default\": 2.5\n },\n \"colorBarStroke\": {\n \"title\": \"Bar stroke color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"colorBar\": {\n \"title\": \"Bar background color\",\n \"type\": \"string\",\n \"default\": \"#fff\"\n },\n \"colorBarEnd\": {\n \"title\": \"Bar background color - end gradient\",\n \"type\": \"string\",\n \"default\": \"#ddd\"\n },\n \"colorBarProgress\": {\n \"title\": \"Progress bar color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"colorBarProgressEnd\": {\n \"title\": \"Progress bar color - end gradient\",\n \"type\": \"string\",\n \"default\": null\n } \n \n },\n \"required\": []\n },\n \"form\": [\n \"barStrokeWidth\",\n {\n \"key\": \"colorBarStroke\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorBar\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorBarEnd\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorBarProgress\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorBarProgressEnd\",\n \"type\": \"color\"\n },\n \"minValue\",\n \"maxValue\",\n \"unitTitle\",\n \"showUnitTitle\",\n \"units\",\n \"majorTicksCount\",\n \"minorTicks\",\n \"valueBox\",\n \"valueInt\",\n \"valueDec\",\n {\n \"key\": \"defaultColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorPlate\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorMajorTicks\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorMinorTicks\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorNeedle\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorNeedleEnd\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorNeedleShadowUp\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorNeedleShadowDown\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorValueBoxRect\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorValueBoxRectEnd\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorValueBoxBackground\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorValueBoxShadow\",\n \"type\": \"color\"\n },\n {\n \"key\": \"highlights\",\n \"items\": [\n \"highlights[].from\",\n \"highlights[].to\",\n {\n \"key\": \"highlights[].color\",\n \"type\": \"color\"\n }\n ]\n },\n \"highlightsWidth\",\n \"showBorder\",\n {\n \"key\": \"numbersFont\",\n \"items\": [\n \"numbersFont.family\",\n \"numbersFont.size\",\n {\n \"key\": \"numbersFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"numbersFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"numbersFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"titleFont\",\n \"items\": [\n \"titleFont.family\",\n \"titleFont.size\",\n {\n \"key\": \"titleFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"titleFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"titleFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"unitsFont\",\n \"items\": [\n \"unitsFont.family\",\n \"unitsFont.size\",\n {\n \"key\": \"unitsFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"unitsFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"unitsFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"valueFont\",\n \"items\": [\n \"valueFont.family\",\n \"valueFont.size\",\n {\n \"key\": \"valueFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"valueFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"valueFont.color\",\n \"type\": \"color\"\n },\n {\n \"key\": \"valueFont.shadowColor\",\n \"type\": \"color\"\n }\n ]\n }, \n \"animation\",\n \"animationDuration\",\n {\n \"key\": \"animationRule\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \"quad\",\n \"label\": \"Quad\"\n },\n {\n \"value\": \"quint\",\n \"label\": \"Quint\"\n },\n {\n \"value\": \"cycle\",\n \"label\": \"Cycle\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n },\n {\n \"value\": \"elastic\",\n \"label\": \"Elastic\"\n },\n {\n \"value\": \"dequad\",\n \"label\": \"Dequad\"\n },\n {\n \"value\": \"dequint\",\n \"label\": \"Dequint\"\n },\n {\n \"value\": \"decycle\",\n \"label\": \"Decycle\"\n },\n {\n \"value\": \"debounce\",\n \"label\": \"Debounce\"\n },\n {\n \"value\": \"delastic\",\n \"label\": \"Delastic\"\n }\n ]\n }\n ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 30 - 15;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":100,\"defaultColor\":\"#e64a19\",\"barStrokeWidth\":2.5,\"colorBar\":\"rgba(255, 255, 255, 0.4)\",\"colorBarEnd\":\"rgba(221, 221, 221, 0.38)\",\"showUnitTitle\":true,\"minorTicks\":2,\"valueBox\":true,\"valueInt\":3,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"colorNeedleShadowUp\":\"rgba(2,255,255,0.2)\",\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\",\"highlightsWidth\":10,\"animation\":true,\"animationDuration\":1500,\"animationRule\":\"linear\",\"showBorder\":false,\"majorTicksCount\":8,\"numbersFont\":{\"family\":\"Arial\",\"size\":18,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"titleFont\":{\"family\":\"RobotoDraft\",\"size\":24,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#78909c\"},\"unitsFont\":{\"family\":\"RobotoDraft\",\"size\":26,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#37474f\"},\"valueFont\":{\"family\":\"RobotoDraft\",\"size\":40,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#444\",\"shadowColor\":\"rgba(0,0,0,0.3)\"},\"minValue\":-60,\"highlights\":[{\"from\":0,\"to\":20,\"color\":\"#90caf9\"},{\"from\":20,\"to\":40,\"color\":\"rgba(144, 202, 249, 0.66)\"},{\"from\":40,\"to\":60,\"color\":\"rgba(144, 202, 249, 0.33)\"},{\"from\":60,\"to\":80,\"color\":\"rgba(244, 67, 54, 0.2)\"},{\"from\":80,\"to\":100,\"color\":\"rgba(244, 67, 54, 0.4)\"},{\"from\":100,\"to\":120,\"color\":\"rgba(244, 67, 54, 0.6)\"},{\"from\":120,\"to\":140,\"color\":\"rgba(244, 67, 54, 0.8)\"},{\"from\":140,\"to\":160,\"color\":\"#f44336\"}],\"unitTitle\":\"Temperature\",\"units\":\"°C\",\"colorBarProgress\":\"#90caf9\",\"colorBarProgressEnd\":\"#f44336\",\"colorBarStroke\":\"#b0bec5\",\"valueDec\":1},\"title\":\"Temperature gauge - Canvas Gauges\"}"}',
+'Temperature gauge - Canvas Gauges' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'gpio_widgets', 'gpio_panel',
+'{"type":"latest","sizeX":5,"sizeY":2,"resources":[],"templateHtml":"<div class=\"gpio-panel\" style=\"height: 100%;\">\n <section layout=\"row\" ng-repeat=\"row in rows\">\n <section flex layout=\"row\" ng-repeat=\"cell in row\">\n <section layout=\"row\" flex ng-if=\"cell\" layout-align=\"{{$index===0 ? ''end center'' : ''start center''}}\">\n <span class=\"gpio-left-label\" ng-show=\"$index===0\">{{ cell.label }}</span>\n <section layout=\"row\" class=\"led-panel\" ng-class=\"$index===0 ? ''col-0'' : ''col-1''\"\n style=\"background-color: {{ ledPanelBackgroundColor }};\">\n <span class=\"pin\" ng-show=\"$index===0\">{{cell.pin}}</span>\n <span class=\"led-container\">\n <tb-led-light size=\"prefferedRowHeight\"\n color-on=\"cell.colorOn\"\n color-off=\"cell.colorOff\"\n off-opacity=\"''0.9''\"\n tb-enabled=\"cell.enabled\">\n </tb-led-light>\n </span>\n <span class=\"pin\" ng-show=\"$index===1\">{{cell.pin}}</span>\n </section>\n <span class=\"gpio-right-label\" ng-show=\"$index===1\">{{ cell.label }}</span>\n </section>\n <section layout=\"row\" flex ng-if=\"!cell\">\n <span flex ng-show=\"$index===0\"></span>\n <span class=\"led-panel\"\n style=\"background-color: {{ ledPanelBackgroundColor }};\"></span>\n <span flex ng-show=\"$index===1\"></span>\n </section>\n </section>\n </section> \n</div>","templateCss":".error {\n font-size: 14px !important;\n color: maroon;/*rgb(250,250,250);*/\n background-color: transparent;\n padding: 6px;\n}\n\n.error span {\n margin: auto;\n}\n\n.gpio-panel {\n padding-top: 10px;\n white-space: nowrap;\n}\n\n.gpio-panel tb-led-light > div {\n margin: auto;\n}\n\n.led-panel {\n margin: 0;\n width: 66px;\n min-width: 66px;\n}\n\n.led-container {\n width: 48px;\n min-width: 48px;\n}\n\n.pin {\n margin-top: auto;\n margin-bottom: auto;\n color: white;\n font-size: 12px;\n width: 16px;\n min-width: 16px;\n}\n\n.led-panel.col-0 .pin {\n margin-left: auto;\n padding-left: 2px;\n text-align: right;\n}\n\n.led-panel.col-1 .pin {\n margin-right: auto;\n \n text-align: left;\n}\n\n.gpio-left-label {\n margin-right: 8px;\n}\n\n.gpio-right-label {\n margin-left: 8px;\n}","controllerScript":"\nfns.init = function(containerElement, settings, datasources, data, scope, controlApi) {\n \n var i, gpio;\n \n scope.gpioList = [];\n scope.gpioByPin = {};\n for (var g in settings.gpioList) {\n gpio = settings.gpioList[g];\n scope.gpioList.push(\n {\n row: gpio.row,\n col: gpio.col,\n pin: gpio.pin,\n label: gpio.label,\n enabled: false,\n colorOn: tinycolor(gpio.color).lighten(20).toHexString(),\n colorOff: tinycolor(gpio.color).darken().toHexString()\n }\n );\n scope.gpioByPin[gpio.pin] = scope.gpioList[scope.gpioList.length-1];\n }\n\n scope.ledPanelBackgroundColor = settings.ledPanelBackgroundColor || tinycolor(''green'').lighten(2).toRgbString();\n\n scope.gpioCells = {};\n var rowCount = 0;\n for (i = 0; i < scope.gpioList.length; i++) {\n gpio = scope.gpioList[i];\n scope.gpioCells[gpio.row+''_''+gpio.col] = gpio;\n rowCount = Math.max(rowCount, gpio.row+1);\n }\n \n scope.prefferedRowHeight = 32;\n scope.rows = [];\n for (i = 0; i < rowCount; i++) {\n var row = [];\n for (var c =0; c<2;c++) {\n if (scope.gpioCells[i+''_''+c]) {\n row[c] = scope.gpioCells[i+''_''+c];\n } else {\n row[c] = null;\n }\n }\n scope.rows.push(row);\n }\n\n};\n\nfns.redraw = function(containerElement, width, height, data, timeWindow, sizeChanged, scope) {\n \n if (sizeChanged) {\n var rowCount = scope.rows.length;\n var prefferedRowHeight = (height - 35)/rowCount;\n prefferedRowHeight = Math.min(32, prefferedRowHeight);\n prefferedRowHeight = Math.max(12, prefferedRowHeight);\n scope.$apply(function(scope) {\n scope.prefferedRowHeight = prefferedRowHeight;\n });\n var ratio = prefferedRowHeight/32;\n \n var leftLabels = $(''.gpio-left-label'', containerElement);\n leftLabels.css(''font-size'', 16*ratio+''px'');\n var rightLabels = $(''.gpio-right-label'', containerElement);\n rightLabels.css(''font-size'', 16*ratio+''px'');\n var pins = $(''.pin'', containerElement);\n var pinsFontSize = Math.max(9, 12*ratio);\n pins.css(''font-size'', pinsFontSize+''px'');\n }\n \n var changed = false;\n for (var d in data) {\n var cellData = data[d];\n var dataKey = cellData.dataKey;\n var gpio = scope.gpioByPin[dataKey.label];\n if (gpio) {\n var enabled = false;\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n enabled = (tvPair[1] === true || tvPair[1] === ''true'');\n }\n if (gpio.enabled != enabled) {\n changed = true;\n gpio.enabled = enabled;\n }\n }\n }\n if (changed) {\n scope.$apply();\n }\n};\n\nfns.destroy = function() {\n};","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"gpioList\": {\n \"title\": \"Gpio leds\",\n \"type\": \"array\",\n \"minItems\" : 1,\n \"items\": {\n \"title\": \"Gpio led\",\n \"type\": \"object\",\n \"properties\": {\n \"pin\": {\n \"title\": \"Pin\",\n \"type\": \"number\"\n },\n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"row\": {\n \"title\": \"Row\",\n \"type\": \"number\"\n },\n \"col\": {\n \"title\": \"Column\",\n \"type\": \"number\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\",\n \"default\": \"red\"\n }\n },\n \"required\": [\"pin\", \"label\", \"row\", \"col\", \"color\"]\n }\n },\n \"ledPanelBackgroundColor\": {\n \"title\": \"LED panel background color\",\n \"type\": \"string\",\n \"default\": \"#008a00\"\n } \n },\n \"required\": [\"gpioList\", \n \"ledPanelBackgroundColor\"]\n },\n \"form\": [\n {\n \"key\": \"gpioList\",\n \"items\": [\n \"gpioList[].pin\",\n \"gpioList[].label\",\n \"gpioList[].row\",\n \"gpioList[].col\",\n {\n \"key\": \"gpioList[].color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"ledPanelBackgroundColor\",\n \"type\": \"color\"\n }\n ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"gpioList\":[{\"pin\":1,\"label\":\"GPIO 1\",\"row\":0,\"col\":0,\"color\":\"#008000\",\"_uniqueKey\":0},{\"pin\":2,\"label\":\"GPIO 2\",\"row\":0,\"col\":1,\"color\":\"#ffff00\",\"_uniqueKey\":1},{\"pin\":3,\"label\":\"GPIO 3\",\"row\":1,\"col\":0,\"color\":\"#cf006f\",\"_uniqueKey\":2}],\"ledPanelBackgroundColor\":\"#b71c1c\"},\"title\":\"Basic GPIO Panel\",\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"1\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.22518255793320163,\"funcBody\":\"var period = time % 1500;\\nreturn period < 500;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"2\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.7008206860666621,\"funcBody\":\"var period = time % 1500;\\nreturn period >= 500 && period < 1000;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"3\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.42600325102193426,\"funcBody\":\"var period = time % 1500;\\nreturn period >= 1000;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}}}"}',
+'Basic GPIO Panel' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'gpio_widgets', 'basic_gpio_control',
+'{"type":"rpc","sizeX":4,"sizeY":2,"resources":[],"templateHtml":"<fieldset class=\"gpio-panel\" ng-disabled=\"!rpcEnabled || executingRpcRequest\" style=\"height: 100%;\">\n <section class=\"gpio-row\" layout=\"row\" ng-repeat=\"row in rows track by $index\" \n ng-style=\"{''height'': prefferedRowHeight+''px''}\">\n <section flex layout=\"row\" ng-repeat=\"cell in row track by $index\">\n <section layout=\"row\" flex ng-if=\"cell\" layout-align=\"{{$index===0 ? ''end center'' : ''start center''}}\">\n <span class=\"gpio-left-label\" ng-show=\"$index===0\">{{ cell.label }}</span>\n <section layout=\"row\" class=\"switch-panel\" layout-align=\"start center\" ng-class=\"$index===0 ? ''col-0'' : ''col-1''\"\n ng-style=\"{''height'': prefferedRowHeight+''px'', ''backgroundColor'': ''{{ switchPanelBackgroundColor }}''}\">\n <span class=\"pin\" ng-show=\"$index===0\">{{cell.pin}}</span>\n <span flex ng-show=\"$index===1\"></span>\n <md-switch\n aria-label=\"{{ cell.label }}\"\n ng-disabled=\"!rpcEnabled || executingRpcRequest\"\n ng-model=\"cell.enabled\" \n ng-change=\"cell.enabled = !cell.enabled\" \n ng-click=\"gpioClick($event, cell)\">\n </md-switch>\n <span flex ng-show=\"$index===0\"></span>\n <span class=\"pin\" ng-show=\"$index===1\">{{cell.pin}}</span>\n </section>\n <span class=\"gpio-right-label\" ng-show=\"$index===1\">{{ cell.label }}</span>\n </section>\n <section layout=\"row\" flex ng-if=\"!cell\">\n <span flex ng-show=\"$index===0\"></span>\n <span class=\"switch-panel\"\n ng-style=\"{''height'': prefferedRowHeight+''px'', ''backgroundColor'': ''{{ switchPanelBackgroundColor }}''}\"></span>\n <span flex ng-show=\"$index===1\"></span>\n </section>\n </section>\n </section> \n <span class=\"error\" style=\"position: absolute; bottom: 5px;\" ng-show=\"rpcErrorText\">{{rpcErrorText}}</span>\n <md-progress-linear ng-show=\"executingRpcRequest\" style=\"position: absolute; bottom: 0;\" md-mode=\"indeterminate\"></md-progress-linear> \n</fieldset>","templateCss":".error {\n font-size: 14px !important;\n color: maroon;/*rgb(250,250,250);*/\n background-color: transparent;\n padding: 6px;\n}\n\n.error span {\n margin: auto;\n}\n\n.gpio-panel {\n padding-top: 10px;\n white-space: nowrap;\n}\n\n.switch-panel {\n margin: 0;\n height: 32px;\n width: 66px;\n min-width: 66px;\n}\n\n.switch-panel md-switch {\n margin: 0;\n width: 36px;\n min-width: 36px;\n}\n\n.switch-panel md-switch > div.md-container {\n margin: 0;\n}\n\n.switch-panel.col-0 md-switch {\n padding-left: 8px;\n padding-right: 4px;\n}\n\n.switch-panel.col-1 md-switch {\n padding-left: 4px;\n padding-right: 8px;\n}\n\n.gpio-row {\n height: 32px;\n}\n\n.pin {\n margin-top: auto;\n margin-bottom: auto;\n color: white;\n font-size: 12px;\n width: 16px;\n min-width: 16px;\n}\n\n.switch-panel.col-0 .pin {\n margin-left: auto;\n padding-left: 2px;\n text-align: right;\n}\n\n.switch-panel.col-1 .pin {\n margin-right: auto;\n \n text-align: left;\n}\n\n.gpio-left-label {\n margin-right: 8px;\n}\n\n.gpio-right-label {\n margin-left: 8px;\n}","controllerScript":"\nfns.init = function(containerElement, settings, datasources, data, scope, controlApi) {\n \n var i, gpio;\n \n scope.gpioList = [];\n for (var g in settings.gpioList) {\n gpio = settings.gpioList[g];\n scope.gpioList.push(\n {\n row: gpio.row,\n col: gpio.col,\n pin: gpio.pin,\n label: gpio.label,\n enabled: false\n }\n );\n }\n\n scope.requestTimeout = settings.requestTimeout || 1000;\n\n scope.switchPanelBackgroundColor = settings.switchPanelBackgroundColor || tinycolor(''green'').lighten(2).toRgbString();\n\n scope.gpioStatusRequest = {\n method: \"getGpioStatus\",\n paramsBody: \"{}\"\n };\n \n if (settings.gpioStatusRequest) {\n scope.gpioStatusRequest.method = settings.gpioStatusRequest.method || scope.gpioStatusRequest.method;\n scope.gpioStatusRequest.paramsBody = settings.gpioStatusRequest.paramsBody || scope.gpioStatusRequest.paramsBody;\n }\n \n scope.gpioStatusChangeRequest = {\n method: \"setGpioStatus\",\n paramsBody: \"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"\n };\n \n if (settings.gpioStatusChangeRequest) {\n scope.gpioStatusChangeRequest.method = settings.gpioStatusChangeRequest.method || scope.gpioStatusChangeRequest.method;\n scope.gpioStatusChangeRequest.paramsBody = settings.gpioStatusChangeRequest.paramsBody || scope.gpioStatusChangeRequest.paramsBody;\n }\n \n scope.parseGpioStatusFunction = \"return body[pin] === true;\";\n \n if (settings.parseGpioStatusFunction && settings.parseGpioStatusFunction.length > 0) {\n scope.parseGpioStatusFunction = settings.parseGpioStatusFunction;\n }\n \n scope.parseGpioStatusFunction = new Function(\"body, pin\", scope.parseGpioStatusFunction);\n \n function requestGpioStatus() {\n controlApi.sendTwoWayCommand(scope.gpioStatusRequest.method, \n scope.gpioStatusRequest.paramsBody, \n scope.requestTimeout)\n .then(\n function success(responseBody) {\n for (var g in scope.gpioList) {\n var gpio = scope.gpioList[g];\n var enabled = scope.parseGpioStatusFunction.apply(this, [responseBody, gpio.pin]);\n gpio.enabled = enabled; \n }\n }\n );\n }\n \n function changeGpioStatus(gpio) {\n var pin = gpio.pin + '''';\n var enabled = !gpio.enabled;\n enabled = enabled === true ? ''true'' : ''false'';\n var paramsBody = scope.gpioStatusChangeRequest.paramsBody;\n var requestBody = JSON.parse(paramsBody.replace(\"\\\"{$pin}\\\"\", pin).replace(\"\\\"{$enabled}\\\"\", enabled));\n controlApi.sendTwoWayCommand(scope.gpioStatusChangeRequest.method, \n requestBody, scope.requestTimeout)\n .then(\n function success(responseBody) {\n var enabled = scope.parseGpioStatusFunction.apply(this, [responseBody, gpio.pin]);\n gpio.enabled = enabled;\n }\n );\n }\n \n scope.gpioCells = {};\n var rowCount = 0;\n for (i = 0; i < scope.gpioList.length; i++) {\n gpio = scope.gpioList[i];\n scope.gpioCells[gpio.row+''_''+gpio.col] = gpio;\n rowCount = Math.max(rowCount, gpio.row+1);\n }\n \n scope.prefferedRowHeight = 32;\n scope.rows = [];\n for (i = 0; i < rowCount; i++) {\n var row = [];\n for (var c =0; c<2;c++) {\n if (scope.gpioCells[i+''_''+c]) {\n row[c] = scope.gpioCells[i+''_''+c];\n } else {\n row[c] = null;\n }\n }\n scope.rows.push(row);\n }\n\n scope.gpioClick = function($event, gpio) {\n changeGpioStatus(gpio);\n };\n\n requestGpioStatus();\n \n};\n\nfns.redraw = function(containerElement, width, height, data, timeWindow, sizeChanged, scope) {\n if (sizeChanged) {\n var rowCount = scope.rows.length;\n var prefferedRowHeight = (height - 35)/rowCount;\n prefferedRowHeight = Math.min(32, prefferedRowHeight);\n prefferedRowHeight = Math.max(12, prefferedRowHeight);\n scope.$apply(function(scope) {\n scope.prefferedRowHeight = prefferedRowHeight;\n });\n var ratio = prefferedRowHeight/32;\n var switches = $(''md-switch'', containerElement);\n switches.css(''height'', 30*ratio+''px'');\n switches.css(''width'', 36*ratio+''px'');\n switches.css(''min-width'', 36*ratio+''px'');\n $(''.md-container'', switches).css(''height'', 24*ratio+''px'');\n $(''.md-container'', switches).css(''width'', 36*ratio+''px'');\n var bars = $(''.md-bar'', containerElement);\n bars.css(''height'', 14*ratio+''px'');\n bars.css(''width'', 34*ratio+''px'');\n var thumbs = $(''.md-thumb'', containerElement);\n thumbs.css(''height'', 20*ratio+''px'');\n thumbs.css(''width'', 20*ratio+''px'');\n \n var leftLabels = $(''.gpio-left-label'', containerElement);\n leftLabels.css(''font-size'', 16*ratio+''px'');\n var rightLabels = $(''.gpio-right-label'', containerElement);\n rightLabels.css(''font-size'', 16*ratio+''px'');\n var pins = $(''.pin'', containerElement);\n var pinsFontSize = Math.max(9, 12*ratio);\n pins.css(''font-size'', pinsFontSize+''px'');\n }\n};\n\nfns.destroy = function() {\n};","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"gpioList\": {\n \"title\": \"Gpio switches\",\n \"type\": \"array\",\n \"minItems\" : 1,\n \"items\": {\n \"title\": \"Gpio switch\",\n \"type\": \"object\",\n \"properties\": {\n \"pin\": {\n \"title\": \"Pin\",\n \"type\": \"number\"\n },\n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"row\": {\n \"title\": \"Row\",\n \"type\": \"number\"\n },\n \"col\": {\n \"title\": \"Column\",\n \"type\": \"number\"\n }\n },\n \"required\": [\"pin\", \"label\", \"row\", \"col\"]\n }\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"switchPanelBackgroundColor\": {\n \"title\": \"Switches panel background color\",\n \"type\": \"string\",\n \"default\": \"#008a00\"\n },\n \"gpioStatusRequest\": {\n \"title\": \"GPIO status request\",\n \"type\": \"object\",\n \"properties\": {\n \"method\": {\n \"title\": \"Method name\",\n \"type\": \"string\",\n \"default\": \"getGpioStatus\"\n },\n \"paramsBody\": {\n \"title\": \"Method body\",\n \"type\": \"string\",\n \"default\": \"{}\"\n }\n },\n \"required\": [\"method\", \"paramsBody\"]\n },\n \"gpioStatusChangeRequest\": {\n \"title\": \"GPIO status change request\",\n \"type\": \"object\",\n \"properties\": {\n \"method\": {\n \"title\": \"Method name\",\n \"type\": \"string\",\n \"default\": \"setGpioStatus\"\n },\n \"paramsBody\": {\n \"title\": \"Method body\",\n \"type\": \"string\",\n \"default\": \"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"\n }\n },\n \"required\": [\"method\", \"paramsBody\"]\n },\n \"parseGpioStatusFunction\": {\n \"title\": \"Parse gpio status function\",\n \"type\": \"string\",\n \"default\": \"return body[pin] === true;\"\n } \n },\n \"required\": [\"gpioList\", \n \"requestTimeout\",\n \"switchPanelBackgroundColor\",\n \"gpioStatusRequest\",\n \"gpioStatusChangeRequest\",\n \"parseGpioStatusFunction\"]\n },\n \"form\": [\n \"gpioList\",\n \"requestTimeout\",\n {\n \"key\": \"switchPanelBackgroundColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"gpioStatusRequest\",\n \"items\": [\n \"gpioStatusRequest.method\",\n {\n \"key\": \"gpioStatusRequest.paramsBody\",\n \"type\": \"json\"\n }\n ]\n },\n {\n \"key\": \"gpioStatusChangeRequest\",\n \"items\": [\n \"gpioStatusChangeRequest.method\",\n {\n \"key\": \"gpioStatusChangeRequest.paramsBody\",\n \"type\": \"json\"\n }\n ]\n },\n {\n \"key\": \"parseGpioStatusFunction\",\n \"type\": \"javascript\"\n }\n ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"targetDeviceAliases\":[],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 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\":\"Basic GPIO Control\"}"}',
+'Basic GPIO Control' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'gpio_widgets', 'raspberry_pi_gpio_panel',
+'{"type":"latest","sizeX":7,"sizeY":10.5,"resources":[],"templateHtml":"<div class=\"gpio-panel\" style=\"height: 100%;\">\n <section layout=\"row\" ng-repeat=\"row in rows\">\n <section flex layout=\"row\" ng-repeat=\"cell in row\">\n <section layout=\"row\" flex ng-if=\"cell\" layout-align=\"{{$index===0 ? ''end center'' : ''start center''}}\">\n <span class=\"gpio-left-label\" ng-show=\"$index===0\">{{ cell.label }}</span>\n <section layout=\"row\" class=\"led-panel\" ng-class=\"$index===0 ? ''col-0'' : ''col-1''\"\n style=\"background-color: {{ ledPanelBackgroundColor }};\">\n <span class=\"pin\" ng-show=\"$index===0\">{{cell.pin}}</span>\n <span class=\"led-container\">\n <tb-led-light size=\"prefferedRowHeight\"\n color-on=\"cell.colorOn\"\n color-off=\"cell.colorOff\"\n off-opacity=\"''0.9''\"\n tb-enabled=\"cell.enabled\">\n </tb-led-light>\n </span>\n <span class=\"pin\" ng-show=\"$index===1\">{{cell.pin}}</span>\n </section>\n <span class=\"gpio-right-label\" ng-show=\"$index===1\">{{ cell.label }}</span>\n </section>\n <section layout=\"row\" flex ng-if=\"!cell\">\n <span flex ng-show=\"$index===0\"></span>\n <span class=\"led-panel\"\n style=\"background-color: {{ ledPanelBackgroundColor }};\"></span>\n <span flex ng-show=\"$index===1\"></span>\n </section>\n </section>\n </section> \n</div>","templateCss":".error {\n font-size: 14px !important;\n color: maroon;/*rgb(250,250,250);*/\n background-color: transparent;\n padding: 6px;\n}\n\n.error span {\n margin: auto;\n}\n\n.gpio-panel {\n padding-top: 10px;\n white-space: nowrap;\n}\n\n.gpio-panel tb-led-light > div {\n margin: auto;\n}\n\n.led-panel {\n margin: 0;\n width: 66px;\n min-width: 66px;\n}\n\n.led-container {\n width: 48px;\n min-width: 48px;\n}\n\n.pin {\n margin-top: auto;\n margin-bottom: auto;\n color: white;\n font-size: 12px;\n width: 16px;\n min-width: 16px;\n}\n\n.led-panel.col-0 .pin {\n margin-left: auto;\n padding-left: 2px;\n text-align: right;\n}\n\n.led-panel.col-1 .pin {\n margin-right: auto;\n \n text-align: left;\n}\n\n.gpio-left-label {\n margin-right: 8px;\n}\n\n.gpio-right-label {\n margin-left: 8px;\n}","controllerScript":"\nfns.init = function(containerElement, settings, datasources, data, scope, controlApi) {\n \n var i, gpio;\n \n scope.gpioList = [];\n scope.gpioByPin = {};\n for (var g in settings.gpioList) {\n gpio = settings.gpioList[g];\n scope.gpioList.push(\n {\n row: gpio.row,\n col: gpio.col,\n pin: gpio.pin,\n label: gpio.label,\n enabled: false,\n colorOn: tinycolor(gpio.color).lighten(20).toHexString(),\n colorOff: tinycolor(gpio.color).darken().toHexString()\n }\n );\n scope.gpioByPin[gpio.pin] = scope.gpioList[scope.gpioList.length-1];\n }\n\n scope.ledPanelBackgroundColor = settings.ledPanelBackgroundColor || tinycolor(''green'').lighten(2).toRgbString();\n\n scope.gpioCells = {};\n var rowCount = 0;\n for (i = 0; i < scope.gpioList.length; i++) {\n gpio = scope.gpioList[i];\n scope.gpioCells[gpio.row+''_''+gpio.col] = gpio;\n rowCount = Math.max(rowCount, gpio.row+1);\n }\n \n scope.prefferedRowHeight = 32;\n scope.rows = [];\n for (i = 0; i < rowCount; i++) {\n var row = [];\n for (var c =0; c<2;c++) {\n if (scope.gpioCells[i+''_''+c]) {\n row[c] = scope.gpioCells[i+''_''+c];\n } else {\n row[c] = null;\n }\n }\n scope.rows.push(row);\n }\n\n};\n\nfns.redraw = function(containerElement, width, height, data, timeWindow, sizeChanged, scope) {\n \n if (sizeChanged) {\n var rowCount = scope.rows.length;\n var prefferedRowHeight = (height - 35)/rowCount;\n prefferedRowHeight = Math.min(32, prefferedRowHeight);\n prefferedRowHeight = Math.max(12, prefferedRowHeight);\n scope.$apply(function(scope) {\n scope.prefferedRowHeight = prefferedRowHeight;\n });\n var ratio = prefferedRowHeight/32;\n \n var leftLabels = $(''.gpio-left-label'', containerElement);\n leftLabels.css(''font-size'', 16*ratio+''px'');\n var rightLabels = $(''.gpio-right-label'', containerElement);\n rightLabels.css(''font-size'', 16*ratio+''px'');\n var pins = $(''.pin'', containerElement);\n var pinsFontSize = Math.max(9, 12*ratio);\n pins.css(''font-size'', pinsFontSize+''px'');\n }\n \n var changed = false;\n for (var d in data) {\n var cellData = data[d];\n var dataKey = cellData.dataKey;\n var gpio = scope.gpioByPin[dataKey.label];\n if (gpio) {\n var enabled = false;\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n enabled = (tvPair[1] === true || tvPair[1] === ''true'');\n }\n if (gpio.enabled != enabled) {\n changed = true;\n gpio.enabled = enabled;\n }\n }\n }\n if (changed) {\n scope.$apply();\n }\n};\n\nfns.destroy = function() {\n};","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"gpioList\": {\n \"title\": \"Gpio leds\",\n \"type\": \"array\",\n \"minItems\" : 1,\n \"items\": {\n \"title\": \"Gpio led\",\n \"type\": \"object\",\n \"properties\": {\n \"pin\": {\n \"title\": \"Pin\",\n \"type\": \"number\"\n },\n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"row\": {\n \"title\": \"Row\",\n \"type\": \"number\"\n },\n \"col\": {\n \"title\": \"Column\",\n \"type\": \"number\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\",\n \"default\": \"red\"\n }\n },\n \"required\": [\"pin\", \"label\", \"row\", \"col\", \"color\"]\n }\n },\n \"ledPanelBackgroundColor\": {\n \"title\": \"LED panel background color\",\n \"type\": \"string\",\n \"default\": \"#008a00\"\n } \n },\n \"required\": [\"gpioList\", \n \"ledPanelBackgroundColor\"]\n },\n \"form\": [\n {\n \"key\": \"gpioList\",\n \"items\": [\n \"gpioList[].pin\",\n \"gpioList[].label\",\n \"gpioList[].row\",\n \"gpioList[].col\",\n {\n \"key\": \"gpioList[].color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"ledPanelBackgroundColor\",\n \"type\": \"color\"\n }\n ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"gpioList\":[{\"pin\":1,\"label\":\"3.3V\",\"row\":0,\"col\":0,\"color\":\"#fc9700\",\"_uniqueKey\":0},{\"pin\":2,\"label\":\"5V\",\"row\":0,\"col\":1,\"color\":\"#fb0000\",\"_uniqueKey\":1},{\"pin\":3,\"label\":\"GPIO 2 (I2C1_SDA)\",\"row\":1,\"col\":0,\"color\":\"#02fefb\",\"_uniqueKey\":2},{\"color\":\"#fb0000\",\"pin\":4,\"label\":\"5V\",\"row\":1,\"col\":1},{\"color\":\"#02fefb\",\"pin\":5,\"label\":\"GPIO 3 (I2C1_SCL)\",\"row\":2,\"col\":0},{\"color\":\"#000000\",\"pin\":6,\"label\":\"GND\",\"row\":2,\"col\":1},{\"color\":\"#00fd00\",\"pin\":7,\"label\":\"GPIO 4 (GPCLK0)\",\"row\":3,\"col\":0},{\"color\":\"#fdfb00\",\"pin\":8,\"label\":\"GPIO 14 (UART_TXD)\",\"row\":3,\"col\":1},{\"color\":\"#000000\",\"pin\":9,\"label\":\"GND\",\"row\":4,\"col\":0},{\"color\":\"#fdfb00\",\"pin\":10,\"label\":\"GPIO 15 (UART_RXD)\",\"row\":4,\"col\":1},{\"color\":\"#00fd00\",\"pin\":11,\"label\":\"GPIO 17\",\"row\":5,\"col\":0},{\"color\":\"#00fd00\",\"pin\":12,\"label\":\"GPIO 18\",\"row\":5,\"col\":1},{\"color\":\"#00fd00\",\"pin\":13,\"label\":\"GPIO 27\",\"row\":6,\"col\":0},{\"color\":\"#000000\",\"pin\":14,\"label\":\"GND\",\"row\":6,\"col\":1},{\"color\":\"#00fd00\",\"pin\":15,\"label\":\"GPIO 22\",\"row\":7,\"col\":0},{\"color\":\"#00fd00\",\"pin\":16,\"label\":\"GPIO 23\",\"row\":7,\"col\":1},{\"color\":\"#fc9700\",\"pin\":17,\"label\":\"3.3V\",\"row\":8,\"col\":0},{\"color\":\"#00fd00\",\"pin\":18,\"label\":\"GPIO 24\",\"row\":8,\"col\":1},{\"color\":\"#fd01fd\",\"pin\":19,\"label\":\"GPIO 10 (SPI_MOSI)\",\"row\":9,\"col\":0},{\"color\":\"#000000\",\"pin\":20,\"label\":\"GND\",\"row\":9,\"col\":1},{\"color\":\"#fd01fd\",\"pin\":21,\"label\":\"GPIO 9 (SPI_MISO)\",\"row\":10,\"col\":0},{\"color\":\"#00fd00\",\"pin\":22,\"label\":\"GPIO 25\",\"row\":10,\"col\":1},{\"color\":\"#fd01fd\",\"pin\":23,\"label\":\"GPIO 11 (SPI_SCLK)\",\"row\":11,\"col\":0},{\"color\":\"#fd01fd\",\"pin\":24,\"label\":\"GPIO 8 (SPI_CE0)\",\"row\":11,\"col\":1},{\"color\":\"#000000\",\"pin\":25,\"label\":\"GND\",\"row\":12,\"col\":0},{\"color\":\"#fd01fd\",\"pin\":26,\"label\":\"GPIO 7 (SPI_CE1)\",\"row\":12,\"col\":1},{\"color\":\"#ffffff\",\"pin\":27,\"label\":\"ID_SD\",\"row\":13,\"col\":0},{\"color\":\"#ffffff\",\"pin\":28,\"label\":\"ID_SC\",\"row\":13,\"col\":1},{\"color\":\"#00fd00\",\"pin\":29,\"label\":\"GPIO 5\",\"row\":14,\"col\":0},{\"color\":\"#000000\",\"pin\":30,\"label\":\"GND\",\"row\":14,\"col\":1},{\"color\":\"#00fd00\",\"pin\":31,\"label\":\"GPIO 6\",\"row\":15,\"col\":0},{\"color\":\"#00fd00\",\"pin\":32,\"label\":\"GPIO 12\",\"row\":15,\"col\":1},{\"color\":\"#00fd00\",\"pin\":33,\"label\":\"GPIO 13\",\"row\":16,\"col\":0},{\"color\":\"#000000\",\"pin\":34,\"label\":\"GND\",\"row\":16,\"col\":1},{\"color\":\"#00fd00\",\"pin\":35,\"label\":\"GPIO 19\",\"row\":17,\"col\":0},{\"color\":\"#00fd00\",\"pin\":36,\"label\":\"GPIO 16\",\"row\":17,\"col\":1},{\"color\":\"#00fd00\",\"pin\":37,\"label\":\"GPIO 26\",\"row\":18,\"col\":0},{\"color\":\"#00fd00\",\"pin\":38,\"label\":\"GPIO 20\",\"row\":18,\"col\":1},{\"color\":\"#000000\",\"pin\":39,\"label\":\"GND\",\"row\":19,\"col\":0},{\"color\":\"#00fd00\",\"pin\":40,\"label\":\"GPIO 21\",\"row\":19,\"col\":1}],\"ledPanelBackgroundColor\":\"#008a00\"},\"title\":\"Raspberry Pi GPIO Panel\",\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"7\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.22518255793320163,\"funcBody\":\"var period = time % 1500;\\nreturn period < 500;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"11\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.7008206860666621,\"funcBody\":\"var period = time % 1500;\\nreturn period >= 500 && period < 1000;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"12\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.42600325102193426,\"funcBody\":\"var period = time % 1500;\\nreturn period >= 1000;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"13\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.48362241571415243,\"funcBody\":\"var period = time % 1500;\\nreturn period < 500;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"29\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.7217670147518815,\"funcBody\":\"var period = time % 1500;\\nreturn period >= 500 && period < 1000;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}}}"}',
+'Raspberry Pi GPIO Panel' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'gpio_widgets', 'raspberry_pi_gpio_control',
+'{"type":"rpc","sizeX":6,"sizeY":10.5,"resources":[],"templateHtml":"<fieldset class=\"gpio-panel\" ng-disabled=\"!rpcEnabled || executingRpcRequest\" style=\"height: 100%;\">\n <section class=\"gpio-row\" layout=\"row\" ng-repeat=\"row in rows track by $index\" \n ng-style=\"{''height'': prefferedRowHeight+''px''}\">\n <section flex layout=\"row\" ng-repeat=\"cell in row track by $index\">\n <section layout=\"row\" flex ng-if=\"cell\" layout-align=\"{{$index===0 ? ''end center'' : ''start center''}}\">\n <span class=\"gpio-left-label\" ng-show=\"$index===0\">{{ cell.label }}</span>\n <section layout=\"row\" class=\"switch-panel\" layout-align=\"start center\" ng-class=\"$index===0 ? ''col-0'' : ''col-1''\"\n ng-style=\"{''height'': prefferedRowHeight+''px'', ''backgroundColor'': ''{{ switchPanelBackgroundColor }}''}\">\n <span class=\"pin\" ng-show=\"$index===0\">{{cell.pin}}</span>\n <span flex ng-show=\"$index===1\"></span>\n <md-switch\n aria-label=\"{{ cell.label }}\"\n ng-disabled=\"!rpcEnabled || executingRpcRequest\"\n ng-model=\"cell.enabled\" \n ng-change=\"cell.enabled = !cell.enabled\" \n ng-click=\"gpioClick($event, cell)\">\n </md-switch>\n <span flex ng-show=\"$index===0\"></span>\n <span class=\"pin\" ng-show=\"$index===1\">{{cell.pin}}</span>\n </section>\n <span class=\"gpio-right-label\" ng-show=\"$index===1\">{{ cell.label }}</span>\n </section>\n <section layout=\"row\" flex ng-if=\"!cell\">\n <span flex ng-show=\"$index===0\"></span>\n <span class=\"switch-panel\"\n ng-style=\"{''height'': prefferedRowHeight+''px'', ''backgroundColor'': ''{{ switchPanelBackgroundColor }}''}\"></span>\n <span flex ng-show=\"$index===1\"></span>\n </section>\n </section>\n </section> \n <span class=\"error\" style=\"position: absolute; bottom: 5px;\" ng-show=\"rpcErrorText\">{{rpcErrorText}}</span>\n <md-progress-linear ng-show=\"executingRpcRequest\" style=\"position: absolute; bottom: 0;\" md-mode=\"indeterminate\"></md-progress-linear> \n</fieldset>","templateCss":".error {\n font-size: 14px !important;\n color: maroon;/*rgb(250,250,250);*/\n background-color: transparent;\n padding: 6px;\n}\n\n.error span {\n margin: auto;\n}\n\n.gpio-panel {\n padding-top: 10px;\n white-space: nowrap;\n}\n\n.switch-panel {\n margin: 0;\n height: 32px;\n width: 66px;\n min-width: 66px;\n}\n\n.switch-panel md-switch {\n margin: 0;\n width: 36px;\n min-width: 36px;\n}\n\n.switch-panel md-switch > div.md-container {\n margin: 0;\n}\n\n.switch-panel.col-0 md-switch {\n margin-left: 8px;\n margin-right: 4px;\n}\n\n.switch-panel.col-1 md-switch {\n margin-left: 4px;\n margin-right: 8px;\n}\n\n.gpio-row {\n height: 32px;\n}\n\n.pin {\n margin-top: auto;\n margin-bottom: auto;\n color: white;\n font-size: 12px;\n width: 16px;\n min-width: 16px;\n}\n\n.switch-panel.col-0 .pin {\n margin-left: auto;\n padding-left: 2px;\n text-align: right;\n}\n\n.switch-panel.col-1 .pin {\n margin-right: auto;\n \n text-align: left;\n}\n\n.gpio-left-label {\n margin-right: 8px;\n}\n\n.gpio-right-label {\n margin-left: 8px;\n}","controllerScript":"\nfns.init = function(containerElement, settings, datasources, data, scope, controlApi) {\n \n var i, gpio;\n \n scope.gpioList = [];\n for (var g in settings.gpioList) {\n gpio = settings.gpioList[g];\n scope.gpioList.push(\n {\n row: gpio.row,\n col: gpio.col,\n pin: gpio.pin,\n label: gpio.label,\n enabled: false\n }\n );\n }\n\n scope.requestTimeout = settings.requestTimeout || 1000;\n\n scope.switchPanelBackgroundColor = settings.switchPanelBackgroundColor || tinycolor(''green'').lighten(2).toRgbString();\n\n scope.gpioStatusRequest = {\n method: \"getGpioStatus\",\n paramsBody: \"{}\"\n };\n \n if (settings.gpioStatusRequest) {\n scope.gpioStatusRequest.method = settings.gpioStatusRequest.method || scope.gpioStatusRequest.method;\n scope.gpioStatusRequest.paramsBody = settings.gpioStatusRequest.paramsBody || scope.gpioStatusRequest.paramsBody;\n }\n \n scope.gpioStatusChangeRequest = {\n method: \"setGpioStatus\",\n paramsBody: \"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"\n };\n \n if (settings.gpioStatusChangeRequest) {\n scope.gpioStatusChangeRequest.method = settings.gpioStatusChangeRequest.method || scope.gpioStatusChangeRequest.method;\n scope.gpioStatusChangeRequest.paramsBody = settings.gpioStatusChangeRequest.paramsBody || scope.gpioStatusChangeRequest.paramsBody;\n }\n \n scope.parseGpioStatusFunction = \"return body[pin] === true;\";\n \n if (settings.parseGpioStatusFunction && settings.parseGpioStatusFunction.length > 0) {\n scope.parseGpioStatusFunction = settings.parseGpioStatusFunction;\n }\n \n scope.parseGpioStatusFunction = new Function(\"body, pin\", scope.parseGpioStatusFunction);\n \n function requestGpioStatus() {\n controlApi.sendTwoWayCommand(scope.gpioStatusRequest.method, \n scope.gpioStatusRequest.paramsBody, \n scope.requestTimeout)\n .then(\n function success(responseBody) {\n for (var g in scope.gpioList) {\n var gpio = scope.gpioList[g];\n var enabled = scope.parseGpioStatusFunction.apply(this, [responseBody, gpio.pin]);\n gpio.enabled = enabled; \n }\n }\n );\n }\n \n function changeGpioStatus(gpio) {\n var pin = gpio.pin + '''';\n var enabled = !gpio.enabled;\n enabled = enabled === true ? ''true'' : ''false'';\n var paramsBody = scope.gpioStatusChangeRequest.paramsBody;\n var requestBody = JSON.parse(paramsBody.replace(\"\\\"{$pin}\\\"\", pin).replace(\"\\\"{$enabled}\\\"\", enabled));\n controlApi.sendTwoWayCommand(scope.gpioStatusChangeRequest.method, \n requestBody, scope.requestTimeout)\n .then(\n function success(responseBody) {\n var enabled = scope.parseGpioStatusFunction.apply(this, [responseBody, gpio.pin]);\n gpio.enabled = enabled;\n }\n );\n }\n \n scope.gpioCells = {};\n var rowCount = 0;\n for (i = 0; i < scope.gpioList.length; i++) {\n gpio = scope.gpioList[i];\n scope.gpioCells[gpio.row+''_''+gpio.col] = gpio;\n rowCount = Math.max(rowCount, gpio.row+1);\n }\n \n scope.prefferedRowHeight = 32;\n scope.rows = [];\n for (i = 0; i < rowCount; i++) {\n var row = [];\n for (var c =0; c<2;c++) {\n if (scope.gpioCells[i+''_''+c]) {\n row[c] = scope.gpioCells[i+''_''+c];\n } else {\n row[c] = null;\n }\n }\n scope.rows.push(row);\n }\n\n scope.gpioClick = function($event, gpio) {\n changeGpioStatus(gpio);\n };\n\n requestGpioStatus();\n \n};\n\nfns.redraw = function(containerElement, width, height, data, timeWindow, sizeChanged, scope) {\n if (sizeChanged) {\n var rowCount = scope.rows.length;\n var prefferedRowHeight = (height - 35)/rowCount;\n prefferedRowHeight = Math.min(32, prefferedRowHeight);\n prefferedRowHeight = Math.max(12, prefferedRowHeight);\n scope.$apply(function(scope) {\n scope.prefferedRowHeight = prefferedRowHeight;\n });\n var ratio = prefferedRowHeight/32;\n var switches = $(''md-switch'', containerElement);\n switches.css(''height'', 30*ratio+''px'');\n switches.css(''width'', 36*ratio+''px'');\n switches.css(''min-width'', 36*ratio+''px'');\n $(''.md-container'', switches).css(''height'', 24*ratio+''px'');\n $(''.md-container'', switches).css(''width'', 36*ratio+''px'');\n var bars = $(''.md-bar'', containerElement);\n bars.css(''height'', 14*ratio+''px'');\n bars.css(''width'', 34*ratio+''px'');\n var thumbs = $(''.md-thumb'', containerElement);\n thumbs.css(''height'', 20*ratio+''px'');\n thumbs.css(''width'', 20*ratio+''px'');\n \n var leftLabels = $(''.gpio-left-label'', containerElement);\n leftLabels.css(''font-size'', 16*ratio+''px'');\n var rightLabels = $(''.gpio-right-label'', containerElement);\n rightLabels.css(''font-size'', 16*ratio+''px'');\n var pins = $(''.pin'', containerElement);\n var pinsFontSize = Math.max(9, 12*ratio);\n pins.css(''font-size'', pinsFontSize+''px'');\n }\n};\n\nfns.destroy = function() {\n};","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"gpioList\": {\n \"title\": \"Gpio switches\",\n \"type\": \"array\",\n \"minItems\" : 1,\n \"items\": {\n \"title\": \"Gpio switch\",\n \"type\": \"object\",\n \"properties\": {\n \"pin\": {\n \"title\": \"Pin\",\n \"type\": \"number\"\n },\n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"row\": {\n \"title\": \"Row\",\n \"type\": \"number\"\n },\n \"col\": {\n \"title\": \"Column\",\n \"type\": \"number\"\n }\n },\n \"required\": [\"pin\", \"label\", \"row\", \"col\"]\n }\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"switchPanelBackgroundColor\": {\n \"title\": \"Switches panel background color\",\n \"type\": \"string\",\n \"default\": \"#008a00\"\n },\n \"gpioStatusRequest\": {\n \"title\": \"GPIO status request\",\n \"type\": \"object\",\n \"properties\": {\n \"method\": {\n \"title\": \"Method name\",\n \"type\": \"string\",\n \"default\": \"getGpioStatus\"\n },\n \"paramsBody\": {\n \"title\": \"Method body\",\n \"type\": \"string\",\n \"default\": \"{}\"\n }\n },\n \"required\": [\"method\", \"paramsBody\"]\n },\n \"gpioStatusChangeRequest\": {\n \"title\": \"GPIO status change request\",\n \"type\": \"object\",\n \"properties\": {\n \"method\": {\n \"title\": \"Method name\",\n \"type\": \"string\",\n \"default\": \"setGpioStatus\"\n },\n \"paramsBody\": {\n \"title\": \"Method body\",\n \"type\": \"string\",\n \"default\": \"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"\n }\n },\n \"required\": [\"method\", \"paramsBody\"]\n },\n \"parseGpioStatusFunction\": {\n \"title\": \"Parse gpio status function\",\n \"type\": \"string\",\n \"default\": \"return body[pin] === true;\"\n } \n },\n \"required\": [\"gpioList\", \n \"requestTimeout\",\n \"switchPanelBackgroundColor\",\n \"gpioStatusRequest\",\n \"gpioStatusChangeRequest\",\n \"parseGpioStatusFunction\"]\n },\n \"form\": [\n \"gpioList\",\n \"requestTimeout\",\n {\n \"key\": \"switchPanelBackgroundColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"gpioStatusRequest\",\n \"items\": [\n \"gpioStatusRequest.method\",\n {\n \"key\": \"gpioStatusRequest.paramsBody\",\n \"type\": \"json\"\n }\n ]\n },\n {\n \"key\": \"gpioStatusChangeRequest\",\n \"items\": [\n \"gpioStatusChangeRequest.method\",\n {\n \"key\": \"gpioStatusChangeRequest.paramsBody\",\n \"type\": \"json\"\n }\n ]\n },\n {\n \"key\": \"parseGpioStatusFunction\",\n \"type\": \"javascript\"\n }\n ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"targetDeviceAliases\":[],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"parseGpioStatusFunction\":\"return body[pin] === true;\",\"gpioStatusChangeRequest\":{\"method\":\"setGpioStatus\",\"paramsBody\":\"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"},\"requestTimeout\":500,\"switchPanelBackgroundColor\":\"#008a00\",\"gpioStatusRequest\":{\"method\":\"getGpioStatus\",\"paramsBody\":\"{}\"},\"gpioList\":[{\"pin\":7,\"label\":\"GPIO 4 (GPCLK0)\",\"row\":3,\"col\":0,\"_uniqueKey\":0},{\"pin\":11,\"label\":\"GPIO 17\",\"row\":5,\"col\":0,\"_uniqueKey\":1},{\"pin\":12,\"label\":\"GPIO 18\",\"row\":5,\"col\":1,\"_uniqueKey\":2},{\"_uniqueKey\":3,\"pin\":13,\"label\":\"GPIO 27\",\"row\":6,\"col\":0},{\"_uniqueKey\":4,\"pin\":15,\"label\":\"GPIO 22\",\"row\":7,\"col\":0},{\"_uniqueKey\":5,\"pin\":16,\"label\":\"GPIO 23\",\"row\":7,\"col\":1},{\"_uniqueKey\":6,\"pin\":18,\"label\":\"GPIO 24\",\"row\":8,\"col\":1},{\"_uniqueKey\":7,\"pin\":22,\"label\":\"GPIO 25\",\"row\":10,\"col\":1},{\"_uniqueKey\":8,\"pin\":29,\"label\":\"GPIO 5\",\"row\":14,\"col\":0},{\"_uniqueKey\":9,\"pin\":31,\"label\":\"GPIO 6\",\"row\":15,\"col\":0},{\"_uniqueKey\":10,\"pin\":32,\"label\":\"GPIO 12\",\"row\":15,\"col\":1},{\"_uniqueKey\":11,\"pin\":33,\"label\":\"GPIO 13\",\"row\":16,\"col\":0},{\"_uniqueKey\":12,\"pin\":35,\"label\":\"GPIO 19\",\"row\":17,\"col\":0},{\"_uniqueKey\":13,\"pin\":36,\"label\":\"GPIO 16\",\"row\":17,\"col\":1},{\"_uniqueKey\":14,\"pin\":37,\"label\":\"GPIO 26\",\"row\":18,\"col\":0},{\"_uniqueKey\":15,\"pin\":38,\"label\":\"GPIO 20\",\"row\":18,\"col\":1},{\"_uniqueKey\":16,\"pin\":40,\"label\":\"GPIO 21\",\"row\":19,\"col\":1}]},\"title\":\"Raspberry Pi GPIO Control\"}"}',
+'Raspberry Pi GPIO Control' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'digital_gauges', 'lcd_bar_gauge',
+'{"type":"latest","sizeX":2,"sizeY":3.5,"resources":[],"templateHtml":"","templateCss":"#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n","controllerScript":"var gauge;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n gauge = new TbDigitalGauge(containerElement, settings, data); \n\n}\n\n\nfns.redraw = function(containerElement, width, height, data) {\n gauge.redraw(data);\n};\n\nfns.destroy = function() {\n}\n","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"minValue\": {\n \"title\": \"Minimum value\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"maxValue\": {\n \"title\": \"Maximum value\",\n \"type\": \"number\",\n \"default\": 100\n }, \n \"gaugeType\": {\n \"title\": \"Gauge type\",\n \"type\": \"string\",\n \"default\": \"arc\"\n }, \n \"donutStartAngle\": {\n \"title\": \"Angle to start from when in donut mode\",\n \"type\": \"number\",\n \"default\": 90\n }, \n \"neonGlowBrightness\": {\n \"title\": \"Neon glow effect brightness, (0-100), 0 - disable effect\",\n \"type\": \"number\",\n \"default\": 0\n }, \n \"dashThickness\": {\n \"title\": \"Thickness of the stripes, 0 - no stripes\",\n \"type\": \"number\",\n \"default\": 0\n }, \n \"roundedLineCap\": {\n \"title\": \"Display rounded line cap\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"title\": {\n \"title\": \"Gauge title\",\n \"type\": \"string\",\n \"default\": null\n }, \n \"showTitle\": {\n \"title\": \"Show gauge title\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"unitTitle\": {\n \"title\": \"Unit title\",\n \"type\": \"string\",\n \"default\": null\n }, \n \"showUnitTitle\": {\n \"title\": \"Show unit title\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"showValue\": {\n \"title\": \"Show value text\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"showMinMax\": {\n \"title\": \"Show min and max values\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"gaugeWidthScale\": {\n \"title\": \"Width of the gauge element\",\n \"type\": \"number\",\n \"default\": 0.75\n },\n \"defaultColor\": {\n \"title\": \"Default color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"gaugeColor\": {\n \"title\": \"Background color of the gauge element\",\n \"type\": \"string\",\n \"default\": null\n },\n \"levelColors\": {\n \"title\": \"Colors of indicator, from lower to upper\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n }\n },\n \"refreshAnimationType\": {\n \"title\": \"Type of refresh animation\",\n \"type\": \"string\",\n \"default\": \">\"\n },\n \"refreshAnimationTime\": {\n \"title\": \"Duration of refresh animation (ms)\",\n \"type\": \"number\",\n \"default\": 700\n },\n \"startAnimationType\": {\n \"title\": \"Type of start animation\",\n \"type\": \"string\",\n \"default\": \">\"\n },\n \"startAnimationTime\": {\n \"title\": \"Duration of start animation (ms)\",\n \"type\": \"number\",\n \"default\": 700\n },\n \"decimals\": {\n \"title\": \"Number of digits after floating point\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"units\": {\n \"title\": \"Special symbol to show next to value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"titleFont\": {\n \"title\": \"Gauge title font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 12\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"labelFont\": {\n \"title\": \"Font of label showing under value\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 8\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"valueFont\": {\n \"title\": \"Font of label showing current value\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 18\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"minMaxFont\": {\n \"title\": \"Font of minimum and maximum labels\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n }\n }\n },\n \"form\": [\n \"minValue\",\n \"maxValue\",\n {\n \"key\": \"gaugeType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"arc\",\n \"label\": \"Arc\"\n },\n {\n \"value\": \"donut\",\n \"label\": \"Donut\"\n },\n {\n \"value\": \"horizontalBar\",\n \"label\": \"Horizontal bar\"\n },\n {\n \"value\": \"verticalBar\",\n \"label\": \"Vertical bar\"\n }\n ]\n },\n \"donutStartAngle\",\n \"neonGlowBrightness\",\n \"dashThickness\",\n \"roundedLineCap\",\n \"title\",\n \"showTitle\",\n \"unitTitle\",\n \"showUnitTitle\",\n \"showValue\",\n \"showMinMax\",\n \"gaugeWidthScale\",\n {\n \"key\": \"defaultColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"gaugeColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"levelColors\",\n \"items\": [\n {\n \"key\": \"levelColors[]\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"refreshAnimationType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \">\",\n \"label\": \">\"\n },\n {\n \"value\": \"<\",\n \"label\": \"<\"\n },\n {\n \"value\": \"<>\",\n \"label\": \"<>\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n }\n ]\n },\n \"refreshAnimationTime\",\n {\n \"key\": \"startAnimationType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \">\",\n \"label\": \">\"\n },\n {\n \"value\": \"<\",\n \"label\": \"<\"\n },\n {\n \"value\": \"<>\",\n \"label\": \"<>\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n }\n ]\n },\n \"startAnimationTime\",\n \"decimals\",\n \"units\",\n {\n \"key\": \"titleFont\",\n \"items\": [\n \"titleFont.family\",\n \"titleFont.size\",\n {\n \"key\": \"titleFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"titleFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"titleFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"labelFont\",\n \"items\": [\n \"labelFont.family\",\n \"labelFont.size\",\n {\n \"key\": \"labelFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"labelFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"labelFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"valueFont\",\n \"items\": [\n \"valueFont.family\",\n \"valueFont.size\",\n {\n \"key\": \"valueFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"valueFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"valueFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont\",\n \"items\": [\n \"minMaxFont.family\",\n \"minMaxFont.size\",\n {\n \"key\": \"minMaxFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont.color\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n}\n ","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#babab2\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\"linear\",\"refreshAnimationTime\":700,\"startAnimationType\":\"linear\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"RobotoDraft\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"RobotoDraft\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"RobotoDraft\",\"style\":\"normal\",\"weight\":\"400\",\"size\":16},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":0,\"dashThickness\":1.5,\"decimals\":0,\"showUnitTitle\":true,\"defaultColor\":\"#444444\",\"gaugeType\":\"verticalBar\",\"units\":\"%\"},\"title\":\"LCD bar gauge\"}"}',
+'LCD bar gauge' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'charts', 'pie',
+'{"type":"latest","sizeX":8,"sizeY":6,"resources":[{"url":"https://rawgithub.com/HumbleSoftware/Flotr2/master/flotr2.min.js"}],"templateHtml":"","templateCss":"","controllerScript":"var options, graph, pieData = [];\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n\n var colors = [];\n for (var i in data) {\n colors.push(data[i].dataKey.color);\n var pieCell = {\n data: [[0,0]],\n label: data[i].dataKey.label\n }\n pieData.push(pieCell);\n }\n\n options = {\n colors: colors,\n HtmlText : false,\n grid : {\n verticalLines : false,\n horizontalLines : false\n },\n xaxis : { showLabels : false },\n yaxis : { showLabels : false },\n pie : {\n show : true, \n explode : 6\n }\n };\n}\n\nfns.redraw = function(containerElement, width, height, data,\n timeWindow, sizeChanged) {\n\n for (var i in pieData) {\n cellData = data[i];\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length -\n 1];\n var value = tvPair[1];\n pieData[i].data[0][1] = parseFloat(value);\n }\n }\n graph = Flotr.draw(containerElement, pieData, options);\n};\n","settingsSchema":"{}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#0097a7\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#f57f17\",\"settings\":{},\"_hash\":0.6114638304362894,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#e91e63\",\"settings\":{},\"_hash\":0.9955906536344441,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#66bb6a\",\"settings\":{},\"_hash\":0.9430835931647599,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Pie - Flotr2\"}"}',
+'Pie - Flotr2' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'digital_gauges', 'mini_gauge_justgage',
+'{"type":"latest","sizeX":2,"sizeY":2,"resources":[],"templateHtml":"","templateCss":"#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n","controllerScript":"var gauge;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n gauge = new TbDigitalGauge(containerElement, settings, data); \n\n}\n\n\nfns.redraw = function(containerElement, width, height, data) {\n gauge.redraw(data);\n};\n\nfns.destroy = function() {\n}\n","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"minValue\": {\n \"title\": \"Minimum value\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"maxValue\": {\n \"title\": \"Maximum value\",\n \"type\": \"number\",\n \"default\": 100\n }, \n \"gaugeType\": {\n \"title\": \"Gauge type\",\n \"type\": \"string\",\n \"default\": \"arc\"\n }, \n \"donutStartAngle\": {\n \"title\": \"Angle to start from when in donut mode\",\n \"type\": \"number\",\n \"default\": 90\n }, \n \"neonGlowBrightness\": {\n \"title\": \"Neon glow effect brightness, (0-100), 0 - disable effect\",\n \"type\": \"number\",\n \"default\": 0\n }, \n \"dashThickness\": {\n \"title\": \"Thickness of the stripes, 0 - no stripes\",\n \"type\": \"number\",\n \"default\": 0\n }, \n \"roundedLineCap\": {\n \"title\": \"Display rounded line cap\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"title\": {\n \"title\": \"Gauge title\",\n \"type\": \"string\",\n \"default\": null\n }, \n \"showTitle\": {\n \"title\": \"Show gauge title\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"unitTitle\": {\n \"title\": \"Unit title\",\n \"type\": \"string\",\n \"default\": null\n }, \n \"showUnitTitle\": {\n \"title\": \"Show unit title\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"showValue\": {\n \"title\": \"Show value text\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"showMinMax\": {\n \"title\": \"Show min and max values\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"gaugeWidthScale\": {\n \"title\": \"Width of the gauge element\",\n \"type\": \"number\",\n \"default\": 0.75\n },\n \"defaultColor\": {\n \"title\": \"Default color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"gaugeColor\": {\n \"title\": \"Background color of the gauge element\",\n \"type\": \"string\",\n \"default\": null\n },\n \"levelColors\": {\n \"title\": \"Colors of indicator, from lower to upper\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n }\n },\n \"refreshAnimationType\": {\n \"title\": \"Type of refresh animation\",\n \"type\": \"string\",\n \"default\": \">\"\n },\n \"refreshAnimationTime\": {\n \"title\": \"Duration of refresh animation (ms)\",\n \"type\": \"number\",\n \"default\": 700\n },\n \"startAnimationType\": {\n \"title\": \"Type of start animation\",\n \"type\": \"string\",\n \"default\": \">\"\n },\n \"startAnimationTime\": {\n \"title\": \"Duration of start animation (ms)\",\n \"type\": \"number\",\n \"default\": 700\n },\n \"decimals\": {\n \"title\": \"Number of digits after floating point\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"units\": {\n \"title\": \"Special symbol to show next to value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"titleFont\": {\n \"title\": \"Gauge title font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 12\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"labelFont\": {\n \"title\": \"Font of label showing under value\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 8\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"valueFont\": {\n \"title\": \"Font of label showing current value\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 18\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"minMaxFont\": {\n \"title\": \"Font of minimum and maximum labels\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n }\n }\n },\n \"form\": [\n \"minValue\",\n \"maxValue\",\n {\n \"key\": \"gaugeType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"arc\",\n \"label\": \"Arc\"\n },\n {\n \"value\": \"donut\",\n \"label\": \"Donut\"\n },\n {\n \"value\": \"horizontalBar\",\n \"label\": \"Horizontal bar\"\n },\n {\n \"value\": \"verticalBar\",\n \"label\": \"Vertical bar\"\n }\n ]\n },\n \"donutStartAngle\",\n \"neonGlowBrightness\",\n \"dashThickness\",\n \"roundedLineCap\",\n \"title\",\n \"showTitle\",\n \"unitTitle\",\n \"showUnitTitle\",\n \"showValue\",\n \"showMinMax\",\n \"gaugeWidthScale\",\n {\n \"key\": \"defaultColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"gaugeColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"levelColors\",\n \"items\": [\n {\n \"key\": \"levelColors[]\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"refreshAnimationType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \">\",\n \"label\": \">\"\n },\n {\n \"value\": \"<\",\n \"label\": \"<\"\n },\n {\n \"value\": \"<>\",\n \"label\": \"<>\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n }\n ]\n },\n \"refreshAnimationTime\",\n {\n \"key\": \"startAnimationType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \">\",\n \"label\": \">\"\n },\n {\n \"value\": \"<\",\n \"label\": \"<\"\n },\n {\n \"value\": \"<>\",\n \"label\": \"<>\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n }\n ]\n },\n \"startAnimationTime\",\n \"decimals\",\n \"units\",\n {\n \"key\": \"titleFont\",\n \"items\": [\n \"titleFont.family\",\n \"titleFont.size\",\n {\n \"key\": \"titleFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"titleFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"titleFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"labelFont\",\n \"items\": [\n \"labelFont.family\",\n \"labelFont.size\",\n {\n \"key\": \"labelFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"labelFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"labelFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"valueFont\",\n \"items\": [\n \"valueFont.family\",\n \"valueFont.size\",\n {\n \"key\": \"valueFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"valueFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"valueFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont\",\n \"items\": [\n \"minMaxFont.family\",\n \"minMaxFont.size\",\n {\n \"key\": \"minMaxFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont.color\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n}\n ","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#7cb342\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"RobotoDraft\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"RobotoDraft\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"RobotoDraft\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":0,\"dashThickness\":0,\"decimals\":0,\"roundedLineCap\":true,\"gaugeType\":\"donut\"},\"title\":\"Mini gauge - justGage\"}"}',
+'Mini gauge - justGage' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'analogue_gauges', 'radial_gauge_canvas_gauges',
+'{"type":"latest","sizeX":6,"sizeY":5,"resources":[],"templateHtml":"<canvas id=\"radialGauge\"></canvas>\n","templateCss":"","controllerScript":"var gauge;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n gauge = new TbAnalogueRadialGauge(containerElement, settings, data, ''radialGauge''); \n\n}\n\n\nfns.redraw = function(containerElement, width, height, data, timeWindow, sizeChanged) {\n gauge.redraw(width, height, data, sizeChanged);\n};\n\nfns.destroy = function() {\n}\n","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"minValue\": {\n \"title\": \"Minimum value\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"maxValue\": {\n \"title\": \"Maximum value\",\n \"type\": \"number\",\n \"default\": 100\n },\n \"unitTitle\": {\n \"title\": \"Unit title\",\n \"type\": \"string\",\n \"default\": null\n },\n \"showUnitTitle\": {\n \"title\": \"Show unit title\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"units\": {\n \"title\": \"Units\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"majorTicksCount\": {\n \"title\": \"Major ticks count\",\n \"type\": \"number\",\n \"default\": null\n },\n \"minorTicks\": {\n \"title\": \"Minor ticks count\",\n \"type\": \"number\",\n \"default\": 2\n },\n \"valueBox\": {\n \"title\": \"Show value box\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"valueInt\": {\n \"title\": \"Digits count for integer part of value\",\n \"type\": \"number\",\n \"default\": 3\n },\n \"valueDec\": {\n \"title\": \"Digits count for decimal part of value\",\n \"type\": \"number\",\n \"default\": 2\n },\n \"defaultColor\": {\n \"title\": \"Default color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"colorPlate\": {\n \"title\": \"Plate color\",\n \"type\": \"string\",\n \"default\": \"#fff\"\n },\n \"colorMajorTicks\": {\n \"title\": \"Major ticks color\",\n \"type\": \"string\",\n \"default\": \"#444\"\n },\n \"colorMinorTicks\": {\n \"title\": \"Minor ticks color\",\n \"type\": \"string\",\n \"default\": \"#666\"\n },\n \"colorNeedle\": {\n \"title\": \"Needle color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"colorNeedleEnd\": {\n \"title\": \"Needle color - end gradient\",\n \"type\": \"string\",\n \"default\": null\n },\n \"colorNeedleShadowUp\": {\n \"title\": \"Upper half of the needle shadow color\",\n \"type\": \"string\",\n \"default\": \"rgba(2,255,255,0.2)\"\n },\n \"colorNeedleShadowDown\": {\n \"title\": \"Drop shadow needle color.\",\n \"type\": \"string\",\n \"default\": \"rgba(188,143,143,0.45)\"\n },\n \"colorValueBoxRect\": {\n \"title\": \"Value box rectangle stroke color\",\n \"type\": \"string\",\n \"default\": \"#888\"\n },\n \"colorValueBoxRectEnd\": {\n \"title\": \"Value box rectangle stroke color - end gradient\",\n \"type\": \"string\",\n \"default\": \"#666\"\n },\n \"colorValueBoxBackground\": {\n \"title\": \"Value box background color\",\n \"type\": \"string\",\n \"default\": \"#babab2\"\n },\n \"colorValueBoxShadow\": {\n \"title\": \"Value box shadow color\",\n \"type\": \"string\",\n \"default\": \"rgba(0,0,0,1)\"\n },\n \"highlights\": {\n \"title\": \"Highlights\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Highlight\",\n \"type\": \"object\",\n \"properties\": {\n \"from\": {\n \"title\": \"From\",\n \"type\": \"number\"\n },\n \"to\": {\n \"title\": \"To\",\n \"type\": \"number\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n }\n }\n }\n },\n \"highlightsWidth\": {\n \"title\": \"Highlights width\",\n \"type\": \"number\",\n \"default\": 15\n },\n \"showBorder\": {\n \"title\": \"Show border\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"numbersFont\": {\n \"title\": \"Tick numbers font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 18\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"titleFont\": {\n \"title\": \"Title text font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 24\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": \"#888\"\n }\n }\n },\n \"unitsFont\": {\n \"title\": \"Units text font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 22\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": \"#888\"\n }\n }\n },\n \"valueFont\": {\n \"title\": \"Value text font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 40\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": \"#444\"\n },\n \"shadowColor\": {\n \"title\": \"Shadow color\",\n \"type\": \"string\",\n \"default\": \"rgba(0,0,0,0.3)\"\n }\n }\n },\n \"animation\": {\n \"title\": \"Enable animation\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"animationDuration\": {\n \"title\": \"Animation duration\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"animationRule\": {\n \"title\": \"Animation rule\",\n \"type\": \"string\",\n \"default\": \"cycle\"\n },\n \"startAngle\": {\n \"title\": \"Start ticks angle\",\n \"type\": \"number\",\n \"default\": 45\n },\n \"ticksAngle\": {\n \"title\": \"Ticks angle\",\n \"type\": \"number\",\n \"default\": 270\n },\n \"needleCircleSize\": {\n \"title\": \"Needle circle size\",\n \"type\": \"number\",\n \"default\": 10\n }\n },\n \"required\": []\n },\n \"form\": [\n \"startAngle\",\n \"ticksAngle\",\n \"needleCircleSize\",\n \"minValue\",\n \"maxValue\",\n \"unitTitle\",\n \"showUnitTitle\",\n \"units\",\n \"majorTicksCount\",\n \"minorTicks\",\n \"valueBox\",\n \"valueInt\",\n \"valueDec\",\n {\n \"key\": \"defaultColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorPlate\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorMajorTicks\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorMinorTicks\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorNeedle\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorNeedleEnd\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorNeedleShadowUp\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorNeedleShadowDown\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorValueBoxRect\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorValueBoxRectEnd\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorValueBoxBackground\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorValueBoxShadow\",\n \"type\": \"color\"\n },\n {\n \"key\": \"highlights\",\n \"items\": [\n \"highlights[].from\",\n \"highlights[].to\",\n {\n \"key\": \"highlights[].color\",\n \"type\": \"color\"\n }\n ]\n },\n \"highlightsWidth\",\n \"showBorder\",\n {\n \"key\": \"numbersFont\",\n \"items\": [\n \"numbersFont.family\",\n \"numbersFont.size\",\n {\n \"key\": \"numbersFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"numbersFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"numbersFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"titleFont\",\n \"items\": [\n \"titleFont.family\",\n \"titleFont.size\",\n {\n \"key\": \"titleFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"titleFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"titleFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"unitsFont\",\n \"items\": [\n \"unitsFont.family\",\n \"unitsFont.size\",\n {\n \"key\": \"unitsFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"unitsFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"unitsFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"valueFont\",\n \"items\": [\n \"valueFont.family\",\n \"valueFont.size\",\n {\n \"key\": \"valueFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"valueFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"valueFont.color\",\n \"type\": \"color\"\n },\n {\n \"key\": \"valueFont.shadowColor\",\n \"type\": \"color\"\n }\n ]\n }, \n \"animation\",\n \"animationDuration\",\n {\n \"key\": \"animationRule\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \"quad\",\n \"label\": \"Quad\"\n },\n {\n \"value\": \"quint\",\n \"label\": \"Quint\"\n },\n {\n \"value\": \"cycle\",\n \"label\": \"Cycle\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n },\n {\n \"value\": \"elastic\",\n \"label\": \"Elastic\"\n },\n {\n \"value\": \"dequad\",\n \"label\": \"Dequad\"\n },\n {\n \"value\": \"dequint\",\n \"label\": \"Dequint\"\n },\n {\n \"value\": \"decycle\",\n \"label\": \"Decycle\"\n },\n {\n \"value\": \"debounce\",\n \"label\": \"Debounce\"\n },\n {\n \"value\": \"delastic\",\n \"label\": \"Delastic\"\n }\n ]\n }\n ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nif (value < -100) {\\n\\tvalue = -100;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":100,\"startAngle\":45,\"ticksAngle\":270,\"showBorder\":true,\"defaultColor\":\"#e65100\",\"needleCircleSize\":10,\"highlights\":[],\"showUnitTitle\":true,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":10,\"valueInt\":3,\"valueDec\":0,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":500,\"animationRule\":\"cycle\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"numbersFont\":{\"family\":\"RobotoDraft\",\"size\":18,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"titleFont\":{\"family\":\"RobotoDraft\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"unitsFont\":{\"family\":\"RobotoDraft\",\"size\":22,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"size\":36,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\"},\"minValue\":-100,\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\"},\"title\":\"Radial gauge - Canvas Gauges\"}"}',
+'Radial gauge - Canvas Gauges' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'google_maps',
+'{"type":"latest","sizeX":8.5,"sizeY":6,"resources":[],"templateHtml":"","templateCss":".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 100px;\n white-space: nowrap;\n}","controllerScript":"var map;\nvar positions;\nvar markers = [];\nvar markersSettings = [];\nvar defaultZoomLevel;\nvar dontFitMapBounds;\n\nvar markerCluster;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n \n if (settings.defaultZoomLevel) {\n if (settings.defaultZoomLevel > 0 && settings.defaultZoomLevel < 21) {\n defaultZoomLevel = Math.floor(settings.defaultZoomLevel);\n }\n }\n \n dontFitMapBounds = settings.fitMapBounds === false;\n \n var configuredMarkersSettings = settings.markersSettings;\n if (!configuredMarkersSettings) {\n configuredMarkersSettings = [];\n }\n \n for (var i=0;i<datasources.length;i++) {\n markersSettings[i] = {\n latKeyName: \"lat\",\n lngKeyName: \"lng\",\n showLabel: true,\n label: datasources[i].name,\n color: \"FE7569\"\n };\n if (configuredMarkersSettings[i]) {\n markersSettings[i].latKeyName = configuredMarkersSettings[i].latKeyName || markersSettings[i].latKeyName;\n markersSettings[i].lngKeyName = configuredMarkersSettings[i].lngKeyName || markersSettings[i].lngKeyName;\n markersSettings[i].showLabel = configuredMarkersSettings[i].showLabel !== false;\n markersSettings[i].label = configuredMarkersSettings[i].label || markersSettings[i].label;\n markersSettings[i].color = configuredMarkersSettings[i].color ? tinycolor(configuredMarkersSettings[i].color).toHex() : markersSettings[i].color;\n }\n }\n\n var mapId = '''' + Math.random().toString(36).substr(2, 9);\n \n function clearGlobalId() {\n if ($window.loadingGmId && $window.loadingGmId === mapId) {\n $window.loadingGmId = null;\n }\n }\n \n $window.gm_authFailure = function() {\n if ($window.loadingGmId && $window.loadingGmId === mapId) {\n $window.loadingGmId = null;\n $window.gmApiKeys[apiKey].error = ''Unable to authentificate for Google Map API.</br>Please check your API key.'';\n displayError($window.gmApiKeys[apiKey].error);\n }\n };\n \n function displayError(message) {\n $(containerElement).html(\n \"<div class=''error''>\"+ message + \"</div>\"\n );\n }\n\n var initMapFunctionName = ''initGoogleMap_'' + mapId;\n $window[initMapFunctionName] = function() {\n lazyLoad.load({ type: ''js'', path: ''https://cdn.rawgit.com/googlemaps/v3-utility-library/master/markerwithlabel/src/markerwithlabel.js'' }).then(\n function success() {\n initMap();\n },\n function fail() {\n clearGloabalId();\n $window.gmApiKeys[apiKey].error = ''Google map api load failed!</br>''+e;\n displayError($window.gmApiKeys[apiKey].error);\n }\n );\n \n }; \n \n var apiKey = settings.gmApiKey || '''';\n\n if (apiKey && apiKey.length > 0) {\n if (!$window.gmApiKeys) {\n $window.gmApiKeys = {};\n }\n if ($window.gmApiKeys[apiKey]) {\n if ($window.gmApiKeys[apiKey].error) {\n displayError($window.gmApiKeys[apiKey].error);\n } else {\n initMap();\n }\n } else {\n $window.gmApiKeys[apiKey] = {};\n var googleMapScriptRes = ''https://maps.googleapis.com/maps/api/js?key=''+apiKey+''&callback=''+initMapFunctionName;\n \n $window.loadingGmId = mapId;\n lazyLoad.load({ type: ''js'', path: googleMapScriptRes }).then(\n function success() {\n setTimeout(clearGlobalId, 2000);\n },\n function fail(e) {\n clearGloabalId();\n $window.gmApiKeys[apiKey].error = ''Google map api load failed!</br>''+e;\n displayError($window.gmApiKeys[apiKey].error);\n }\n );\n }\n } else {\n displayError(''No Google Map Api Key provided!'');\n }\n\n function initMap() {\n \n map = new google.maps.Map(containerElement, {\n scrollwheel: false,\n zoom: defaultZoomLevel || 8\n });\n\n };\n\n}\n\n\nfns.redraw = function(containerElement, width, height, data,\n timeWindow, sizeChanged) {\n \n function createMarker(location, settings) {\n var pinColor = settings.color;\n var pinImage = new google.maps.MarkerImage(\"http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|\" + pinColor,\n new google.maps.Size(21, 34),\n new google.maps.Point(0,0),\n new google.maps.Point(10, 34));\n var pinShadow = new google.maps.MarkerImage(\"http://chart.apis.google.com/chart?chst=d_map_pin_shadow\",\n new google.maps.Size(40, 37),\n new google.maps.Point(0, 0),\n new google.maps.Point(12, 35)); \n var marker;\n if (settings.showLabel) { \n marker = new MarkerWithLabel({\n position: location, \n map: map,\n icon: pinImage,\n shadow: pinShadow,\n labelContent: ''<b>''+settings.label+''</b>'',\n labelClass: \"tb-labels\",\n labelAnchor: new google.maps.Point(50, 55)\n }); \n } else {\n marker = new google.maps.Marker({\n position: location, \n map: map,\n icon: pinImage,\n shadow: pinShadow\n }); \n }\n \n return marker; \n }\n \n function updatePosition(position, data) {\n if (position.latIndex > -1 && position.lngIndex > -1) {\n var latData = data[position.latIndex].data;\n var lngData = data[position.lngIndex].data;\n if (latData.length > 0 && lngData.length > 0) {\n var lat = latData[latData.length-1][1];\n var lng = lngData[lngData.length-1][1];\n var location = new google.maps.LatLng(lat, lng);\n if (!position.marker) {\n position.marker = createMarker(location, position.settings);\n markers.push(position.marker);\n return true;\n } else {\n var prevPosition = position.marker.getPosition();\n if (!prevPosition.equals(location)) {\n position.marker.setPosition(location);\n return true;\n }\n }\n }\n }\n return false;\n }\n \n function loadPositions(data) {\n var bounds = new google.maps.LatLngBounds();\n positions = [];\n var datasourceIndex = -1;\n var markerSettings;\n var datasource;\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n if (!datasource || datasource != datasourceData.datasource) {\n datasourceIndex++;\n datasource = datasourceData.datasource;\n markerSettings = markersSettings[datasourceIndex];\n }\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === markerSettings.latKeyName ||\n dataKey.label === markerSettings.lngKeyName) {\n var position = positions[datasourceIndex];\n if (!position) {\n position = {\n latIndex: -1,\n lngIndex: -1,\n settings: markerSettings\n };\n positions[datasourceIndex] = position;\n } else if (position.marker) {\n continue;\n }\n if (dataKey.label === markerSettings.latKeyName) {\n position.latIndex = i;\n } else {\n position.lngIndex = i;\n }\n if (position.latIndex > -1 && position.lngIndex > -1) {\n updatePosition(position, data);\n if (position.marker) {\n bounds.extend(position.marker.getPosition());\n }\n }\n }\n }\n fitMapBounds(bounds);\n }\n \n function updatePositions(data) {\n var positionsChanged = false;\n var bounds = new google.maps.LatLngBounds();\n for (var p in positions) {\n var position = positions[p];\n positionsChanged |= updatePosition(position, data);\n if (position.marker) {\n bounds.extend(position.marker.getPosition());\n }\n }\n if (!dontFitMapBounds && positionsChanged) {\n fitMapBounds(bounds);\n }\n }\n \n function fitMapBounds(bounds) {\n google.maps.event.addListenerOnce(map, ''bounds_changed'', function(event) {\n var zoomLevel = defaultZoomLevel || map.getZoom();\n this.setZoom(zoomLevel);\n if (!defaultZoomLevel && this.getZoom() > 15) {\n this.setZoom(15);\n }\n });\n map.fitBounds(bounds);\n }\n\n if (map) {\n if (data) {\n if (!positions) {\n loadPositions(data);\n } else {\n updatePositions(data);\n }\n }\n if (sizeChanged) {\n google.maps.event.trigger(map, \"resize\");\n var bounds = new google.maps.LatLngBounds();\n for (var m in markers) {\n bounds.extend(markers[m].getPosition());\n }\n fitMapBounds(bounds);\n }\n }\n\n};","settingsSchema":"{\n \"schema\": {\n \"title\": \"Google Map Configuration\",\n \"type\": \"object\",\n \"properties\": {\n \"gmApiKey\": {\n \"title\": \"Google Maps API Key\",\n \"type\": \"string\"\n },\n \"defaultZoomLevel\": {\n \"title\": \"Default map zoom level (1 - 20)\",\n \"type\": \"number\"\n },\n \"fitMapBounds\": {\n \"title\": \"Fit map bounds to cover all markers\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"markersSettings\": {\n \"title\": \"Markers settings, same order as datasources\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Marker settings\",\n \"type\": \"object\",\n \"properties\": {\n \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"lat\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"lng\"\n }, \n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n }\n }\n }\n }\n },\n \"required\": [\n \"gmApiKey\"\n ]\n },\n \"form\": [\n \"gmApiKey\",\n \"defaultZoomLevel\",\n \"fitMapBounds\",\n {\n \"key\": \"markersSettings\",\n \"items\": [\n \"markersSettings[].latKeyName\",\n \"markersSettings[].lngKeyName\",\n \"markersSettings[].showLabel\",\n \"markersSettings[].label\",\n {\n \"key\": \"markersSettings[].color\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"lat\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"lng\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"markersSettings\":[{\"label\":\"First point\",\"color\":\"#1e88e5\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true},{\"label\":\"Second point\",\"color\":\"#fdd835\",\"latKeyName\":\"lat\",\"lngKeyName\":\"lng\",\"showLabel\":true}],\"fitMapBounds\":true},\"title\":\"Google Maps\"}"}',
+'Google Maps' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'charts', 'basic_timeseries',
+'{"type":"timeseries","sizeX":8,"sizeY":6,"resources":[{"url":"https://rawgithub.com/HumbleSoftware/Flotr2/master/flotr2.min.js"}],"templateHtml":"","templateCss":"","controllerScript":"var graph, options;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n\n var colors = [];\n for (var i in data) {\n data[i].label = data[i].dataKey.label;\n colors.push(data[i].dataKey.color);\n var keySettings = data[i].dataKey.settings;\n\n data[i].lines = {\n fill: keySettings.fillLines || false,\n show: keySettings.showLines || true\n };\n\n data[i].points = {\n show: keySettings.showPoints || false\n };\n }\n options = {\n colors: colors,\n title: null,\n subtitle: null,\n shadowSize: settings.shadowSize || 4,\n fontColor: settings.fontColor || \"#545454\",\n fontSize: settings.fontSize || 7.5,\n xaxis: {\n mode: ''time'',\n timeMode: ''local''\n },\n yaxis: {\n },\n HtmlText: false,\n grid: {\n verticalLines: true,\n horizontalLines: true\n }\n };\n if (settings.grid) {\n options.grid.color = settings.grid.color || \"#545454\";\n options.grid.backgroundColor = settings.grid.backgroundColor || null;\n options.grid.tickColor = settings.grid.tickColor || \"#DDDDDD\";\n options.grid.verticalLines = settings.grid.verticalLines !== false;\n options.grid.horizontalLines = settings.grid.horizontalLines !== false;\n }\n if (settings.xaxis) {\n options.xaxis.showLabels = settings.xaxis.showLabels !== false;\n options.xaxis.color = settings.xaxis.color || null;\n options.xaxis.title = settings.xaxis.title || null;\n options.xaxis.titleAngle = settings.xaxis.titleAngle || 0;\n }\n if (settings.yaxis) {\n options.yaxis.showLabels = settings.yaxis.showLabels !== false;\n options.yaxis.color = settings.yaxis.color || null;\n options.yaxis.title = settings.yaxis.title || null;\n options.yaxis.titleAngle = settings.yaxis.titleAngle || 0;\n }\n}\n\nfns.redraw = function(containerElement, width, height, data,\n timeWindow, sizeChanged) {\n options.xaxis.min = timeWindow.minTime;\n options.xaxis.max = timeWindow.maxTime;\n graph = Flotr.draw(containerElement, data, options);\n};\n\nfns.destroy = function() {\n //console.log(''destroy!'');\n};","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"shadowSize\": {\n \"title\": \"Shadow size\",\n \"type\": \"number\",\n \"default\": 4\n },\n \"fontColor\": {\n \"title\": \"Font color\",\n \"type\": \"string\",\n \"default\": \"#545454\"\n },\n \"fontSize\": {\n \"title\": \"Font size\",\n \"type\": \"number\",\n \"default\": 7.5\n },\n \"grid\": {\n \"title\": \"Grid settings\",\n \"type\": \"object\",\n \"properties\": {\n \"color\": {\n \"title\": \"Primary color\",\n \"type\": \"string\",\n \"default\": \"#545454\"\n },\n \"backgroundColor\": {\n \"title\": \"Background color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"tickColor\": {\n \"title\": \"Ticks color\",\n \"type\": \"string\",\n \"default\": \"#DDDDDD\"\n },\n \"verticalLines\": {\n \"title\": \"Show vertical lines\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"horizontalLines\": {\n \"title\": \"Show horizontal lines\",\n \"type\": \"boolean\",\n \"default\": true\n }\n }\n },\n \"xaxis\": {\n \"title\": \"X axis settings\",\n \"type\": \"object\",\n \"properties\": {\n \"showLabels\": {\n \"title\": \"Show labels\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"title\": {\n \"title\": \"Axis title\",\n \"type\": \"string\",\n \"default\": null\n },\n \"titleAngle\": {\n \"title\": \"Axis title''s angle in degrees\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"color\": {\n \"title\": \"Ticks color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"yaxis\": {\n \"title\": \"Y axis settings\",\n \"type\": \"object\",\n \"properties\": {\n \"showLabels\": {\n \"title\": \"Show labels\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"title\": {\n \"title\": \"Axis title\",\n \"type\": \"string\",\n \"default\": null\n },\n \"titleAngle\": {\n \"title\": \"Axis title''s angle in degrees\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"color\": {\n \"title\": \"Ticks color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n }\n },\n \"required\": []\n },\n \"form\": [\n \"shadowSize\", \n {\n \"key\": \"fontColor\",\n \"type\": \"color\"\n },\n \"fontSize\", \n {\n \"key\": \"grid\",\n \"items\": [\n {\n \"key\": \"grid.color\",\n \"type\": \"color\"\n },\n {\n \"key\": \"grid.backgroundColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"grid.tickColor\",\n \"type\": \"color\"\n },\n \"grid.verticalLines\",\n \"grid.horizontalLines\"\n ]\n },\n {\n \"key\": \"xaxis\",\n \"items\": [\n \"xaxis.showLabels\",\n \"xaxis.title\",\n \"xaxis.titleAngle\",\n {\n \"key\": \"xaxis.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"yaxis\",\n \"items\": [\n \"yaxis.showLabels\",\n \"yaxis.title\",\n \"yaxis.titleAngle\",\n {\n \"key\": \"yaxis.color\",\n \"type\": \"color\"\n }\n ]\n }\n\n ]\n}","dataKeySettingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"showLines\": {\n \"title\": \"Show lines\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"fillLines\": {\n \"title\": \"Fill lines\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showPoints\": {\n \"title\": \"Show points\",\n \"type\": \"boolean\",\n \"default\": false\n }\n },\n \"required\": [\"showLines\", \"fillLines\", \"showPoints\"]\n },\n \"form\": [\n \"showLines\",\n \"fillLines\",\n \"showPoints\"\n ]\n}","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":7.5,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"backgroundColor\":\"#ffffff\"}},\"title\":\"Timeseries - Flotr2\"}"}',
+'Timeseries - Flotr2' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'digital_gauges', 'horizontal_bar_justgage',
+'{"type":"latest","sizeX":7,"sizeY":3,"resources":[],"templateHtml":"","templateCss":"#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n","controllerScript":"var gauge;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n gauge = new TbDigitalGauge(containerElement, settings, data); \n\n}\n\n\nfns.redraw = function(containerElement, width, height, data) {\n gauge.redraw(data);\n};\n\nfns.destroy = function() {\n}\n","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"minValue\": {\n \"title\": \"Minimum value\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"maxValue\": {\n \"title\": \"Maximum value\",\n \"type\": \"number\",\n \"default\": 100\n }, \n \"gaugeType\": {\n \"title\": \"Gauge type\",\n \"type\": \"string\",\n \"default\": \"arc\"\n }, \n \"donutStartAngle\": {\n \"title\": \"Angle to start from when in donut mode\",\n \"type\": \"number\",\n \"default\": 90\n }, \n \"neonGlowBrightness\": {\n \"title\": \"Neon glow effect brightness, (0-100), 0 - disable effect\",\n \"type\": \"number\",\n \"default\": 0\n }, \n \"dashThickness\": {\n \"title\": \"Thickness of the stripes, 0 - no stripes\",\n \"type\": \"number\",\n \"default\": 0\n }, \n \"roundedLineCap\": {\n \"title\": \"Display rounded line cap\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"title\": {\n \"title\": \"Gauge title\",\n \"type\": \"string\",\n \"default\": null\n }, \n \"showTitle\": {\n \"title\": \"Show gauge title\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"unitTitle\": {\n \"title\": \"Unit title\",\n \"type\": \"string\",\n \"default\": null\n }, \n \"showUnitTitle\": {\n \"title\": \"Show unit title\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"showValue\": {\n \"title\": \"Show value text\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"showMinMax\": {\n \"title\": \"Show min and max values\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"gaugeWidthScale\": {\n \"title\": \"Width of the gauge element\",\n \"type\": \"number\",\n \"default\": 0.75\n },\n \"defaultColor\": {\n \"title\": \"Default color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"gaugeColor\": {\n \"title\": \"Background color of the gauge element\",\n \"type\": \"string\",\n \"default\": null\n },\n \"levelColors\": {\n \"title\": \"Colors of indicator, from lower to upper\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n }\n },\n \"refreshAnimationType\": {\n \"title\": \"Type of refresh animation\",\n \"type\": \"string\",\n \"default\": \">\"\n },\n \"refreshAnimationTime\": {\n \"title\": \"Duration of refresh animation (ms)\",\n \"type\": \"number\",\n \"default\": 700\n },\n \"startAnimationType\": {\n \"title\": \"Type of start animation\",\n \"type\": \"string\",\n \"default\": \">\"\n },\n \"startAnimationTime\": {\n \"title\": \"Duration of start animation (ms)\",\n \"type\": \"number\",\n \"default\": 700\n },\n \"decimals\": {\n \"title\": \"Number of digits after floating point\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"units\": {\n \"title\": \"Special symbol to show next to value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"titleFont\": {\n \"title\": \"Gauge title font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 12\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"labelFont\": {\n \"title\": \"Font of label showing under value\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 8\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"valueFont\": {\n \"title\": \"Font of label showing current value\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 18\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"minMaxFont\": {\n \"title\": \"Font of minimum and maximum labels\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n }\n }\n },\n \"form\": [\n \"minValue\",\n \"maxValue\",\n {\n \"key\": \"gaugeType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"arc\",\n \"label\": \"Arc\"\n },\n {\n \"value\": \"donut\",\n \"label\": \"Donut\"\n },\n {\n \"value\": \"horizontalBar\",\n \"label\": \"Horizontal bar\"\n },\n {\n \"value\": \"verticalBar\",\n \"label\": \"Vertical bar\"\n }\n ]\n },\n \"donutStartAngle\",\n \"neonGlowBrightness\",\n \"dashThickness\",\n \"roundedLineCap\",\n \"title\",\n \"showTitle\",\n \"unitTitle\",\n \"showUnitTitle\",\n \"showValue\",\n \"showMinMax\",\n \"gaugeWidthScale\",\n {\n \"key\": \"defaultColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"gaugeColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"levelColors\",\n \"items\": [\n {\n \"key\": \"levelColors[]\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"refreshAnimationType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \">\",\n \"label\": \">\"\n },\n {\n \"value\": \"<\",\n \"label\": \"<\"\n },\n {\n \"value\": \"<>\",\n \"label\": \"<>\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n }\n ]\n },\n \"refreshAnimationTime\",\n {\n \"key\": \"startAnimationType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \">\",\n \"label\": \">\"\n },\n {\n \"value\": \"<\",\n \"label\": \"<\"\n },\n {\n \"value\": \"<>\",\n \"label\": \"<>\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n }\n ]\n },\n \"startAnimationTime\",\n \"decimals\",\n \"units\",\n {\n \"key\": \"titleFont\",\n \"items\": [\n \"titleFont.family\",\n \"titleFont.size\",\n {\n \"key\": \"titleFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"titleFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"titleFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"labelFont\",\n \"items\": [\n \"labelFont.family\",\n \"labelFont.size\",\n {\n \"key\": \"labelFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"labelFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"labelFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"valueFont\",\n \"items\": [\n \"valueFont.family\",\n \"valueFont.size\",\n {\n \"key\": \"valueFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"valueFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"valueFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont\",\n \"items\": [\n \"minMaxFont.family\",\n \"minMaxFont.size\",\n {\n \"key\": \"minMaxFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont.color\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n}\n ","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"RobotoDraft\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#999999\"},\"labelFont\":{\"family\":\"RobotoDraft\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"RobotoDraft\",\"style\":\"normal\",\"weight\":\"500\",\"size\":18,\"color\":\"#666666\"},\"minMaxFont\":{\"family\":\"RobotoDraft\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#666666\"},\"neonGlowBrightness\":0,\"decimals\":0,\"dashThickness\":0,\"gaugeColor\":\"#eeeeee\",\"showTitle\":true,\"gaugeType\":\"horizontalBar\"},\"title\":\"Horizontal bar - justGage\"}"}',
+'Horizontal bar - justGage' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'digital_gauges', 'lcd_gauge',
+'{"type":"latest","sizeX":5,"sizeY":3,"resources":[],"templateHtml":"","templateCss":"#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n","controllerScript":"var gauge;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n gauge = new TbDigitalGauge(containerElement, settings, data); \n\n}\n\n\nfns.redraw = function(containerElement, width, height, data) {\n gauge.redraw(data);\n};\n\nfns.destroy = function() {\n}\n","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"minValue\": {\n \"title\": \"Minimum value\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"maxValue\": {\n \"title\": \"Maximum value\",\n \"type\": \"number\",\n \"default\": 100\n }, \n \"gaugeType\": {\n \"title\": \"Gauge type\",\n \"type\": \"string\",\n \"default\": \"arc\"\n }, \n \"donutStartAngle\": {\n \"title\": \"Angle to start from when in donut mode\",\n \"type\": \"number\",\n \"default\": 90\n }, \n \"neonGlowBrightness\": {\n \"title\": \"Neon glow effect brightness, (0-100), 0 - disable effect\",\n \"type\": \"number\",\n \"default\": 0\n }, \n \"dashThickness\": {\n \"title\": \"Thickness of the stripes, 0 - no stripes\",\n \"type\": \"number\",\n \"default\": 0\n }, \n \"roundedLineCap\": {\n \"title\": \"Display rounded line cap\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"title\": {\n \"title\": \"Gauge title\",\n \"type\": \"string\",\n \"default\": null\n }, \n \"showTitle\": {\n \"title\": \"Show gauge title\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"unitTitle\": {\n \"title\": \"Unit title\",\n \"type\": \"string\",\n \"default\": null\n }, \n \"showUnitTitle\": {\n \"title\": \"Show unit title\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"showValue\": {\n \"title\": \"Show value text\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"showMinMax\": {\n \"title\": \"Show min and max values\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"gaugeWidthScale\": {\n \"title\": \"Width of the gauge element\",\n \"type\": \"number\",\n \"default\": 0.75\n },\n \"defaultColor\": {\n \"title\": \"Default color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"gaugeColor\": {\n \"title\": \"Background color of the gauge element\",\n \"type\": \"string\",\n \"default\": null\n },\n \"levelColors\": {\n \"title\": \"Colors of indicator, from lower to upper\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n }\n },\n \"refreshAnimationType\": {\n \"title\": \"Type of refresh animation\",\n \"type\": \"string\",\n \"default\": \">\"\n },\n \"refreshAnimationTime\": {\n \"title\": \"Duration of refresh animation (ms)\",\n \"type\": \"number\",\n \"default\": 700\n },\n \"startAnimationType\": {\n \"title\": \"Type of start animation\",\n \"type\": \"string\",\n \"default\": \">\"\n },\n \"startAnimationTime\": {\n \"title\": \"Duration of start animation (ms)\",\n \"type\": \"number\",\n \"default\": 700\n },\n \"decimals\": {\n \"title\": \"Number of digits after floating point\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"units\": {\n \"title\": \"Special symbol to show next to value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"titleFont\": {\n \"title\": \"Gauge title font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 12\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"labelFont\": {\n \"title\": \"Font of label showing under value\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 8\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"valueFont\": {\n \"title\": \"Font of label showing current value\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 18\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"minMaxFont\": {\n \"title\": \"Font of minimum and maximum labels\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n }\n }\n },\n \"form\": [\n \"minValue\",\n \"maxValue\",\n {\n \"key\": \"gaugeType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"arc\",\n \"label\": \"Arc\"\n },\n {\n \"value\": \"donut\",\n \"label\": \"Donut\"\n },\n {\n \"value\": \"horizontalBar\",\n \"label\": \"Horizontal bar\"\n },\n {\n \"value\": \"verticalBar\",\n \"label\": \"Vertical bar\"\n }\n ]\n },\n \"donutStartAngle\",\n \"neonGlowBrightness\",\n \"dashThickness\",\n \"roundedLineCap\",\n \"title\",\n \"showTitle\",\n \"unitTitle\",\n \"showUnitTitle\",\n \"showValue\",\n \"showMinMax\",\n \"gaugeWidthScale\",\n {\n \"key\": \"defaultColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"gaugeColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"levelColors\",\n \"items\": [\n {\n \"key\": \"levelColors[]\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"refreshAnimationType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \">\",\n \"label\": \">\"\n },\n {\n \"value\": \"<\",\n \"label\": \"<\"\n },\n {\n \"value\": \"<>\",\n \"label\": \"<>\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n }\n ]\n },\n \"refreshAnimationTime\",\n {\n \"key\": \"startAnimationType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \">\",\n \"label\": \">\"\n },\n {\n \"value\": \"<\",\n \"label\": \"<\"\n },\n {\n \"value\": \"<>\",\n \"label\": \"<>\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n }\n ]\n },\n \"startAnimationTime\",\n \"decimals\",\n \"units\",\n {\n \"key\": \"titleFont\",\n \"items\": [\n \"titleFont.family\",\n \"titleFont.size\",\n {\n \"key\": \"titleFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"titleFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"titleFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"labelFont\",\n \"items\": [\n \"labelFont.family\",\n \"labelFont.size\",\n {\n \"key\": \"labelFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"labelFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"labelFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"valueFont\",\n \"items\": [\n \"valueFont.family\",\n \"valueFont.size\",\n {\n \"key\": \"valueFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"valueFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"valueFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont\",\n \"items\": [\n \"minMaxFont.family\",\n \"minMaxFont.size\",\n {\n \"key\": \"minMaxFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont.color\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n}\n ","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 180) {\\n\\tvalue = 180;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#babab2\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":180,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\"linear\",\"refreshAnimationTime\":700,\"startAnimationType\":\"linear\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"RobotoDraft\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"RobotoDraft\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":0,\"dashThickness\":1.5,\"decimals\":0,\"unitTitle\":\"MPH\",\"showUnitTitle\":true,\"defaultColor\":\"#444444\",\"gaugeType\":\"arc\"},\"title\":\"LCD gauge\"}"}',
+'LCD gauge' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'digital_gauges', 'digital_bar',
+'{"type":"latest","sizeX":6,"sizeY":2.5,"resources":[],"templateHtml":"","templateCss":"#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n","controllerScript":"var gauge;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n gauge = new TbDigitalGauge(containerElement, settings, data); \n\n}\n\n\nfns.redraw = function(containerElement, width, height, data) {\n gauge.redraw(data);\n};\n\nfns.destroy = function() {\n}\n","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"minValue\": {\n \"title\": \"Minimum value\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"maxValue\": {\n \"title\": \"Maximum value\",\n \"type\": \"number\",\n \"default\": 100\n }, \n \"gaugeType\": {\n \"title\": \"Gauge type\",\n \"type\": \"string\",\n \"default\": \"arc\"\n }, \n \"donutStartAngle\": {\n \"title\": \"Angle to start from when in donut mode\",\n \"type\": \"number\",\n \"default\": 90\n }, \n \"neonGlowBrightness\": {\n \"title\": \"Neon glow effect brightness, (0-100), 0 - disable effect\",\n \"type\": \"number\",\n \"default\": 0\n }, \n \"dashThickness\": {\n \"title\": \"Thickness of the stripes, 0 - no stripes\",\n \"type\": \"number\",\n \"default\": 0\n }, \n \"roundedLineCap\": {\n \"title\": \"Display rounded line cap\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"title\": {\n \"title\": \"Gauge title\",\n \"type\": \"string\",\n \"default\": null\n }, \n \"showTitle\": {\n \"title\": \"Show gauge title\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"unitTitle\": {\n \"title\": \"Unit title\",\n \"type\": \"string\",\n \"default\": null\n }, \n \"showUnitTitle\": {\n \"title\": \"Show unit title\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"showValue\": {\n \"title\": \"Show value text\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"showMinMax\": {\n \"title\": \"Show min and max values\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"gaugeWidthScale\": {\n \"title\": \"Width of the gauge element\",\n \"type\": \"number\",\n \"default\": 0.75\n },\n \"defaultColor\": {\n \"title\": \"Default color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"gaugeColor\": {\n \"title\": \"Background color of the gauge element\",\n \"type\": \"string\",\n \"default\": null\n },\n \"levelColors\": {\n \"title\": \"Colors of indicator, from lower to upper\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n }\n },\n \"refreshAnimationType\": {\n \"title\": \"Type of refresh animation\",\n \"type\": \"string\",\n \"default\": \">\"\n },\n \"refreshAnimationTime\": {\n \"title\": \"Duration of refresh animation (ms)\",\n \"type\": \"number\",\n \"default\": 700\n },\n \"startAnimationType\": {\n \"title\": \"Type of start animation\",\n \"type\": \"string\",\n \"default\": \">\"\n },\n \"startAnimationTime\": {\n \"title\": \"Duration of start animation (ms)\",\n \"type\": \"number\",\n \"default\": 700\n },\n \"decimals\": {\n \"title\": \"Number of digits after floating point\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"units\": {\n \"title\": \"Special symbol to show next to value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"titleFont\": {\n \"title\": \"Gauge title font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 12\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"labelFont\": {\n \"title\": \"Font of label showing under value\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 8\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"valueFont\": {\n \"title\": \"Font of label showing current value\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 18\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"minMaxFont\": {\n \"title\": \"Font of minimum and maximum labels\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n }\n }\n },\n \"form\": [\n \"minValue\",\n \"maxValue\",\n {\n \"key\": \"gaugeType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"arc\",\n \"label\": \"Arc\"\n },\n {\n \"value\": \"donut\",\n \"label\": \"Donut\"\n },\n {\n \"value\": \"horizontalBar\",\n \"label\": \"Horizontal bar\"\n },\n {\n \"value\": \"verticalBar\",\n \"label\": \"Vertical bar\"\n }\n ]\n },\n \"donutStartAngle\",\n \"neonGlowBrightness\",\n \"dashThickness\",\n \"roundedLineCap\",\n \"title\",\n \"showTitle\",\n \"unitTitle\",\n \"showUnitTitle\",\n \"showValue\",\n \"showMinMax\",\n \"gaugeWidthScale\",\n {\n \"key\": \"defaultColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"gaugeColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"levelColors\",\n \"items\": [\n {\n \"key\": \"levelColors[]\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"refreshAnimationType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \">\",\n \"label\": \">\"\n },\n {\n \"value\": \"<\",\n \"label\": \"<\"\n },\n {\n \"value\": \"<>\",\n \"label\": \"<>\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n }\n ]\n },\n \"refreshAnimationTime\",\n {\n \"key\": \"startAnimationType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \">\",\n \"label\": \">\"\n },\n {\n \"value\": \"<\",\n \"label\": \"<\"\n },\n {\n \"value\": \"<>\",\n \"label\": \"<>\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n }\n ]\n },\n \"startAnimationTime\",\n \"decimals\",\n \"units\",\n {\n \"key\": \"titleFont\",\n \"items\": [\n \"titleFont.family\",\n \"titleFont.size\",\n {\n \"key\": \"titleFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"titleFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"titleFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"labelFont\",\n \"items\": [\n \"labelFont.family\",\n \"labelFont.size\",\n {\n \"key\": \"labelFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"labelFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"labelFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"valueFont\",\n \"items\": [\n \"valueFont.family\",\n \"valueFont.size\",\n {\n \"key\": \"valueFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"valueFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"valueFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont\",\n \"items\": [\n \"minMaxFont.family\",\n \"minMaxFont.size\",\n {\n \"key\": \"minMaxFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont.color\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n}\n ","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 80) {\\n\\tvalue = 80;\\n} else if (value > 160) {\\n\\tvalue = 160;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":180,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[\"#008000\",\"#fbc02d\",\"#f44336\"],\"refreshAnimationType\":\"linear\",\"refreshAnimationTime\":700,\"startAnimationType\":\"linear\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"RobotoDraft\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"RobotoDraft\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":18},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#ffffff\"},\"neonGlowBrightness\":40,\"dashThickness\":1.5,\"decimals\":0,\"unitTitle\":\"MPH\",\"showUnitTitle\":true,\"gaugeColor\":\"#171a1c\",\"gaugeType\":\"horizontalBar\",\"showTitle\":false},\"title\":\"Digital horizontal bar\"}"}',
+'Digital horizontal bar' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'digital_gauges', 'simple_gauge_justgage',
+'{"type":"latest","sizeX":2,"sizeY":2,"resources":[],"templateHtml":"","templateCss":"#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n","controllerScript":"var gauge;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n gauge = new TbDigitalGauge(containerElement, settings, data); \n\n}\n\n\nfns.redraw = function(containerElement, width, height, data) {\n gauge.redraw(data);\n};\n\nfns.destroy = function() {\n}\n","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"minValue\": {\n \"title\": \"Minimum value\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"maxValue\": {\n \"title\": \"Maximum value\",\n \"type\": \"number\",\n \"default\": 100\n }, \n \"gaugeType\": {\n \"title\": \"Gauge type\",\n \"type\": \"string\",\n \"default\": \"arc\"\n }, \n \"donutStartAngle\": {\n \"title\": \"Angle to start from when in donut mode\",\n \"type\": \"number\",\n \"default\": 90\n }, \n \"neonGlowBrightness\": {\n \"title\": \"Neon glow effect brightness, (0-100), 0 - disable effect\",\n \"type\": \"number\",\n \"default\": 0\n }, \n \"dashThickness\": {\n \"title\": \"Thickness of the stripes, 0 - no stripes\",\n \"type\": \"number\",\n \"default\": 0\n }, \n \"roundedLineCap\": {\n \"title\": \"Display rounded line cap\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"title\": {\n \"title\": \"Gauge title\",\n \"type\": \"string\",\n \"default\": null\n }, \n \"showTitle\": {\n \"title\": \"Show gauge title\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"unitTitle\": {\n \"title\": \"Unit title\",\n \"type\": \"string\",\n \"default\": null\n }, \n \"showUnitTitle\": {\n \"title\": \"Show unit title\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"showValue\": {\n \"title\": \"Show value text\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"showMinMax\": {\n \"title\": \"Show min and max values\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"gaugeWidthScale\": {\n \"title\": \"Width of the gauge element\",\n \"type\": \"number\",\n \"default\": 0.75\n },\n \"defaultColor\": {\n \"title\": \"Default color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"gaugeColor\": {\n \"title\": \"Background color of the gauge element\",\n \"type\": \"string\",\n \"default\": null\n },\n \"levelColors\": {\n \"title\": \"Colors of indicator, from lower to upper\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n }\n },\n \"refreshAnimationType\": {\n \"title\": \"Type of refresh animation\",\n \"type\": \"string\",\n \"default\": \">\"\n },\n \"refreshAnimationTime\": {\n \"title\": \"Duration of refresh animation (ms)\",\n \"type\": \"number\",\n \"default\": 700\n },\n \"startAnimationType\": {\n \"title\": \"Type of start animation\",\n \"type\": \"string\",\n \"default\": \">\"\n },\n \"startAnimationTime\": {\n \"title\": \"Duration of start animation (ms)\",\n \"type\": \"number\",\n \"default\": 700\n },\n \"decimals\": {\n \"title\": \"Number of digits after floating point\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"units\": {\n \"title\": \"Special symbol to show next to value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"titleFont\": {\n \"title\": \"Gauge title font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 12\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"labelFont\": {\n \"title\": \"Font of label showing under value\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 8\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"valueFont\": {\n \"title\": \"Font of label showing current value\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 18\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"minMaxFont\": {\n \"title\": \"Font of minimum and maximum labels\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n }\n }\n },\n \"form\": [\n \"minValue\",\n \"maxValue\",\n {\n \"key\": \"gaugeType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"arc\",\n \"label\": \"Arc\"\n },\n {\n \"value\": \"donut\",\n \"label\": \"Donut\"\n },\n {\n \"value\": \"horizontalBar\",\n \"label\": \"Horizontal bar\"\n },\n {\n \"value\": \"verticalBar\",\n \"label\": \"Vertical bar\"\n }\n ]\n },\n \"donutStartAngle\",\n \"neonGlowBrightness\",\n \"dashThickness\",\n \"roundedLineCap\",\n \"title\",\n \"showTitle\",\n \"unitTitle\",\n \"showUnitTitle\",\n \"showValue\",\n \"showMinMax\",\n \"gaugeWidthScale\",\n {\n \"key\": \"defaultColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"gaugeColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"levelColors\",\n \"items\": [\n {\n \"key\": \"levelColors[]\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"refreshAnimationType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \">\",\n \"label\": \">\"\n },\n {\n \"value\": \"<\",\n \"label\": \"<\"\n },\n {\n \"value\": \"<>\",\n \"label\": \"<>\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n }\n ]\n },\n \"refreshAnimationTime\",\n {\n \"key\": \"startAnimationType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \">\",\n \"label\": \">\"\n },\n {\n \"value\": \"<\",\n \"label\": \"<\"\n },\n {\n \"value\": \"<>\",\n \"label\": \"<>\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n }\n ]\n },\n \"startAnimationTime\",\n \"decimals\",\n \"units\",\n {\n \"key\": \"titleFont\",\n \"items\": [\n \"titleFont.family\",\n \"titleFont.size\",\n {\n \"key\": \"titleFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"titleFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"titleFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"labelFont\",\n \"items\": [\n \"labelFont.family\",\n \"labelFont.size\",\n {\n \"key\": \"labelFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"labelFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"labelFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"valueFont\",\n \"items\": [\n \"valueFont.family\",\n \"valueFont.size\",\n {\n \"key\": \"valueFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"valueFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"valueFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont\",\n \"items\": [\n \"minMaxFont.family\",\n \"minMaxFont.size\",\n {\n \"key\": \"minMaxFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont.color\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n}\n ","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#ef6c00\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"RobotoDraft\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"RobotoDraft\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"RobotoDraft\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32,\"color\":\"#666666\"},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":0,\"dashThickness\":0,\"decimals\":0,\"gaugeColor\":\"#eeeeee\",\"gaugeType\":\"donut\"},\"title\":\"Simple gauge - justGage\"}"}',
+'Simple gauge - justGage' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'charts', 'radar_chart_js',
+'{"type":"latest","sizeX":7,"sizeY":6,"resources":[{"url":"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.3.0/Chart.min.js"}],"templateHtml":"<canvas id=\"radarChart\"></canvas>\n","templateCss":"","controllerScript":"var chart;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n\n var barData = {\n labels: [],\n datasets: []\n };\n\n var backgroundColor = tinycolor(data[0].dataKey.color);\n backgroundColor.setAlpha(0.2);\n var borderColor = tinycolor(data[0].dataKey.color);\n borderColor.setAlpha(1);\n var dataset = {\n label: datasources[0].name,\n data: [],\n backgroundColor: backgroundColor.toRgbString(),\n borderColor: borderColor.toRgbString(),\n pointBackgroundColor: borderColor.toRgbString(),\n pointBorderColor: borderColor.darken().toRgbString(),\n borderWidth: 1\n }\n \n barData.datasets.push(dataset);\n \n for (var i in data) {\n var dataKey = data[i].dataKey;\n barData.labels.push(dataKey.label);\n dataset.data.push(0);\n }\n\n var ctx = $(''#radarChart'', containerElement);\n chart = new Chart(ctx, {\n type: ''radar'',\n data: barData,\n options: {\n maintainAspectRatio: false\n }\n });\n \n}\n\nfns.redraw = function(containerElement, width, height, data,\n timeWindow, sizeChanged) {\n\n for (var i = 0; i < data.length; i++) {\n var cellData = data[i];\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = tvPair[1];\n chart.data.datasets[0].data[i] = parseFloat(value);\n }\n }\n \n chart.update();\n if (sizeChanged) {\n chart.resize();\n }\n \n};\n","settingsSchema":"{}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Radar - Chart.js\"}"}',
+'Radar - Chart.js' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'digital_gauges', 'digital_speedometer',
+'{"type":"latest","sizeX":5,"sizeY":3,"resources":[],"templateHtml":"","templateCss":"#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n","controllerScript":"var gauge;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n gauge = new TbDigitalGauge(containerElement, settings, data); \n\n}\n\n\nfns.redraw = function(containerElement, width, height, data) {\n gauge.redraw(data);\n};\n\nfns.destroy = function() {\n}\n","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"minValue\": {\n \"title\": \"Minimum value\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"maxValue\": {\n \"title\": \"Maximum value\",\n \"type\": \"number\",\n \"default\": 100\n }, \n \"gaugeType\": {\n \"title\": \"Gauge type\",\n \"type\": \"string\",\n \"default\": \"arc\"\n }, \n \"donutStartAngle\": {\n \"title\": \"Angle to start from when in donut mode\",\n \"type\": \"number\",\n \"default\": 90\n }, \n \"neonGlowBrightness\": {\n \"title\": \"Neon glow effect brightness, (0-100), 0 - disable effect\",\n \"type\": \"number\",\n \"default\": 0\n }, \n \"dashThickness\": {\n \"title\": \"Thickness of the stripes, 0 - no stripes\",\n \"type\": \"number\",\n \"default\": 0\n }, \n \"roundedLineCap\": {\n \"title\": \"Display rounded line cap\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"title\": {\n \"title\": \"Gauge title\",\n \"type\": \"string\",\n \"default\": null\n }, \n \"showTitle\": {\n \"title\": \"Show gauge title\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"unitTitle\": {\n \"title\": \"Unit title\",\n \"type\": \"string\",\n \"default\": null\n }, \n \"showUnitTitle\": {\n \"title\": \"Show unit title\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"showValue\": {\n \"title\": \"Show value text\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"showMinMax\": {\n \"title\": \"Show min and max values\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"gaugeWidthScale\": {\n \"title\": \"Width of the gauge element\",\n \"type\": \"number\",\n \"default\": 0.75\n },\n \"defaultColor\": {\n \"title\": \"Default color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"gaugeColor\": {\n \"title\": \"Background color of the gauge element\",\n \"type\": \"string\",\n \"default\": null\n },\n \"levelColors\": {\n \"title\": \"Colors of indicator, from lower to upper\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n }\n },\n \"refreshAnimationType\": {\n \"title\": \"Type of refresh animation\",\n \"type\": \"string\",\n \"default\": \">\"\n },\n \"refreshAnimationTime\": {\n \"title\": \"Duration of refresh animation (ms)\",\n \"type\": \"number\",\n \"default\": 700\n },\n \"startAnimationType\": {\n \"title\": \"Type of start animation\",\n \"type\": \"string\",\n \"default\": \">\"\n },\n \"startAnimationTime\": {\n \"title\": \"Duration of start animation (ms)\",\n \"type\": \"number\",\n \"default\": 700\n },\n \"decimals\": {\n \"title\": \"Number of digits after floating point\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"units\": {\n \"title\": \"Special symbol to show next to value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"titleFont\": {\n \"title\": \"Gauge title font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 12\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"labelFont\": {\n \"title\": \"Font of label showing under value\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 8\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"valueFont\": {\n \"title\": \"Font of label showing current value\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 18\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"minMaxFont\": {\n \"title\": \"Font of minimum and maximum labels\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n }\n }\n },\n \"form\": [\n \"minValue\",\n \"maxValue\",\n {\n \"key\": \"gaugeType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"arc\",\n \"label\": \"Arc\"\n },\n {\n \"value\": \"donut\",\n \"label\": \"Donut\"\n },\n {\n \"value\": \"horizontalBar\",\n \"label\": \"Horizontal bar\"\n },\n {\n \"value\": \"verticalBar\",\n \"label\": \"Vertical bar\"\n }\n ]\n },\n \"donutStartAngle\",\n \"neonGlowBrightness\",\n \"dashThickness\",\n \"roundedLineCap\",\n \"title\",\n \"showTitle\",\n \"unitTitle\",\n \"showUnitTitle\",\n \"showValue\",\n \"showMinMax\",\n \"gaugeWidthScale\",\n {\n \"key\": \"defaultColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"gaugeColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"levelColors\",\n \"items\": [\n {\n \"key\": \"levelColors[]\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"refreshAnimationType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \">\",\n \"label\": \">\"\n },\n {\n \"value\": \"<\",\n \"label\": \"<\"\n },\n {\n \"value\": \"<>\",\n \"label\": \"<>\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n }\n ]\n },\n \"refreshAnimationTime\",\n {\n \"key\": \"startAnimationType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \">\",\n \"label\": \">\"\n },\n {\n \"value\": \"<\",\n \"label\": \"<\"\n },\n {\n \"value\": \"<>\",\n \"label\": \"<>\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n }\n ]\n },\n \"startAnimationTime\",\n \"decimals\",\n \"units\",\n {\n \"key\": \"titleFont\",\n \"items\": [\n \"titleFont.family\",\n \"titleFont.size\",\n {\n \"key\": \"titleFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"titleFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"titleFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"labelFont\",\n \"items\": [\n \"labelFont.family\",\n \"labelFont.size\",\n {\n \"key\": \"labelFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"labelFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"labelFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"valueFont\",\n \"items\": [\n \"valueFont.family\",\n \"valueFont.size\",\n {\n \"key\": \"valueFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"valueFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"valueFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont\",\n \"items\": [\n \"minMaxFont.family\",\n \"minMaxFont.size\",\n {\n \"key\": \"minMaxFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont.color\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n}\n ","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 45) {\\n\\tvalue = 45;\\n} else if (value > 130) {\\n\\tvalue = 130;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":180,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[\"#008000\",\"#fbc02d\",\"#f44336\"],\"refreshAnimationType\":\"linear\",\"refreshAnimationTime\":700,\"startAnimationType\":\"linear\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"RobotoDraft\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"RobotoDraft\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#ffffff\"},\"neonGlowBrightness\":40,\"dashThickness\":1.5,\"decimals\":0,\"unitTitle\":\"MPH\",\"showUnitTitle\":true,\"gaugeColor\":\"#171a1c\",\"gaugeType\":\"arc\"},\"title\":\"Digital speedometer\"}"}',
+'Digital speedometer' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'charts', 'timeseries',
+'{"type":"timeseries","sizeX":8,"sizeY":6,"resources":[{"url":"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.3.0/Chart.min.js"}],"templateHtml":"<canvas id=\"lineChart\"></canvas>\n","templateCss":"","controllerScript":"var chart;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n\n var lineData = {\n labels: [],\n datasets: []\n };\n \n for (var i in data) {\n var dataKey = data[i].dataKey;\n var keySettings = dataKey.settings;\n var backgroundColor = tinycolor(dataKey.color);\n backgroundColor.setAlpha(0.4);\n var dataset = {\n label: dataKey.label,\n data: [],\n borderColor: dataKey.color,\n borderWidth: 2,\n backgroundColor: backgroundColor.toRgbString(),\n pointRadius: keySettings.showPoints ? 1 : 0,\n fill: keySettings.fillLines || false,\n showLine: keySettings.showLines || true,\n spanGaps: false\n }\n lineData.datasets.push(dataset);\n }\n\n var ctx = $(''#lineChart'', containerElement);\n chart = new Chart(ctx, {\n type: ''line'',\n data: lineData,\n options: {\n maintainAspectRatio: false,\n /*animation: {\n duration: 200,\n easing: ''linear''\n },*/\n elements: {\n line: {\n tension: 0.2\n } \n },\n scales: {\n xAxes: [{\n type: ''time'',\n ticks: {\n maxRotation: 20,\n autoSkip: true\n },\n time: {\n displayFormats: {\n second: ''hh:mm:ss'',\n minute: ''hh:mm:ss''\n }\n }\n }]\n }\n }\n });\n\n}\n\nfns.redraw = function(containerElement, width, height, data,\n timeWindow, sizeChanged) {\n\n for (var i = 0; i < data.length; i++) {\n var dataSetData = [];\n var dataKeyData = data[i].data;\n for (var i2 = 0; i2 < dataKeyData.length; i2 ++) {\n dataSetData.push({x: moment(dataKeyData[i2][0]), y: dataKeyData[i2][1]});\n \n }\n chart.data.datasets[i].data = dataSetData; \n }\n\n chart.options.scales.xAxes[0].time.min = moment(timeWindow.minTime);\n chart.options.scales.xAxes[0].time.max = moment(timeWindow.maxTime);\n\n if (sizeChanged) {\n chart.resize();\n }\n \n chart.update(0, true);\n\n};\n\nfns.destroy = function() {\n};","settingsSchema":"{}","dataKeySettingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"showLines\": {\n \"title\": \"Show lines\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"fillLines\": {\n \"title\": \"Fill lines\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showPoints\": {\n \"title\": \"Show points\",\n \"type\": \"boolean\",\n \"default\": false\n }\n },\n \"required\": [\"showLines\", \"fillLines\", \"showPoints\"]\n },\n \"form\": [\n \"showLines\",\n \"fillLines\",\n \"showPoints\"\n ]\n}","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.5644745944820795,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.18379294198604845,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Timeseries - Chart.js\"}"}',
+'Timeseries - Chart.js' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'charts', 'bars',
+'{"type":"latest","sizeX":7,"sizeY":6,"resources":[{"url":"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.3.0/Chart.min.js"}],"templateHtml":"<canvas id=\"barChart\"></canvas>\n","templateCss":"","controllerScript":"var chart;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n \n var barData = {\n labels: [],\n datasets: []\n };\n \n for (var i in datasources) {\n var datasource = datasources[i];\n for (i in datasource.dataKeys) {\n var dataset = {\n label: datasource.dataKeys[i].label,\n data: [0],\n backgroundColor: [datasource.dataKeys[i].color],\n borderColor: [datasource.dataKeys[i].color],\n borderWidth: 1\n }\n barData.datasets.push(dataset);\n }\n }\n\n var ctx = $(''#barChart'', containerElement);\n chart = new Chart(ctx, {\n type: ''bar'',\n data: barData,\n options: {\n maintainAspectRatio: false,\n scales: {\n yAxes: [{\n ticks: {\n beginAtZero:true\n }\n }]\n }\n }\n });\n \n}\n\nfns.redraw = function(containerElement, width, height, data,\n timeWindow, sizeChanged) {\n var c = 0;\n for (var i = 0; i < chart.data.datasets.length; i++) {\n var dataset = chart.data.datasets[i];\n var cellData = data[i]; \n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = tvPair[1];\n dataset.data[0] = parseFloat(value);\n }\n }\n chart.update();\n if (sizeChanged) {\n chart.resize();\n }\n \n};\n","settingsSchema":"{}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Bars - Chart.js\"}"}',
+'Bars - Chart.js' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'charts', 'polar_area_chart_js',
+'{"type":"latest","sizeX":7,"sizeY":6,"resources":[{"url":"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.3.0/Chart.min.js"}],"templateHtml":"<canvas id=\"pieChart\"></canvas>\n","templateCss":"","controllerScript":"var chart;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n\n var pieData = {\n labels: [],\n datasets: []\n };\n\n var dataset = {\n data: [],\n backgroundColor: [],\n borderColor: [],\n borderWidth: [],\n hoverBackgroundColor: []\n }\n \n pieData.datasets.push(dataset);\n \n for (var i in data) {\n var dataKey = data[i].dataKey;\n pieData.labels.push(dataKey.label);\n dataset.data.push(0);\n var hoverBackgroundColor = tinycolor(dataKey.color).lighten(15);\n var borderColor = tinycolor(dataKey.color).darken();\n dataset.backgroundColor.push(dataKey.color);\n dataset.borderColor.push(''#fff'');\n dataset.borderWidth.push(5);\n dataset.hoverBackgroundColor.push(hoverBackgroundColor.toRgbString());\n }\n\n var ctx = $(''#pieChart'', containerElement);\n chart = new Chart(ctx, {\n type: ''polarArea'',\n data: pieData,\n options: {\n maintainAspectRatio: false\n }\n });\n \n}\n\nfns.redraw = function(containerElement, width, height, data,\n timeWindow, sizeChanged) {\n\n for (var i = 0; i < data.length; i++) {\n var cellData = data[i];\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = tvPair[1];\n chart.data.datasets[0].data[i] = parseFloat(value);\n }\n }\n \n chart.update();\n if (sizeChanged) {\n chart.resize();\n }\n \n};\n","settingsSchema":"{}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fifth\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.2074391823443591,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Polar Area - Chart.js\"}"}',
+'Polar Area - Chart.js' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'digital_gauges', 'neon_gauge_justgage',
+'{"type":"latest","sizeX":5,"sizeY":3,"resources":[],"templateHtml":"","templateCss":"#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n","controllerScript":"var gauge;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n gauge = new TbDigitalGauge(containerElement, settings, data); \n\n}\n\n\nfns.redraw = function(containerElement, width, height, data) {\n gauge.redraw(data);\n};\n\nfns.destroy = function() {\n}\n","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"minValue\": {\n \"title\": \"Minimum value\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"maxValue\": {\n \"title\": \"Maximum value\",\n \"type\": \"number\",\n \"default\": 100\n }, \n \"gaugeType\": {\n \"title\": \"Gauge type\",\n \"type\": \"string\",\n \"default\": \"arc\"\n }, \n \"donutStartAngle\": {\n \"title\": \"Angle to start from when in donut mode\",\n \"type\": \"number\",\n \"default\": 90\n }, \n \"neonGlowBrightness\": {\n \"title\": \"Neon glow effect brightness, (0-100), 0 - disable effect\",\n \"type\": \"number\",\n \"default\": 0\n }, \n \"dashThickness\": {\n \"title\": \"Thickness of the stripes, 0 - no stripes\",\n \"type\": \"number\",\n \"default\": 0\n }, \n \"roundedLineCap\": {\n \"title\": \"Display rounded line cap\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"title\": {\n \"title\": \"Gauge title\",\n \"type\": \"string\",\n \"default\": null\n }, \n \"showTitle\": {\n \"title\": \"Show gauge title\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"unitTitle\": {\n \"title\": \"Unit title\",\n \"type\": \"string\",\n \"default\": null\n }, \n \"showUnitTitle\": {\n \"title\": \"Show unit title\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"showValue\": {\n \"title\": \"Show value text\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"showMinMax\": {\n \"title\": \"Show min and max values\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"gaugeWidthScale\": {\n \"title\": \"Width of the gauge element\",\n \"type\": \"number\",\n \"default\": 0.75\n },\n \"defaultColor\": {\n \"title\": \"Default color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"gaugeColor\": {\n \"title\": \"Background color of the gauge element\",\n \"type\": \"string\",\n \"default\": null\n },\n \"levelColors\": {\n \"title\": \"Colors of indicator, from lower to upper\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n }\n },\n \"refreshAnimationType\": {\n \"title\": \"Type of refresh animation\",\n \"type\": \"string\",\n \"default\": \">\"\n },\n \"refreshAnimationTime\": {\n \"title\": \"Duration of refresh animation (ms)\",\n \"type\": \"number\",\n \"default\": 700\n },\n \"startAnimationType\": {\n \"title\": \"Type of start animation\",\n \"type\": \"string\",\n \"default\": \">\"\n },\n \"startAnimationTime\": {\n \"title\": \"Duration of start animation (ms)\",\n \"type\": \"number\",\n \"default\": 700\n },\n \"decimals\": {\n \"title\": \"Number of digits after floating point\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"units\": {\n \"title\": \"Special symbol to show next to value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"titleFont\": {\n \"title\": \"Gauge title font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 12\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"labelFont\": {\n \"title\": \"Font of label showing under value\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 8\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"valueFont\": {\n \"title\": \"Font of label showing current value\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 18\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"minMaxFont\": {\n \"title\": \"Font of minimum and maximum labels\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n }\n }\n },\n \"form\": [\n \"minValue\",\n \"maxValue\",\n {\n \"key\": \"gaugeType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"arc\",\n \"label\": \"Arc\"\n },\n {\n \"value\": \"donut\",\n \"label\": \"Donut\"\n },\n {\n \"value\": \"horizontalBar\",\n \"label\": \"Horizontal bar\"\n },\n {\n \"value\": \"verticalBar\",\n \"label\": \"Vertical bar\"\n }\n ]\n },\n \"donutStartAngle\",\n \"neonGlowBrightness\",\n \"dashThickness\",\n \"roundedLineCap\",\n \"title\",\n \"showTitle\",\n \"unitTitle\",\n \"showUnitTitle\",\n \"showValue\",\n \"showMinMax\",\n \"gaugeWidthScale\",\n {\n \"key\": \"defaultColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"gaugeColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"levelColors\",\n \"items\": [\n {\n \"key\": \"levelColors[]\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"refreshAnimationType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \">\",\n \"label\": \">\"\n },\n {\n \"value\": \"<\",\n \"label\": \"<\"\n },\n {\n \"value\": \"<>\",\n \"label\": \"<>\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n }\n ]\n },\n \"refreshAnimationTime\",\n {\n \"key\": \"startAnimationType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \">\",\n \"label\": \">\"\n },\n {\n \"value\": \"<\",\n \"label\": \"<\"\n },\n {\n \"value\": \"<>\",\n \"label\": \"<>\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n }\n ]\n },\n \"startAnimationTime\",\n \"decimals\",\n \"units\",\n {\n \"key\": \"titleFont\",\n \"items\": [\n \"titleFont.family\",\n \"titleFont.size\",\n {\n \"key\": \"titleFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"titleFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"titleFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"labelFont\",\n \"items\": [\n \"labelFont.family\",\n \"labelFont.size\",\n {\n \"key\": \"labelFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"labelFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"labelFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"valueFont\",\n \"items\": [\n \"valueFont.family\",\n \"valueFont.size\",\n {\n \"key\": \"valueFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"valueFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"valueFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont\",\n \"items\": [\n \"minMaxFont.family\",\n \"minMaxFont.size\",\n {\n \"key\": \"minMaxFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont.color\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n}\n ","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"RobotoDraft\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"RobotoDraft\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":70,\"dashThickness\":1,\"decimals\":1,\"gaugeType\":\"arc\"},\"title\":\"Neon gauge - justGage\"}"}',
+'Neon gauge - justGage' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'charts', 'doughnut_chart_js',
+'{"type":"latest","sizeX":7,"sizeY":6,"resources":[{"url":"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.3.0/Chart.min.js"}],"templateHtml":"<canvas id=\"pieChart\"></canvas>\n","templateCss":"","controllerScript":"var chart;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n\n var pieData = {\n labels: [],\n datasets: []\n };\n\n var dataset = {\n data: [],\n backgroundColor: [],\n borderColor: [],\n borderWidth: [],\n hoverBackgroundColor: []\n }\n \n pieData.datasets.push(dataset);\n \n for (var i in data) {\n var dataKey = data[i].dataKey;\n pieData.labels.push(dataKey.label);\n dataset.data.push(0);\n var hoverBackgroundColor = tinycolor(dataKey.color).lighten(15);\n var borderColor = tinycolor(dataKey.color).darken();\n dataset.backgroundColor.push(dataKey.color);\n dataset.borderColor.push(''#fff'');\n dataset.borderWidth.push(5);\n dataset.hoverBackgroundColor.push(hoverBackgroundColor.toRgbString());\n }\n\n var ctx = $(''#pieChart'', containerElement);\n chart = new Chart(ctx, {\n type: ''doughnut'',\n data: pieData,\n options: {\n maintainAspectRatio: false\n }\n });\n \n}\n\nfns.redraw = function(containerElement, width, height, data,\n timeWindow, sizeChanged) {\n\n for (var i = 0; i < data.length; i++) {\n var cellData = data[i];\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = tvPair[1];\n chart.data.datasets[0].data[i] = parseFloat(value);\n }\n }\n \n chart.update();\n if (sizeChanged) {\n chart.resize();\n }\n \n};\n","settingsSchema":"{}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#26a69a\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#f57c00\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#afb42b\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#673ab7\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Doughnut - Chart.js\"}"}',
+'Doughnut - Chart.js' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'digital_gauges', 'digital_vertical_bar',
+'{"type":"latest","sizeX":2.5,"sizeY":4.5,"resources":[],"templateHtml":"","templateCss":"#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n","controllerScript":"var gauge;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n gauge = new TbDigitalGauge(containerElement, settings, data); \n\n}\n\n\nfns.redraw = function(containerElement, width, height, data) {\n gauge.redraw(data);\n};\n\nfns.destroy = function() {\n}\n","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"minValue\": {\n \"title\": \"Minimum value\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"maxValue\": {\n \"title\": \"Maximum value\",\n \"type\": \"number\",\n \"default\": 100\n }, \n \"gaugeType\": {\n \"title\": \"Gauge type\",\n \"type\": \"string\",\n \"default\": \"arc\"\n }, \n \"donutStartAngle\": {\n \"title\": \"Angle to start from when in donut mode\",\n \"type\": \"number\",\n \"default\": 90\n }, \n \"neonGlowBrightness\": {\n \"title\": \"Neon glow effect brightness, (0-100), 0 - disable effect\",\n \"type\": \"number\",\n \"default\": 0\n }, \n \"dashThickness\": {\n \"title\": \"Thickness of the stripes, 0 - no stripes\",\n \"type\": \"number\",\n \"default\": 0\n }, \n \"roundedLineCap\": {\n \"title\": \"Display rounded line cap\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"title\": {\n \"title\": \"Gauge title\",\n \"type\": \"string\",\n \"default\": null\n }, \n \"showTitle\": {\n \"title\": \"Show gauge title\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"unitTitle\": {\n \"title\": \"Unit title\",\n \"type\": \"string\",\n \"default\": null\n }, \n \"showUnitTitle\": {\n \"title\": \"Show unit title\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"showValue\": {\n \"title\": \"Show value text\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"showMinMax\": {\n \"title\": \"Show min and max values\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"gaugeWidthScale\": {\n \"title\": \"Width of the gauge element\",\n \"type\": \"number\",\n \"default\": 0.75\n },\n \"defaultColor\": {\n \"title\": \"Default color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"gaugeColor\": {\n \"title\": \"Background color of the gauge element\",\n \"type\": \"string\",\n \"default\": null\n },\n \"levelColors\": {\n \"title\": \"Colors of indicator, from lower to upper\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n }\n },\n \"refreshAnimationType\": {\n \"title\": \"Type of refresh animation\",\n \"type\": \"string\",\n \"default\": \">\"\n },\n \"refreshAnimationTime\": {\n \"title\": \"Duration of refresh animation (ms)\",\n \"type\": \"number\",\n \"default\": 700\n },\n \"startAnimationType\": {\n \"title\": \"Type of start animation\",\n \"type\": \"string\",\n \"default\": \">\"\n },\n \"startAnimationTime\": {\n \"title\": \"Duration of start animation (ms)\",\n \"type\": \"number\",\n \"default\": 700\n },\n \"decimals\": {\n \"title\": \"Number of digits after floating point\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"units\": {\n \"title\": \"Special symbol to show next to value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"titleFont\": {\n \"title\": \"Gauge title font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 12\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"labelFont\": {\n \"title\": \"Font of label showing under value\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 8\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"valueFont\": {\n \"title\": \"Font of label showing current value\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 18\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"minMaxFont\": {\n \"title\": \"Font of minimum and maximum labels\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n }\n }\n },\n \"form\": [\n \"minValue\",\n \"maxValue\",\n {\n \"key\": \"gaugeType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"arc\",\n \"label\": \"Arc\"\n },\n {\n \"value\": \"donut\",\n \"label\": \"Donut\"\n },\n {\n \"value\": \"horizontalBar\",\n \"label\": \"Horizontal bar\"\n },\n {\n \"value\": \"verticalBar\",\n \"label\": \"Vertical bar\"\n }\n ]\n },\n \"donutStartAngle\",\n \"neonGlowBrightness\",\n \"dashThickness\",\n \"roundedLineCap\",\n \"title\",\n \"showTitle\",\n \"unitTitle\",\n \"showUnitTitle\",\n \"showValue\",\n \"showMinMax\",\n \"gaugeWidthScale\",\n {\n \"key\": \"defaultColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"gaugeColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"levelColors\",\n \"items\": [\n {\n \"key\": \"levelColors[]\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"refreshAnimationType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \">\",\n \"label\": \">\"\n },\n {\n \"value\": \"<\",\n \"label\": \"<\"\n },\n {\n \"value\": \"<>\",\n \"label\": \"<>\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n }\n ]\n },\n \"refreshAnimationTime\",\n {\n \"key\": \"startAnimationType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \">\",\n \"label\": \">\"\n },\n {\n \"value\": \"<\",\n \"label\": \"<\"\n },\n {\n \"value\": \"<>\",\n \"label\": \"<>\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n }\n ]\n },\n \"startAnimationTime\",\n \"decimals\",\n \"units\",\n {\n \"key\": \"titleFont\",\n \"items\": [\n \"titleFont.family\",\n \"titleFont.size\",\n {\n \"key\": \"titleFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"titleFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"titleFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"labelFont\",\n \"items\": [\n \"labelFont.family\",\n \"labelFont.size\",\n {\n \"key\": \"labelFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"labelFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"labelFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"valueFont\",\n \"items\": [\n \"valueFont.family\",\n \"valueFont.size\",\n {\n \"key\": \"valueFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"valueFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"valueFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont\",\n \"items\": [\n \"minMaxFont.family\",\n \"minMaxFont.size\",\n {\n \"key\": \"minMaxFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont.color\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n}\n ","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":60,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[\"#3d5afe\",\"#f44336\"],\"refreshAnimationType\":\"<>\",\"refreshAnimationTime\":700,\"startAnimationType\":\"<>\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"RobotoDraft\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"RobotoDraft\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":14},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":8,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#cccccc\"},\"neonGlowBrightness\":20,\"decimals\":0,\"showUnitTitle\":true,\"gaugeColor\":\"#171a1c\",\"gaugeType\":\"verticalBar\",\"showTitle\":false,\"units\":\"°C\",\"minValue\":-60,\"dashThickness\":1.2},\"title\":\"Digital vertical bar\"}"}',
+'Digital vertical bar' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'openstreetmap',
+'{"type":"latest","sizeX":8.5,"sizeY":6,"resources":[{"url":"https://unpkg.com/leaflet@1.0.1/dist/leaflet.css"},{"url":"https://unpkg.com/leaflet@1.0.1/dist/leaflet.js"}],"templateHtml":"","templateCss":".tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n","controllerScript":"var map;\nvar positions;\nvar markers = [];\nvar markersSettings = [];\nvar defaultZoomLevel;\nvar dontFitMapBounds;\n\nvar markerCluster;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n \n if (settings.defaultZoomLevel) {\n if (settings.defaultZoomLevel > 0 && settings.defaultZoomLevel < 21) {\n defaultZoomLevel = Math.floor(settings.defaultZoomLevel);\n }\n }\n \n dontFitMapBounds = settings.fitMapBounds === false;\n \n var configuredMarkersSettings = settings.markersSettings;\n if (!configuredMarkersSettings) {\n configuredMarkersSettings = [];\n }\n \n for (var i=0;i<datasources.length;i++) {\n markersSettings[i] = {\n latKeyName: \"lat\",\n lngKeyName: \"lng\",\n showLabel: true,\n label: datasources[i].name,\n color: \"FE7569\"\n };\n if (configuredMarkersSettings[i]) {\n markersSettings[i].latKeyName = configuredMarkersSettings[i].latKeyName || markersSettings[i].latKeyName;\n markersSettings[i].lngKeyName = configuredMarkersSettings[i].lngKeyName || markersSettings[i].lngKeyName;\n markersSettings[i].showLabel = configuredMarkersSettings[i].showLabel !== false;\n markersSettings[i].label = configuredMarkersSettings[i].label || markersSettings[i].label;\n markersSettings[i].color = configuredMarkersSettings[i].color ? tinycolor(configuredMarkersSettings[i].color).toHex() : markersSettings[i].color;\n }\n }\n \n map = L.map(containerElement).setView([0, 0], defaultZoomLevel || 8);\n\n L.tileLayer(''http://{s}.tile.osm.org/{z}/{x}/{y}.png'', {\n attribution: ''© <a href=\"http://osm.org/copyright\">OpenStreetMap</a> contributors''\n }).addTo(map);\n\n\n}\n\n\nfns.redraw = function(containerElement, width, height, data,\n timeWindow, sizeChanged) {\n \n function createMarker(location, settings) {\n var pinColor = settings.color;\n\n var icon = L.icon({\n iconUrl: ''http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|'' + pinColor,\n iconSize: [21, 34],\n iconAnchor: [10, 34],\n popupAnchor: [0, -34],\n shadowUrl: ''http://chart.apis.google.com/chart?chst=d_map_pin_shadow'',\n shadowSize: [40, 37],\n shadowAnchor: [12, 35]\n });\n \n var marker = L.marker(location, {icon: icon}).addTo(map);\n marker.bindPopup(''<b>'' + settings.label + ''</b>'');\n if (settings.showLabel) {\n marker.bindTooltip(''<b>'' + settings.label + ''</b>'', { className: ''tb-marker-label'', permanent: true, direction: ''top'', offset: [0, -24] });\n }\n return marker;\n }\n \n function updatePosition(position, data) {\n if (position.latIndex > -1 && position.lngIndex > -1) {\n var latData = data[position.latIndex].data;\n var lngData = data[position.lngIndex].data;\n if (latData.length > 0 && lngData.length > 0) {\n var lat = latData[latData.length-1][1];\n var lng = lngData[lngData.length-1][1];\n var location = L.latLng(lat, lng);\n if (!position.marker) {\n position.marker = createMarker(location, position.settings);\n markers.push(position.marker);\n return true;\n } else {\n var prevPosition = position.marker.getLatLng();\n if (!prevPosition.equals(location)) {\n position.marker.setLatLng(location);\n return true;\n }\n }\n }\n }\n return false;\n }\n \n function loadPositions(data) {\n var bounds = L.latLngBounds();\n positions = [];\n var datasourceIndex = -1;\n var markerSettings;\n var datasource;\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n if (!datasource || datasource != datasourceData.datasource) {\n datasourceIndex++;\n datasource = datasourceData.datasource;\n markerSettings = markersSettings[datasourceIndex];\n }\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === markerSettings.latKeyName ||\n dataKey.label === markerSettings.lngKeyName) {\n var position = positions[datasourceIndex];\n if (!position) {\n position = {\n latIndex: -1,\n lngIndex: -1,\n settings: markerSettings\n };\n positions[datasourceIndex] = position;\n } else if (position.marker) {\n continue;\n }\n if (dataKey.label === markerSettings.latKeyName) {\n position.latIndex = i;\n } else {\n position.lngIndex = i;\n }\n if (position.latIndex > -1 && position.lngIndex > -1) {\n updatePosition(position, data);\n if (position.marker) {\n bounds.extend(position.marker.getLatLng());\n }\n }\n }\n }\n fitMapBounds(bounds);\n }\n \n function updatePositions(data) {\n var positionsChanged = false;\n var bounds = L.latLngBounds();\n for (var p in positions) {\n var position = positions[p];\n positionsChanged |= updatePosition(position, data);\n if (position.marker) {\n bounds.extend(position.marker.getLatLng());\n }\n }\n if (!dontFitMapBounds && positionsChanged) {\n fitMapBounds(bounds);\n }\n }\n \n function fitMapBounds(bounds) {\n map.once(''zoomend'', function(event) {\n var zoomLevel = defaultZoomLevel || map.getZoom();\n map.setZoom(zoomLevel, {animate: false});\n if (!defaultZoomLevel && this.getZoom() > 15) {\n map.setZoom(15, {animate: false});\n }\n });\n map.fitBounds(bounds, {padding: [50, 50], animate: false});\n }\n \n if (map) {\n if (data) {\n if (!positions) {\n loadPositions(data);\n } else {\n updatePositions(data);\n }\n }\n if (sizeChanged) {\n map.invalidateSize(true);\n var bounds = L.latLngBounds();\n for (var m in markers) {\n bounds.extend(markers[m].getLatLng());\n }\n fitMapBounds(bounds);\n }\n }\n\n};","settingsSchema":"{\n \"schema\": {\n \"title\": \"Google Map Configuration\",\n \"type\": \"object\",\n \"properties\": {\n \"defaultZoomLevel\": {\n \"title\": \"Default map zoom level (1 - 20)\",\n \"type\": \"number\"\n },\n \"fitMapBounds\": {\n \"title\": \"Fit map bounds to cover all markers\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"markersSettings\": {\n \"title\": \"Markers settings, same order as datasources\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Marker settings\",\n \"type\": \"object\",\n \"properties\": {\n \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"lat\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"lng\"\n }, \n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n }\n }\n }\n }\n },\n \"required\": [\n ]\n },\n \"form\": [\n \"defaultZoomLevel\",\n \"fitMapBounds\",\n {\n \"key\": \"markersSettings\",\n \"items\": [\n \"markersSettings[].latKeyName\",\n \"markersSettings[].lngKeyName\",\n \"markersSettings[].showLabel\",\n \"markersSettings[].label\",\n {\n \"key\": \"markersSettings[].color\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"lat\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"lng\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"markersSettings\":[{\"label\":\"First point\",\"color\":\"#1e88e5\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true},{\"label\":\"Second point\",\"color\":\"#fdd835\",\"latKeyName\":\"lat\",\"lngKeyName\":\"lng\",\"showLabel\":true}],\"fitMapBounds\":true},\"title\":\"OpenStreetMap\"}"}',
+'OpenStreetMap' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'analogue_gauges', 'temperature_radial_gauge_canvas_gauges',
+'{"type":"latest","sizeX":6,"sizeY":5,"resources":[],"templateHtml":"<canvas id=\"radialGauge\"></canvas>\n","templateCss":"","controllerScript":"var gauge;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n gauge = new TbAnalogueRadialGauge(containerElement, settings, data, ''radialGauge''); \n\n}\n\n\nfns.redraw = function(containerElement, width, height, data, timeWindow, sizeChanged) {\n gauge.redraw(width, height, data, sizeChanged);\n};\n\nfns.destroy = function() {\n}\n","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"minValue\": {\n \"title\": \"Minimum value\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"maxValue\": {\n \"title\": \"Maximum value\",\n \"type\": \"number\",\n \"default\": 100\n },\n \"unitTitle\": {\n \"title\": \"Unit title\",\n \"type\": \"string\",\n \"default\": null\n },\n \"showUnitTitle\": {\n \"title\": \"Show unit title\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"units\": {\n \"title\": \"Units\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"majorTicksCount\": {\n \"title\": \"Major ticks count\",\n \"type\": \"number\",\n \"default\": null\n },\n \"minorTicks\": {\n \"title\": \"Minor ticks count\",\n \"type\": \"number\",\n \"default\": 2\n },\n \"valueBox\": {\n \"title\": \"Show value box\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"valueInt\": {\n \"title\": \"Digits count for integer part of value\",\n \"type\": \"number\",\n \"default\": 3\n },\n \"valueDec\": {\n \"title\": \"Digits count for decimal part of value\",\n \"type\": \"number\",\n \"default\": 2\n },\n \"defaultColor\": {\n \"title\": \"Default color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"colorPlate\": {\n \"title\": \"Plate color\",\n \"type\": \"string\",\n \"default\": \"#fff\"\n },\n \"colorMajorTicks\": {\n \"title\": \"Major ticks color\",\n \"type\": \"string\",\n \"default\": \"#444\"\n },\n \"colorMinorTicks\": {\n \"title\": \"Minor ticks color\",\n \"type\": \"string\",\n \"default\": \"#666\"\n },\n \"colorNeedle\": {\n \"title\": \"Needle color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"colorNeedleEnd\": {\n \"title\": \"Needle color - end gradient\",\n \"type\": \"string\",\n \"default\": null\n },\n \"colorNeedleShadowUp\": {\n \"title\": \"Upper half of the needle shadow color\",\n \"type\": \"string\",\n \"default\": \"rgba(2,255,255,0.2)\"\n },\n \"colorNeedleShadowDown\": {\n \"title\": \"Drop shadow needle color.\",\n \"type\": \"string\",\n \"default\": \"rgba(188,143,143,0.45)\"\n },\n \"colorValueBoxRect\": {\n \"title\": \"Value box rectangle stroke color\",\n \"type\": \"string\",\n \"default\": \"#888\"\n },\n \"colorValueBoxRectEnd\": {\n \"title\": \"Value box rectangle stroke color - end gradient\",\n \"type\": \"string\",\n \"default\": \"#666\"\n },\n \"colorValueBoxBackground\": {\n \"title\": \"Value box background color\",\n \"type\": \"string\",\n \"default\": \"#babab2\"\n },\n \"colorValueBoxShadow\": {\n \"title\": \"Value box shadow color\",\n \"type\": \"string\",\n \"default\": \"rgba(0,0,0,1)\"\n },\n \"highlights\": {\n \"title\": \"Highlights\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Highlight\",\n \"type\": \"object\",\n \"properties\": {\n \"from\": {\n \"title\": \"From\",\n \"type\": \"number\"\n },\n \"to\": {\n \"title\": \"To\",\n \"type\": \"number\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n }\n }\n }\n },\n \"highlightsWidth\": {\n \"title\": \"Highlights width\",\n \"type\": \"number\",\n \"default\": 15\n },\n \"showBorder\": {\n \"title\": \"Show border\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"numbersFont\": {\n \"title\": \"Tick numbers font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 18\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"titleFont\": {\n \"title\": \"Title text font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 24\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": \"#888\"\n }\n }\n },\n \"unitsFont\": {\n \"title\": \"Units text font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 22\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": \"#888\"\n }\n }\n },\n \"valueFont\": {\n \"title\": \"Value text font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 40\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": \"#444\"\n },\n \"shadowColor\": {\n \"title\": \"Shadow color\",\n \"type\": \"string\",\n \"default\": \"rgba(0,0,0,0.3)\"\n }\n }\n },\n \"animation\": {\n \"title\": \"Enable animation\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"animationDuration\": {\n \"title\": \"Animation duration\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"animationRule\": {\n \"title\": \"Animation rule\",\n \"type\": \"string\",\n \"default\": \"cycle\"\n },\n \"startAngle\": {\n \"title\": \"Start ticks angle\",\n \"type\": \"number\",\n \"default\": 45\n },\n \"ticksAngle\": {\n \"title\": \"Ticks angle\",\n \"type\": \"number\",\n \"default\": 270\n },\n \"needleCircleSize\": {\n \"title\": \"Needle circle size\",\n \"type\": \"number\",\n \"default\": 10\n }\n },\n \"required\": []\n },\n \"form\": [\n \"startAngle\",\n \"ticksAngle\",\n \"needleCircleSize\",\n \"minValue\",\n \"maxValue\",\n \"unitTitle\",\n \"showUnitTitle\",\n \"units\",\n \"majorTicksCount\",\n \"minorTicks\",\n \"valueBox\",\n \"valueInt\",\n \"valueDec\",\n {\n \"key\": \"defaultColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorPlate\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorMajorTicks\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorMinorTicks\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorNeedle\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorNeedleEnd\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorNeedleShadowUp\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorNeedleShadowDown\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorValueBoxRect\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorValueBoxRectEnd\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorValueBoxBackground\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorValueBoxShadow\",\n \"type\": \"color\"\n },\n {\n \"key\": \"highlights\",\n \"items\": [\n \"highlights[].from\",\n \"highlights[].to\",\n {\n \"key\": \"highlights[].color\",\n \"type\": \"color\"\n }\n ]\n },\n \"highlightsWidth\",\n \"showBorder\",\n {\n \"key\": \"numbersFont\",\n \"items\": [\n \"numbersFont.family\",\n \"numbersFont.size\",\n {\n \"key\": \"numbersFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"numbersFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"numbersFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"titleFont\",\n \"items\": [\n \"titleFont.family\",\n \"titleFont.size\",\n {\n \"key\": \"titleFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"titleFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"titleFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"unitsFont\",\n \"items\": [\n \"unitsFont.family\",\n \"unitsFont.size\",\n {\n \"key\": \"unitsFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"unitsFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"unitsFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"valueFont\",\n \"items\": [\n \"valueFont.family\",\n \"valueFont.size\",\n {\n \"key\": \"valueFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"valueFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"valueFont.color\",\n \"type\": \"color\"\n },\n {\n \"key\": \"valueFont.shadowColor\",\n \"type\": \"color\"\n }\n ]\n }, \n \"animation\",\n \"animationDuration\",\n {\n \"key\": \"animationRule\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \"quad\",\n \"label\": \"Quad\"\n },\n {\n \"value\": \"quint\",\n \"label\": \"Quint\"\n },\n {\n \"value\": \"cycle\",\n \"label\": \"Cycle\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n },\n {\n \"value\": \"elastic\",\n \"label\": \"Elastic\"\n },\n {\n \"value\": \"dequad\",\n \"label\": \"Dequad\"\n },\n {\n \"value\": \"dequint\",\n \"label\": \"Dequint\"\n },\n {\n \"value\": \"decycle\",\n \"label\": \"Decycle\"\n },\n {\n \"value\": \"debounce\",\n \"label\": \"Debounce\"\n },\n {\n \"value\": \"delastic\",\n \"label\": \"Delastic\"\n }\n ]\n }\n ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":60,\"startAngle\":67.5,\"ticksAngle\":225,\"showBorder\":true,\"defaultColor\":\"#e65100\",\"needleCircleSize\":7,\"highlights\":[{\"from\":-60,\"to\":-50,\"color\":\"#42a5f5\"},{\"from\":-50,\"to\":-40,\"color\":\"rgba(66, 165, 245, 0.83)\"},{\"from\":-40,\"to\":-30,\"color\":\"rgba(66, 165, 245, 0.66)\"},{\"from\":-30,\"to\":-20,\"color\":\"rgba(66, 165, 245, 0.5)\"},{\"from\":-20,\"to\":-10,\"color\":\"rgba(66, 165, 245, 0.33)\"},{\"from\":-10,\"to\":0,\"color\":\"rgba(66, 165, 245, 0.16)\"},{\"from\":0,\"to\":10,\"color\":\"rgba(229, 115, 115, 0.16)\"},{\"from\":10,\"to\":20,\"color\":\"rgba(229, 115, 115, 0.33)\"},{\"from\":20,\"to\":30,\"color\":\"rgba(229, 115, 115, 0.5)\"},{\"from\":30,\"to\":40,\"color\":\"rgba(229, 115, 115, 0.66)\"},{\"from\":40,\"to\":50,\"color\":\"rgba(229, 115, 115, 0.83)\"},{\"from\":50,\"to\":60,\"color\":\"#e57373\"}],\"showUnitTitle\":true,\"colorPlate\":\"#cfd8dc\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":2,\"valueInt\":3,\"valueDec\":1,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":1000,\"animationRule\":\"bounce\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188, 143, 143, 0.78)\",\"units\":\"°C\",\"majorTicksCount\":12,\"numbersFont\":{\"family\":\"RobotoDraft\",\"size\":20,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"titleFont\":{\"family\":\"RobotoDraft\",\"size\":24,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"unitsFont\":{\"family\":\"RobotoDraft\",\"size\":28,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"size\":30,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\"},\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\",\"unitTitle\":\"Temperature\",\"minValue\":-60},\"title\":\"Temperature radial gauge - Canvas Gauges\"}"}',
+'Temperature radial gauge - Canvas Gauges' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'digital_gauges', 'vertical_bar_justgage',
+'{"type":"latest","sizeX":2,"sizeY":3.5,"resources":[],"templateHtml":"","templateCss":"#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n","controllerScript":"var gauge;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n gauge = new TbDigitalGauge(containerElement, settings, data); \n\n}\n\n\nfns.redraw = function(containerElement, width, height, data) {\n gauge.redraw(data);\n};\n\nfns.destroy = function() {\n}\n","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"minValue\": {\n \"title\": \"Minimum value\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"maxValue\": {\n \"title\": \"Maximum value\",\n \"type\": \"number\",\n \"default\": 100\n }, \n \"gaugeType\": {\n \"title\": \"Gauge type\",\n \"type\": \"string\",\n \"default\": \"arc\"\n }, \n \"donutStartAngle\": {\n \"title\": \"Angle to start from when in donut mode\",\n \"type\": \"number\",\n \"default\": 90\n }, \n \"neonGlowBrightness\": {\n \"title\": \"Neon glow effect brightness, (0-100), 0 - disable effect\",\n \"type\": \"number\",\n \"default\": 0\n }, \n \"dashThickness\": {\n \"title\": \"Thickness of the stripes, 0 - no stripes\",\n \"type\": \"number\",\n \"default\": 0\n }, \n \"roundedLineCap\": {\n \"title\": \"Display rounded line cap\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"title\": {\n \"title\": \"Gauge title\",\n \"type\": \"string\",\n \"default\": null\n }, \n \"showTitle\": {\n \"title\": \"Show gauge title\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"unitTitle\": {\n \"title\": \"Unit title\",\n \"type\": \"string\",\n \"default\": null\n }, \n \"showUnitTitle\": {\n \"title\": \"Show unit title\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"showValue\": {\n \"title\": \"Show value text\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"showMinMax\": {\n \"title\": \"Show min and max values\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"gaugeWidthScale\": {\n \"title\": \"Width of the gauge element\",\n \"type\": \"number\",\n \"default\": 0.75\n },\n \"defaultColor\": {\n \"title\": \"Default color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"gaugeColor\": {\n \"title\": \"Background color of the gauge element\",\n \"type\": \"string\",\n \"default\": null\n },\n \"levelColors\": {\n \"title\": \"Colors of indicator, from lower to upper\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n }\n },\n \"refreshAnimationType\": {\n \"title\": \"Type of refresh animation\",\n \"type\": \"string\",\n \"default\": \">\"\n },\n \"refreshAnimationTime\": {\n \"title\": \"Duration of refresh animation (ms)\",\n \"type\": \"number\",\n \"default\": 700\n },\n \"startAnimationType\": {\n \"title\": \"Type of start animation\",\n \"type\": \"string\",\n \"default\": \">\"\n },\n \"startAnimationTime\": {\n \"title\": \"Duration of start animation (ms)\",\n \"type\": \"number\",\n \"default\": 700\n },\n \"decimals\": {\n \"title\": \"Number of digits after floating point\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"units\": {\n \"title\": \"Special symbol to show next to value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"titleFont\": {\n \"title\": \"Gauge title font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 12\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"labelFont\": {\n \"title\": \"Font of label showing under value\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 8\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"valueFont\": {\n \"title\": \"Font of label showing current value\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 18\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"minMaxFont\": {\n \"title\": \"Font of minimum and maximum labels\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n }\n }\n },\n \"form\": [\n \"minValue\",\n \"maxValue\",\n {\n \"key\": \"gaugeType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"arc\",\n \"label\": \"Arc\"\n },\n {\n \"value\": \"donut\",\n \"label\": \"Donut\"\n },\n {\n \"value\": \"horizontalBar\",\n \"label\": \"Horizontal bar\"\n },\n {\n \"value\": \"verticalBar\",\n \"label\": \"Vertical bar\"\n }\n ]\n },\n \"donutStartAngle\",\n \"neonGlowBrightness\",\n \"dashThickness\",\n \"roundedLineCap\",\n \"title\",\n \"showTitle\",\n \"unitTitle\",\n \"showUnitTitle\",\n \"showValue\",\n \"showMinMax\",\n \"gaugeWidthScale\",\n {\n \"key\": \"defaultColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"gaugeColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"levelColors\",\n \"items\": [\n {\n \"key\": \"levelColors[]\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"refreshAnimationType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \">\",\n \"label\": \">\"\n },\n {\n \"value\": \"<\",\n \"label\": \"<\"\n },\n {\n \"value\": \"<>\",\n \"label\": \"<>\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n }\n ]\n },\n \"refreshAnimationTime\",\n {\n \"key\": \"startAnimationType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \">\",\n \"label\": \">\"\n },\n {\n \"value\": \"<\",\n \"label\": \"<\"\n },\n {\n \"value\": \"<>\",\n \"label\": \"<>\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n }\n ]\n },\n \"startAnimationTime\",\n \"decimals\",\n \"units\",\n {\n \"key\": \"titleFont\",\n \"items\": [\n \"titleFont.family\",\n \"titleFont.size\",\n {\n \"key\": \"titleFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"titleFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"titleFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"labelFont\",\n \"items\": [\n \"labelFont.family\",\n \"labelFont.size\",\n {\n \"key\": \"labelFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"labelFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"labelFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"valueFont\",\n \"items\": [\n \"valueFont.family\",\n \"valueFont.size\",\n {\n \"key\": \"valueFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"valueFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"valueFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont\",\n \"items\": [\n \"minMaxFont.family\",\n \"minMaxFont.size\",\n {\n \"key\": \"minMaxFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont.color\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n}\n ","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#f57c00\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"RobotoDraft\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#999999\"},\"labelFont\":{\"family\":\"RobotoDraft\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"RobotoDraft\",\"style\":\"normal\",\"weight\":\"500\",\"size\":12,\"color\":\"#666666\"},\"minMaxFont\":{\"family\":\"RobotoDraft\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#666666\"},\"neonGlowBrightness\":0,\"decimals\":0,\"dashThickness\":1.5,\"gaugeColor\":\"#eeeeee\",\"showTitle\":false,\"gaugeType\":\"verticalBar\"},\"title\":\"Vertical bar - justGage\"}"}',
+'Vertical bar - justGage' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'digital_gauges', 'gauge_justgage',
+'{"type":"latest","sizeX":4,"sizeY":3,"resources":[],"templateHtml":"","templateCss":"#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n","controllerScript":"var gauge;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n gauge = new TbDigitalGauge(containerElement, settings, data); \n\n}\n\n\nfns.redraw = function(containerElement, width, height, data) {\n gauge.redraw(data);\n};\n\nfns.destroy = function() {\n}\n","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"minValue\": {\n \"title\": \"Minimum value\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"maxValue\": {\n \"title\": \"Maximum value\",\n \"type\": \"number\",\n \"default\": 100\n }, \n \"gaugeType\": {\n \"title\": \"Gauge type\",\n \"type\": \"string\",\n \"default\": \"arc\"\n }, \n \"donutStartAngle\": {\n \"title\": \"Angle to start from when in donut mode\",\n \"type\": \"number\",\n \"default\": 90\n }, \n \"neonGlowBrightness\": {\n \"title\": \"Neon glow effect brightness, (0-100), 0 - disable effect\",\n \"type\": \"number\",\n \"default\": 0\n }, \n \"dashThickness\": {\n \"title\": \"Thickness of the stripes, 0 - no stripes\",\n \"type\": \"number\",\n \"default\": 0\n }, \n \"roundedLineCap\": {\n \"title\": \"Display rounded line cap\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"title\": {\n \"title\": \"Gauge title\",\n \"type\": \"string\",\n \"default\": null\n }, \n \"showTitle\": {\n \"title\": \"Show gauge title\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"unitTitle\": {\n \"title\": \"Unit title\",\n \"type\": \"string\",\n \"default\": null\n }, \n \"showUnitTitle\": {\n \"title\": \"Show unit title\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"showValue\": {\n \"title\": \"Show value text\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"showMinMax\": {\n \"title\": \"Show min and max values\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"gaugeWidthScale\": {\n \"title\": \"Width of the gauge element\",\n \"type\": \"number\",\n \"default\": 0.75\n },\n \"defaultColor\": {\n \"title\": \"Default color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"gaugeColor\": {\n \"title\": \"Background color of the gauge element\",\n \"type\": \"string\",\n \"default\": null\n },\n \"levelColors\": {\n \"title\": \"Colors of indicator, from lower to upper\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n }\n },\n \"refreshAnimationType\": {\n \"title\": \"Type of refresh animation\",\n \"type\": \"string\",\n \"default\": \">\"\n },\n \"refreshAnimationTime\": {\n \"title\": \"Duration of refresh animation (ms)\",\n \"type\": \"number\",\n \"default\": 700\n },\n \"startAnimationType\": {\n \"title\": \"Type of start animation\",\n \"type\": \"string\",\n \"default\": \">\"\n },\n \"startAnimationTime\": {\n \"title\": \"Duration of start animation (ms)\",\n \"type\": \"number\",\n \"default\": 700\n },\n \"decimals\": {\n \"title\": \"Number of digits after floating point\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"units\": {\n \"title\": \"Special symbol to show next to value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"titleFont\": {\n \"title\": \"Gauge title font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 12\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"labelFont\": {\n \"title\": \"Font of label showing under value\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 8\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"valueFont\": {\n \"title\": \"Font of label showing current value\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 18\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"minMaxFont\": {\n \"title\": \"Font of minimum and maximum labels\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n }\n }\n },\n \"form\": [\n \"minValue\",\n \"maxValue\",\n {\n \"key\": \"gaugeType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"arc\",\n \"label\": \"Arc\"\n },\n {\n \"value\": \"donut\",\n \"label\": \"Donut\"\n },\n {\n \"value\": \"horizontalBar\",\n \"label\": \"Horizontal bar\"\n },\n {\n \"value\": \"verticalBar\",\n \"label\": \"Vertical bar\"\n }\n ]\n },\n \"donutStartAngle\",\n \"neonGlowBrightness\",\n \"dashThickness\",\n \"roundedLineCap\",\n \"title\",\n \"showTitle\",\n \"unitTitle\",\n \"showUnitTitle\",\n \"showValue\",\n \"showMinMax\",\n \"gaugeWidthScale\",\n {\n \"key\": \"defaultColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"gaugeColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"levelColors\",\n \"items\": [\n {\n \"key\": \"levelColors[]\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"refreshAnimationType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \">\",\n \"label\": \">\"\n },\n {\n \"value\": \"<\",\n \"label\": \"<\"\n },\n {\n \"value\": \"<>\",\n \"label\": \"<>\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n }\n ]\n },\n \"refreshAnimationTime\",\n {\n \"key\": \"startAnimationType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \">\",\n \"label\": \">\"\n },\n {\n \"value\": \"<\",\n \"label\": \"<\"\n },\n {\n \"value\": \"<>\",\n \"label\": \"<>\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n }\n ]\n },\n \"startAnimationTime\",\n \"decimals\",\n \"units\",\n {\n \"key\": \"titleFont\",\n \"items\": [\n \"titleFont.family\",\n \"titleFont.size\",\n {\n \"key\": \"titleFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"titleFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"titleFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"labelFont\",\n \"items\": [\n \"labelFont.family\",\n \"labelFont.size\",\n {\n \"key\": \"labelFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"labelFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"labelFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"valueFont\",\n \"items\": [\n \"valueFont.family\",\n \"valueFont.size\",\n {\n \"key\": \"valueFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"valueFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"valueFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont\",\n \"items\": [\n \"minMaxFont.family\",\n \"minMaxFont.size\",\n {\n \"key\": \"minMaxFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont.color\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n}\n ","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"RobotoDraft\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#999999\"},\"labelFont\":{\"family\":\"RobotoDraft\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"RobotoDraft\",\"style\":\"normal\",\"weight\":\"500\",\"size\":36,\"color\":\"#666666\"},\"minMaxFont\":{\"family\":\"RobotoDraft\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#666666\"},\"neonGlowBrightness\":0,\"decimals\":0,\"dashThickness\":0,\"gaugeColor\":\"#eeeeee\",\"showTitle\":true,\"gaugeType\":\"arc\"},\"title\":\"Gauge - justGage\"}"}',
+'Gauge - justGage' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'digital_gauges', 'digital_thermometer',
+'{"type":"latest","sizeX":3,"sizeY":3,"resources":[],"templateHtml":"","templateCss":"#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n","controllerScript":"var gauge;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n gauge = new TbDigitalGauge(containerElement, settings, data); \n\n}\n\n\nfns.redraw = function(containerElement, width, height, data) {\n gauge.redraw(data);\n};\n\nfns.destroy = function() {\n}\n","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"minValue\": {\n \"title\": \"Minimum value\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"maxValue\": {\n \"title\": \"Maximum value\",\n \"type\": \"number\",\n \"default\": 100\n }, \n \"gaugeType\": {\n \"title\": \"Gauge type\",\n \"type\": \"string\",\n \"default\": \"arc\"\n }, \n \"donutStartAngle\": {\n \"title\": \"Angle to start from when in donut mode\",\n \"type\": \"number\",\n \"default\": 90\n }, \n \"neonGlowBrightness\": {\n \"title\": \"Neon glow effect brightness, (0-100), 0 - disable effect\",\n \"type\": \"number\",\n \"default\": 0\n }, \n \"dashThickness\": {\n \"title\": \"Thickness of the stripes, 0 - no stripes\",\n \"type\": \"number\",\n \"default\": 0\n }, \n \"roundedLineCap\": {\n \"title\": \"Display rounded line cap\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"title\": {\n \"title\": \"Gauge title\",\n \"type\": \"string\",\n \"default\": null\n }, \n \"showTitle\": {\n \"title\": \"Show gauge title\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"unitTitle\": {\n \"title\": \"Unit title\",\n \"type\": \"string\",\n \"default\": null\n }, \n \"showUnitTitle\": {\n \"title\": \"Show unit title\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"showValue\": {\n \"title\": \"Show value text\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"showMinMax\": {\n \"title\": \"Show min and max values\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"gaugeWidthScale\": {\n \"title\": \"Width of the gauge element\",\n \"type\": \"number\",\n \"default\": 0.75\n },\n \"defaultColor\": {\n \"title\": \"Default color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"gaugeColor\": {\n \"title\": \"Background color of the gauge element\",\n \"type\": \"string\",\n \"default\": null\n },\n \"levelColors\": {\n \"title\": \"Colors of indicator, from lower to upper\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n }\n },\n \"refreshAnimationType\": {\n \"title\": \"Type of refresh animation\",\n \"type\": \"string\",\n \"default\": \">\"\n },\n \"refreshAnimationTime\": {\n \"title\": \"Duration of refresh animation (ms)\",\n \"type\": \"number\",\n \"default\": 700\n },\n \"startAnimationType\": {\n \"title\": \"Type of start animation\",\n \"type\": \"string\",\n \"default\": \">\"\n },\n \"startAnimationTime\": {\n \"title\": \"Duration of start animation (ms)\",\n \"type\": \"number\",\n \"default\": 700\n },\n \"decimals\": {\n \"title\": \"Number of digits after floating point\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"units\": {\n \"title\": \"Special symbol to show next to value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"titleFont\": {\n \"title\": \"Gauge title font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 12\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"labelFont\": {\n \"title\": \"Font of label showing under value\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 8\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"valueFont\": {\n \"title\": \"Font of label showing current value\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 18\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"minMaxFont\": {\n \"title\": \"Font of minimum and maximum labels\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n }\n }\n },\n \"form\": [\n \"minValue\",\n \"maxValue\",\n {\n \"key\": \"gaugeType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"arc\",\n \"label\": \"Arc\"\n },\n {\n \"value\": \"donut\",\n \"label\": \"Donut\"\n },\n {\n \"value\": \"horizontalBar\",\n \"label\": \"Horizontal bar\"\n },\n {\n \"value\": \"verticalBar\",\n \"label\": \"Vertical bar\"\n }\n ]\n },\n \"donutStartAngle\",\n \"neonGlowBrightness\",\n \"dashThickness\",\n \"roundedLineCap\",\n \"title\",\n \"showTitle\",\n \"unitTitle\",\n \"showUnitTitle\",\n \"showValue\",\n \"showMinMax\",\n \"gaugeWidthScale\",\n {\n \"key\": \"defaultColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"gaugeColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"levelColors\",\n \"items\": [\n {\n \"key\": \"levelColors[]\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"refreshAnimationType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \">\",\n \"label\": \">\"\n },\n {\n \"value\": \"<\",\n \"label\": \"<\"\n },\n {\n \"value\": \"<>\",\n \"label\": \"<>\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n }\n ]\n },\n \"refreshAnimationTime\",\n {\n \"key\": \"startAnimationType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \">\",\n \"label\": \">\"\n },\n {\n \"value\": \"<\",\n \"label\": \"<\"\n },\n {\n \"value\": \"<>\",\n \"label\": \"<>\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n }\n ]\n },\n \"startAnimationTime\",\n \"decimals\",\n \"units\",\n {\n \"key\": \"titleFont\",\n \"items\": [\n \"titleFont.family\",\n \"titleFont.size\",\n {\n \"key\": \"titleFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"titleFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"titleFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"labelFont\",\n \"items\": [\n \"labelFont.family\",\n \"labelFont.size\",\n {\n \"key\": \"labelFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"labelFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"labelFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"valueFont\",\n \"items\": [\n \"valueFont.family\",\n \"valueFont.size\",\n {\n \"key\": \"valueFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"valueFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"valueFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont\",\n \"items\": [\n \"minMaxFont.family\",\n \"minMaxFont.size\",\n {\n \"key\": \"minMaxFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont.color\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n}\n ","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < -60) {\\n\\tvalue = 60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":60,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":1,\"levelColors\":[\"#304ffe\",\"#7e57c2\",\"#ff4081\",\"#d32f2f\"],\"refreshAnimationType\":\"<>\",\"refreshAnimationTime\":700,\"startAnimationType\":\"<>\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"RobotoDraft\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"RobotoDraft\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":18},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"dashThickness\":1.5,\"decimals\":0,\"minValue\":-60,\"units\":\"°C\",\"gaugeColor\":\"#333333\",\"neonGlowBrightness\":35,\"gaugeType\":\"donut\"},\"title\":\"Digital thermometer\"}"}',
+'Digital thermometer' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'analogue_gauges', 'linear_gauge_canvas_gauges',
+'{"type":"latest","sizeX":7,"sizeY":3,"resources":[],"templateHtml":"<canvas id=\"linearGauge\"></canvas>\n","templateCss":"","controllerScript":"var gauge;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n gauge = new TbAnalogueLinearGauge(containerElement, settings, data, ''linearGauge''); \n\n}\n\n\nfns.redraw = function(containerElement, width, height, data, timeWindow, sizeChanged) {\n gauge.redraw(width, height, data, sizeChanged);\n};\n\nfns.destroy = function() {\n}\n","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"minValue\": {\n \"title\": \"Minimum value\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"maxValue\": {\n \"title\": \"Maximum value\",\n \"type\": \"number\",\n \"default\": 100\n },\n \"unitTitle\": {\n \"title\": \"Unit title\",\n \"type\": \"string\",\n \"default\": null\n },\n \"showUnitTitle\": {\n \"title\": \"Show unit title\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"units\": {\n \"title\": \"Units\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"majorTicksCount\": {\n \"title\": \"Major ticks count\",\n \"type\": \"number\",\n \"default\": null\n },\n \"minorTicks\": {\n \"title\": \"Minor ticks count\",\n \"type\": \"number\",\n \"default\": 2\n },\n \"valueBox\": {\n \"title\": \"Show value box\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"valueInt\": {\n \"title\": \"Digits count for integer part of value\",\n \"type\": \"number\",\n \"default\": 3\n },\n \"valueDec\": {\n \"title\": \"Digits count for decimal part of value\",\n \"type\": \"number\",\n \"default\": 2\n },\n \"defaultColor\": {\n \"title\": \"Default color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"colorPlate\": {\n \"title\": \"Plate color\",\n \"type\": \"string\",\n \"default\": \"#fff\"\n },\n \"colorMajorTicks\": {\n \"title\": \"Major ticks color\",\n \"type\": \"string\",\n \"default\": \"#444\"\n },\n \"colorMinorTicks\": {\n \"title\": \"Minor ticks color\",\n \"type\": \"string\",\n \"default\": \"#666\"\n },\n \"colorNeedle\": {\n \"title\": \"Needle color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"colorNeedleEnd\": {\n \"title\": \"Needle color - end gradient\",\n \"type\": \"string\",\n \"default\": null\n },\n \"colorNeedleShadowUp\": {\n \"title\": \"Upper half of the needle shadow color\",\n \"type\": \"string\",\n \"default\": \"rgba(2,255,255,0.2)\"\n },\n \"colorNeedleShadowDown\": {\n \"title\": \"Drop shadow needle color.\",\n \"type\": \"string\",\n \"default\": \"rgba(188,143,143,0.45)\"\n },\n \"colorValueBoxRect\": {\n \"title\": \"Value box rectangle stroke color\",\n \"type\": \"string\",\n \"default\": \"#888\"\n },\n \"colorValueBoxRectEnd\": {\n \"title\": \"Value box rectangle stroke color - end gradient\",\n \"type\": \"string\",\n \"default\": \"#666\"\n },\n \"colorValueBoxBackground\": {\n \"title\": \"Value box background color\",\n \"type\": \"string\",\n \"default\": \"#babab2\"\n },\n \"colorValueBoxShadow\": {\n \"title\": \"Value box shadow color\",\n \"type\": \"string\",\n \"default\": \"rgba(0,0,0,1)\"\n },\n \"highlights\": {\n \"title\": \"Highlights\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Highlight\",\n \"type\": \"object\",\n \"properties\": {\n \"from\": {\n \"title\": \"From\",\n \"type\": \"number\"\n },\n \"to\": {\n \"title\": \"To\",\n \"type\": \"number\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n }\n }\n }\n },\n \"highlightsWidth\": {\n \"title\": \"Highlights width\",\n \"type\": \"number\",\n \"default\": 15\n },\n \"showBorder\": {\n \"title\": \"Show border\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"numbersFont\": {\n \"title\": \"Tick numbers font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 18\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"titleFont\": {\n \"title\": \"Title text font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 24\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": \"#888\"\n }\n }\n },\n \"unitsFont\": {\n \"title\": \"Units text font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 22\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": \"#888\"\n }\n }\n },\n \"valueFont\": {\n \"title\": \"Value text font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 40\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": \"#444\"\n },\n \"shadowColor\": {\n \"title\": \"Shadow color\",\n \"type\": \"string\",\n \"default\": \"rgba(0,0,0,0.3)\"\n }\n }\n },\n \"animation\": {\n \"title\": \"Enable animation\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"animationDuration\": {\n \"title\": \"Animation duration\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"animationRule\": {\n \"title\": \"Animation rule\",\n \"type\": \"string\",\n \"default\": \"cycle\"\n },\n \"barStrokeWidth\": {\n \"title\": \"Bar stroke width\",\n \"type\": \"number\",\n \"default\": 2.5\n },\n \"colorBarStroke\": {\n \"title\": \"Bar stroke color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"colorBar\": {\n \"title\": \"Bar background color\",\n \"type\": \"string\",\n \"default\": \"#fff\"\n },\n \"colorBarEnd\": {\n \"title\": \"Bar background color - end gradient\",\n \"type\": \"string\",\n \"default\": \"#ddd\"\n },\n \"colorBarProgress\": {\n \"title\": \"Progress bar color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"colorBarProgressEnd\": {\n \"title\": \"Progress bar color - end gradient\",\n \"type\": \"string\",\n \"default\": null\n } \n \n },\n \"required\": []\n },\n \"form\": [\n \"barStrokeWidth\",\n {\n \"key\": \"colorBarStroke\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorBar\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorBarEnd\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorBarProgress\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorBarProgressEnd\",\n \"type\": \"color\"\n },\n \"minValue\",\n \"maxValue\",\n \"unitTitle\",\n \"showUnitTitle\",\n \"units\",\n \"majorTicksCount\",\n \"minorTicks\",\n \"valueBox\",\n \"valueInt\",\n \"valueDec\",\n {\n \"key\": \"defaultColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorPlate\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorMajorTicks\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorMinorTicks\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorNeedle\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorNeedleEnd\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorNeedleShadowUp\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorNeedleShadowDown\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorValueBoxRect\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorValueBoxRectEnd\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorValueBoxBackground\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorValueBoxShadow\",\n \"type\": \"color\"\n },\n {\n \"key\": \"highlights\",\n \"items\": [\n \"highlights[].from\",\n \"highlights[].to\",\n {\n \"key\": \"highlights[].color\",\n \"type\": \"color\"\n }\n ]\n },\n \"highlightsWidth\",\n \"showBorder\",\n {\n \"key\": \"numbersFont\",\n \"items\": [\n \"numbersFont.family\",\n \"numbersFont.size\",\n {\n \"key\": \"numbersFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"numbersFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"numbersFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"titleFont\",\n \"items\": [\n \"titleFont.family\",\n \"titleFont.size\",\n {\n \"key\": \"titleFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"titleFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"titleFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"unitsFont\",\n \"items\": [\n \"unitsFont.family\",\n \"unitsFont.size\",\n {\n \"key\": \"unitsFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"unitsFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"unitsFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"valueFont\",\n \"items\": [\n \"valueFont.family\",\n \"valueFont.size\",\n {\n \"key\": \"valueFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"valueFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"valueFont.color\",\n \"type\": \"color\"\n },\n {\n \"key\": \"valueFont.shadowColor\",\n \"type\": \"color\"\n }\n ]\n }, \n \"animation\",\n \"animationDuration\",\n {\n \"key\": \"animationRule\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \"quad\",\n \"label\": \"Quad\"\n },\n {\n \"value\": \"quint\",\n \"label\": \"Quint\"\n },\n {\n \"value\": \"cycle\",\n \"label\": \"Cycle\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n },\n {\n \"value\": \"elastic\",\n \"label\": \"Elastic\"\n },\n {\n \"value\": \"dequad\",\n \"label\": \"Dequad\"\n },\n {\n \"value\": \"dequint\",\n \"label\": \"Dequint\"\n },\n {\n \"value\": \"decycle\",\n \"label\": \"Decycle\"\n },\n {\n \"value\": \"debounce\",\n \"label\": \"Debounce\"\n },\n {\n \"value\": \"delastic\",\n \"label\": \"Delastic\"\n }\n ]\n }\n ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 10 - 5;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":100,\"defaultColor\":\"#e64a19\",\"barStrokeWidth\":2.5,\"colorBar\":\"#fff\",\"colorBarEnd\":\"#ddd\",\"showUnitTitle\":true,\"minorTicks\":2,\"valueBox\":true,\"valueInt\":3,\"valueDec\":0,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"colorNeedleShadowUp\":\"rgba(2,255,255,0.2)\",\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\",\"highlightsWidth\":10,\"animation\":true,\"animationDuration\":500,\"animationRule\":\"cycle\",\"showBorder\":false,\"majorTicksCount\":10,\"numbersFont\":{\"family\":\"RobotoDraft\",\"size\":18,\"style\":\"normal\",\"weight\":\"500\"},\"titleFont\":{\"family\":\"RobotoDraft\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"unitsFont\":{\"family\":\"RobotoDraft\",\"size\":22,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"valueFont\":{\"family\":\"RobotoDraft\",\"size\":40,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#444\",\"shadowColor\":\"rgba(0,0,0,0.3)\"},\"minValue\":-100,\"highlights\":[]},\"title\":\"Linear gauge - Canvas Gauges\"}"}',
+'Linear gauge - Canvas Gauges' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'charts', 'pie_chart_js',
+'{"type":"latest","sizeX":8,"sizeY":6,"resources":[{"url":"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.3.0/Chart.min.js"}],"templateHtml":"<canvas id=\"pieChart\"></canvas>\n","templateCss":"","controllerScript":"var chart;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n\n var pieData = {\n labels: [],\n datasets: []\n };\n\n var dataset = {\n data: [],\n backgroundColor: [],\n borderColor: [],\n borderWidth: [],\n hoverBackgroundColor: []\n }\n \n pieData.datasets.push(dataset);\n \n for (var i in data) {\n var dataKey = data[i].dataKey;\n pieData.labels.push(dataKey.label);\n dataset.data.push(0);\n var hoverBackgroundColor = tinycolor(dataKey.color).lighten(15);\n var borderColor = tinycolor(dataKey.color).darken();\n dataset.backgroundColor.push(dataKey.color);\n dataset.borderColor.push(''#fff'');\n dataset.borderWidth.push(5);\n dataset.hoverBackgroundColor.push(hoverBackgroundColor.toRgbString());\n }\n\n var ctx = $(''#pieChart'', containerElement);\n chart = new Chart(ctx, {\n type: ''pie'',\n data: pieData,\n options: {\n maintainAspectRatio: false\n }\n });\n \n}\n\nfns.redraw = function(containerElement, width, height, data,\n timeWindow, sizeChanged) {\n\n for (var i = 0; i < data.length; i++) {\n var cellData = data[i];\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = tvPair[1];\n chart.data.datasets[0].data[i] = parseFloat(value);\n }\n }\n \n chart.update();\n if (sizeChanged) {\n chart.resize();\n }\n \n};\n","settingsSchema":"{}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Pie - Chart.js\"}"}',
+'Pie - Chart.js' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'digital_gauges', 'simple_neon_gauge_justgage',
+'{"type":"latest","sizeX":3,"sizeY":3,"resources":[],"templateHtml":"","templateCss":"#gauge {\n text-align: center;\n /* margin-left: -100px;\n margin-right: -100px;*/\n /*margin-top: -50px;*/\n \n}\n","controllerScript":"var gauge;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n gauge = new TbDigitalGauge(containerElement, settings, data); \n\n}\n\n\nfns.redraw = function(containerElement, width, height, data) {\n gauge.redraw(data);\n};\n\nfns.destroy = function() {\n}\n","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"minValue\": {\n \"title\": \"Minimum value\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"maxValue\": {\n \"title\": \"Maximum value\",\n \"type\": \"number\",\n \"default\": 100\n }, \n \"gaugeType\": {\n \"title\": \"Gauge type\",\n \"type\": \"string\",\n \"default\": \"arc\"\n }, \n \"donutStartAngle\": {\n \"title\": \"Angle to start from when in donut mode\",\n \"type\": \"number\",\n \"default\": 90\n }, \n \"neonGlowBrightness\": {\n \"title\": \"Neon glow effect brightness, (0-100), 0 - disable effect\",\n \"type\": \"number\",\n \"default\": 0\n }, \n \"dashThickness\": {\n \"title\": \"Thickness of the stripes, 0 - no stripes\",\n \"type\": \"number\",\n \"default\": 0\n }, \n \"roundedLineCap\": {\n \"title\": \"Display rounded line cap\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"title\": {\n \"title\": \"Gauge title\",\n \"type\": \"string\",\n \"default\": null\n }, \n \"showTitle\": {\n \"title\": \"Show gauge title\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"unitTitle\": {\n \"title\": \"Unit title\",\n \"type\": \"string\",\n \"default\": null\n }, \n \"showUnitTitle\": {\n \"title\": \"Show unit title\",\n \"type\": \"boolean\",\n \"default\": false\n }, \n \"showValue\": {\n \"title\": \"Show value text\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"showMinMax\": {\n \"title\": \"Show min and max values\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"gaugeWidthScale\": {\n \"title\": \"Width of the gauge element\",\n \"type\": \"number\",\n \"default\": 0.75\n },\n \"defaultColor\": {\n \"title\": \"Default color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"gaugeColor\": {\n \"title\": \"Background color of the gauge element\",\n \"type\": \"string\",\n \"default\": null\n },\n \"levelColors\": {\n \"title\": \"Colors of indicator, from lower to upper\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n }\n },\n \"refreshAnimationType\": {\n \"title\": \"Type of refresh animation\",\n \"type\": \"string\",\n \"default\": \">\"\n },\n \"refreshAnimationTime\": {\n \"title\": \"Duration of refresh animation (ms)\",\n \"type\": \"number\",\n \"default\": 700\n },\n \"startAnimationType\": {\n \"title\": \"Type of start animation\",\n \"type\": \"string\",\n \"default\": \">\"\n },\n \"startAnimationTime\": {\n \"title\": \"Duration of start animation (ms)\",\n \"type\": \"number\",\n \"default\": 700\n },\n \"decimals\": {\n \"title\": \"Number of digits after floating point\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"units\": {\n \"title\": \"Special symbol to show next to value\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"titleFont\": {\n \"title\": \"Gauge title font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 12\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"labelFont\": {\n \"title\": \"Font of label showing under value\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 8\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"valueFont\": {\n \"title\": \"Font of label showing current value\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 18\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"minMaxFont\": {\n \"title\": \"Font of minimum and maximum labels\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n }\n }\n },\n \"form\": [\n \"minValue\",\n \"maxValue\",\n {\n \"key\": \"gaugeType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"arc\",\n \"label\": \"Arc\"\n },\n {\n \"value\": \"donut\",\n \"label\": \"Donut\"\n },\n {\n \"value\": \"horizontalBar\",\n \"label\": \"Horizontal bar\"\n },\n {\n \"value\": \"verticalBar\",\n \"label\": \"Vertical bar\"\n }\n ]\n },\n \"donutStartAngle\",\n \"neonGlowBrightness\",\n \"dashThickness\",\n \"roundedLineCap\",\n \"title\",\n \"showTitle\",\n \"unitTitle\",\n \"showUnitTitle\",\n \"showValue\",\n \"showMinMax\",\n \"gaugeWidthScale\",\n {\n \"key\": \"defaultColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"gaugeColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"levelColors\",\n \"items\": [\n {\n \"key\": \"levelColors[]\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"refreshAnimationType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \">\",\n \"label\": \">\"\n },\n {\n \"value\": \"<\",\n \"label\": \"<\"\n },\n {\n \"value\": \"<>\",\n \"label\": \"<>\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n }\n ]\n },\n \"refreshAnimationTime\",\n {\n \"key\": \"startAnimationType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \">\",\n \"label\": \">\"\n },\n {\n \"value\": \"<\",\n \"label\": \"<\"\n },\n {\n \"value\": \"<>\",\n \"label\": \"<>\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n }\n ]\n },\n \"startAnimationTime\",\n \"decimals\",\n \"units\",\n {\n \"key\": \"titleFont\",\n \"items\": [\n \"titleFont.family\",\n \"titleFont.size\",\n {\n \"key\": \"titleFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"titleFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"titleFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"labelFont\",\n \"items\": [\n \"labelFont.family\",\n \"labelFont.size\",\n {\n \"key\": \"labelFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"labelFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"labelFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"valueFont\",\n \"items\": [\n \"valueFont.family\",\n \"valueFont.size\",\n {\n \"key\": \"valueFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"valueFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"valueFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont\",\n \"items\": [\n \"minMaxFont.family\",\n \"minMaxFont.size\",\n {\n \"key\": \"minMaxFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"minMaxFont.color\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n}\n ","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#388e3c\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":1,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"RobotoDraft\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"RobotoDraft\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":40,\"dashThickness\":1.5,\"decimals\":0,\"gaugeType\":\"donut\"},\"title\":\"Simple neon gauge - justGage\"}"}',
+'Simple neon gauge - justGage' );
+
+/** System plugins and rules **/
+INSERT INTO thingsboard.plugin ( id, tenant_id, name, state, search_text, api_token, plugin_class, public_access,
+configuration )
+VALUES ( minTimeuuid ( '2016-11-01 01:01:01+0000' ), minTimeuuid ( 0 ), 'System Telemetry Plugin', 'ACTIVE',
+'system telemetry plugin', 'telemetry',
+'org.thingsboard.server.extensions.core.plugin.telemetry.TelemetryStoragePlugin', true, '{}' );
+
+INSERT INTO thingsboard.rule ( id, tenant_id, name, plugin_token, state, search_text, weight, filters, processor,
+action )
+VALUES ( minTimeuuid ( '2016-11-01 01:01:02+0000' ), minTimeuuid ( 0 ), 'System Telemetry Rule', 'telemetry', 'ACTIVE',
+'system telemetry rule', 0,
+'[{"clazz":"org.thingsboard.server.extensions.core.filter.MsgTypeFilter", "name":"TelemetryFilter", "configuration": {"messageTypes":["POST_TELEMETRY","POST_ATTRIBUTES","GET_ATTRIBUTES"]}}]',
+null,
+'{"clazz":"org.thingsboard.server.extensions.core.action.telemetry.TelemetryPluginAction", "name":"TelemetryMsgConverterAction", "configuration":{}}'
+);
+
+INSERT INTO thingsboard.plugin ( id, tenant_id, name, state, search_text, api_token, plugin_class, public_access,
+configuration )
+VALUES ( minTimeuuid ( '2016-11-01 01:01:03+0000' ), minTimeuuid ( 0 ), 'System RPC Plugin', 'ACTIVE',
+'system rpc plugin', 'rpc', 'org.thingsboard.server.extensions.core.plugin.rpc.RpcPlugin', true, '{
+ "defaultTimeout": 20000
+ }' );
+
+/** SYSTEM **/
diff --git a/dao/src/test/java/org/thingsboard/server/dao/attributes/BaseAttributesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/attributes/BaseAttributesServiceTest.java
new file mode 100644
index 0000000..e2f5acb
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/attributes/BaseAttributesServiceTest.java
@@ -0,0 +1,103 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.attributes;
+
+import com.datastax.driver.core.utils.UUIDs;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.data.kv.StringDataEntry;
+import org.thingsboard.server.dao.service.AbstractServiceTest;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import java.util.Collections;
+import java.util.List;
+
+import static org.thingsboard.server.common.data.DataConstants.CLIENT_SCOPE;
+import static org.thingsboard.server.common.data.DataConstants.DEVICE;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class BaseAttributesServiceTest extends AbstractServiceTest {
+
+ @Autowired
+ private AttributesService attributesService;
+
+ @Before
+ public void before() {
+ }
+
+ @Test
+ public void saveAndFetch() throws Exception {
+ DeviceId deviceId = new DeviceId(UUIDs.timeBased());
+ KvEntry attrValue = new StringDataEntry("attribute1", "value1");
+ AttributeKvEntry attr = new BaseAttributeKvEntry(attrValue, 42L);
+ attributesService.save(deviceId, DataConstants.CLIENT_SCOPE, Collections.singletonList(attr)).get();
+ AttributeKvEntry saved = attributesService.find(deviceId, DataConstants.CLIENT_SCOPE, attr.getKey());
+ Assert.assertEquals(attr, saved);
+ }
+
+ @Test
+ public void saveMultipleTypeAndFetch() throws Exception {
+ DeviceId deviceId = new DeviceId(UUIDs.timeBased());
+ KvEntry attrOldValue = new StringDataEntry("attribute1", "value1");
+ AttributeKvEntry attrOld = new BaseAttributeKvEntry(attrOldValue, 42L);
+
+ attributesService.save(deviceId, DataConstants.CLIENT_SCOPE, Collections.singletonList(attrOld)).get();
+ AttributeKvEntry saved = attributesService.find(deviceId, DataConstants.CLIENT_SCOPE, attrOld.getKey());
+ Assert.assertEquals(attrOld, saved);
+
+ KvEntry attrNewValue = new StringDataEntry("attribute1", "value2");
+ AttributeKvEntry attrNew = new BaseAttributeKvEntry(attrNewValue, 73L);
+ attributesService.save(deviceId, DataConstants.CLIENT_SCOPE, Collections.singletonList(attrNew)).get();
+
+ saved = attributesService.find(deviceId, DataConstants.CLIENT_SCOPE, attrOld.getKey());
+ Assert.assertEquals(attrNew, saved);
+ }
+
+ @Test
+ public void findAll() throws Exception {
+ DeviceId deviceId = new DeviceId(UUIDs.timeBased());
+
+ KvEntry attrAOldValue = new StringDataEntry("A", "value1");
+ AttributeKvEntry attrAOld = new BaseAttributeKvEntry(attrAOldValue, 42L);
+ KvEntry attrANewValue = new StringDataEntry("A", "value2");
+ AttributeKvEntry attrANew = new BaseAttributeKvEntry(attrANewValue, 73L);
+ KvEntry attrBNewValue = new StringDataEntry("B", "value3");
+ AttributeKvEntry attrBNew = new BaseAttributeKvEntry(attrBNewValue, 73L);
+
+ attributesService.save(deviceId, DataConstants.CLIENT_SCOPE, Collections.singletonList(attrAOld)).get();
+ attributesService.save(deviceId, DataConstants.CLIENT_SCOPE, Collections.singletonList(attrANew)).get();
+ attributesService.save(deviceId, DataConstants.CLIENT_SCOPE, Collections.singletonList(attrBNew)).get();
+
+ List<AttributeKvEntry> saved = attributesService.findAll(deviceId, DataConstants.CLIENT_SCOPE);
+
+ Assert.assertNotNull(saved);
+ Assert.assertEquals(2, saved.size());
+
+ Assert.assertEquals(attrANew, saved.get(0));
+ Assert.assertEquals(attrBNew, saved.get(1));
+ }
+
+}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/CustomCassandraCQLUnit.java b/dao/src/test/java/org/thingsboard/server/dao/CustomCassandraCQLUnit.java
new file mode 100644
index 0000000..503fd6c
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/CustomCassandraCQLUnit.java
@@ -0,0 +1,97 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao;
+
+import org.cassandraunit.BaseCassandraUnit;
+import org.cassandraunit.CQLDataLoader;
+import org.cassandraunit.dataset.CQLDataSet;
+import org.cassandraunit.utils.EmbeddedCassandraServerHelper;
+
+import com.datastax.driver.core.Cluster;
+import com.datastax.driver.core.Session;
+
+import java.util.List;
+
+public class CustomCassandraCQLUnit extends BaseCassandraUnit {
+ private List<CQLDataSet> dataSets;
+
+ public Session session;
+ public Cluster cluster;
+
+ public CustomCassandraCQLUnit(List<CQLDataSet> dataSets) {
+ this.dataSets = dataSets;
+ }
+
+ public CustomCassandraCQLUnit(List<CQLDataSet> dataSets, int readTimeoutMillis) {
+ this.dataSets = dataSets;
+ this.readTimeoutMillis = readTimeoutMillis;
+ }
+
+ public CustomCassandraCQLUnit(List<CQLDataSet> dataSets, String configurationFileName) {
+ this(dataSets);
+ this.configurationFileName = configurationFileName;
+ }
+
+ public CustomCassandraCQLUnit(List<CQLDataSet> dataSets, String configurationFileName, int readTimeoutMillis) {
+ this(dataSets);
+ this.configurationFileName = configurationFileName;
+ this.readTimeoutMillis = readTimeoutMillis;
+ }
+
+ public CustomCassandraCQLUnit(List<CQLDataSet> dataSets, String configurationFileName, long startUpTimeoutMillis) {
+ super(startUpTimeoutMillis);
+ this.dataSets = dataSets;
+ this.configurationFileName = configurationFileName;
+ }
+
+ public CustomCassandraCQLUnit(List<CQLDataSet> dataSets, String configurationFileName, long startUpTimeoutMillis, int readTimeoutMillis) {
+ super(startUpTimeoutMillis);
+ this.dataSets = dataSets;
+ this.configurationFileName = configurationFileName;
+ this.readTimeoutMillis = readTimeoutMillis;
+ }
+
+ @Override
+ protected void load() {
+ String hostIp = EmbeddedCassandraServerHelper.getHost();
+ int port = EmbeddedCassandraServerHelper.getNativeTransportPort();
+ cluster = new Cluster.Builder().addContactPoints(hostIp).withPort(port).withSocketOptions(getSocketOptions())
+ .build();
+ session = cluster.connect();
+ CQLDataLoader dataLoader = new CQLDataLoader(session);
+ dataSets.forEach(dataLoader::load);
+ session = dataLoader.getSession();
+ }
+
+ @Override
+ protected void after() {
+ super.after();
+ try (Cluster c = cluster; Session s = session) {
+ session = null;
+ cluster = null;
+ }
+ }
+
+ // Getters for those who do not like to directly access fields
+
+ public Session getSession() {
+ return session;
+ }
+
+ public Cluster getCluster() {
+ return cluster;
+ }
+}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/DaoTestSuite.java b/dao/src/test/java/org/thingsboard/server/dao/DaoTestSuite.java
new file mode 100644
index 0000000..21b27ad
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/DaoTestSuite.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao;
+
+import org.cassandraunit.dataset.cql.ClassPathCQLDataSet;
+import org.junit.ClassRule;
+import org.junit.extensions.cpsuite.ClasspathSuite;
+import org.junit.extensions.cpsuite.ClasspathSuite.ClassnameFilters;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+@RunWith(ClasspathSuite.class)
+@ClassnameFilters({
+ "org.thingsboard.server.dao.service.*Test",
+ "org.thingsboard.server.dao.kv.*Test",
+ "org.thingsboard.server.dao.plugin.*Test",
+ "org.thingsboard.server.dao.rule.*Test",
+ "org.thingsboard.server.dao.attributes.*Test",
+ "org.thingsboard.server.dao.timeseries.*Test"
+})
+public class DaoTestSuite {
+
+ @ClassRule
+ public static CustomCassandraCQLUnit cassandraUnit =
+ new CustomCassandraCQLUnit(
+ Arrays.asList(new ClassPathCQLDataSet("schema.cql", false, false),
+ new ClassPathCQLDataSet("system-data.cql", false, false),
+ new ClassPathCQLDataSet("system-test.cql", false, false)),
+ "cassandra-test.yaml", 30000l);
+
+}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/event/BaseEventServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/event/BaseEventServiceTest.java
new file mode 100644
index 0000000..ba346bf
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/event/BaseEventServiceTest.java
@@ -0,0 +1,138 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.event;
+
+import com.datastax.driver.core.utils.UUIDs;
+import org.apache.cassandra.utils.UUIDGen;
+import org.junit.Assert;
+import org.junit.Test;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EventId;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.dao.service.AbstractServiceTest;
+
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.time.Month;
+import java.time.ZoneOffset;
+import java.util.List;
+import java.util.Optional;
+
+public class BaseEventServiceTest extends AbstractServiceTest {
+
+ @Test
+ public void saveEvent() throws Exception {
+ DeviceId devId = new DeviceId(UUIDs.timeBased());
+ Event event = generateEvent(null, devId, "ALARM", UUIDs.timeBased().toString());
+ Event saved = eventService.save(event);
+ Optional<Event> loaded = eventService.findEvent(event.getTenantId(), event.getEntityId(), event.getType(), event.getUid());
+ Assert.assertTrue(loaded.isPresent());
+ Assert.assertNotNull(loaded.get());
+ Assert.assertEquals(saved, loaded.get());
+ }
+
+ @Test
+ public void saveEventIfNotExists() throws Exception {
+ DeviceId devId = new DeviceId(UUIDs.timeBased());
+ Event event = generateEvent(null, devId, "ALARM", UUIDs.timeBased().toString());
+ Optional<Event> saved = eventService.saveIfNotExists(event);
+ Assert.assertTrue(saved.isPresent());
+ saved = eventService.saveIfNotExists(event);
+ Assert.assertFalse(saved.isPresent());
+ }
+
+ @Test
+ public void findEventsByTypeAndTimeAscOrder() throws Exception {
+ long timeBeforeStartTime = LocalDateTime.of(2016, Month.NOVEMBER, 1, 11, 30).toEpochSecond(ZoneOffset.UTC);
+ long startTime = LocalDateTime.of(2016, Month.NOVEMBER, 1, 12, 0).toEpochSecond(ZoneOffset.UTC);
+ long eventTime = LocalDateTime.of(2016, Month.NOVEMBER, 1, 12, 30).toEpochSecond(ZoneOffset.UTC);
+ long endTime = LocalDateTime.of(2016, Month.NOVEMBER, 1, 13, 0).toEpochSecond(ZoneOffset.UTC);
+ long timeAfterEndTime = LocalDateTime.of(2016, Month.NOVEMBER, 1, 13, 30).toEpochSecond(ZoneOffset.UTC);
+
+ RuleId ruleId = new RuleId(UUIDs.timeBased());
+ TenantId tenantId = new TenantId(UUIDs.timeBased());
+ saveEventWithProvidedTime(timeBeforeStartTime, ruleId, tenantId);
+ Event savedEvent = saveEventWithProvidedTime(eventTime, ruleId, tenantId);
+ Event savedEvent2 = saveEventWithProvidedTime(eventTime+1, ruleId, tenantId);
+ Event savedEvent3 = saveEventWithProvidedTime(eventTime+2, ruleId, tenantId);
+ saveEventWithProvidedTime(timeAfterEndTime, ruleId, tenantId);
+
+ TimePageData<Event> events = eventService.findEvents(tenantId, ruleId, DataConstants.STATS,
+ new TimePageLink(2, startTime, endTime, true));
+
+ Assert.assertNotNull(events.getData());
+ Assert.assertTrue(events.getData().size() == 2);
+ Assert.assertTrue(events.getData().get(0).getUuidId().equals(savedEvent.getUuidId()));
+ Assert.assertTrue(events.getData().get(1).getUuidId().equals(savedEvent2.getUuidId()));
+ Assert.assertTrue(events.hasNext());
+ Assert.assertNotNull(events.getNextPageLink());
+
+ events = eventService.findEvents(tenantId, ruleId, DataConstants.STATS, events.getNextPageLink());
+
+ Assert.assertNotNull(events.getData());
+ Assert.assertTrue(events.getData().size() == 1);
+ Assert.assertTrue(events.getData().get(0).getUuidId().equals(savedEvent3.getUuidId()));
+ Assert.assertFalse(events.hasNext());
+ Assert.assertNull(events.getNextPageLink());
+ }
+
+ @Test
+ public void findEventsByTypeAndTimeDescOrder() throws Exception {
+ long timeBeforeStartTime = LocalDateTime.of(2016, Month.NOVEMBER, 1, 11, 30).toEpochSecond(ZoneOffset.UTC);
+ long startTime = LocalDateTime.of(2016, Month.NOVEMBER, 1, 12, 0).toEpochSecond(ZoneOffset.UTC);
+ long eventTime = LocalDateTime.of(2016, Month.NOVEMBER, 1, 12, 30).toEpochSecond(ZoneOffset.UTC);
+ long endTime = LocalDateTime.of(2016, Month.NOVEMBER, 1, 13, 0).toEpochSecond(ZoneOffset.UTC);
+ long timeAfterEndTime = LocalDateTime.of(2016, Month.NOVEMBER, 1, 13, 30).toEpochSecond(ZoneOffset.UTC);
+
+ RuleId ruleId = new RuleId(UUIDs.timeBased());
+ TenantId tenantId = new TenantId(UUIDs.timeBased());
+ saveEventWithProvidedTime(timeBeforeStartTime, ruleId, tenantId);
+ Event savedEvent = saveEventWithProvidedTime(eventTime, ruleId, tenantId);
+ Event savedEvent2 = saveEventWithProvidedTime(eventTime+1, ruleId, tenantId);
+ Event savedEvent3 = saveEventWithProvidedTime(eventTime+2, ruleId, tenantId);
+ saveEventWithProvidedTime(timeAfterEndTime, ruleId, tenantId);
+
+ TimePageData<Event> events = eventService.findEvents(tenantId, ruleId, DataConstants.STATS,
+ new TimePageLink(2, startTime, endTime, false));
+
+ Assert.assertNotNull(events.getData());
+ Assert.assertTrue(events.getData().size() == 2);
+ Assert.assertTrue(events.getData().get(0).getUuidId().equals(savedEvent3.getUuidId()));
+ Assert.assertTrue(events.getData().get(1).getUuidId().equals(savedEvent2.getUuidId()));
+ Assert.assertTrue(events.hasNext());
+ Assert.assertNotNull(events.getNextPageLink());
+
+ events = eventService.findEvents(tenantId, ruleId, DataConstants.STATS, events.getNextPageLink());
+
+ Assert.assertNotNull(events.getData());
+ Assert.assertTrue(events.getData().size() == 1);
+ Assert.assertTrue(events.getData().get(0).getUuidId().equals(savedEvent.getUuidId()));
+ Assert.assertFalse(events.hasNext());
+ Assert.assertNull(events.getNextPageLink());
+ }
+
+ private Event saveEventWithProvidedTime(long time, EntityId entityId, TenantId tenantId) throws IOException {
+ Event event = generateEvent(tenantId, entityId, DataConstants.STATS, null);
+ event.setId(new EventId(UUIDs.startOf(time)));
+ return eventService.save(event);
+ }
+}
\ No newline at end of file
diff --git a/dao/src/test/java/org/thingsboard/server/dao/plugin/BasePluginServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/plugin/BasePluginServiceTest.java
new file mode 100644
index 0000000..4fd3d38
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/plugin/BasePluginServiceTest.java
@@ -0,0 +1,118 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.plugin;
+
+import java.util.UUID;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.plugin.PluginMetaData;
+import org.thingsboard.server.common.data.rule.RuleMetaData;
+import org.thingsboard.server.dao.service.AbstractServiceTest;
+import org.junit.Assert;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.datastax.driver.core.utils.UUIDs;
+import org.thingsboard.server.dao.model.ModelConstants;
+
+@Slf4j
+public class BasePluginServiceTest extends AbstractServiceTest {
+
+ @Test
+ public void savePlugin() throws Exception {
+ PluginMetaData pluginMetaData = pluginService.savePlugin(generatePlugin(null, null));
+ Assert.assertNotNull(pluginMetaData.getId());
+ Assert.assertNotNull(pluginMetaData.getAdditionalInfo());
+
+ pluginMetaData.setAdditionalInfo(mapper.readTree("{\"description\":\"test\"}"));
+ PluginMetaData newPluginMetaData = pluginService.savePlugin(pluginMetaData);
+ Assert.assertEquals(pluginMetaData.getAdditionalInfo(), newPluginMetaData.getAdditionalInfo());
+
+ }
+
+ @Test
+ public void findPluginById() throws Exception {
+ PluginMetaData expected = pluginService.savePlugin(generatePlugin(null, null));
+ Assert.assertNotNull(expected.getId());
+ PluginMetaData found = pluginService.findPluginById(expected.getId());
+ Assert.assertEquals(expected, found);
+ }
+
+ @Test
+ public void findPluginByTenantIdAndApiToken() throws Exception {
+ String token = UUID.randomUUID().toString();
+ TenantId tenantId = new TenantId(UUIDs.timeBased());
+ pluginService.savePlugin(generatePlugin(null, null));
+ pluginService.savePlugin(generatePlugin(tenantId, null));
+ pluginService.savePlugin(generatePlugin(tenantId, null));
+ pluginService.savePlugin(generatePlugin(tenantId, null));
+ PluginMetaData expected = pluginService.savePlugin(generatePlugin(tenantId, token));
+ Assert.assertNotNull(expected.getId());
+ PluginMetaData found = pluginService.findPluginByApiToken(token);
+ Assert.assertEquals(expected, found);
+ }
+
+ @Test
+ public void findSystemPlugins() throws Exception {
+ TenantId systemTenant = new TenantId(ModelConstants.NULL_UUID); // system tenant id
+ pluginService.savePlugin(generatePlugin(null, null));
+ pluginService.savePlugin(generatePlugin(null, null));
+ pluginService.savePlugin(generatePlugin(systemTenant, null));
+ pluginService.savePlugin(generatePlugin(systemTenant, null));
+ TextPageData<PluginMetaData> found = pluginService.findSystemPlugins(new TextPageLink(100));
+ Assert.assertEquals(2, found.getData().size());
+ Assert.assertFalse(found.hasNext());
+ }
+
+ @Test
+ public void findTenantPlugins() throws Exception {
+ TenantId tenantId = new TenantId(UUIDs.timeBased());
+ pluginService.savePlugin(generatePlugin(null, null));
+ pluginService.savePlugin(generatePlugin(null, null));
+ pluginService.savePlugin(generatePlugin(tenantId, null));
+ pluginService.savePlugin(generatePlugin(tenantId, null));
+ pluginService.savePlugin(generatePlugin(tenantId, null));
+ TextPageData<PluginMetaData> found = pluginService.findTenantPlugins(tenantId, new TextPageLink(100));
+ Assert.assertEquals(3, found.getData().size());
+ }
+
+ @Test
+ public void deletePluginById() throws Exception {
+ PluginMetaData expected = pluginService.savePlugin(generatePlugin(null, null));
+ Assert.assertNotNull(expected.getId());
+ pluginService.deletePluginById(expected.getId());
+ PluginMetaData found = pluginService.findPluginById(expected.getId());
+ Assert.assertNull(found);
+ }
+
+ @Test
+ public void deletePluginsByTenantId() throws Exception {
+ TenantId tenantId = new TenantId(UUIDs.timeBased());
+ pluginService.savePlugin(generatePlugin(tenantId, null));
+ pluginService.savePlugin(generatePlugin(tenantId, null));
+ pluginService.savePlugin(generatePlugin(tenantId, null));
+ TextPageData<PluginMetaData> found = pluginService.findTenantPlugins(tenantId, new TextPageLink(100));
+ Assert.assertEquals(3, found.getData().size());
+ pluginService.deletePluginsByTenantId(tenantId);
+ found = pluginService.findTenantPlugins(tenantId, new TextPageLink(100));
+ Assert.assertEquals(0, found.getData().size());
+ }
+
+}
\ No newline at end of file
diff --git a/dao/src/test/java/org/thingsboard/server/dao/rule/BaseRuleServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/rule/BaseRuleServiceTest.java
new file mode 100644
index 0000000..12566ad
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/rule/BaseRuleServiceTest.java
@@ -0,0 +1,163 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.rule;
+
+import com.datastax.driver.core.utils.UUIDs;
+import org.junit.Assert;
+import org.junit.Test;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.plugin.PluginMetaData;
+import org.thingsboard.server.common.data.rule.RuleMetaData;
+import org.thingsboard.server.dao.model.ModelConstants;
+import org.thingsboard.server.dao.service.AbstractServiceTest;
+
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+
+public class BaseRuleServiceTest extends AbstractServiceTest {
+
+ @Test
+ public void saveRule() throws Exception {
+ PluginMetaData plugin = generatePlugin(null, "testPluginToken" + ThreadLocalRandom.current().nextInt());
+ pluginService.savePlugin(plugin);
+ RuleMetaData ruleMetaData = ruleService.saveRule(generateRule(plugin.getTenantId(), null, plugin.getApiToken()));
+ Assert.assertNotNull(ruleMetaData.getId());
+ Assert.assertNotNull(ruleMetaData.getAdditionalInfo());
+ ruleMetaData.setAdditionalInfo(mapper.readTree("{\"description\":\"test\"}"));
+ RuleMetaData newRuleMetaData = ruleService.saveRule(ruleMetaData);
+ Assert.assertEquals(ruleMetaData.getAdditionalInfo(), newRuleMetaData.getAdditionalInfo());
+ }
+
+ @Test
+ public void findRuleById() throws Exception {
+ PluginMetaData plugin = generatePlugin(null, "testPluginToken" + ThreadLocalRandom.current().nextInt());
+ pluginService.savePlugin(plugin);
+
+ RuleMetaData expected = ruleService.saveRule(generateRule(plugin.getTenantId(), null, plugin.getApiToken()));
+ Assert.assertNotNull(expected.getId());
+ RuleMetaData found = ruleService.findRuleById(expected.getId());
+ Assert.assertEquals(expected, found);
+ }
+
+ @Test
+ public void findPluginRules() throws Exception {
+ TenantId tenantIdA = new TenantId(UUIDs.timeBased());
+ TenantId tenantIdB = new TenantId(UUIDs.timeBased());
+
+ PluginMetaData pluginA = generatePlugin(tenantIdA, "testPluginToken" + ThreadLocalRandom.current().nextInt());
+ PluginMetaData pluginB = generatePlugin(tenantIdB, "testPluginToken" + ThreadLocalRandom.current().nextInt());
+ pluginService.savePlugin(pluginA);
+ pluginService.savePlugin(pluginB);
+
+ ruleService.saveRule(generateRule(tenantIdA, null, pluginA.getApiToken()));
+ ruleService.saveRule(generateRule(tenantIdA, null, pluginA.getApiToken()));
+ ruleService.saveRule(generateRule(tenantIdA, null, pluginA.getApiToken()));
+
+ ruleService.saveRule(generateRule(tenantIdB, null, pluginB.getApiToken()));
+ ruleService.saveRule(generateRule(tenantIdB, null, pluginB.getApiToken()));
+
+ List<RuleMetaData> foundA = ruleService.findPluginRules(pluginA.getApiToken());
+ Assert.assertEquals(3, foundA.size());
+
+ List<RuleMetaData> foundB = ruleService.findPluginRules(pluginB.getApiToken());
+ Assert.assertEquals(2, foundB.size());
+ }
+
+ @Test
+ public void findSystemRules() throws Exception {
+ TenantId systemTenant = new TenantId(ModelConstants.NULL_UUID); // system tenant id
+
+ PluginMetaData plugin = generatePlugin(systemTenant, "testPluginToken" + ThreadLocalRandom.current().nextInt());
+ pluginService.savePlugin(plugin);
+ ruleService.saveRule(generateRule(systemTenant, null, plugin.getApiToken()));
+ ruleService.saveRule(generateRule(systemTenant, null, plugin.getApiToken()));
+ ruleService.saveRule(generateRule(systemTenant, null, plugin.getApiToken()));
+ TextPageData<RuleMetaData> found = ruleService.findSystemRules(new TextPageLink(100));
+ Assert.assertEquals(3, found.getData().size());
+ }
+
+ @Test
+ public void findTenantRules() throws Exception {
+ TenantId tenantIdA = new TenantId(UUIDs.timeBased());
+ TenantId tenantIdB = new TenantId(UUIDs.timeBased());
+
+ PluginMetaData pluginA = generatePlugin(tenantIdA, "testPluginToken" + ThreadLocalRandom.current().nextInt());
+ PluginMetaData pluginB = generatePlugin(tenantIdB, "testPluginToken" + ThreadLocalRandom.current().nextInt());
+ pluginService.savePlugin(pluginA);
+ pluginService.savePlugin(pluginB);
+
+ ruleService.saveRule(generateRule(tenantIdA, null, pluginA.getApiToken()));
+ ruleService.saveRule(generateRule(tenantIdA, null, pluginA.getApiToken()));
+ ruleService.saveRule(generateRule(tenantIdA, null, pluginA.getApiToken()));
+
+ ruleService.saveRule(generateRule(tenantIdB, null, pluginB.getApiToken()));
+ ruleService.saveRule(generateRule(tenantIdB, null, pluginB.getApiToken()));
+
+ TextPageData<RuleMetaData> foundA = ruleService.findTenantRules(tenantIdA, new TextPageLink(100));
+ Assert.assertEquals(3, foundA.getData().size());
+
+ TextPageData<RuleMetaData> foundB = ruleService.findTenantRules(tenantIdB, new TextPageLink(100));
+ Assert.assertEquals(2, foundB.getData().size());
+ }
+
+ @Test
+ public void deleteRuleById() throws Exception {
+ PluginMetaData plugin = generatePlugin(null, "testPluginToken" + ThreadLocalRandom.current().nextInt());
+ pluginService.savePlugin(plugin);
+
+ RuleMetaData expected = ruleService.saveRule(generateRule(plugin.getTenantId(), null, plugin.getApiToken()));
+ Assert.assertNotNull(expected.getId());
+ RuleMetaData found = ruleService.findRuleById(expected.getId());
+ Assert.assertEquals(expected, found);
+ ruleService.deleteRuleById(expected.getId());
+ found = ruleService.findRuleById(expected.getId());
+ Assert.assertNull(found);
+ }
+
+ @Test
+ public void deleteRulesByTenantId() throws Exception {
+ TenantId tenantIdA = new TenantId(UUIDs.timeBased());
+ TenantId tenantIdB = new TenantId(UUIDs.timeBased());
+
+ PluginMetaData pluginA = generatePlugin(tenantIdA, "testPluginToken" + ThreadLocalRandom.current().nextInt());
+ PluginMetaData pluginB = generatePlugin(tenantIdB, "testPluginToken" + ThreadLocalRandom.current().nextInt());
+ pluginService.savePlugin(pluginA);
+ pluginService.savePlugin(pluginB);
+
+ ruleService.saveRule(generateRule(tenantIdA, null, pluginA.getApiToken()));
+ ruleService.saveRule(generateRule(tenantIdA, null, pluginA.getApiToken()));
+ ruleService.saveRule(generateRule(tenantIdA, null, pluginA.getApiToken()));
+
+ ruleService.saveRule(generateRule(tenantIdB, null, pluginB.getApiToken()));
+ ruleService.saveRule(generateRule(tenantIdB, null, pluginB.getApiToken()));
+
+ TextPageData<RuleMetaData> foundA = ruleService.findTenantRules(tenantIdA, new TextPageLink(100));
+ Assert.assertEquals(3, foundA.getData().size());
+
+ TextPageData<RuleMetaData> foundB = ruleService.findTenantRules(tenantIdB, new TextPageLink(100));
+ Assert.assertEquals(2, foundB.getData().size());
+
+ ruleService.deleteRulesByTenantId(tenantIdA);
+
+ foundA = ruleService.findTenantRules(tenantIdA, new TextPageLink(100));
+ Assert.assertEquals(0, foundA.getData().size());
+
+ foundB = ruleService.findTenantRules(tenantIdB, new TextPageLink(100));
+ Assert.assertEquals(2, foundB.getData().size());
+ }
+}
\ No newline at end of file
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
new file mode 100644
index 0000000..8d60568
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
@@ -0,0 +1,221 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.service;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.test.context.support.AnnotationConfigContextLoader;
+import org.thingsboard.server.common.data.BaseData;
+import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UUIDBased;
+import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
+import org.thingsboard.server.common.data.plugin.ComponentScope;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.data.plugin.PluginMetaData;
+import org.thingsboard.server.common.data.rule.RuleMetaData;
+import org.thingsboard.server.dao.component.ComponentDescriptorService;
+import org.thingsboard.server.dao.customer.CustomerService;
+import org.thingsboard.server.dao.dashboard.DashboardService;
+import org.thingsboard.server.dao.device.DeviceCredentialsService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.event.EventService;
+import org.thingsboard.server.dao.plugin.PluginService;
+import org.thingsboard.server.dao.rule.RuleService;
+import org.thingsboard.server.dao.settings.AdminSettingsService;
+import org.thingsboard.server.dao.tenant.TenantService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.dao.user.UserService;
+import org.thingsboard.server.dao.widget.WidgetTypeService;
+import org.thingsboard.server.dao.widget.WidgetsBundleService;
+
+import java.io.IOException;
+import java.util.Comparator;
+import java.util.UUID;
+import java.util.concurrent.ThreadLocalRandom;
+
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration(classes = AbstractServiceTest.class, loader = AnnotationConfigContextLoader.class)
+@TestPropertySource(locations = {"classpath:cassandra-test.properties", "classpath:application-test.properties"})
+@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
+@Configuration
+@EnableAutoConfiguration
+@ComponentScan("org.thingsboard.server")
+public abstract class AbstractServiceTest {
+
+ protected ObjectMapper mapper = new ObjectMapper();
+
+ @Autowired
+ protected UserService userService;
+
+ @Autowired
+ protected AdminSettingsService adminSettingsService;
+
+ @Autowired
+ protected TenantService tenantService;
+
+ @Autowired
+ protected CustomerService customerService;
+
+ @Autowired
+ protected DeviceService deviceService;
+
+ @Autowired
+ protected DeviceCredentialsService deviceCredentialsService;
+
+ @Autowired
+ protected WidgetsBundleService widgetsBundleService;
+
+ @Autowired
+ protected WidgetTypeService widgetTypeService;
+
+ @Autowired
+ protected DashboardService dashboardService;
+
+ @Autowired
+ protected TimeseriesService tsService;
+
+ @Autowired
+ protected PluginService pluginService;
+
+ @Autowired
+ protected RuleService ruleService;
+
+ @Autowired
+ protected EventService eventService;
+
+ @Autowired
+ private ComponentDescriptorService componentDescriptorService;
+
+ class IdComparator<D extends BaseData<? extends UUIDBased>> implements Comparator<D> {
+ @Override
+ public int compare(D o1, D o2) {
+ return o1.getId().getId().compareTo(o2.getId().getId());
+ }
+ }
+
+
+ protected Event generateEvent(TenantId tenantId, EntityId entityId, String eventType, String eventUid) throws IOException {
+ if (tenantId == null) {
+ tenantId = new TenantId(UUIDs.timeBased());
+ }
+ Event event = new Event();
+ event.setTenantId(tenantId);
+ event.setEntityId(entityId);
+ event.setType(eventType);
+ event.setUid(eventUid);
+ event.setBody(readFromResource("TestJsonData.json"));
+ return event;
+ }
+
+ protected PluginMetaData generatePlugin(TenantId tenantId, String token) throws IOException {
+ return generatePlugin(tenantId, token, "org.thingsboard.component.PluginTest", "org.thingsboard.component.ActionTest", "TestJsonDescriptor.json", "TestJsonData.json");
+ }
+
+ protected PluginMetaData generatePlugin(TenantId tenantId, String token, String clazz, String actions, String configurationDescriptorResource, String dataResource) throws IOException {
+ if (tenantId == null) {
+ tenantId = new TenantId(UUIDs.timeBased());
+ }
+ if (token == null) {
+ token = UUID.randomUUID().toString();
+ }
+ getOrCreateDescriptor(ComponentScope.TENANT, ComponentType.PLUGIN, clazz, configurationDescriptorResource, actions);
+ PluginMetaData pluginMetaData = new PluginMetaData();
+ pluginMetaData.setName("Testing");
+ pluginMetaData.setClazz(clazz);
+ pluginMetaData.setTenantId(tenantId);
+ pluginMetaData.setApiToken(token);
+ pluginMetaData.setAdditionalInfo(mapper.readTree("{\"test\":\"test\"}"));
+ try {
+ pluginMetaData.setConfiguration(readFromResource(dataResource));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return pluginMetaData;
+ }
+
+ private ComponentDescriptor getOrCreateDescriptor(ComponentScope scope, ComponentType type, String clazz, String configurationDescriptorResource) throws IOException {
+ return getOrCreateDescriptor(scope, type, clazz, configurationDescriptorResource, null);
+ }
+
+ private ComponentDescriptor getOrCreateDescriptor(ComponentScope scope, ComponentType type, String clazz, String configurationDescriptorResource, String actions) throws IOException {
+ ComponentDescriptor descriptor = componentDescriptorService.findByClazz(clazz);
+ if (descriptor == null) {
+ descriptor = new ComponentDescriptor();
+ descriptor.setName("test");
+ descriptor.setClazz(clazz);
+ descriptor.setScope(scope);
+ descriptor.setType(type);
+ descriptor.setActions(actions);
+ descriptor.setConfigurationDescriptor(readFromResource(configurationDescriptorResource));
+ componentDescriptorService.saveComponent(descriptor);
+ }
+ return descriptor;
+ }
+
+ public JsonNode readFromResource(String resourceName) throws IOException {
+ return mapper.readTree(this.getClass().getClassLoader().getResourceAsStream(resourceName));
+ }
+
+ protected RuleMetaData generateRule(TenantId tenantId, Integer weight, String pluginToken) throws IOException {
+ if (tenantId == null) {
+ tenantId = new TenantId(UUIDs.timeBased());
+ }
+ if (weight == null) {
+ weight = ThreadLocalRandom.current().nextInt();
+ }
+
+ RuleMetaData ruleMetaData = new RuleMetaData();
+ ruleMetaData.setName("Testing");
+ ruleMetaData.setTenantId(tenantId);
+ ruleMetaData.setWeight(weight);
+ ruleMetaData.setPluginToken(pluginToken);
+
+ ruleMetaData.setAction(createNode(ComponentScope.TENANT, ComponentType.ACTION,
+ "org.thingsboard.component.ActionTest", "TestJsonDescriptor.json", "TestJsonData.json"));
+ ruleMetaData.setProcessor(createNode(ComponentScope.TENANT, ComponentType.PROCESSOR,
+ "org.thingsboard.component.ProcessorTest", "TestJsonDescriptor.json", "TestJsonData.json"));
+ ruleMetaData.setFilters(mapper.createArrayNode().add(
+ createNode(ComponentScope.TENANT, ComponentType.FILTER,
+ "org.thingsboard.component.FilterTest", "TestJsonDescriptor.json", "TestJsonData.json")
+ ));
+
+ ruleMetaData.setAdditionalInfo(mapper.readTree("{}"));
+ return ruleMetaData;
+ }
+
+ protected JsonNode createNode(ComponentScope scope, ComponentType type, String clazz, String configurationDescriptor, String configuration) throws IOException {
+ getOrCreateDescriptor(scope, type, clazz, configurationDescriptor);
+ ObjectNode oNode = mapper.createObjectNode();
+ oNode.set("name", new TextNode("test action"));
+ oNode.set("clazz", new TextNode(clazz));
+ oNode.set("configuration", readFromResource(configuration));
+ return oNode;
+ }
+}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AdminSettingsServiceImplTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AdminSettingsServiceImplTest.java
new file mode 100644
index 0000000..4e06e49
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/AdminSettingsServiceImplTest.java
@@ -0,0 +1,99 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.service;
+
+import org.thingsboard.server.common.data.AdminSettings;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+
+public class AdminSettingsServiceImplTest extends AbstractServiceTest {
+
+ @Test
+ public void testFindAdminSettingsByKey() {
+ AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey("general");
+ Assert.assertNotNull(adminSettings);
+ adminSettings = adminSettingsService.findAdminSettingsByKey("mail");
+ Assert.assertNotNull(adminSettings);
+ adminSettings = adminSettingsService.findAdminSettingsByKey("unknown");
+ Assert.assertNull(adminSettings);
+ }
+
+ @Test
+ public void testFindAdminSettingsById() {
+ AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey("general");
+ AdminSettings foundAdminSettings = adminSettingsService.findAdminSettingsById(adminSettings.getId());
+ Assert.assertNotNull(foundAdminSettings);
+ Assert.assertEquals(adminSettings, foundAdminSettings);
+ }
+
+ @Test
+ public void testSaveAdminSettings() throws Exception {
+ AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey("general");
+ JsonNode json = adminSettings.getJsonValue();
+ ((ObjectNode) json).put("baseUrl", "http://myhost.org");
+ adminSettings.setJsonValue(json);
+ adminSettingsService.saveAdminSettings(adminSettings);
+ AdminSettings savedAdminSettings = adminSettingsService.findAdminSettingsByKey("general");
+ Assert.assertNotNull(savedAdminSettings);
+ Assert.assertEquals(adminSettings.getJsonValue(), savedAdminSettings.getJsonValue());
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testCreateAdminSettings() throws Exception {
+ AdminSettings adminSettings = new AdminSettings();
+ adminSettings.setKey("someKey");
+ adminSettings.setJsonValue(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+ adminSettingsService.saveAdminSettings(adminSettings);
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveAdminSettingsWithEmptyKey() {
+ AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey("mail");
+ adminSettings.setKey(null);
+ adminSettingsService.saveAdminSettings(adminSettings);
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testChangeAdminSettingsKey() {
+ AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey("mail");
+ adminSettings.setKey("newKey");
+ adminSettingsService.saveAdminSettings(adminSettings);
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveAdminSettingsWithNewJsonStructure() throws Exception {
+ AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey("mail");
+ JsonNode json = adminSettings.getJsonValue();
+ ((ObjectNode) json).put("newKey", "my new value");
+ adminSettings.setJsonValue(json);
+ adminSettingsService.saveAdminSettings(adminSettings);
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveAdminSettingsWithNonTextValue() throws Exception {
+ AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey("mail");
+ JsonNode json = adminSettings.getJsonValue();
+ ((ObjectNode) json).put("timeout", 10000L);
+ adminSettings.setJsonValue(json);
+ adminSettingsService.saveAdminSettings(adminSettings);
+ }
+}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceImplTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceImplTest.java
new file mode 100644
index 0000000..f9df77f
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceImplTest.java
@@ -0,0 +1,249 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.service;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.lang3.RandomStringUtils;
+import org.junit.After;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.datastax.driver.core.utils.UUIDs;
+
+public class CustomerServiceImplTest extends AbstractServiceTest {
+
+ private IdComparator<Customer> idComparator = new IdComparator<>();
+
+ private TenantId tenantId;
+
+ @Before
+ public void before() {
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ Tenant savedTenant = tenantService.saveTenant(tenant);
+ Assert.assertNotNull(savedTenant);
+ tenantId = savedTenant.getId();
+ }
+
+ @After
+ public void after() {
+ tenantService.deleteTenant(tenantId);
+ }
+
+ @Test
+ public void testSaveCustomer() {
+ Customer customer = new Customer();
+ customer.setTenantId(tenantId);
+ customer.setTitle("My customer");
+ Customer savedCustomer = customerService.saveCustomer(customer);
+
+ Assert.assertNotNull(savedCustomer);
+ Assert.assertNotNull(savedCustomer.getId());
+ Assert.assertTrue(savedCustomer.getCreatedTime() > 0);
+ Assert.assertEquals(customer.getTenantId(), savedCustomer.getTenantId());
+ Assert.assertEquals(customer.getTitle(), savedCustomer.getTitle());
+
+
+ savedCustomer.setTitle("My new customer");
+
+ customerService.saveCustomer(savedCustomer);
+ Customer foundCustomer = customerService.findCustomerById(savedCustomer.getId());
+ Assert.assertEquals(foundCustomer.getTitle(), savedCustomer.getTitle());
+
+ customerService.deleteCustomer(savedCustomer.getId());
+ }
+
+ @Test
+ public void testFindCustomerById() {
+ Customer customer = new Customer();
+ customer.setTenantId(tenantId);
+ customer.setTitle("My customer");
+ Customer savedCustomer = customerService.saveCustomer(customer);
+ Customer foundCustomer = customerService.findCustomerById(savedCustomer.getId());
+ Assert.assertNotNull(foundCustomer);
+ Assert.assertEquals(savedCustomer, foundCustomer);
+ customerService.deleteCustomer(savedCustomer.getId());
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveCustomerWithEmptyTitle() {
+ Customer customer = new Customer();
+ customer.setTenantId(tenantId);
+ customerService.saveCustomer(customer);
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveCustomerWithEmptyTenant() {
+ Customer customer = new Customer();
+ customer.setTitle("My customer");
+ customerService.saveCustomer(customer);
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveCustomerWithInvalidTenant() {
+ Customer customer = new Customer();
+ customer.setTitle("My customer");
+ customer.setTenantId(new TenantId(UUIDs.timeBased()));
+ customerService.saveCustomer(customer);
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveCustomerWithInvalidEmail() {
+ Customer customer = new Customer();
+ customer.setTenantId(tenantId);
+ customer.setTitle("My customer");
+ customer.setEmail("invalid@mail");
+ customerService.saveCustomer(customer);
+ }
+
+ @Test
+ public void testDeleteCustomer() {
+ Customer customer = new Customer();
+ customer.setTitle("My customer");
+ customer.setTenantId(tenantId);
+ Customer savedCustomer = customerService.saveCustomer(customer);
+ customerService.deleteCustomer(savedCustomer.getId());
+ Customer foundCustomer = customerService.findCustomerById(savedCustomer.getId());
+ Assert.assertNull(foundCustomer);
+ }
+
+ @Test
+ public void testFindCustomersByTenantId() {
+ Tenant tenant = new Tenant();
+ tenant.setTitle("Test tenant");
+ tenant = tenantService.saveTenant(tenant);
+
+ TenantId tenantId = tenant.getId();
+
+ List<Customer> customers = new ArrayList<>();
+ for (int i=0;i<135;i++) {
+ Customer customer = new Customer();
+ customer.setTenantId(tenantId);
+ customer.setTitle("Customer"+i);
+ customers.add(customerService.saveCustomer(customer));
+ }
+
+ List<Customer> loadedCustomers = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(23);
+ TextPageData<Customer> pageData = null;
+ do {
+ pageData = customerService.findCustomersByTenantId(tenantId, pageLink);
+ loadedCustomers.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(customers, idComparator);
+ Collections.sort(loadedCustomers, idComparator);
+
+ Assert.assertEquals(customers, loadedCustomers);
+
+ customerService.deleteCustomersByTenantId(tenantId);
+
+ pageLink = new TextPageLink(33);
+ pageData = customerService.findCustomersByTenantId(tenantId, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertTrue(pageData.getData().isEmpty());
+
+ tenantService.deleteTenant(tenantId);
+ }
+
+ @Test
+ public void testFindCustomersByTenantIdAndTitle() {
+ String title1 = "Customer title 1";
+ List<Customer> customersTitle1 = new ArrayList<>();
+ for (int i=0;i<143;i++) {
+ Customer customer = new Customer();
+ customer.setTenantId(tenantId);
+ String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15));
+ String title = title1+suffix;
+ title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
+ customer.setTitle(title);
+ customersTitle1.add(customerService.saveCustomer(customer));
+ }
+ String title2 = "Customer title 2";
+ List<Customer> customersTitle2 = new ArrayList<>();
+ for (int i=0;i<175;i++) {
+ Customer customer = new Customer();
+ customer.setTenantId(tenantId);
+ String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15));
+ String title = title2+suffix;
+ title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
+ customer.setTitle(title);
+ customersTitle2.add(customerService.saveCustomer(customer));
+ }
+
+ List<Customer> loadedCustomersTitle1 = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(15, title1);
+ TextPageData<Customer> pageData = null;
+ do {
+ pageData = customerService.findCustomersByTenantId(tenantId, pageLink);
+ loadedCustomersTitle1.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(customersTitle1, idComparator);
+ Collections.sort(loadedCustomersTitle1, idComparator);
+
+ Assert.assertEquals(customersTitle1, loadedCustomersTitle1);
+
+ List<Customer> loadedCustomersTitle2 = new ArrayList<>();
+ pageLink = new TextPageLink(4, title2);
+ do {
+ pageData = customerService.findCustomersByTenantId(tenantId, pageLink);
+ loadedCustomersTitle2.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(customersTitle2, idComparator);
+ Collections.sort(loadedCustomersTitle2, idComparator);
+
+ Assert.assertEquals(customersTitle2, loadedCustomersTitle2);
+
+ for (Customer customer : loadedCustomersTitle1) {
+ customerService.deleteCustomer(customer.getId());
+ }
+
+ pageLink = new TextPageLink(4, title1);
+ pageData = customerService.findCustomersByTenantId(tenantId, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+
+ for (Customer customer : loadedCustomersTitle2) {
+ customerService.deleteCustomer(customer.getId());
+ }
+
+ pageLink = new TextPageLink(4, title2);
+ pageData = customerService.findCustomersByTenantId(tenantId, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+ }
+}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DashboardServiceImplTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DashboardServiceImplTest.java
new file mode 100644
index 0000000..842463c
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/DashboardServiceImplTest.java
@@ -0,0 +1,414 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.service;
+
+import com.datastax.driver.core.utils.UUIDs;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.Dashboard;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.model.ModelConstants;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class DashboardServiceImplTest extends AbstractServiceTest {
+
+ private IdComparator<Dashboard> idComparator = new IdComparator<>();
+
+ private TenantId tenantId;
+
+ @Before
+ public void before() {
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ Tenant savedTenant = tenantService.saveTenant(tenant);
+ Assert.assertNotNull(savedTenant);
+ tenantId = savedTenant.getId();
+ }
+
+ @After
+ public void after() {
+ tenantService.deleteTenant(tenantId);
+ }
+
+ @Test
+ public void testSaveDashboard() throws IOException {
+ Dashboard dashboard = new Dashboard();
+ dashboard.setTenantId(tenantId);
+ dashboard.setTitle("My dashboard");
+ Dashboard savedDashboard = dashboardService.saveDashboard(dashboard);
+
+ Assert.assertNotNull(savedDashboard);
+ Assert.assertNotNull(savedDashboard.getId());
+ Assert.assertTrue(savedDashboard.getCreatedTime() > 0);
+ Assert.assertEquals(dashboard.getTenantId(), savedDashboard.getTenantId());
+ Assert.assertNotNull(savedDashboard.getCustomerId());
+ Assert.assertEquals(ModelConstants.NULL_UUID, savedDashboard.getCustomerId().getId());
+ Assert.assertEquals(dashboard.getTitle(), savedDashboard.getTitle());
+
+ savedDashboard.setTitle("My new dashboard");
+
+ dashboardService.saveDashboard(savedDashboard);
+ Dashboard foundDashboard = dashboardService.findDashboardById(savedDashboard.getId());
+ Assert.assertEquals(foundDashboard.getTitle(), savedDashboard.getTitle());
+
+ dashboardService.deleteDashboard(savedDashboard.getId());
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveDashboardWithEmptyTitle() {
+ Dashboard dashboard = new Dashboard();
+ dashboard.setTenantId(tenantId);
+ dashboardService.saveDashboard(dashboard);
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveDashboardWithEmptyTenant() {
+ Dashboard dashboard = new Dashboard();
+ dashboard.setTitle("My dashboard");
+ dashboardService.saveDashboard(dashboard);
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveDashboardWithInvalidTenant() {
+ Dashboard dashboard = new Dashboard();
+ dashboard.setTitle("My dashboard");
+ dashboard.setTenantId(new TenantId(UUIDs.timeBased()));
+ dashboardService.saveDashboard(dashboard);
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testAssignDashboardToNonExistentCustomer() {
+ Dashboard dashboard = new Dashboard();
+ dashboard.setTitle("My dashboard");
+ dashboard.setTenantId(tenantId);
+ dashboard = dashboardService.saveDashboard(dashboard);
+ try {
+ dashboardService.assignDashboardToCustomer(dashboard.getId(), new CustomerId(UUIDs.timeBased()));
+ } finally {
+ dashboardService.deleteDashboard(dashboard.getId());
+ }
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testAssignDashboardToCustomerFromDifferentTenant() {
+ Dashboard dashboard = new Dashboard();
+ dashboard.setTitle("My dashboard");
+ dashboard.setTenantId(tenantId);
+ dashboard = dashboardService.saveDashboard(dashboard);
+ Tenant tenant = new Tenant();
+ tenant.setTitle("Test different tenant");
+ tenant = tenantService.saveTenant(tenant);
+ Customer customer = new Customer();
+ customer.setTenantId(tenant.getId());
+ customer.setTitle("Test different customer");
+ customer = customerService.saveCustomer(customer);
+ try {
+ dashboardService.assignDashboardToCustomer(dashboard.getId(), customer.getId());
+ } finally {
+ dashboardService.deleteDashboard(dashboard.getId());
+ tenantService.deleteTenant(tenant.getId());
+ }
+ }
+
+ @Test
+ public void testFindDashboardById() {
+ Dashboard dashboard = new Dashboard();
+ dashboard.setTenantId(tenantId);
+ dashboard.setTitle("My dashboard");
+ Dashboard savedDashboard = dashboardService.saveDashboard(dashboard);
+ Dashboard foundDashboard = dashboardService.findDashboardById(savedDashboard.getId());
+ Assert.assertNotNull(foundDashboard);
+ Assert.assertEquals(savedDashboard, foundDashboard);
+ dashboardService.deleteDashboard(savedDashboard.getId());
+ }
+
+ @Test
+ public void testDeleteDashboard() {
+ Dashboard dashboard = new Dashboard();
+ dashboard.setTenantId(tenantId);
+ dashboard.setTitle("My dashboard");
+ Dashboard savedDashboard = dashboardService.saveDashboard(dashboard);
+ Dashboard foundDashboard = dashboardService.findDashboardById(savedDashboard.getId());
+ Assert.assertNotNull(foundDashboard);
+ dashboardService.deleteDashboard(savedDashboard.getId());
+ foundDashboard = dashboardService.findDashboardById(savedDashboard.getId());
+ Assert.assertNull(foundDashboard);
+ }
+
+ @Test
+ public void testFindDashboardsByTenantId() {
+ Tenant tenant = new Tenant();
+ tenant.setTitle("Test tenant");
+ tenant = tenantService.saveTenant(tenant);
+
+ TenantId tenantId = tenant.getId();
+
+ List<Dashboard> dashboards = new ArrayList<>();
+ for (int i=0;i<165;i++) {
+ Dashboard dashboard = new Dashboard();
+ dashboard.setTenantId(tenantId);
+ dashboard.setTitle("Dashboard"+i);
+ dashboards.add(dashboardService.saveDashboard(dashboard));
+ }
+
+ List<Dashboard> loadedDashboards = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(16);
+ TextPageData<Dashboard> pageData = null;
+ do {
+ pageData = dashboardService.findDashboardsByTenantId(tenantId, pageLink);
+ loadedDashboards.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(dashboards, idComparator);
+ Collections.sort(loadedDashboards, idComparator);
+
+ Assert.assertEquals(dashboards, loadedDashboards);
+
+ dashboardService.deleteDashboardsByTenantId(tenantId);
+
+ pageLink = new TextPageLink(31);
+ pageData = dashboardService.findDashboardsByTenantId(tenantId, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertTrue(pageData.getData().isEmpty());
+
+ tenantService.deleteTenant(tenantId);
+ }
+
+ @Test
+ public void testFindDashboardsByTenantIdAndTitle() {
+ String title1 = "Dashboard title 1";
+ List<Dashboard> dashboardsTitle1 = new ArrayList<>();
+ for (int i=0;i<123;i++) {
+ Dashboard dashboard = new Dashboard();
+ dashboard.setTenantId(tenantId);
+ String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*17));
+ String title = title1+suffix;
+ title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
+ dashboard.setTitle(title);
+ dashboardsTitle1.add(dashboardService.saveDashboard(dashboard));
+ }
+ String title2 = "Dashboard title 2";
+ List<Dashboard> dashboardsTitle2 = new ArrayList<>();
+ for (int i=0;i<193;i++) {
+ Dashboard dashboard = new Dashboard();
+ dashboard.setTenantId(tenantId);
+ String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15));
+ String title = title2+suffix;
+ title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
+ dashboard.setTitle(title);
+ dashboardsTitle2.add(dashboardService.saveDashboard(dashboard));
+ }
+
+ List<Dashboard> loadedDashboardsTitle1 = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(19, title1);
+ TextPageData<Dashboard> pageData = null;
+ do {
+ pageData = dashboardService.findDashboardsByTenantId(tenantId, pageLink);
+ loadedDashboardsTitle1.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(dashboardsTitle1, idComparator);
+ Collections.sort(loadedDashboardsTitle1, idComparator);
+
+ Assert.assertEquals(dashboardsTitle1, loadedDashboardsTitle1);
+
+ List<Dashboard> loadedDashboardsTitle2 = new ArrayList<>();
+ pageLink = new TextPageLink(4, title2);
+ do {
+ pageData = dashboardService.findDashboardsByTenantId(tenantId, pageLink);
+ loadedDashboardsTitle2.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(dashboardsTitle2, idComparator);
+ Collections.sort(loadedDashboardsTitle2, idComparator);
+
+ Assert.assertEquals(dashboardsTitle2, loadedDashboardsTitle2);
+
+ for (Dashboard dashboard : loadedDashboardsTitle1) {
+ dashboardService.deleteDashboard(dashboard.getId());
+ }
+
+ pageLink = new TextPageLink(4, title1);
+ pageData = dashboardService.findDashboardsByTenantId(tenantId, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+
+ for (Dashboard dashboard : loadedDashboardsTitle2) {
+ dashboardService.deleteDashboard(dashboard.getId());
+ }
+
+ pageLink = new TextPageLink(4, title2);
+ pageData = dashboardService.findDashboardsByTenantId(tenantId, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+ }
+
+ @Test
+ public void testFindDashboardsByTenantIdAndCustomerId() {
+ Tenant tenant = new Tenant();
+ tenant.setTitle("Test tenant");
+ tenant = tenantService.saveTenant(tenant);
+
+ TenantId tenantId = tenant.getId();
+
+ Customer customer = new Customer();
+ customer.setTitle("Test customer");
+ customer.setTenantId(tenantId);
+ customer = customerService.saveCustomer(customer);
+ CustomerId customerId = customer.getId();
+
+ List<Dashboard> dashboards = new ArrayList<>();
+ for (int i=0;i<223;i++) {
+ Dashboard dashboard = new Dashboard();
+ dashboard.setTenantId(tenantId);
+ dashboard.setTitle("Dashboard"+i);
+ dashboard = dashboardService.saveDashboard(dashboard);
+ dashboards.add(dashboardService.assignDashboardToCustomer(dashboard.getId(), customerId));
+ }
+
+ List<Dashboard> loadedDashboards = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(23);
+ TextPageData<Dashboard> pageData = null;
+ do {
+ pageData = dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink);
+ loadedDashboards.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(dashboards, idComparator);
+ Collections.sort(loadedDashboards, idComparator);
+
+ Assert.assertEquals(dashboards, loadedDashboards);
+
+ dashboardService.unassignCustomerDashboards(tenantId, customerId);
+
+ pageLink = new TextPageLink(42);
+ pageData = dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertTrue(pageData.getData().isEmpty());
+
+ tenantService.deleteTenant(tenantId);
+ }
+
+ @Test
+ public void testFindDashboardsByTenantIdCustomerIdAndTitle() {
+
+ Customer customer = new Customer();
+ customer.setTitle("Test customer");
+ customer.setTenantId(tenantId);
+ customer = customerService.saveCustomer(customer);
+ CustomerId customerId = customer.getId();
+
+ String title1 = "Dashboard title 1";
+ List<Dashboard> dashboardsTitle1 = new ArrayList<>();
+ for (int i=0;i<124;i++) {
+ Dashboard dashboard = new Dashboard();
+ dashboard.setTenantId(tenantId);
+ String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15));
+ String title = title1+suffix;
+ title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
+ dashboard.setTitle(title);
+ dashboard = dashboardService.saveDashboard(dashboard);
+ dashboardsTitle1.add(dashboardService.assignDashboardToCustomer(dashboard.getId(), customerId));
+ }
+ String title2 = "Dashboard title 2";
+ List<Dashboard> dashboardsTitle2 = new ArrayList<>();
+ for (int i=0;i<151;i++) {
+ Dashboard dashboard = new Dashboard();
+ dashboard.setTenantId(tenantId);
+ String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15));
+ String title = title2+suffix;
+ title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
+ dashboard.setTitle(title);
+ dashboard = dashboardService.saveDashboard(dashboard);
+ dashboardsTitle2.add(dashboardService.assignDashboardToCustomer(dashboard.getId(), customerId));
+ }
+
+ List<Dashboard> loadedDashboardsTitle1 = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(24, title1);
+ TextPageData<Dashboard> pageData = null;
+ do {
+ pageData = dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink);
+ loadedDashboardsTitle1.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(dashboardsTitle1, idComparator);
+ Collections.sort(loadedDashboardsTitle1, idComparator);
+
+ Assert.assertEquals(dashboardsTitle1, loadedDashboardsTitle1);
+
+ List<Dashboard> loadedDashboardsTitle2 = new ArrayList<>();
+ pageLink = new TextPageLink(4, title2);
+ do {
+ pageData = dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink);
+ loadedDashboardsTitle2.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(dashboardsTitle2, idComparator);
+ Collections.sort(loadedDashboardsTitle2, idComparator);
+
+ Assert.assertEquals(dashboardsTitle2, loadedDashboardsTitle2);
+
+ for (Dashboard dashboard : loadedDashboardsTitle1) {
+ dashboardService.deleteDashboard(dashboard.getId());
+ }
+
+ pageLink = new TextPageLink(4, title1);
+ pageData = dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+
+ for (Dashboard dashboard : loadedDashboardsTitle2) {
+ dashboardService.deleteDashboard(dashboard.getId());
+ }
+
+ pageLink = new TextPageLink(4, title2);
+ pageData = dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+ customerService.deleteCustomer(customerId);
+ }
+}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceCredentialsCacheTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceCredentialsCacheTest.java
new file mode 100644
index 0000000..fd71fa4
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceCredentialsCacheTest.java
@@ -0,0 +1,163 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.service;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.hazelcast.core.HazelcastInstance;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.aop.framework.Advised;
+import org.springframework.aop.support.AopUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.thingsboard.server.common.data.CacheConstants;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.id.DeviceCredentialsId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.security.DeviceCredentials;
+import org.thingsboard.server.common.data.security.DeviceCredentialsType;
+import org.thingsboard.server.dao.device.DeviceCredentialsDao;
+import org.thingsboard.server.dao.device.DeviceCredentialsService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.model.DeviceCredentialsEntity;
+
+import java.util.UUID;
+
+import static org.mockito.Mockito.*;
+
+@TestPropertySource(properties = {"cache.enabled = true"})
+public class DeviceCredentialsCacheTest extends AbstractServiceTest {
+
+ private static final String CREDENTIALS_ID_1 = RandomStringUtils.randomAlphanumeric(20);
+ private static final String CREDENTIALS_ID_2 = RandomStringUtils.randomAlphanumeric(20);
+
+ @Autowired
+ private DeviceCredentialsService deviceCredentialsService;
+
+ private DeviceCredentialsDao deviceCredentialsDao;
+ private DeviceService deviceService;
+
+ @Autowired
+ private HazelcastInstance hazelcastInstance;
+
+ private UUID deviceId = UUIDs.timeBased();
+
+ @Before
+ public void setup() throws Exception {
+ deviceCredentialsDao = mock(DeviceCredentialsDao.class);
+ deviceService = mock(DeviceService.class);
+ ReflectionTestUtils.setField(unwrapDeviceCredentialsService(), "deviceCredentialsDao", deviceCredentialsDao);
+ ReflectionTestUtils.setField(unwrapDeviceCredentialsService(), "deviceService", deviceService);
+ }
+
+ @After
+ public void cleanup() {
+ hazelcastInstance.getMap(CacheConstants.DEVICE_CREDENTIALS_CACHE).evictAll();
+ }
+
+ @Test
+ public void testFindDeviceCredentialsByCredentialsId_Cached() {
+ when(deviceCredentialsDao.findByCredentialsId(CREDENTIALS_ID_1)).thenReturn(createDummyDeviceCredentialsEntity(CREDENTIALS_ID_1));
+
+ deviceCredentialsService.findDeviceCredentialsByCredentialsId(CREDENTIALS_ID_1);
+ deviceCredentialsService.findDeviceCredentialsByCredentialsId(CREDENTIALS_ID_1);
+
+ Assert.assertEquals(1, hazelcastInstance.getMap(CacheConstants.DEVICE_CREDENTIALS_CACHE).size());
+ verify(deviceCredentialsDao, times(1)).findByCredentialsId(CREDENTIALS_ID_1);
+ }
+
+ @Test
+ public void testDeleteDeviceCredentials_EvictsCache() {
+ when(deviceCredentialsDao.findByCredentialsId(CREDENTIALS_ID_1)).thenReturn(createDummyDeviceCredentialsEntity(CREDENTIALS_ID_1));
+
+ deviceCredentialsService.findDeviceCredentialsByCredentialsId(CREDENTIALS_ID_1);
+ deviceCredentialsService.findDeviceCredentialsByCredentialsId(CREDENTIALS_ID_1);
+
+ Assert.assertEquals(1, hazelcastInstance.getMap(CacheConstants.DEVICE_CREDENTIALS_CACHE).size());
+ verify(deviceCredentialsDao, times(1)).findByCredentialsId(CREDENTIALS_ID_1);
+
+ deviceCredentialsService.deleteDeviceCredentials(createDummyDeviceCredentials(CREDENTIALS_ID_1, deviceId));
+
+ Assert.assertEquals(0, hazelcastInstance.getMap(CacheConstants.DEVICE_CREDENTIALS_CACHE).size());
+
+ deviceCredentialsService.findDeviceCredentialsByCredentialsId(CREDENTIALS_ID_1);
+ deviceCredentialsService.findDeviceCredentialsByCredentialsId(CREDENTIALS_ID_1);
+
+ Assert.assertEquals(1, hazelcastInstance.getMap(CacheConstants.DEVICE_CREDENTIALS_CACHE).size());
+ verify(deviceCredentialsDao, times(2)).findByCredentialsId(CREDENTIALS_ID_1);
+ }
+
+ @Test
+ public void testSaveDeviceCredentials_EvictsPreviousCache() throws Exception {
+ when(deviceCredentialsDao.findByCredentialsId(CREDENTIALS_ID_1)).thenReturn(createDummyDeviceCredentialsEntity(CREDENTIALS_ID_1));
+
+ deviceCredentialsService.findDeviceCredentialsByCredentialsId(CREDENTIALS_ID_1);
+ deviceCredentialsService.findDeviceCredentialsByCredentialsId(CREDENTIALS_ID_1);
+
+ Assert.assertEquals(1, hazelcastInstance.getMap(CacheConstants.DEVICE_CREDENTIALS_CACHE).size());
+ verify(deviceCredentialsDao, times(1)).findByCredentialsId(CREDENTIALS_ID_1);
+
+ when(deviceCredentialsDao.findByDeviceId(deviceId)).thenReturn(createDummyDeviceCredentialsEntity(CREDENTIALS_ID_1));
+
+ UUID deviceCredentialsId = UUIDs.timeBased();
+ when(deviceCredentialsDao.findById(deviceCredentialsId)).thenReturn(createDummyDeviceCredentialsEntity(CREDENTIALS_ID_1));
+ when(deviceService.findDeviceById(new DeviceId(deviceId))).thenReturn(new Device());
+
+ deviceCredentialsService.updateDeviceCredentials(createDummyDeviceCredentials(deviceCredentialsId, CREDENTIALS_ID_2, deviceId));
+ Assert.assertEquals(0, hazelcastInstance.getMap(CacheConstants.DEVICE_CREDENTIALS_CACHE).size());
+
+ when(deviceCredentialsDao.findByCredentialsId(CREDENTIALS_ID_1)).thenReturn(null);
+
+ deviceCredentialsService.findDeviceCredentialsByCredentialsId(CREDENTIALS_ID_1);
+ deviceCredentialsService.findDeviceCredentialsByCredentialsId(CREDENTIALS_ID_1);
+ Assert.assertEquals(0, hazelcastInstance.getMap(CacheConstants.DEVICE_CREDENTIALS_CACHE).size());
+
+ verify(deviceCredentialsDao, times(3)).findByCredentialsId(CREDENTIALS_ID_1);
+ }
+
+ private DeviceCredentialsService unwrapDeviceCredentialsService() throws Exception {
+ if (AopUtils.isAopProxy(deviceCredentialsService) && deviceCredentialsService instanceof Advised) {
+ Object target = ((Advised) deviceCredentialsService).getTargetSource().getTarget();
+ return (DeviceCredentialsService) target;
+ }
+ return null;
+ }
+
+ private DeviceCredentialsEntity createDummyDeviceCredentialsEntity(String deviceCredentialsId) {
+ DeviceCredentialsEntity result = new DeviceCredentialsEntity();
+ result.setId(UUIDs.timeBased());
+ result.setCredentialsId(deviceCredentialsId);
+ return result;
+ }
+
+ private DeviceCredentials createDummyDeviceCredentials(String deviceCredentialsId, UUID deviceId) {
+ return createDummyDeviceCredentials(null, deviceCredentialsId, deviceId);
+ }
+
+ private DeviceCredentials createDummyDeviceCredentials(UUID id, String deviceCredentialsId, UUID deviceId) {
+ DeviceCredentials result = new DeviceCredentials();
+ result.setId(new DeviceCredentialsId(id));
+ result.setDeviceId(new DeviceId(deviceId));
+ result.setCredentialsId(deviceCredentialsId);
+ result.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN);
+ return result;
+ }
+}
+
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceCredentialsServiceImplTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceCredentialsServiceImplTest.java
new file mode 100644
index 0000000..d7dd552
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceCredentialsServiceImplTest.java
@@ -0,0 +1,195 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.service;
+
+import com.datastax.driver.core.utils.UUIDs;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.id.DeviceCredentialsId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.security.DeviceCredentials;
+import org.thingsboard.server.common.data.security.DeviceCredentialsType;
+import org.thingsboard.server.dao.exception.DataValidationException;
+
+public class DeviceCredentialsServiceImplTest extends AbstractServiceTest {
+
+ private TenantId tenantId;
+
+ @Before
+ public void before() {
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ Tenant savedTenant = tenantService.saveTenant(tenant);
+ Assert.assertNotNull(savedTenant);
+ tenantId = savedTenant.getId();
+ }
+
+ @After
+ public void after() {
+ tenantService.deleteTenant(tenantId);
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testCreateDeviceCredentials() {
+ DeviceCredentials deviceCredentials = new DeviceCredentials();
+ deviceCredentialsService.updateDeviceCredentials(deviceCredentials);
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveDeviceCredentialsWithEmptyDevice() {
+ Device device = new Device();
+ device.setName("My device");
+ device.setTenantId(tenantId);
+ device = deviceService.saveDevice(device);
+ DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getId());
+ deviceCredentials.setDeviceId(null);
+ try {
+ deviceCredentialsService.updateDeviceCredentials(deviceCredentials);
+ } finally {
+ deviceService.deleteDevice(device.getId());
+ }
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveDeviceCredentialsWithEmptyCredentialsType() {
+ Device device = new Device();
+ device.setName("My device");
+ device.setTenantId(tenantId);
+ device = deviceService.saveDevice(device);
+ DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getId());
+ deviceCredentials.setCredentialsType(null);
+ try {
+ deviceCredentialsService.updateDeviceCredentials(deviceCredentials);
+ } finally {
+ deviceService.deleteDevice(device.getId());
+ }
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveDeviceCredentialsWithEmptyCredentialsId() {
+ Device device = new Device();
+ device.setName("My device");
+ device.setTenantId(tenantId);
+ device = deviceService.saveDevice(device);
+ DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getId());
+ deviceCredentials.setCredentialsId(null);
+ try {
+ deviceCredentialsService.updateDeviceCredentials(deviceCredentials);
+ } finally {
+ deviceService.deleteDevice(device.getId());
+ }
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveNonExistentDeviceCredentials() {
+ Device device = new Device();
+ device.setName("My device");
+ device.setTenantId(tenantId);
+ device = deviceService.saveDevice(device);
+ DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getId());
+ DeviceCredentials newDeviceCredentials = new DeviceCredentials(new DeviceCredentialsId(UUIDs.timeBased()));
+ newDeviceCredentials.setCreatedTime(deviceCredentials.getCreatedTime());
+ newDeviceCredentials.setDeviceId(deviceCredentials.getDeviceId());
+ newDeviceCredentials.setCredentialsType(deviceCredentials.getCredentialsType());
+ newDeviceCredentials.setCredentialsId(deviceCredentials.getCredentialsId());
+ try {
+ deviceCredentialsService.updateDeviceCredentials(newDeviceCredentials);
+ } finally {
+ deviceService.deleteDevice(device.getId());
+ }
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveDeviceCredentialsWithNonExistentDevice() {
+ Device device = new Device();
+ device.setName("My device");
+ device.setTenantId(tenantId);
+ device = deviceService.saveDevice(device);
+ DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getId());
+ deviceCredentials.setDeviceId(new DeviceId(UUIDs.timeBased()));
+ try {
+ deviceCredentialsService.updateDeviceCredentials(deviceCredentials);
+ } finally {
+ deviceService.deleteDevice(device.getId());
+ }
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveDeviceCredentialsWithInvalidCredemtialsIdLength() {
+ Device device = new Device();
+ device.setName("My device");
+ device.setTenantId(tenantId);
+ device = deviceService.saveDevice(device);
+ DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getId());
+ deviceCredentials.setCredentialsId(RandomStringUtils.randomAlphanumeric(21));
+ try {
+ deviceCredentialsService.updateDeviceCredentials(deviceCredentials);
+ } finally {
+ deviceService.deleteDevice(device.getId());
+ }
+ }
+
+ @Test
+ public void testFindDeviceCredentialsByDeviceId() {
+ Device device = new Device();
+ device.setTenantId(tenantId);
+ device.setName("My device");
+ Device savedDevice = deviceService.saveDevice(device);
+ DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedDevice.getId());
+ Assert.assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
+ deviceService.deleteDevice(savedDevice.getId());
+ deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedDevice.getId());
+ Assert.assertNull(deviceCredentials);
+ }
+
+ @Test
+ public void testFindDeviceCredentialsByCredentialsId() {
+ Device device = new Device();
+ device.setTenantId(tenantId);
+ device.setName("My device");
+ Device savedDevice = deviceService.saveDevice(device);
+ DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedDevice.getId());
+ Assert.assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
+ DeviceCredentials foundDeviceCredentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(deviceCredentials.getCredentialsId());
+ Assert.assertEquals(deviceCredentials, foundDeviceCredentials);
+ deviceService.deleteDevice(savedDevice.getId());
+ foundDeviceCredentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(deviceCredentials.getCredentialsId());
+ Assert.assertNull(foundDeviceCredentials);
+ }
+
+ @Test
+ public void testSaveDeviceCredentials() {
+ Device device = new Device();
+ device.setTenantId(tenantId);
+ device.setName("My device");
+ Device savedDevice = deviceService.saveDevice(device);
+ DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedDevice.getId());
+ Assert.assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
+ deviceCredentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN);
+ deviceCredentials.setCredentialsId("access_token");
+ deviceCredentialsService.updateDeviceCredentials(deviceCredentials);
+ DeviceCredentials foundDeviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedDevice.getId());
+ Assert.assertEquals(deviceCredentials, foundDeviceCredentials);
+ deviceService.deleteDevice(savedDevice.getId());
+ }
+}
+
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceImplTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceImplTest.java
new file mode 100644
index 0000000..f4eb22b
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceImplTest.java
@@ -0,0 +1,428 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.service;
+
+import com.datastax.driver.core.utils.UUIDs;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceCredentialsId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.security.DeviceCredentials;
+import org.thingsboard.server.common.data.security.DeviceCredentialsType;
+import org.thingsboard.server.dao.exception.DataValidationException;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
+
+public class DeviceServiceImplTest extends AbstractServiceTest {
+
+ private IdComparator<Device> idComparator = new IdComparator<>();
+
+ private TenantId tenantId;
+
+ @Before
+ public void before() {
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ Tenant savedTenant = tenantService.saveTenant(tenant);
+ Assert.assertNotNull(savedTenant);
+ tenantId = savedTenant.getId();
+ }
+
+ @After
+ public void after() {
+ tenantService.deleteTenant(tenantId);
+ }
+
+ @Test
+ public void testSaveDevice() {
+ Device device = new Device();
+ device.setTenantId(tenantId);
+ device.setName("My device");
+ Device savedDevice = deviceService.saveDevice(device);
+
+ Assert.assertNotNull(savedDevice);
+ Assert.assertNotNull(savedDevice.getId());
+ Assert.assertTrue(savedDevice.getCreatedTime() > 0);
+ Assert.assertEquals(device.getTenantId(), savedDevice.getTenantId());
+ Assert.assertNotNull(savedDevice.getCustomerId());
+ Assert.assertEquals(NULL_UUID, savedDevice.getCustomerId().getId());
+ Assert.assertEquals(device.getName(), savedDevice.getName());
+
+ DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedDevice.getId());
+ Assert.assertNotNull(deviceCredentials);
+ Assert.assertNotNull(deviceCredentials.getId());
+ Assert.assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
+ Assert.assertEquals(DeviceCredentialsType.ACCESS_TOKEN, deviceCredentials.getCredentialsType());
+ Assert.assertNotNull(deviceCredentials.getCredentialsId());
+ Assert.assertEquals(20, deviceCredentials.getCredentialsId().length());
+
+ savedDevice.setName("My new device");
+
+ deviceService.saveDevice(savedDevice);
+ Device foundDevice = deviceService.findDeviceById(savedDevice.getId());
+ Assert.assertEquals(foundDevice.getName(), savedDevice.getName());
+
+ deviceService.deleteDevice(savedDevice.getId());
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveDeviceWithEmptyName() {
+ Device device = new Device();
+ device.setTenantId(tenantId);
+ deviceService.saveDevice(device);
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveDeviceWithEmptyTenant() {
+ Device device = new Device();
+ device.setName("My device");
+ deviceService.saveDevice(device);
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveDeviceWithInvalidTenant() {
+ Device device = new Device();
+ device.setName("My device");
+ device.setTenantId(new TenantId(UUIDs.timeBased()));
+ deviceService.saveDevice(device);
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testAssignDeviceToNonExistentCustomer() {
+ Device device = new Device();
+ device.setName("My device");
+ device.setTenantId(tenantId);
+ device = deviceService.saveDevice(device);
+ try {
+ deviceService.assignDeviceToCustomer(device.getId(), new CustomerId(UUIDs.timeBased()));
+ } finally {
+ deviceService.deleteDevice(device.getId());
+ }
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testAssignDeviceToCustomerFromDifferentTenant() {
+ Device device = new Device();
+ device.setName("My device");
+ device.setTenantId(tenantId);
+ device = deviceService.saveDevice(device);
+ Tenant tenant = new Tenant();
+ tenant.setTitle("Test different tenant");
+ tenant = tenantService.saveTenant(tenant);
+ Customer customer = new Customer();
+ customer.setTenantId(tenant.getId());
+ customer.setTitle("Test different customer");
+ customer = customerService.saveCustomer(customer);
+ try {
+ deviceService.assignDeviceToCustomer(device.getId(), customer.getId());
+ } finally {
+ deviceService.deleteDevice(device.getId());
+ tenantService.deleteTenant(tenant.getId());
+ }
+ }
+
+ @Test
+ public void testFindDeviceById() {
+ Device device = new Device();
+ device.setTenantId(tenantId);
+ device.setName("My device");
+ Device savedDevice = deviceService.saveDevice(device);
+ Device foundDevice = deviceService.findDeviceById(savedDevice.getId());
+ Assert.assertNotNull(foundDevice);
+ Assert.assertEquals(savedDevice, foundDevice);
+ deviceService.deleteDevice(savedDevice.getId());
+ }
+
+ @Test
+ public void testDeleteDevice() {
+ Device device = new Device();
+ device.setTenantId(tenantId);
+ device.setName("My device");
+ Device savedDevice = deviceService.saveDevice(device);
+ Device foundDevice = deviceService.findDeviceById(savedDevice.getId());
+ Assert.assertNotNull(foundDevice);
+ deviceService.deleteDevice(savedDevice.getId());
+ foundDevice = deviceService.findDeviceById(savedDevice.getId());
+ Assert.assertNull(foundDevice);
+ DeviceCredentials foundDeviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedDevice.getId());
+ Assert.assertNull(foundDeviceCredentials);
+ }
+
+ @Test
+ public void testFindDevicesByTenantId() {
+ Tenant tenant = new Tenant();
+ tenant.setTitle("Test tenant");
+ tenant = tenantService.saveTenant(tenant);
+
+ TenantId tenantId = tenant.getId();
+
+ List<Device> devices = new ArrayList<>();
+ for (int i=0;i<178;i++) {
+ Device device = new Device();
+ device.setTenantId(tenantId);
+ device.setName("Device"+i);
+ devices.add(deviceService.saveDevice(device));
+ }
+
+ List<Device> loadedDevices = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(23);
+ TextPageData<Device> pageData = null;
+ do {
+ pageData = deviceService.findDevicesByTenantId(tenantId, pageLink);
+ loadedDevices.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(devices, idComparator);
+ Collections.sort(loadedDevices, idComparator);
+
+ Assert.assertEquals(devices, loadedDevices);
+
+ deviceService.deleteDevicesByTenantId(tenantId);
+
+ pageLink = new TextPageLink(33);
+ pageData = deviceService.findDevicesByTenantId(tenantId, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertTrue(pageData.getData().isEmpty());
+
+ tenantService.deleteTenant(tenantId);
+ }
+
+ @Test
+ public void testFindDevicesByTenantIdAndName() {
+ String title1 = "Device title 1";
+ List<Device> devicesTitle1 = new ArrayList<>();
+ for (int i=0;i<143;i++) {
+ Device device = new Device();
+ device.setTenantId(tenantId);
+ String suffix = RandomStringUtils.randomAlphanumeric(15);
+ String name = title1+suffix;
+ name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+ device.setName(name);
+ devicesTitle1.add(deviceService.saveDevice(device));
+ }
+ String title2 = "Device title 2";
+ List<Device> devicesTitle2 = new ArrayList<>();
+ for (int i=0;i<175;i++) {
+ Device device = new Device();
+ device.setTenantId(tenantId);
+ String suffix = RandomStringUtils.randomAlphanumeric(15);
+ String name = title2+suffix;
+ name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+ device.setName(name);
+ devicesTitle2.add(deviceService.saveDevice(device));
+ }
+
+ List<Device> loadedDevicesTitle1 = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(15, title1);
+ TextPageData<Device> pageData = null;
+ do {
+ pageData = deviceService.findDevicesByTenantId(tenantId, pageLink);
+ loadedDevicesTitle1.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(devicesTitle1, idComparator);
+ Collections.sort(loadedDevicesTitle1, idComparator);
+
+ Assert.assertEquals(devicesTitle1, loadedDevicesTitle1);
+
+ List<Device> loadedDevicesTitle2 = new ArrayList<>();
+ pageLink = new TextPageLink(4, title2);
+ do {
+ pageData = deviceService.findDevicesByTenantId(tenantId, pageLink);
+ loadedDevicesTitle2.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(devicesTitle2, idComparator);
+ Collections.sort(loadedDevicesTitle2, idComparator);
+
+ Assert.assertEquals(devicesTitle2, loadedDevicesTitle2);
+
+ for (Device device : loadedDevicesTitle1) {
+ deviceService.deleteDevice(device.getId());
+ }
+
+ pageLink = new TextPageLink(4, title1);
+ pageData = deviceService.findDevicesByTenantId(tenantId, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+
+ for (Device device : loadedDevicesTitle2) {
+ deviceService.deleteDevice(device.getId());
+ }
+
+ pageLink = new TextPageLink(4, title2);
+ pageData = deviceService.findDevicesByTenantId(tenantId, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+ }
+
+ @Test
+ public void testFindDevicesByTenantIdAndCustomerId() {
+ Tenant tenant = new Tenant();
+ tenant.setTitle("Test tenant");
+ tenant = tenantService.saveTenant(tenant);
+
+ TenantId tenantId = tenant.getId();
+
+ Customer customer = new Customer();
+ customer.setTitle("Test customer");
+ customer.setTenantId(tenantId);
+ customer = customerService.saveCustomer(customer);
+ CustomerId customerId = customer.getId();
+
+ List<Device> devices = new ArrayList<>();
+ for (int i=0;i<278;i++) {
+ Device device = new Device();
+ device.setTenantId(tenantId);
+ device.setName("Device"+i);
+ device = deviceService.saveDevice(device);
+ devices.add(deviceService.assignDeviceToCustomer(device.getId(), customerId));
+ }
+
+ List<Device> loadedDevices = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(23);
+ TextPageData<Device> pageData = null;
+ do {
+ pageData = deviceService.findDevicesByTenantIdAndCustomerId(tenantId, customerId, pageLink);
+ loadedDevices.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(devices, idComparator);
+ Collections.sort(loadedDevices, idComparator);
+
+ Assert.assertEquals(devices, loadedDevices);
+
+ deviceService.unassignCustomerDevices(tenantId, customerId);
+
+ pageLink = new TextPageLink(33);
+ pageData = deviceService.findDevicesByTenantIdAndCustomerId(tenantId, customerId, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertTrue(pageData.getData().isEmpty());
+
+ tenantService.deleteTenant(tenantId);
+ }
+
+ @Test
+ public void testFindDevicesByTenantIdCustomerIdAndName() {
+
+ Customer customer = new Customer();
+ customer.setTitle("Test customer");
+ customer.setTenantId(tenantId);
+ customer = customerService.saveCustomer(customer);
+ CustomerId customerId = customer.getId();
+
+ String title1 = "Device title 1";
+ List<Device> devicesTitle1 = new ArrayList<>();
+ for (int i=0;i<175;i++) {
+ Device device = new Device();
+ device.setTenantId(tenantId);
+ String suffix = RandomStringUtils.randomAlphanumeric(15);
+ String name = title1+suffix;
+ name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+ device.setName(name);
+ device = deviceService.saveDevice(device);
+ devicesTitle1.add(deviceService.assignDeviceToCustomer(device.getId(), customerId));
+ }
+ String title2 = "Device title 2";
+ List<Device> devicesTitle2 = new ArrayList<>();
+ for (int i=0;i<143;i++) {
+ Device device = new Device();
+ device.setTenantId(tenantId);
+ String suffix = RandomStringUtils.randomAlphanumeric(15);
+ String name = title2+suffix;
+ name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+ device.setName(name);
+ device = deviceService.saveDevice(device);
+ devicesTitle2.add(deviceService.assignDeviceToCustomer(device.getId(), customerId));
+ }
+
+ List<Device> loadedDevicesTitle1 = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(15, title1);
+ TextPageData<Device> pageData = null;
+ do {
+ pageData = deviceService.findDevicesByTenantIdAndCustomerId(tenantId, customerId, pageLink);
+ loadedDevicesTitle1.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(devicesTitle1, idComparator);
+ Collections.sort(loadedDevicesTitle1, idComparator);
+
+ Assert.assertEquals(devicesTitle1, loadedDevicesTitle1);
+
+ List<Device> loadedDevicesTitle2 = new ArrayList<>();
+ pageLink = new TextPageLink(4, title2);
+ do {
+ pageData = deviceService.findDevicesByTenantIdAndCustomerId(tenantId, customerId, pageLink);
+ loadedDevicesTitle2.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(devicesTitle2, idComparator);
+ Collections.sort(loadedDevicesTitle2, idComparator);
+
+ Assert.assertEquals(devicesTitle2, loadedDevicesTitle2);
+
+ for (Device device : loadedDevicesTitle1) {
+ deviceService.deleteDevice(device.getId());
+ }
+
+ pageLink = new TextPageLink(4, title1);
+ pageData = deviceService.findDevicesByTenantIdAndCustomerId(tenantId, customerId, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+
+ for (Device device : loadedDevicesTitle2) {
+ deviceService.deleteDevice(device.getId());
+ }
+
+ pageLink = new TextPageLink(4, title2);
+ pageData = deviceService.findDevicesByTenantIdAndCustomerId(tenantId, customerId, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+ customerService.deleteCustomer(customerId);
+ }
+}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/TenantServiceImplTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/TenantServiceImplTest.java
new file mode 100644
index 0000000..074afe5
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/TenantServiceImplTest.java
@@ -0,0 +1,203 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.service;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.lang3.RandomStringUtils;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TenantServiceImplTest extends AbstractServiceTest {
+
+ private IdComparator<Tenant> idComparator = new IdComparator<>();
+
+ @Test
+ public void testSaveTenant() {
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ Tenant savedTenant = tenantService.saveTenant(tenant);
+ Assert.assertNotNull(savedTenant);
+ Assert.assertNotNull(savedTenant.getId());
+ Assert.assertTrue(savedTenant.getCreatedTime() > 0);
+ Assert.assertEquals(tenant.getTitle(), savedTenant.getTitle());
+
+ savedTenant.setTitle("My new tenant");
+ tenantService.saveTenant(savedTenant);
+ Tenant foundTenant = tenantService.findTenantById(savedTenant.getId());
+ Assert.assertEquals(foundTenant.getTitle(), savedTenant.getTitle());
+
+ tenantService.deleteTenant(savedTenant.getId());
+ }
+
+ @Test
+ public void testFindTenantById() {
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ Tenant savedTenant = tenantService.saveTenant(tenant);
+ Tenant foundTenant = tenantService.findTenantById(savedTenant.getId());
+ Assert.assertNotNull(foundTenant);
+ Assert.assertEquals(savedTenant, foundTenant);
+ tenantService.deleteTenant(savedTenant.getId());
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveTenantWithEmptyTitle() {
+ Tenant tenant = new Tenant();
+ tenantService.saveTenant(tenant);
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveTenantWithInvalidEmail() {
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ tenant.setEmail("invalid@mail");
+ tenantService.saveTenant(tenant);
+ }
+
+ @Test
+ public void testDeleteTenant() {
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ Tenant savedTenant = tenantService.saveTenant(tenant);
+ tenantService.deleteTenant(savedTenant.getId());
+ Tenant foundTenant = tenantService.findTenantById(savedTenant.getId());
+ Assert.assertNull(foundTenant);
+ }
+
+ @Test
+ public void testFindTenants() {
+
+ List<Tenant> tenants = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(17);
+ TextPageData<Tenant> pageData = tenantService.findTenants(pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertTrue(pageData.getData().isEmpty());
+ tenants.addAll(pageData.getData());
+
+ for (int i=0;i<156;i++) {
+ Tenant tenant = new Tenant();
+ tenant.setTitle("Tenant"+i);
+ tenants.add(tenantService.saveTenant(tenant));
+ }
+
+ List<Tenant> loadedTenants = new ArrayList<>();
+ pageLink = new TextPageLink(17);
+ do {
+ pageData = tenantService.findTenants(pageLink);
+ loadedTenants.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(tenants, idComparator);
+ Collections.sort(loadedTenants, idComparator);
+
+ Assert.assertEquals(tenants, loadedTenants);
+
+ for (Tenant tenant : loadedTenants) {
+ if (!tenant.getTitle().equals("Tenant")) {
+ tenantService.deleteTenant(tenant.getId());
+ }
+ }
+
+ pageLink = new TextPageLink(17);
+ pageData = tenantService.findTenants(pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertTrue(pageData.getData().isEmpty());
+
+ }
+
+ @Test
+ public void testFindTenantsByTitle() {
+ String title1 = "Tenant title 1";
+ List<Tenant> tenantsTitle1 = new ArrayList<>();
+ for (int i=0;i<134;i++) {
+ Tenant tenant = new Tenant();
+ String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15));
+ String title = title1+suffix;
+ title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
+ tenant.setTitle(title);
+ tenantsTitle1.add(tenantService.saveTenant(tenant));
+ }
+ String title2 = "Tenant title 2";
+ List<Tenant> tenantsTitle2 = new ArrayList<>();
+ for (int i=0;i<127;i++) {
+ Tenant tenant = new Tenant();
+ String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15));
+ String title = title2+suffix;
+ title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
+ tenant.setTitle(title);
+ tenantsTitle2.add(tenantService.saveTenant(tenant));
+ }
+
+ List<Tenant> loadedTenantsTitle1 = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(15, title1);
+ TextPageData<Tenant> pageData = null;
+ do {
+ pageData = tenantService.findTenants(pageLink);
+ loadedTenantsTitle1.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(tenantsTitle1, idComparator);
+ Collections.sort(loadedTenantsTitle1, idComparator);
+
+ Assert.assertEquals(tenantsTitle1, loadedTenantsTitle1);
+
+ List<Tenant> loadedTenantsTitle2 = new ArrayList<>();
+ pageLink = new TextPageLink(4, title2);
+ do {
+ pageData = tenantService.findTenants(pageLink);
+ loadedTenantsTitle2.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(tenantsTitle2, idComparator);
+ Collections.sort(loadedTenantsTitle2, idComparator);
+
+ Assert.assertEquals(tenantsTitle2, loadedTenantsTitle2);
+
+ for (Tenant tenant : loadedTenantsTitle1) {
+ tenantService.deleteTenant(tenant.getId());
+ }
+
+ pageLink = new TextPageLink(4, title1);
+ pageData = tenantService.findTenants(pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+
+ for (Tenant tenant : loadedTenantsTitle2) {
+ tenantService.deleteTenant(tenant.getId());
+ }
+
+ pageLink = new TextPageLink(4, title2);
+ pageData = tenantService.findTenants(pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+ }
+}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/UserServiceImplTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/UserServiceImplTest.java
new file mode 100644
index 0000000..6b2f37c
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/UserServiceImplTest.java
@@ -0,0 +1,477 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.service;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.lang3.RandomStringUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.common.data.security.UserCredentials;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class UserServiceImplTest extends AbstractServiceTest {
+
+ private IdComparator<User> idComparator = new IdComparator<>();
+
+ private TenantId tenantId;
+
+ @Before
+ public void before() {
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ Tenant savedTenant = tenantService.saveTenant(tenant);
+ Assert.assertNotNull(savedTenant);
+ tenantId = savedTenant.getId();
+
+ User tenantAdmin = new User();
+ tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+ tenantAdmin.setTenantId(tenantId);
+ tenantAdmin.setEmail("tenant@thingsboard.org");
+ userService.saveUser(tenantAdmin);
+
+ Customer customer = new Customer();
+ customer.setTenantId(tenantId);
+ customer.setTitle("My customer");
+ Customer savedCustomer = customerService.saveCustomer(customer);
+
+ User customerUser = new User();
+ customerUser.setAuthority(Authority.CUSTOMER_USER);
+ customerUser.setTenantId(tenantId);
+ customerUser.setCustomerId(savedCustomer.getId());
+ customerUser.setEmail("customer@thingsboard.org");
+ userService.saveUser(customerUser);
+ }
+
+ @After
+ public void after() {
+ tenantService.deleteTenant(tenantId);
+ }
+
+ @Test
+ public void testFindUserByEmail() {
+ User user = userService.findUserByEmail("sysadmin@thingsboard.org");
+ Assert.assertNotNull(user);
+ Assert.assertEquals(Authority.SYS_ADMIN, user.getAuthority());
+ user = userService.findUserByEmail("tenant@thingsboard.org");
+ Assert.assertNotNull(user);
+ Assert.assertEquals(Authority.TENANT_ADMIN, user.getAuthority());
+ user = userService.findUserByEmail("customer@thingsboard.org");
+ Assert.assertNotNull(user);
+ Assert.assertEquals(Authority.CUSTOMER_USER, user.getAuthority());
+ user = userService.findUserByEmail("fake@thingsboard.org");
+ Assert.assertNull(user);
+ }
+
+ @Test
+ public void testFindUserById() {
+ User user = userService.findUserByEmail("sysadmin@thingsboard.org");
+ Assert.assertNotNull(user);
+ User foundUser = userService.findUserById(user.getId());
+ Assert.assertNotNull(foundUser);
+ Assert.assertEquals(user, foundUser);
+ }
+
+ @Test
+ public void testFindUserCredentials() {
+ User user = userService.findUserByEmail("sysadmin@thingsboard.org");
+ Assert.assertNotNull(user);
+ UserCredentials userCredentials = userService.findUserCredentialsByUserId(user.getId());
+ Assert.assertNotNull(userCredentials);
+ }
+
+ @Test
+ public void testSaveUser() {
+ User tenantAdminUser = userService.findUserByEmail("tenant@thingsboard.org");
+ User user = new User();
+ user.setAuthority(Authority.TENANT_ADMIN);
+ user.setTenantId(tenantAdminUser.getTenantId());
+ user.setEmail("tenant2@thingsboard.org");
+ User savedUser = userService.saveUser(user);
+ Assert.assertNotNull(savedUser);
+ Assert.assertNotNull(savedUser.getId());
+ Assert.assertTrue(savedUser.getCreatedTime() > 0);
+ Assert.assertEquals(user.getEmail(), savedUser.getEmail());
+ Assert.assertEquals(user.getTenantId(), savedUser.getTenantId());
+ Assert.assertEquals(user.getAuthority(), savedUser.getAuthority());
+ UserCredentials userCredentials = userService.findUserCredentialsByUserId(savedUser.getId());
+ Assert.assertNotNull(userCredentials);
+ Assert.assertNotNull(userCredentials.getId());
+ Assert.assertNotNull(userCredentials.getUserId());
+ Assert.assertNotNull(userCredentials.getActivateToken());
+
+ savedUser.setFirstName("Joe");
+ savedUser.setLastName("Downs");
+
+ userService.saveUser(savedUser);
+ savedUser = userService.findUserById(savedUser.getId());
+ Assert.assertEquals("Joe", savedUser.getFirstName());
+ Assert.assertEquals("Downs", savedUser.getLastName());
+
+ userService.deleteUser(savedUser.getId());
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveUserWithSameEmail() {
+ User tenantAdminUser = userService.findUserByEmail("tenant@thingsboard.org");
+ tenantAdminUser.setEmail("sysadmin@thingsboard.org");
+ userService.saveUser(tenantAdminUser);
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveUserWithInvalidEmail() {
+ User tenantAdminUser = userService.findUserByEmail("tenant@thingsboard.org");
+ tenantAdminUser.setEmail("tenant_thingsboard.org");
+ userService.saveUser(tenantAdminUser);
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveUserWithEmptyEmail() {
+ User tenantAdminUser = userService.findUserByEmail("tenant@thingsboard.org");
+ tenantAdminUser.setEmail(null);
+ userService.saveUser(tenantAdminUser);
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveUserWithoutTenant() {
+ User tenantAdminUser = userService.findUserByEmail("tenant@thingsboard.org");
+ tenantAdminUser.setTenantId(null);
+ userService.saveUser(tenantAdminUser);
+ }
+
+ @Test
+ public void testDeleteUser() {
+ User tenantAdminUser = userService.findUserByEmail("tenant@thingsboard.org");
+ User user = new User();
+ user.setAuthority(Authority.TENANT_ADMIN);
+ user.setTenantId(tenantAdminUser.getTenantId());
+ user.setEmail("tenant2@thingsboard.org");
+ User savedUser = userService.saveUser(user);
+ Assert.assertNotNull(savedUser);
+ Assert.assertNotNull(savedUser.getId());
+ User foundUser = userService.findUserById(savedUser.getId());
+ Assert.assertNotNull(foundUser);
+ UserCredentials userCredentials = userService.findUserCredentialsByUserId(foundUser.getId());
+ Assert.assertNotNull(userCredentials);
+ userService.deleteUser(foundUser.getId());
+ userCredentials = userService.findUserCredentialsByUserId(foundUser.getId());
+ foundUser = userService.findUserById(foundUser.getId());
+ Assert.assertNull(foundUser);
+ Assert.assertNull(userCredentials);
+ }
+
+ @Test
+ public void testFindTenantAdmins() {
+ User tenantAdminUser = userService.findUserByEmail("tenant@thingsboard.org");
+ TextPageData<User> pageData = userService.findTenantAdmins(tenantAdminUser.getTenantId(), new TextPageLink(10));
+ Assert.assertFalse(pageData.hasNext());
+ List<User> users = pageData.getData();
+ Assert.assertEquals(1, users.size());
+ Assert.assertEquals(tenantAdminUser, users.get(0));
+
+ Tenant tenant = new Tenant();
+ tenant.setTitle("Test tenant");
+ tenant = tenantService.saveTenant(tenant);
+
+ TenantId tenantId = tenant.getId();
+
+ List<User> tenantAdmins = new ArrayList<>();
+ for (int i=0;i<124;i++) {
+ User user = new User();
+ user.setAuthority(Authority.TENANT_ADMIN);
+ user.setTenantId(tenantId);
+ user.setEmail("testTenant" + i + "@thingsboard.org");
+ tenantAdmins.add(userService.saveUser(user));
+ }
+
+ List<User> loadedTenantAdmins = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(33);
+ do {
+ pageData = userService.findTenantAdmins(tenantId, pageLink);
+ loadedTenantAdmins.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(tenantAdmins, idComparator);
+ Collections.sort(loadedTenantAdmins, idComparator);
+
+ Assert.assertEquals(tenantAdmins, loadedTenantAdmins);
+
+ tenantService.deleteTenant(tenantId);
+
+ pageLink = new TextPageLink(33);
+ pageData = userService.findTenantAdmins(tenantId, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertTrue(pageData.getData().isEmpty());
+
+ }
+
+ @Test
+ public void testFindTenantAdminsByEmail() {
+ Tenant tenant = new Tenant();
+ tenant.setTitle("Test tenant");
+ tenant = tenantService.saveTenant(tenant);
+
+ TenantId tenantId = tenant.getId();
+
+ String email1 = "testEmail1";
+ List<User> tenantAdminsEmail1 = new ArrayList<>();
+
+ for (int i=0;i<94;i++) {
+ User user = new User();
+ user.setAuthority(Authority.TENANT_ADMIN);
+ user.setTenantId(tenantId);
+ String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10));
+ String email = email1+suffix+ "@thingsboard.org";
+ email = i % 2 == 0 ? email.toLowerCase() : email.toUpperCase();
+ user.setEmail(email);
+ tenantAdminsEmail1.add(userService.saveUser(user));
+ }
+
+ String email2 = "testEmail2";
+ List<User> tenantAdminsEmail2 = new ArrayList<>();
+
+ for (int i=0;i<132;i++) {
+ User user = new User();
+ user.setAuthority(Authority.TENANT_ADMIN);
+ user.setTenantId(tenantId);
+ String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10));
+ String email = email2+suffix+ "@thingsboard.org";
+ email = i % 2 == 0 ? email.toLowerCase() : email.toUpperCase();
+ user.setEmail(email);
+ tenantAdminsEmail2.add(userService.saveUser(user));
+ }
+
+ List<User> loadedTenantAdminsEmail1 = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(33, email1);
+ TextPageData<User> pageData = null;
+ do {
+ pageData = userService.findTenantAdmins(tenantId, pageLink);
+ loadedTenantAdminsEmail1.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(tenantAdminsEmail1, idComparator);
+ Collections.sort(loadedTenantAdminsEmail1, idComparator);
+
+ Assert.assertEquals(tenantAdminsEmail1, loadedTenantAdminsEmail1);
+
+ List<User> loadedTenantAdminsEmail2 = new ArrayList<>();
+ pageLink = new TextPageLink(16, email2);
+ do {
+ pageData = userService.findTenantAdmins(tenantId, pageLink);
+ loadedTenantAdminsEmail2.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(tenantAdminsEmail2, idComparator);
+ Collections.sort(loadedTenantAdminsEmail2, idComparator);
+
+ Assert.assertEquals(tenantAdminsEmail2, loadedTenantAdminsEmail2);
+
+ for (User user : loadedTenantAdminsEmail1) {
+ userService.deleteUser(user.getId());
+ }
+
+ pageLink = new TextPageLink(4, email1);
+ pageData = userService.findTenantAdmins(tenantId, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+
+ for (User user : loadedTenantAdminsEmail2) {
+ userService.deleteUser(user.getId());
+ }
+
+ pageLink = new TextPageLink(4, email2);
+ pageData = userService.findTenantAdmins(tenantId, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+
+ tenantService.deleteTenant(tenantId);
+ }
+
+ @Test
+ public void testFindCustomerUsers() {
+ User customerUser = userService.findUserByEmail("customer@thingsboard.org");
+ TextPageData<User> pageData = userService.findCustomerUsers(customerUser.getTenantId(),
+ customerUser.getCustomerId(), new TextPageLink(10));
+ Assert.assertFalse(pageData.hasNext());
+ List<User> users = pageData.getData();
+ Assert.assertEquals(1, users.size());
+ Assert.assertEquals(customerUser, users.get(0));
+
+ Tenant tenant = new Tenant();
+ tenant.setTitle("Test tenant");
+ tenant = tenantService.saveTenant(tenant);
+
+ TenantId tenantId = tenant.getId();
+
+ Customer customer = new Customer();
+ customer.setTitle("Test customer");
+ customer.setTenantId(tenantId);
+ customer = customerService.saveCustomer(customer);
+
+ CustomerId customerId = customer.getId();
+
+ List<User> customerUsers = new ArrayList<>();
+ for (int i=0;i<156;i++) {
+ User user = new User();
+ user.setAuthority(Authority.CUSTOMER_USER);
+ user.setTenantId(tenantId);
+ user.setCustomerId(customerId);
+ user.setEmail("testCustomer" + i + "@thingsboard.org");
+ customerUsers.add(userService.saveUser(user));
+ }
+
+ List<User> loadedCustomerUsers = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(33);
+ do {
+ pageData = userService.findCustomerUsers(tenantId, customerId, pageLink);
+ loadedCustomerUsers.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(customerUsers, idComparator);
+ Collections.sort(loadedCustomerUsers, idComparator);
+
+ Assert.assertEquals(customerUsers, loadedCustomerUsers);
+
+ tenantService.deleteTenant(tenantId);
+
+ pageData = userService.findCustomerUsers(tenantId, customerId, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertTrue(pageData.getData().isEmpty());
+
+ }
+
+ @Test
+ public void testFindCustomerUsersByEmail() {
+ Tenant tenant = new Tenant();
+ tenant.setTitle("Test tenant");
+ tenant = tenantService.saveTenant(tenant);
+
+ TenantId tenantId = tenant.getId();
+
+ Customer customer = new Customer();
+ customer.setTitle("Test customer");
+ customer.setTenantId(tenantId);
+ customer = customerService.saveCustomer(customer);
+
+ CustomerId customerId = customer.getId();
+
+ String email1 = "testEmail1";
+ List<User> customerUsersEmail1 = new ArrayList<>();
+
+ for (int i=0;i<124;i++) {
+ User user = new User();
+ user.setAuthority(Authority.CUSTOMER_USER);
+ user.setTenantId(tenantId);
+ user.setCustomerId(customerId);
+ String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10));
+ String email = email1+suffix+ "@thingsboard.org";
+ email = i % 2 == 0 ? email.toLowerCase() : email.toUpperCase();
+ user.setEmail(email);
+ customerUsersEmail1.add(userService.saveUser(user));
+ }
+
+ String email2 = "testEmail2";
+ List<User> customerUsersEmail2 = new ArrayList<>();
+
+ for (int i=0;i<132;i++) {
+ User user = new User();
+ user.setAuthority(Authority.CUSTOMER_USER);
+ user.setTenantId(tenantId);
+ user.setCustomerId(customerId);
+ String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10));
+ String email = email2+suffix+ "@thingsboard.org";
+ email = i % 2 == 0 ? email.toLowerCase() : email.toUpperCase();
+ user.setEmail(email);
+ customerUsersEmail2.add(userService.saveUser(user));
+ }
+
+ List<User> loadedCustomerUsersEmail1 = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(33, email1);
+ TextPageData<User> pageData = null;
+ do {
+ pageData = userService.findCustomerUsers(tenantId, customerId, pageLink);
+ loadedCustomerUsersEmail1.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(customerUsersEmail1, idComparator);
+ Collections.sort(loadedCustomerUsersEmail1, idComparator);
+
+ Assert.assertEquals(customerUsersEmail1, loadedCustomerUsersEmail1);
+
+ List<User> loadedCustomerUsersEmail2 = new ArrayList<>();
+ pageLink = new TextPageLink(16, email2);
+ do {
+ pageData = userService.findCustomerUsers(tenantId, customerId, pageLink);
+ loadedCustomerUsersEmail2.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(customerUsersEmail2, idComparator);
+ Collections.sort(loadedCustomerUsersEmail2, idComparator);
+
+ Assert.assertEquals(customerUsersEmail2, loadedCustomerUsersEmail2);
+
+ for (User user : loadedCustomerUsersEmail1) {
+ userService.deleteUser(user.getId());
+ }
+
+ pageLink = new TextPageLink(4, email1);
+ pageData = userService.findCustomerUsers(tenantId, customerId, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+
+ for (User user : loadedCustomerUsersEmail2) {
+ userService.deleteUser(user.getId());
+ }
+
+ pageLink = new TextPageLink(4, email2);
+ pageData = userService.findCustomerUsers(tenantId, customerId, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(0, pageData.getData().size());
+
+ tenantService.deleteTenant(tenantId);
+ }
+
+}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/WidgetsBundleServiceImplTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/WidgetsBundleServiceImplTest.java
new file mode 100644
index 0000000..09bb7ba
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/WidgetsBundleServiceImplTest.java
@@ -0,0 +1,430 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.service;
+
+import com.datastax.driver.core.utils.UUIDs;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.widget.WidgetsBundle;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.model.ModelConstants;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class WidgetsBundleServiceImplTest extends AbstractServiceTest {
+
+ private IdComparator<WidgetsBundle> idComparator = new IdComparator<>();
+
+ private TenantId tenantId;
+
+ @Before
+ public void before() {
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ Tenant savedTenant = tenantService.saveTenant(tenant);
+ Assert.assertNotNull(savedTenant);
+ tenantId = savedTenant.getId();
+ }
+
+ @After
+ public void after() {
+ tenantService.deleteTenant(tenantId);
+ }
+
+ @Test
+ public void testSaveWidgetsBundle() throws IOException {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTenantId(tenantId);
+ widgetsBundle.setTitle("My first widgets bundle");
+
+ WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
+
+ Assert.assertNotNull(savedWidgetsBundle);
+ Assert.assertNotNull(savedWidgetsBundle.getId());
+ Assert.assertNotNull(savedWidgetsBundle.getAlias());
+ Assert.assertTrue(savedWidgetsBundle.getCreatedTime() > 0);
+ Assert.assertEquals(widgetsBundle.getTenantId(), savedWidgetsBundle.getTenantId());
+ Assert.assertEquals(widgetsBundle.getTitle(), savedWidgetsBundle.getTitle());
+
+ savedWidgetsBundle.setTitle("My new widgets bundle");
+
+ widgetsBundleService.saveWidgetsBundle(savedWidgetsBundle);
+ WidgetsBundle foundWidgetsBundle = widgetsBundleService.findWidgetsBundleById(savedWidgetsBundle.getId());
+ Assert.assertEquals(foundWidgetsBundle.getTitle(), savedWidgetsBundle.getTitle());
+
+ widgetsBundleService.deleteWidgetsBundle(savedWidgetsBundle.getId());
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveWidgetsBundleWithEmptyTitle() {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTenantId(tenantId);
+ widgetsBundleService.saveWidgetsBundle(widgetsBundle);
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveWidgetsBundleWithInvalidTenant() {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTitle("My widgets bundle");
+ widgetsBundle.setTenantId(new TenantId(UUIDs.timeBased()));
+ widgetsBundleService.saveWidgetsBundle(widgetsBundle);
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testUpdateWidgetsBundleTenant() {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTitle("My widgets bundle");
+ widgetsBundle.setTenantId(tenantId);
+ WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
+ savedWidgetsBundle.setTenantId(new TenantId(ModelConstants.NULL_UUID));
+ try {
+ widgetsBundleService.saveWidgetsBundle(savedWidgetsBundle);
+ } finally {
+ widgetsBundleService.deleteWidgetsBundle(savedWidgetsBundle.getId());
+ }
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testUpdateWidgetsBundleAlias() {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTitle("My widgets bundle");
+ widgetsBundle.setTenantId(tenantId);
+ WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
+ savedWidgetsBundle.setAlias("new_alias");
+ try {
+ widgetsBundleService.saveWidgetsBundle(savedWidgetsBundle);
+ } finally {
+ widgetsBundleService.deleteWidgetsBundle(savedWidgetsBundle.getId());
+ }
+ }
+
+ @Test
+ public void testFindWidgetsBundleById() {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTenantId(tenantId);
+ widgetsBundle.setTitle("My widgets bundle");
+ WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
+ WidgetsBundle foundWidgetsBundle = widgetsBundleService.findWidgetsBundleById(savedWidgetsBundle.getId());
+ Assert.assertNotNull(foundWidgetsBundle);
+ Assert.assertEquals(savedWidgetsBundle, foundWidgetsBundle);
+ widgetsBundleService.deleteWidgetsBundle(savedWidgetsBundle.getId());
+ }
+
+ @Test
+ public void testFindWidgetsBundleByTenantIdAndAlias() {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTenantId(tenantId);
+ widgetsBundle.setTitle("My widgets bundle");
+ WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
+ WidgetsBundle foundWidgetsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(tenantId, savedWidgetsBundle.getAlias());
+ Assert.assertNotNull(foundWidgetsBundle);
+ Assert.assertEquals(savedWidgetsBundle, foundWidgetsBundle);
+ widgetsBundleService.deleteWidgetsBundle(savedWidgetsBundle.getId());
+ }
+
+ @Test
+ public void testDeleteWidgetsBundle() {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTenantId(tenantId);
+ widgetsBundle.setTitle("My widgets bundle");
+ WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
+ WidgetsBundle foundWidgetsBundle = widgetsBundleService.findWidgetsBundleById(savedWidgetsBundle.getId());
+ Assert.assertNotNull(foundWidgetsBundle);
+ widgetsBundleService.deleteWidgetsBundle(savedWidgetsBundle.getId());
+ foundWidgetsBundle = widgetsBundleService.findWidgetsBundleById(savedWidgetsBundle.getId());
+ Assert.assertNull(foundWidgetsBundle);
+ }
+
+ @Test
+ public void testFindSystemWidgetsBundlesByPageLink() {
+
+ TenantId tenantId = new TenantId(ModelConstants.NULL_UUID);
+
+ List<WidgetsBundle> systemWidgetsBundles = widgetsBundleService.findSystemWidgetsBundles();
+ List<WidgetsBundle> createdWidgetsBundles = new ArrayList<>();
+ for (int i=0;i<235;i++) {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTenantId(tenantId);
+ widgetsBundle.setTitle("Widgets bundle "+i);
+ createdWidgetsBundles.add(widgetsBundleService.saveWidgetsBundle(widgetsBundle));
+ }
+
+ List<WidgetsBundle> widgetsBundles = new ArrayList<>(createdWidgetsBundles);
+ widgetsBundles.addAll(systemWidgetsBundles);
+
+ List<WidgetsBundle> loadedWidgetsBundles = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(19);
+ TextPageData<WidgetsBundle> pageData = null;
+ do {
+ pageData = widgetsBundleService.findSystemWidgetsBundlesByPageLink(pageLink);
+ loadedWidgetsBundles.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(widgetsBundles, idComparator);
+ Collections.sort(loadedWidgetsBundles, idComparator);
+
+ Assert.assertEquals(widgetsBundles, loadedWidgetsBundles);
+
+ for (WidgetsBundle widgetsBundle : createdWidgetsBundles) {
+ widgetsBundleService.deleteWidgetsBundle(widgetsBundle.getId());
+ }
+
+ loadedWidgetsBundles = widgetsBundleService.findSystemWidgetsBundles();
+
+ Collections.sort(systemWidgetsBundles, idComparator);
+ Collections.sort(loadedWidgetsBundles, idComparator);
+
+ Assert.assertEquals(systemWidgetsBundles, loadedWidgetsBundles);
+ }
+
+ @Test
+ public void testFindSystemWidgetsBundles() {
+ TenantId tenantId = new TenantId(ModelConstants.NULL_UUID);
+
+ List<WidgetsBundle> systemWidgetsBundles = widgetsBundleService.findSystemWidgetsBundles();
+
+ List<WidgetsBundle> createdWidgetsBundles = new ArrayList<>();
+ for (int i=0;i<135;i++) {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTenantId(tenantId);
+ widgetsBundle.setTitle("Widgets bundle "+i);
+ createdWidgetsBundles.add(widgetsBundleService.saveWidgetsBundle(widgetsBundle));
+ }
+
+ List<WidgetsBundle> widgetsBundles = new ArrayList<>(createdWidgetsBundles);
+ widgetsBundles.addAll(systemWidgetsBundles);
+
+ List<WidgetsBundle> loadedWidgetsBundles = widgetsBundleService.findSystemWidgetsBundles();
+
+ Collections.sort(widgetsBundles, idComparator);
+ Collections.sort(loadedWidgetsBundles, idComparator);
+
+ Assert.assertEquals(widgetsBundles, loadedWidgetsBundles);
+
+ for (WidgetsBundle widgetsBundle : createdWidgetsBundles) {
+ widgetsBundleService.deleteWidgetsBundle(widgetsBundle.getId());
+ }
+
+ loadedWidgetsBundles = widgetsBundleService.findSystemWidgetsBundles();
+
+ Collections.sort(systemWidgetsBundles, idComparator);
+ Collections.sort(loadedWidgetsBundles, idComparator);
+
+ Assert.assertEquals(systemWidgetsBundles, loadedWidgetsBundles);
+ }
+
+ @Test
+ public void testFindTenantWidgetsBundlesByTenantId() {
+ Tenant tenant = new Tenant();
+ tenant.setTitle("Test tenant");
+ tenant = tenantService.saveTenant(tenant);
+
+ TenantId tenantId = tenant.getId();
+
+ List<WidgetsBundle> widgetsBundles = new ArrayList<>();
+ for (int i=0;i<127;i++) {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTenantId(tenantId);
+ widgetsBundle.setTitle("Widgets bundle "+i);
+ widgetsBundles.add(widgetsBundleService.saveWidgetsBundle(widgetsBundle));
+ }
+
+ List<WidgetsBundle> loadedWidgetsBundles = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(11);
+ TextPageData<WidgetsBundle> pageData = null;
+ do {
+ pageData = widgetsBundleService.findTenantWidgetsBundlesByTenantId(tenantId, pageLink);
+ loadedWidgetsBundles.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(widgetsBundles, idComparator);
+ Collections.sort(loadedWidgetsBundles, idComparator);
+
+ Assert.assertEquals(widgetsBundles, loadedWidgetsBundles);
+
+ widgetsBundleService.deleteWidgetsBundlesByTenantId(tenantId);
+
+ pageLink = new TextPageLink(15);
+ pageData = widgetsBundleService.findTenantWidgetsBundlesByTenantId(tenantId, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertTrue(pageData.getData().isEmpty());
+
+ tenantService.deleteTenant(tenantId);
+ }
+
+ @Test
+ public void testFindAllWidgetsBundlesByTenantIdAndPageLink() {
+
+ List<WidgetsBundle> systemWidgetsBundles = widgetsBundleService.findSystemWidgetsBundles();
+
+ Tenant tenant = new Tenant();
+ tenant.setTitle("Test tenant");
+ tenant = tenantService.saveTenant(tenant);
+
+ TenantId tenantId = tenant.getId();
+ TenantId systemTenantId = new TenantId(ModelConstants.NULL_UUID);
+
+ List<WidgetsBundle> createdWidgetsBundles = new ArrayList<>();
+ List<WidgetsBundle> createdSystemWidgetsBundles = new ArrayList<>();
+ for (int i=0;i<177;i++) {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTenantId(i % 2 == 0 ? tenantId : systemTenantId);
+ widgetsBundle.setTitle((i % 2 == 0 ? "Widgets bundle " : "System widget bundle ") + i);
+ WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
+ createdWidgetsBundles.add(savedWidgetsBundle);
+ if (i % 2 == 1) {
+ createdSystemWidgetsBundles.add(savedWidgetsBundle);
+ }
+ }
+
+ List<WidgetsBundle> widgetsBundles = new ArrayList<>(createdWidgetsBundles);
+ widgetsBundles.addAll(systemWidgetsBundles);
+
+ List<WidgetsBundle> loadedWidgetsBundles = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(17);
+ TextPageData<WidgetsBundle> pageData = null;
+ do {
+ pageData = widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, pageLink);
+ loadedWidgetsBundles.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(widgetsBundles, idComparator);
+ Collections.sort(loadedWidgetsBundles, idComparator);
+
+ Assert.assertEquals(widgetsBundles, loadedWidgetsBundles);
+
+ widgetsBundleService.deleteWidgetsBundlesByTenantId(tenantId);
+
+ loadedWidgetsBundles.clear();
+ pageLink = new TextPageLink(14);
+ do {
+ pageData = widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, pageLink);
+ loadedWidgetsBundles.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ List<WidgetsBundle> allSystemWidgetsBundles = new ArrayList<>(systemWidgetsBundles);
+ allSystemWidgetsBundles.addAll(createdSystemWidgetsBundles);
+
+ Collections.sort(allSystemWidgetsBundles, idComparator);
+ Collections.sort(loadedWidgetsBundles, idComparator);
+
+ Assert.assertEquals(allSystemWidgetsBundles, loadedWidgetsBundles);
+
+ for (WidgetsBundle widgetsBundle : createdSystemWidgetsBundles) {
+ widgetsBundleService.deleteWidgetsBundle(widgetsBundle.getId());
+ }
+
+ loadedWidgetsBundles.clear();
+ pageLink = new TextPageLink(18);
+ do {
+ pageData = widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, pageLink);
+ loadedWidgetsBundles.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageData.getNextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(systemWidgetsBundles, idComparator);
+ Collections.sort(loadedWidgetsBundles, idComparator);
+
+ Assert.assertEquals(systemWidgetsBundles, loadedWidgetsBundles);
+
+ tenantService.deleteTenant(tenantId);
+ }
+
+ @Test
+ public void testFindAllWidgetsBundlesByTenantId() {
+
+ List<WidgetsBundle> systemWidgetsBundles = widgetsBundleService.findSystemWidgetsBundles();
+
+ Tenant tenant = new Tenant();
+ tenant.setTitle("Test tenant");
+ tenant = tenantService.saveTenant(tenant);
+
+ TenantId tenantId = tenant.getId();
+ TenantId systemTenantId = new TenantId(ModelConstants.NULL_UUID);
+
+ List<WidgetsBundle> createdWidgetsBundles = new ArrayList<>();
+ List<WidgetsBundle> createdSystemWidgetsBundles = new ArrayList<>();
+ for (int i=0;i<277;i++) {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTenantId(i % 2 == 0 ? tenantId : systemTenantId);
+ widgetsBundle.setTitle((i % 2 == 0 ? "Widgets bundle " : "System widget bundle ") + i);
+ WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
+ createdWidgetsBundles.add(savedWidgetsBundle);
+ if (i % 2 == 1) {
+ createdSystemWidgetsBundles.add(savedWidgetsBundle);
+ }
+ }
+
+ List<WidgetsBundle> widgetsBundles = new ArrayList<>(createdWidgetsBundles);
+ widgetsBundles.addAll(systemWidgetsBundles);
+
+ List<WidgetsBundle> loadedWidgetsBundles = widgetsBundleService.findAllTenantWidgetsBundlesByTenantId(tenantId);
+
+ Collections.sort(widgetsBundles, idComparator);
+ Collections.sort(loadedWidgetsBundles, idComparator);
+
+ Assert.assertEquals(widgetsBundles, loadedWidgetsBundles);
+
+ widgetsBundleService.deleteWidgetsBundlesByTenantId(tenantId);
+
+ loadedWidgetsBundles = widgetsBundleService.findAllTenantWidgetsBundlesByTenantId(tenantId);
+
+ List<WidgetsBundle> allSystemWidgetsBundles = new ArrayList<>(systemWidgetsBundles);
+ allSystemWidgetsBundles.addAll(createdSystemWidgetsBundles);
+
+ Collections.sort(allSystemWidgetsBundles, idComparator);
+ Collections.sort(loadedWidgetsBundles, idComparator);
+
+ Assert.assertEquals(allSystemWidgetsBundles, loadedWidgetsBundles);
+
+ for (WidgetsBundle widgetsBundle : createdSystemWidgetsBundles) {
+ widgetsBundleService.deleteWidgetsBundle(widgetsBundle.getId());
+ }
+
+ loadedWidgetsBundles = widgetsBundleService.findAllTenantWidgetsBundlesByTenantId(tenantId);
+
+ Collections.sort(systemWidgetsBundles, idComparator);
+ Collections.sort(loadedWidgetsBundles, idComparator);
+
+ Assert.assertEquals(systemWidgetsBundles, loadedWidgetsBundles);
+
+ tenantService.deleteTenant(tenantId);
+ }
+
+}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/WidgetTypeServiceImplTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/WidgetTypeServiceImplTest.java
new file mode 100644
index 0000000..7a26c32
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/WidgetTypeServiceImplTest.java
@@ -0,0 +1,324 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.service;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.widget.WidgetType;
+import org.thingsboard.server.common.data.widget.WidgetsBundle;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.model.ModelConstants;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class WidgetTypeServiceImplTest extends AbstractServiceTest {
+
+ private IdComparator<WidgetType> idComparator = new IdComparator<>();
+
+ private TenantId tenantId;
+
+ @Before
+ public void before() {
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ Tenant savedTenant = tenantService.saveTenant(tenant);
+ Assert.assertNotNull(savedTenant);
+ tenantId = savedTenant.getId();
+ }
+
+ @After
+ public void after() {
+ tenantService.deleteTenant(tenantId);
+ }
+
+ @Test
+ public void testSaveWidgetType() throws IOException {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTenantId(tenantId);
+ widgetsBundle.setTitle("Widgets bundle");
+ WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
+
+
+ WidgetType widgetType = new WidgetType();
+ widgetType.setTenantId(tenantId);
+ widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
+ widgetType.setName("Widget Type");
+ widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+ WidgetType savedWidgetType = widgetTypeService.saveWidgetType(widgetType);
+
+ Assert.assertNotNull(savedWidgetType);
+ Assert.assertNotNull(savedWidgetType.getId());
+ Assert.assertNotNull(savedWidgetType.getAlias());
+ Assert.assertTrue(savedWidgetType.getCreatedTime() > 0);
+ Assert.assertEquals(widgetType.getTenantId(), savedWidgetType.getTenantId());
+ Assert.assertEquals(widgetType.getName(), savedWidgetType.getName());
+ Assert.assertEquals(widgetType.getDescriptor(), savedWidgetType.getDescriptor());
+ Assert.assertEquals(savedWidgetsBundle.getAlias(), savedWidgetType.getBundleAlias());
+
+ savedWidgetType.setName("New Widget Type");
+
+ widgetTypeService.saveWidgetType(savedWidgetType);
+ WidgetType foundWidgetType = widgetTypeService.findWidgetTypeById(savedWidgetType.getId());
+ Assert.assertEquals(foundWidgetType.getName(), savedWidgetType.getName());
+
+ widgetsBundleService.deleteWidgetsBundle(savedWidgetsBundle.getId());
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveWidgetTypeWithEmptyName() throws IOException {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTenantId(tenantId);
+ widgetsBundle.setTitle("Widgets bundle");
+ WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
+
+ WidgetType widgetType = new WidgetType();
+ widgetType.setTenantId(tenantId);
+ widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
+ widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+ try {
+ widgetTypeService.saveWidgetType(widgetType);
+ } finally {
+ widgetsBundleService.deleteWidgetsBundle(savedWidgetsBundle.getId());
+ }
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveWidgetTypeWithEmptyBundleAlias() throws IOException {
+ WidgetType widgetType = new WidgetType();
+ widgetType.setTenantId(tenantId);
+ widgetType.setName("Widget Type");
+ widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+ widgetTypeService.saveWidgetType(widgetType);
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveWidgetTypeWithEmptyDescriptor() throws IOException {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTenantId(tenantId);
+ widgetsBundle.setTitle("Widgets bundle");
+ WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
+
+ WidgetType widgetType = new WidgetType();
+ widgetType.setTenantId(tenantId);
+ widgetType.setName("Widget Type");
+ widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
+ widgetType.setDescriptor(new ObjectMapper().readValue("{}", JsonNode.class));
+ try {
+ widgetTypeService.saveWidgetType(widgetType);
+ } finally {
+ widgetsBundleService.deleteWidgetsBundle(savedWidgetsBundle.getId());
+ }
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveWidgetTypeWithInvalidTenant() throws IOException {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTenantId(tenantId);
+ widgetsBundle.setTitle("Widgets bundle");
+ WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
+
+ WidgetType widgetType = new WidgetType();
+ widgetType.setTenantId(new TenantId(UUIDs.timeBased()));
+ widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
+ widgetType.setName("Widget Type");
+ widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+ try {
+ widgetTypeService.saveWidgetType(widgetType);
+ } finally {
+ widgetsBundleService.deleteWidgetsBundle(savedWidgetsBundle.getId());
+ }
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testSaveWidgetTypeWithInvalidBundleAlias() throws IOException {
+ WidgetType widgetType = new WidgetType();
+ widgetType.setTenantId(tenantId);
+ widgetType.setBundleAlias("some_alias");
+ widgetType.setName("Widget Type");
+ widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+ widgetTypeService.saveWidgetType(widgetType);
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testUpdateWidgetTypeTenant() throws IOException {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTenantId(tenantId);
+ widgetsBundle.setTitle("Widgets bundle");
+ WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
+
+ WidgetType widgetType = new WidgetType();
+ widgetType.setTenantId(tenantId);
+ widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
+ widgetType.setName("Widget Type");
+ widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+ WidgetType savedWidgetType = widgetTypeService.saveWidgetType(widgetType);
+ savedWidgetType.setTenantId(new TenantId(ModelConstants.NULL_UUID));
+ try {
+ widgetTypeService.saveWidgetType(savedWidgetType);
+ } finally {
+ widgetsBundleService.deleteWidgetsBundle(savedWidgetsBundle.getId());
+ }
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testUpdateWidgetTypeBundleAlias() throws IOException {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTenantId(tenantId);
+ widgetsBundle.setTitle("Widgets bundle");
+ WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
+
+ WidgetType widgetType = new WidgetType();
+ widgetType.setTenantId(tenantId);
+ widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
+ widgetType.setName("Widget Type");
+ widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+ WidgetType savedWidgetType = widgetTypeService.saveWidgetType(widgetType);
+ savedWidgetType.setBundleAlias("some_alias");
+ try {
+ widgetTypeService.saveWidgetType(savedWidgetType);
+ } finally {
+ widgetsBundleService.deleteWidgetsBundle(savedWidgetsBundle.getId());
+ }
+ }
+
+ @Test(expected = DataValidationException.class)
+ public void testUpdateWidgetTypeAlias() throws IOException {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTenantId(tenantId);
+ widgetsBundle.setTitle("Widgets bundle");
+ WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
+
+ WidgetType widgetType = new WidgetType();
+ widgetType.setTenantId(tenantId);
+ widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
+ widgetType.setName("Widget Type");
+ widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+ WidgetType savedWidgetType = widgetTypeService.saveWidgetType(widgetType);
+ savedWidgetType.setAlias("some_alias");
+ try {
+ widgetTypeService.saveWidgetType(savedWidgetType);
+ } finally {
+ widgetsBundleService.deleteWidgetsBundle(savedWidgetsBundle.getId());
+ }
+ }
+
+ @Test
+ public void testFindWidgetTypeById() throws IOException {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTenantId(tenantId);
+ widgetsBundle.setTitle("Widgets bundle");
+ WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
+
+ WidgetType widgetType = new WidgetType();
+ widgetType.setTenantId(tenantId);
+ widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
+ widgetType.setName("Widget Type");
+ widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+ WidgetType savedWidgetType = widgetTypeService.saveWidgetType(widgetType);
+ WidgetType foundWidgetType = widgetTypeService.findWidgetTypeById(savedWidgetType.getId());
+ Assert.assertNotNull(foundWidgetType);
+ Assert.assertEquals(savedWidgetType, foundWidgetType);
+
+ widgetsBundleService.deleteWidgetsBundle(savedWidgetsBundle.getId());
+ }
+
+ @Test
+ public void testFindWidgetTypeByTenantIdBundleAliasAndAlias() throws IOException {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTenantId(tenantId);
+ widgetsBundle.setTitle("Widgets bundle");
+ WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
+
+ WidgetType widgetType = new WidgetType();
+ widgetType.setTenantId(tenantId);
+ widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
+ widgetType.setName("Widget Type");
+ widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+ WidgetType savedWidgetType = widgetTypeService.saveWidgetType(widgetType);
+ WidgetType foundWidgetType = widgetTypeService.findWidgetTypeByTenantIdBundleAliasAndAlias(tenantId, savedWidgetsBundle.getAlias(), savedWidgetType.getAlias());
+ Assert.assertNotNull(foundWidgetType);
+ Assert.assertEquals(savedWidgetType, foundWidgetType);
+
+ widgetsBundleService.deleteWidgetsBundle(savedWidgetsBundle.getId());
+ }
+
+ @Test
+ public void testDeleteWidgetType() throws IOException {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTenantId(tenantId);
+ widgetsBundle.setTitle("Widgets bundle");
+ WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
+
+ WidgetType widgetType = new WidgetType();
+ widgetType.setTenantId(tenantId);
+ widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
+ widgetType.setName("Widget Type");
+ widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+ WidgetType savedWidgetType = widgetTypeService.saveWidgetType(widgetType);
+ WidgetType foundWidgetType = widgetTypeService.findWidgetTypeById(savedWidgetType.getId());
+ Assert.assertNotNull(foundWidgetType);
+ widgetTypeService.deleteWidgetType(savedWidgetType.getId());
+ foundWidgetType = widgetTypeService.findWidgetTypeById(savedWidgetType.getId());
+ Assert.assertNull(foundWidgetType);
+
+ widgetsBundleService.deleteWidgetsBundle(savedWidgetsBundle.getId());
+ }
+
+ @Test
+ public void testFindWidgetTypesByTenantIdAndBundleAlias() throws IOException {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTenantId(tenantId);
+ widgetsBundle.setTitle("Widgets bundle");
+ WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
+
+ List<WidgetType> widgetTypes = new ArrayList<>();
+ for (int i=0;i<121;i++) {
+ WidgetType widgetType = new WidgetType();
+ widgetType.setTenantId(tenantId);
+ widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
+ widgetType.setName("Widget Type " + i);
+ widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+ widgetTypes.add(widgetTypeService.saveWidgetType(widgetType));
+ }
+
+ List<WidgetType> loadedWidgetTypes = widgetTypeService.findWidgetTypesByTenantIdAndBundleAlias(tenantId, savedWidgetsBundle.getAlias());
+
+ Collections.sort(widgetTypes, idComparator);
+ Collections.sort(loadedWidgetTypes, idComparator);
+
+ Assert.assertEquals(widgetTypes, loadedWidgetTypes);
+
+ widgetTypeService.deleteWidgetTypesByTenantIdAndBundleAlias(tenantId, savedWidgetsBundle.getAlias());
+
+ loadedWidgetTypes = widgetTypeService.findWidgetTypesByTenantIdAndBundleAlias(tenantId, savedWidgetsBundle.getAlias());
+
+ Assert.assertTrue(loadedWidgetTypes.isEmpty());
+
+ widgetsBundleService.deleteWidgetsBundle(savedWidgetsBundle.getId());
+ }
+
+}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/timeseries/TimeseriesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/timeseries/TimeseriesServiceTest.java
new file mode 100644
index 0000000..91dd973
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/timeseries/TimeseriesServiceTest.java
@@ -0,0 +1,140 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.timeseries;
+
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.ResultSetFuture;
+import com.datastax.driver.core.utils.UUIDs;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.Assert;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.dao.service.AbstractServiceTest;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.thingsboard.server.common.data.kv.*;
+
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * @author Andrew Shvayka
+ */
+
+@Slf4j
+public class TimeseriesServiceTest extends AbstractServiceTest {
+
+ private static final String STRING_KEY = "stringKey";
+ private static final String LONG_KEY = "longKey";
+ private static final String DOUBLE_KEY = "doubleKey";
+ private static final String BOOLEAN_KEY = "booleanKey";
+
+ public static final int PARTITION_MINUTES = 1100;
+
+ private static final long TS = 42L;
+
+ KvEntry stringKvEntry = new StringDataEntry(STRING_KEY, "value");
+ KvEntry longKvEntry = new LongDataEntry(LONG_KEY, Long.MAX_VALUE);
+ KvEntry doubleKvEntry = new DoubleDataEntry(DOUBLE_KEY, Double.MAX_VALUE);
+ KvEntry booleanKvEntry = new BooleanDataEntry(BOOLEAN_KEY, Boolean.TRUE);
+
+ @Test
+ public void testFindAllLatest() throws Exception {
+ DeviceId deviceId = new DeviceId(UUIDs.timeBased());
+
+ saveEntries(deviceId, TS - 2);
+ saveEntries(deviceId, TS - 1);
+ saveEntries(deviceId, TS);
+
+ ResultSetFuture rsFuture = tsService.findAllLatest(DataConstants.DEVICE, deviceId);
+ List<TsKvEntry> tsList = tsService.convertResultSetToTsKvEntryList(rsFuture.get());
+
+ assertNotNull(tsList);
+ assertEquals(4, tsList.size());
+ for (int i = 0; i < tsList.size(); i++) {
+ assertEquals(TS, tsList.get(i).getTs());
+ }
+
+ Collections.sort(tsList, (o1, o2) -> o1.getKey().compareTo(o2.getKey()));
+
+ List<TsKvEntry> expected = Arrays.asList(
+ toTsEntry(TS, stringKvEntry),
+ toTsEntry(TS, longKvEntry),
+ toTsEntry(TS, doubleKvEntry),
+ toTsEntry(TS, booleanKvEntry));
+ Collections.sort(expected, (o1, o2) -> o1.getKey().compareTo(o2.getKey()));
+
+ assertEquals(expected, tsList);
+ }
+
+ @Test
+ public void testFindLatest() throws Exception {
+ DeviceId deviceId = new DeviceId(UUIDs.timeBased());
+
+ saveEntries(deviceId, TS - 2);
+ saveEntries(deviceId, TS - 1);
+ saveEntries(deviceId, TS);
+
+ List<ResultSet> rs = tsService.findLatest(DataConstants.DEVICE, deviceId, Collections.singleton(STRING_KEY)).get();
+ Assert.assertEquals(1, rs.size());
+ Assert.assertEquals(toTsEntry(TS, stringKvEntry), tsService.convertResultToTsKvEntry(rs.get(0).one()));
+ }
+
+ @Test
+ public void testFindDeviceTsDataByQuery() throws Exception {
+ DeviceId deviceId = new DeviceId(UUIDs.timeBased());
+ LocalDateTime localDateTime = LocalDateTime.now(ZoneOffset.UTC).minusMinutes(PARTITION_MINUTES);
+ log.debug("Start event time is {}", localDateTime);
+ List<TsKvEntry> entries = new ArrayList<>(PARTITION_MINUTES);
+
+ for (int i = 0; i < PARTITION_MINUTES; i++) {
+ long time = localDateTime.plusMinutes(i).toInstant(ZoneOffset.UTC).toEpochMilli();
+ BasicTsKvEntry tsKvEntry = new BasicTsKvEntry(time, stringKvEntry);
+ tsService.save(DataConstants.DEVICE, deviceId, tsKvEntry).get();
+ entries.add(tsKvEntry);
+ }
+ log.debug("Saved all records {}", localDateTime);
+ List<TsKvEntry> list = tsService.find(DataConstants.DEVICE, deviceId, new BaseTsKvQuery(STRING_KEY, entries.get(599).getTs(),
+ LocalDateTime.now(ZoneOffset.UTC).toInstant(ZoneOffset.UTC).toEpochMilli()));
+ log.debug("Fetched records {}", localDateTime);
+ List<TsKvEntry> expected = entries.subList(600, PARTITION_MINUTES);
+ assertEquals(expected.size(), list.size());
+ assertEquals(expected, list);
+ }
+
+
+ private void saveEntries(DeviceId deviceId, long ts) throws ExecutionException, InterruptedException {
+ tsService.save(DataConstants.DEVICE, deviceId, toTsEntry(ts, stringKvEntry)).get();
+ tsService.save(DataConstants.DEVICE, deviceId, toTsEntry(ts, longKvEntry)).get();
+ tsService.save(DataConstants.DEVICE, deviceId, toTsEntry(ts, doubleKvEntry)).get();
+ tsService.save(DataConstants.DEVICE, deviceId, toTsEntry(ts, booleanKvEntry)).get();
+ }
+
+ private static TsKvEntry toTsEntry(long ts, KvEntry entry) {
+ return new BasicTsKvEntry(ts, entry);
+ }
+
+
+}
diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties
new file mode 100644
index 0000000..b526b00
--- /dev/null
+++ b/dao/src/test/resources/application-test.properties
@@ -0,0 +1,7 @@
+cache.enabled=false
+cache.device_credentials.time_to_live=3600
+cache.device_credentials.max_size=1000000
+
+zk.enabled=false
+zk.url=localhost:2181
+zk.zk_dir=/thingsboard
\ No newline at end of file
diff --git a/dao/src/test/resources/cassandra-test.properties b/dao/src/test/resources/cassandra-test.properties
new file mode 100644
index 0000000..82fcbe1
--- /dev/null
+++ b/dao/src/test/resources/cassandra-test.properties
@@ -0,0 +1,49 @@
+cassandra.cluster_name=Thingsboard Cluster
+
+cassandra.keyspace_name=thingsboard
+
+cassandra.url=127.0.0.1:9142
+
+cassandra.ssl=false
+
+cassandra.jmx=false
+
+cassandra.metrics=true
+
+cassandra.compression=none
+
+cassandra.init_timeout_ms=60000
+
+cassandra.init_retry_interval_ms=3000
+
+cassandra.credentials=false
+
+cassandra.username=
+
+cassandra.password=
+
+cassandra.socket.connect_timeout=5000
+
+cassandra.socket.read_timeout=12000
+
+cassandra.socket.keep_alive=true
+
+cassandra.socket.reuse_address=true
+
+cassandra.socket.so_linger=
+
+cassandra.socket.tcp_no_delay=false
+
+cassandra.socket.receive_buffer_size=
+
+cassandra.socket.send_buffer_size=
+
+cassandra.query.read_consistency_level=ONE
+
+cassandra.query.write_consistency_level=ONE
+
+cassandra.query.default_fetch_size=2000
+
+cassandra.query.ts_key_value_partitioning=HOURS
+
+cassandra.query.max_limit_per_request=1000
dao/src/test/resources/cassandra-test.yaml 590(+590 -0)
diff --git a/dao/src/test/resources/cassandra-test.yaml b/dao/src/test/resources/cassandra-test.yaml
new file mode 100644
index 0000000..6463f64
--- /dev/null
+++ b/dao/src/test/resources/cassandra-test.yaml
@@ -0,0 +1,590 @@
+# Cassandra storage config YAML
+
+# NOTE:
+# See http://wiki.apache.org/cassandra/StorageConfiguration for
+# full explanations of configuration directives
+# /NOTE
+
+# The name of the cluster. This is mainly used to prevent machines in
+# one logical cluster from joining another.
+cluster_name: 'Test Cluster'
+
+# You should always specify InitialToken when setting up a production
+# cluster for the first time, and often when adding capacity later.
+# The principle is that each node should be given an equal slice of
+# the token ring; see http://wiki.apache.org/cassandra/Operations
+# for more details.
+#
+# If blank, Cassandra will request a token bisecting the range of
+# the heaviest-loaded existing node. If there is no load information
+# available, such as is the case with a new cluster, it will pick
+# a random token, which will lead to hot spots.
+num_tokens: 1
+
+initial_token: 0
+
+# See http://wiki.apache.org/cassandra/HintedHandoff
+hinted_handoff_enabled: true
+# this defines the maximum amount of time a dead host will have hints
+# generated. After it has been dead this long, new hints for it will not be
+# created until it has been seen alive and gone down again.
+max_hint_window_in_ms: 10800000 # 3 hours
+# Maximum throttle in KBs per second, per delivery thread. This will be
+# reduced proportionally to the number of nodes in the cluster. (If there
+# are two nodes in the cluster, each delivery thread will use the maximum
+# rate; if there are three, each will throttle to half of the maximum,
+# since we expect two nodes to be delivering hints simultaneously.)
+hinted_handoff_throttle_in_kb: 1024
+# Number of threads with which to deliver hints;
+# Consider increasing this number when you have multi-dc deployments, since
+# cross-dc handoff tends to be slower
+max_hints_delivery_threads: 2
+
+# The following setting populates the page cache on memtable flush and compaction
+# WARNING: Enable this setting only when the whole node's data fits in memory.
+# Defaults to: false
+# populate_io_cache_on_flush: false
+
+# Authentication backend, implementing IAuthenticator; used to identify users
+# Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllAuthenticator,
+# PasswordAuthenticator}.
+#
+# - AllowAllAuthenticator performs no checks - set it to disable authentication.
+# - PasswordAuthenticator relies on username/password pairs to authenticate
+# users. It keeps usernames and hashed passwords in system_auth.credentials table.
+# Please increase system_auth keyspace replication factor if you use this authenticator.
+authenticator: AllowAllAuthenticator
+
+# Authorization backend, implementing IAuthorizer; used to limit access/provide permissions
+# Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllAuthorizer,
+# CassandraAuthorizer}.
+#
+# - AllowAllAuthorizer allows any action to any user - set it to disable authorization.
+# - CassandraAuthorizer stores permissions in system_auth.permissions table. Please
+# increase system_auth keyspace replication factor if you use this authorizer.
+authorizer: AllowAllAuthorizer
+
+# Validity period for permissions cache (fetching permissions can be an
+# expensive operation depending on the authorizer, CassandraAuthorizer is
+# one example). Defaults to 2000, set to 0 to disable.
+# Will be disabled automatically for AllowAllAuthorizer.
+permissions_validity_in_ms: 2000
+
+
+# The partitioner is responsible for distributing rows (by key) across
+# nodes in the cluster. Any IPartitioner may be used, including your
+# own as long as it is on the classpath. Out of the box, Cassandra
+# provides org.apache.cassandra.dht.{Murmur3Partitioner, RandomPartitioner
+# ByteOrderedPartitioner, OrderPreservingPartitioner (deprecated)}.
+#
+# - RandomPartitioner distributes rows across the cluster evenly by md5.
+# This is the default prior to 1.2 and is retained for compatibility.
+# - Murmur3Partitioner is similar to RandomPartioner but uses Murmur3_128
+# Hash Function instead of md5. When in doubt, this is the best option.
+# - ByteOrderedPartitioner orders rows lexically by key bytes. BOP allows
+# scanning rows in key order, but the ordering can generate hot spots
+# for sequential insertion workloads.
+# - OrderPreservingPartitioner is an obsolete form of BOP, that stores
+# - keys in a less-efficient format and only works with keys that are
+# UTF8-encoded Strings.
+# - CollatingOPP collates according to EN,US rules rather than lexical byte
+# ordering. Use this as an example if you need custom collation.
+#
+# See http://wiki.apache.org/cassandra/Operations for more on
+# partitioners and token selection.
+partitioner: org.apache.cassandra.dht.Murmur3Partitioner
+
+# directories where Cassandra should store data on disk.
+data_file_directories:
+ - target/embeddedCassandra/data
+
+# commit log
+commitlog_directory: target/embeddedCassandra/commitlog
+
+hints_directory: target/embeddedCassandra/hints
+
+# policy for data disk failures:
+# stop: shut down gossip and Thrift, leaving the node effectively dead, but
+# can still be inspected via JMX.
+# best_effort: stop using the failed disk and respond to requests based on
+# remaining available sstables. This means you WILL see obsolete
+# data at CL.ONE!
+# ignore: ignore fatal errors and let requests fail, as in pre-1.2 Cassandra
+disk_failure_policy: stop
+
+
+# Maximum size of the key cache in memory.
+#
+# Each key cache hit saves 1 seek and each row cache hit saves 2 seeks at the
+# minimum, sometimes more. The key cache is fairly tiny for the amount of
+# time it saves, so it's worthwhile to use it at large numbers.
+# The row cache saves even more time, but must store the whole values of
+# its rows, so it is extremely space-intensive. It's best to only use the
+# row cache if you have hot rows or static rows.
+#
+# NOTE: if you reduce the size, you may not get you hottest keys loaded on startup.
+#
+# Default value is empty to make it "auto" (min(5% of Heap (in MB), 100MB)). Set to 0 to disable key cache.
+key_cache_size_in_mb:
+
+# Duration in seconds after which Cassandra should
+# safe the keys cache. Caches are saved to saved_caches_directory as
+# specified in this configuration file.
+#
+# Saved caches greatly improve cold-start speeds, and is relatively cheap in
+# terms of I/O for the key cache. Row cache saving is much more expensive and
+# has limited use.
+#
+# Default is 14400 or 4 hours.
+key_cache_save_period: 14400
+
+# Number of keys from the key cache to save
+# Disabled by default, meaning all keys are going to be saved
+# key_cache_keys_to_save: 100
+
+# Maximum size of the row cache in memory.
+# NOTE: if you reduce the size, you may not get you hottest keys loaded on startup.
+#
+# Default value is 0, to disable row caching.
+row_cache_size_in_mb: 0
+
+# Duration in seconds after which Cassandra should
+# safe the row cache. Caches are saved to saved_caches_directory as specified
+# in this configuration file.
+#
+# Saved caches greatly improve cold-start speeds, and is relatively cheap in
+# terms of I/O for the key cache. Row cache saving is much more expensive and
+# has limited use.
+#
+# Default is 0 to disable saving the row cache.
+row_cache_save_period: 0
+
+# Number of keys from the row cache to save
+# Disabled by default, meaning all keys are going to be saved
+# row_cache_keys_to_save: 100
+
+# saved caches
+saved_caches_directory: target/embeddedCassandra/saved_caches
+
+# commitlog_sync may be either "periodic" or "batch."
+# When in batch mode, Cassandra won't ack writes until the commit log
+# has been fsynced to disk. It will wait up to
+# commitlog_sync_batch_window_in_ms milliseconds for other writes, before
+# performing the sync.
+#
+# commitlog_sync: batch
+# commitlog_sync_batch_window_in_ms: 50
+#
+# the other option is "periodic" where writes may be acked immediately
+# and the CommitLog is simply synced every commitlog_sync_period_in_ms
+# milliseconds.
+commitlog_sync: periodic
+commitlog_sync_period_in_ms: 10000
+
+# The size of the individual commitlog file segments. A commitlog
+# segment may be archived, deleted, or recycled once all the data
+# in it (potentially from each columnfamily in the system) has been
+# flushed to sstables.
+#
+# The default size is 32, which is almost always fine, but if you are
+# archiving commitlog segments (see commitlog_archiving.properties),
+# then you probably want a finer granularity of archiving; 8 or 16 MB
+# is reasonable.
+commitlog_segment_size_in_mb: 32
+
+# any class that implements the SeedProvider interface and has a
+# constructor that takes a Map<String, String> of parameters will do.
+seed_provider:
+ # Addresses of hosts that are deemed contact points.
+ # Cassandra nodes use this list of hosts to find each other and learn
+ # the topology of the ring. You must change this if you are running
+ # multiple nodes!
+ - class_name: org.apache.cassandra.locator.SimpleSeedProvider
+ parameters:
+ # seeds is actually a comma-delimited list of addresses.
+ # Ex: "<ip1>,<ip2>,<ip3>"
+ - seeds: "127.0.0.1"
+
+
+# For workloads with more data than can fit in memory, Cassandra's
+# bottleneck will be reads that need to fetch data from
+# disk. "concurrent_reads" should be set to (16 * number_of_drives) in
+# order to allow the operations to enqueue low enough in the stack
+# that the OS and drives can reorder them.
+#
+# On the other hand, since writes are almost never IO bound, the ideal
+# number of "concurrent_writes" is dependent on the number of cores in
+# your system; (8 * number_of_cores) is a good rule of thumb.
+concurrent_reads: 32
+concurrent_writes: 32
+
+# Total memory to use for memtables. Cassandra will flush the largest
+# memtable when this much memory is used.
+# If omitted, Cassandra will set it to 1/3 of the heap.
+# memtable_total_space_in_mb: 2048
+
+# Total space to use for commitlogs.
+# If space gets above this value (it will round up to the next nearest
+# segment multiple), Cassandra will flush every dirty CF in the oldest
+# segment and remove it.
+# commitlog_total_space_in_mb: 4096
+
+# This sets the amount of memtable flush writer threads. These will
+# be blocked by disk io, and each one will hold a memtable in memory
+# while blocked. If you have a large heap and many data directories,
+# you can increase this value for better flush performance.
+# By default this will be set to the amount of data directories defined.
+#memtable_flush_writers: 1
+
+# the number of full memtables to allow pending flush, that is,
+# waiting for a writer thread. At a minimum, this should be set to
+# the maximum number of secondary indexes created on a single CF.
+#memtable_flush_queue_size: 4
+
+# Whether to, when doing sequential writing, fsync() at intervals in
+# order to force the operating system to flush the dirty
+# buffers. Enable this to avoid sudden dirty buffer flushing from
+# impacting read latencies. Almost always a good idea on SSD:s; not
+# necessarily on platters.
+trickle_fsync: false
+trickle_fsync_interval_in_kb: 10240
+
+# TCP port, for commands and data
+storage_port: 7010
+
+# SSL port, for encrypted communication. Unused unless enabled in
+# encryption_options
+ssl_storage_port: 7011
+
+# Address to bind to and tell other Cassandra nodes to connect to. You
+# _must_ change this if you want multiple nodes to be able to
+# communicate!
+#
+# Leaving it blank leaves it up to InetAddress.getLocalHost(). This
+# will always do the Right Thing *if* the node is properly configured
+# (hostname, name resolution, etc), and the Right Thing is to use the
+# address associated with the hostname (it might not be).
+#
+# Setting this to 0.0.0.0 is always wrong.
+listen_address: 127.0.0.1
+
+start_native_transport: true
+# port for the CQL native transport to listen for clients on
+native_transport_port: 9142
+
+# Whether to start the thrift rpc server.
+start_rpc: false
+
+# Address to broadcast to other Cassandra nodes
+# Leaving this blank will set it to the same value as listen_address
+# broadcast_address: 1.2.3.4
+
+# The address to bind the Thrift RPC service to -- clients connect
+# here. Unlike ListenAddress above, you *can* specify 0.0.0.0 here if
+# you want Thrift to listen on all interfaces.
+#
+# Leaving this blank has the same effect it does for ListenAddress,
+# (i.e. it will be based on the configured hostname of the node).
+rpc_address: localhost
+# port for Thrift to listen for clients on
+rpc_port: 9171
+
+# enable or disable keepalive on rpc connections
+rpc_keepalive: true
+
+# Cassandra provides three options for the RPC Server:
+#
+# sync -> One connection per thread in the rpc pool (see below).
+# For a very large number of clients, memory will be your limiting
+# factor; on a 64 bit JVM, 128KB is the minimum stack size per thread.
+# Connection pooling is very, very strongly recommended.
+#
+# async -> Nonblocking server implementation with one thread to serve
+# rpc connections. This is not recommended for high throughput use
+# cases. Async has been tested to be about 50% slower than sync
+# or hsha and is deprecated: it will be removed in the next major release.
+#
+# hsha -> Stands for "half synchronous, half asynchronous." The rpc thread pool
+# (see below) is used to manage requests, but the threads are multiplexed
+# across the different clients.
+#
+# The default is sync because on Windows hsha is about 30% slower. On Linux,
+# sync/hsha performance is about the same, with hsha of course using less memory.
+rpc_server_type: sync
+
+# Uncomment rpc_min|max|thread to set request pool size.
+# You would primarily set max for the sync server to safeguard against
+# misbehaved clients; if you do hit the max, Cassandra will block until one
+# disconnects before accepting more. The defaults for sync are min of 16 and max
+# unlimited.
+#
+# For the Hsha server, the min and max both default to quadruple the number of
+# CPU cores.
+#
+# This configuration is ignored by the async server.
+#
+# rpc_min_threads: 16
+# rpc_max_threads: 2048
+
+# uncomment to set socket buffer sizes on rpc connections
+# rpc_send_buff_size_in_bytes:
+# rpc_recv_buff_size_in_bytes:
+
+# Frame size for thrift (maximum field length).
+# 0 disables TFramedTransport in favor of TSocket. This option
+# is deprecated; we strongly recommend using Framed mode.
+thrift_framed_transport_size_in_mb: 15
+
+# The max length of a thrift message, including all fields and
+# internal thrift overhead.
+thrift_max_message_length_in_mb: 16
+
+# Set to true to have Cassandra create a hard link to each sstable
+# flushed or streamed locally in a backups/ subdirectory of the
+# Keyspace data. Removing these links is the operator's
+# responsibility.
+incremental_backups: false
+
+# Whether or not to take a snapshot before each compaction. Be
+# careful using this option, since Cassandra won't clean up the
+# snapshots for you. Mostly useful if you're paranoid when there
+# is a data format change.
+snapshot_before_compaction: false
+
+# Whether or not a snapshot is taken of the data before keyspace truncation
+# or dropping of column families. The STRONGLY advised default of true
+# should be used to provide data safety. If you set this flag to false, you will
+# lose data on truncation or drop.
+auto_snapshot: false
+
+# Add column indexes to a row after its contents reach this size.
+# Increase if your column values are large, or if you have a very large
+# number of columns. The competing causes are, Cassandra has to
+# deserialize this much of the row to read a single column, so you want
+# it to be small - at least if you do many partial-row reads - but all
+# the index data is read for each access, so you don't want to generate
+# that wastefully either.
+column_index_size_in_kb: 64
+
+# Size limit for rows being compacted in memory. Larger rows will spill
+# over to disk and use a slower two-pass compaction process. A message
+# will be logged specifying the row key.
+#in_memory_compaction_limit_in_mb: 64
+
+# Number of simultaneous compactions to allow, NOT including
+# validation "compactions" for anti-entropy repair. Simultaneous
+# compactions can help preserve read performance in a mixed read/write
+# workload, by mitigating the tendency of small sstables to accumulate
+# during a single long running compactions. The default is usually
+# fine and if you experience problems with compaction running too
+# slowly or too fast, you should look at
+# compaction_throughput_mb_per_sec first.
+#
+# This setting has no effect on LeveledCompactionStrategy.
+#
+# concurrent_compactors defaults to the number of cores.
+# Uncomment to make compaction mono-threaded, the pre-0.8 default.
+#concurrent_compactors: 1
+
+# Multi-threaded compaction. When enabled, each compaction will use
+# up to one thread per core, plus one thread per sstable being merged.
+# This is usually only useful for SSD-based hardware: otherwise,
+# your concern is usually to get compaction to do LESS i/o (see:
+# compaction_throughput_mb_per_sec), not more.
+#multithreaded_compaction: false
+
+# Throttles compaction to the given total throughput across the entire
+# system. The faster you insert data, the faster you need to compact in
+# order to keep the sstable count down, but in general, setting this to
+# 16 to 32 times the rate you are inserting data is more than sufficient.
+# Setting this to 0 disables throttling. Note that this account for all types
+# of compaction, including validation compaction.
+compaction_throughput_mb_per_sec: 16
+
+# Track cached row keys during compaction, and re-cache their new
+# positions in the compacted sstable. Disable if you use really large
+# key caches.
+#compaction_preheat_key_cache: true
+
+# Throttles all outbound streaming file transfers on this node to the
+# given total throughput in Mbps. This is necessary because Cassandra does
+# mostly sequential IO when streaming data during bootstrap or repair, which
+# can lead to saturating the network connection and degrading rpc performance.
+# When unset, the default is 200 Mbps or 25 MB/s.
+# stream_throughput_outbound_megabits_per_sec: 200
+
+# How long the coordinator should wait for read operations to complete
+read_request_timeout_in_ms: 5000
+# How long the coordinator should wait for seq or index scans to complete
+range_request_timeout_in_ms: 10000
+# How long the coordinator should wait for writes to complete
+write_request_timeout_in_ms: 10000
+# How long a coordinator should continue to retry a CAS operation
+# that contends with other proposals for the same row
+cas_contention_timeout_in_ms: 1000
+# How long the coordinator should wait for truncates to complete
+# (This can be much longer, because unless auto_snapshot is disabled
+# we need to flush first so we can snapshot before removing the data.)
+truncate_request_timeout_in_ms: 60000
+# The default timeout for other, miscellaneous operations
+request_timeout_in_ms: 10000
+
+# Enable operation timeout information exchange between nodes to accurately
+# measure request timeouts. If disabled, replicas will assume that requests
+# were forwarded to them instantly by the coordinator, which means that
+# under overload conditions we will waste that much extra time processing
+# already-timed-out requests.
+#
+# Warning: before enabling this property make sure to ntp is installed
+# and the times are synchronized between the nodes.
+cross_node_timeout: false
+
+# Enable socket timeout for streaming operation.
+# When a timeout occurs during streaming, streaming is retried from the start
+# of the current file. This _can_ involve re-streaming an important amount of
+# data, so you should avoid setting the value too low.
+# Default value is 0, which never timeout streams.
+# streaming_socket_timeout_in_ms: 0
+
+# phi value that must be reached for a host to be marked down.
+# most users should never need to adjust this.
+# phi_convict_threshold: 8
+
+# endpoint_snitch -- Set this to a class that implements
+# IEndpointSnitch. The snitch has two functions:
+# - it teaches Cassandra enough about your network topology to route
+# requests efficiently
+# - it allows Cassandra to spread replicas around your cluster to avoid
+# correlated failures. It does this by grouping machines into
+# "datacenters" and "racks." Cassandra will do its best not to have
+# more than one replica on the same "rack" (which may not actually
+# be a physical location)
+#
+# IF YOU CHANGE THE SNITCH AFTER DATA IS INSERTED INTO THE CLUSTER,
+# YOU MUST RUN A FULL REPAIR, SINCE THE SNITCH AFFECTS WHERE REPLICAS
+# ARE PLACED.
+#
+# Out of the box, Cassandra provides
+# - SimpleSnitch:
+# Treats Strategy order as proximity. This improves cache locality
+# when disabling read repair, which can further improve throughput.
+# Only appropriate for single-datacenter deployments.
+# - PropertyFileSnitch:
+# Proximity is determined by rack and data center, which are
+# explicitly configured in cassandra-topology.properties.
+# - RackInferringSnitch:
+# Proximity is determined by rack and data center, which are
+# assumed to correspond to the 3rd and 2nd octet of each node's
+# IP address, respectively. Unless this happens to match your
+# deployment conventions (as it did Facebook's), this is best used
+# as an example of writing a custom Snitch class.
+# - Ec2Snitch:
+# Appropriate for EC2 deployments in a single Region. Loads Region
+# and Availability Zone information from the EC2 API. The Region is
+# treated as the Datacenter, and the Availability Zone as the rack.
+# Only private IPs are used, so this will not work across multiple
+# Regions.
+# - Ec2MultiRegionSnitch:
+# Uses public IPs as broadcast_address to allow cross-region
+# connectivity. (Thus, you should set seed addresses to the public
+# IP as well.) You will need to open the storage_port or
+# ssl_storage_port on the public IP firewall. (For intra-Region
+# traffic, Cassandra will switch to the private IP after
+# establishing a connection.)
+#
+# You can use a custom Snitch by setting this to the full class name
+# of the snitch, which will be assumed to be on your classpath.
+endpoint_snitch: SimpleSnitch
+
+# controls how often to perform the more expensive part of host score
+# calculation
+dynamic_snitch_update_interval_in_ms: 100
+# controls how often to reset all host scores, allowing a bad host to
+# possibly recover
+dynamic_snitch_reset_interval_in_ms: 600000
+# if set greater than zero and read_repair_chance is < 1.0, this will allow
+# 'pinning' of replicas to hosts in order to increase cache capacity.
+# The badness threshold will control how much worse the pinned host has to be
+# before the dynamic snitch will prefer other replicas over it. This is
+# expressed as a double which represents a percentage. Thus, a value of
+# 0.2 means Cassandra would continue to prefer the static snitch values
+# until the pinned host was 20% worse than the fastest.
+dynamic_snitch_badness_threshold: 0.1
+
+# request_scheduler -- Set this to a class that implements
+# RequestScheduler, which will schedule incoming client requests
+# according to the specific policy. This is useful for multi-tenancy
+# with a single Cassandra cluster.
+# NOTE: This is specifically for requests from the client and does
+# not affect inter node communication.
+# org.apache.cassandra.scheduler.NoScheduler - No scheduling takes place
+# org.apache.cassandra.scheduler.RoundRobinScheduler - Round robin of
+# client requests to a node with a separate queue for each
+# request_scheduler_id. The scheduler is further customized by
+# request_scheduler_options as described below.
+request_scheduler: org.apache.cassandra.scheduler.NoScheduler
+
+# Scheduler Options vary based on the type of scheduler
+# NoScheduler - Has no options
+# RoundRobin
+# - throttle_limit -- The throttle_limit is the number of in-flight
+# requests per client. Requests beyond
+# that limit are queued up until
+# running requests can complete.
+# The value of 80 here is twice the number of
+# concurrent_reads + concurrent_writes.
+# - default_weight -- default_weight is optional and allows for
+# overriding the default which is 1.
+# - weights -- Weights are optional and will default to 1 or the
+# overridden default_weight. The weight translates into how
+# many requests are handled during each turn of the
+# RoundRobin, based on the scheduler id.
+#
+# request_scheduler_options:
+# throttle_limit: 80
+# default_weight: 5
+# weights:
+# Keyspace1: 1
+# Keyspace2: 5
+
+# request_scheduler_id -- An identifer based on which to perform
+# the request scheduling. Currently the only valid option is keyspace.
+# request_scheduler_id: keyspace
+
+# index_interval controls the sampling of entries from the primrary
+# row index in terms of space versus time. The larger the interval,
+# the smaller and less effective the sampling will be. In technicial
+# terms, the interval coresponds to the number of index entries that
+# are skipped between taking each sample. All the sampled entries
+# must fit in memory. Generally, a value between 128 and 512 here
+# coupled with a large key cache size on CFs results in the best trade
+# offs. This value is not often changed, however if you have many
+# very small rows (many to an OS page), then increasing this will
+# often lower memory usage without a impact on performance.
+index_interval: 128
+
+# Enable or disable inter-node encryption
+# Default settings are TLS v1, RSA 1024-bit keys (it is imperative that
+# users generate their own keys) TLS_RSA_WITH_AES_128_CBC_SHA as the cipher
+# suite for authentication, key exchange and encryption of the actual data transfers.
+# NOTE: No custom encryption options are enabled at the moment
+# The available internode options are : all, none, dc, rack
+#
+# If set to dc cassandra will encrypt the traffic between the DCs
+# If set to rack cassandra will encrypt the traffic between the racks
+#
+# The passwords used in these options must match the passwords used when generating
+# the keystore and truststore. For instructions on generating these files, see:
+# http://download.oracle.com/javase/6/docs/technotes/guides/security/jsse/JSSERefGuide.html#CreateKeystore
+#
+server_encryption_options:
+ internode_encryption: none
+ keystore: conf/.keystore
+ keystore_password: cassandra
+ truststore: conf/.truststore
+ truststore_password: cassandra
+ # More advanced defaults below:
+ # protocol: TLS
+ # algorithm: SunX509
+ # store_type: JKS
+ # cipher_suites: [TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA]
\ No newline at end of file
dao/src/test/resources/logback.xml 19(+19 -0)
diff --git a/dao/src/test/resources/logback.xml b/dao/src/test/resources/logback.xml
new file mode 100644
index 0000000..0969bbe
--- /dev/null
+++ b/dao/src/test/resources/logback.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<configuration>
+ <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder>
+ <pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern>
+ </encoder>
+ </appender>
+
+ <logger name="org.thingsboard.server.dao" level="TRACE"/>
+ <logger name="org.apache.cassandra" level="WARN"/>
+ <logger name="org.cassandraunit" level="INFO" />
+ <logger name="org.apache.cassandra" level="INFO" />
+
+ <root level="WARN">
+ <appender-ref ref="console"/>
+ </root>
+
+</configuration>
diff --git a/dao/src/test/resources/system-test.cql b/dao/src/test/resources/system-test.cql
new file mode 100644
index 0000000..da5d1f1
--- /dev/null
+++ b/dao/src/test/resources/system-test.cql
@@ -0,0 +1,2 @@
+TRUNCATE thingsboard.plugin;
+TRUNCATE thingsboard.rule;
\ No newline at end of file
diff --git a/dao/src/test/resources/TestJsonData.json b/dao/src/test/resources/TestJsonData.json
new file mode 100644
index 0000000..22e3ab3
--- /dev/null
+++ b/dao/src/test/resources/TestJsonData.json
@@ -0,0 +1,5 @@
+{
+ "fieldA": "field A value",
+ "fieldB": "field B value",
+ "fieldC": 42
+}
\ No newline at end of file
diff --git a/dao/src/test/resources/TestJsonDescriptor.json b/dao/src/test/resources/TestJsonDescriptor.json
new file mode 100644
index 0000000..4c8c7a4
--- /dev/null
+++ b/dao/src/test/resources/TestJsonDescriptor.json
@@ -0,0 +1,27 @@
+{
+ "schema": {
+ "title": "Simple Schema",
+ "type": "object",
+ "properties": {
+ "fieldA": {
+ "type": "string"
+ },
+ "fieldB": {
+ "type": "string"
+ },
+ "fieldC": {
+ "description": "Age in years",
+ "type": "integer",
+ "minimum": 0
+ }
+ },
+ "required": [
+ "fieldA",
+ "fieldB",
+ "fieldC"
+ ]
+ },
+ "form": [
+ "*"
+ ]
+}
\ No newline at end of file
ui/.babelrc 9(+9 -0)
diff --git a/ui/.babelrc b/ui/.babelrc
new file mode 100644
index 0000000..7093a61
--- /dev/null
+++ b/ui/.babelrc
@@ -0,0 +1,9 @@
+{
+ "presets": [
+ "react",
+ "es2015"
+ ],
+ "plugins": [
+ "react-hot-loader/babel"
+ ]
+}
ui/.eslintrc 15(+15 -0)
diff --git a/ui/.eslintrc b/ui/.eslintrc
new file mode 100644
index 0000000..5cb89e5
--- /dev/null
+++ b/ui/.eslintrc
@@ -0,0 +1,15 @@
+{
+ "extends": [
+ "eslint:recommended",
+ "plugin:import/errors",
+ "plugin:import/warnings",
+ "angular"
+ ],
+ "parser": "babel-eslint",
+ "settings": {
+ "import/ignore": [
+ "node_modules",
+ "\\.tpl\\.html$"
+ ]
+ }
+}
ui/.gitignore 2(+2 -0)
diff --git a/ui/.gitignore b/ui/.gitignore
new file mode 100644
index 0000000..09daa48
--- /dev/null
+++ b/ui/.gitignore
@@ -0,0 +1,2 @@
+node_modules
+.tern-project
ui/.jshintrc 13(+13 -0)
diff --git a/ui/.jshintrc b/ui/.jshintrc
new file mode 100644
index 0000000..6f00218
--- /dev/null
+++ b/ui/.jshintrc
@@ -0,0 +1,13 @@
+{
+ "globalstrict": true,
+ "globals": {
+ "angular": false,
+ "describe": false,
+ "it": false,
+ "expect": false,
+ "beforeEach": false,
+ "afterEach": false,
+ "module": false,
+ "inject": false
+ }
+}
\ No newline at end of file
ui/package.json 123(+123 -0)
diff --git a/ui/package.json b/ui/package.json
new file mode 100644
index 0000000..b8a0775
--- /dev/null
+++ b/ui/package.json
@@ -0,0 +1,123 @@
+{
+ "name": "thingsboard",
+ "private": true,
+ "version": "0.0.1",
+ "description": "Thingsboard UI",
+ "licenses": [
+ {
+ "type": "Apache-2.0",
+ "url": "http://www.apache.org/licenses/LICENSE-2.0"
+ }
+ ],
+ "scripts": {
+ "start": "babel-node --max_old_space_size=4096 server.js",
+ "build": "NODE_ENV=production webpack -p"
+ },
+ "dependencies": {
+ "ace-builds": "^1.2.5",
+ "angular": "^1.5.8",
+ "angular-animate": "^1.5.8",
+ "angular-aria": "^1.5.8",
+ "angular-breadcrumb": "^0.4.1",
+ "angular-carousel": "^1.0.1",
+ "angular-cookies": "^1.5.8",
+ "angular-drag-and-drop-lists": "^1.4.0",
+ "angular-fullscreen": "git://github.com/fabiobiondi/angular-fullscreen.git#master",
+ "angular-gridster": "^0.13.14",
+ "angular-hotkeys": "^1.7.0",
+ "angular-jwt": "^0.1.6",
+ "angular-material": "^1.1.1",
+ "angular-material-data-table": "^0.10.9",
+ "angular-material-icons": "^0.7.1",
+ "angular-messages": "^1.5.8",
+ "angular-route": "^1.5.8",
+ "angular-sanitize": "^1.5.8",
+ "angular-storage": "0.0.15",
+ "angular-touch": "^1.5.8",
+ "angular-translate": "^2.12.1",
+ "angular-translate-handler-log": "^2.12.1",
+ "angular-translate-interpolation-messageformat": "^2.12.1",
+ "angular-translate-loader-static-files": "^2.12.1",
+ "angular-translate-storage-cookie": "^2.12.1",
+ "angular-translate-storage-local": "^2.12.1",
+ "angular-ui-ace": "^0.2.3",
+ "angular-ui-router": "^0.3.1",
+ "angular-websocket": "^2.0.1",
+ "brace": "^0.8.0",
+ "canvas-gauges": "^2.0.9",
+ "clipboard": "^1.5.15",
+ "compass-sass-mixins": "^0.12.7",
+ "font-awesome": "^4.6.3",
+ "jquery": "^3.1.0",
+ "js-beautify": "^1.6.4",
+ "json-schema-defaults": "^0.2.0",
+ "justgage": "^1.2.2",
+ "material-ui": "^0.16.1",
+ "md-color-picker": "^0.2.6",
+ "mdPickers": "git://github.com/alenaksu/mdPickers.git#0.7.5",
+ "moment": "^2.15.0",
+ "ngclipboard": "^1.1.1",
+ "ngreact": "^0.3.0",
+ "objectpath": "^1.2.1",
+ "oclazyload": "^1.0.9",
+ "raphael": "^2.2.7",
+ "rc-select": "^6.6.1",
+ "react": "^15.3.2",
+ "react-ace": "^3.7.0",
+ "react-dom": "^15.3.2",
+ "react-schema-form": "^0.2.10",
+ "react-tap-event-plugin": "^1.0.0",
+ "reactcss": "^1.0.9",
+ "sass-material-colors": "0.0.5",
+ "schema-inspector": "^1.6.6",
+ "split.js": "^1.0.7",
+ "tinycolor2": "^1.4.1",
+ "v-accordion": "^1.6.0"
+ },
+ "devDependencies": {
+ "babel-cli": "^6.18.0",
+ "babel-core": "^6.14.0",
+ "babel-eslint": "^6.1.2",
+ "babel-loader": "^6.2.5",
+ "babel-preset-es2015": "^6.14.0",
+ "babel-preset-react": "^6.16.0",
+ "connect-history-api-fallback": "^1.3.0",
+ "copy-webpack-plugin": "^3.0.1",
+ "css-loader": "^0.25.0",
+ "eslint": "^3.4.0",
+ "eslint-config-angular": "^0.5.0",
+ "eslint-loader": "^1.5.0",
+ "eslint-plugin-angular": "^1.3.1",
+ "eslint-plugin-import": "^1.14.0",
+ "extract-text-webpack-plugin": "^1.0.1",
+ "file-loader": "^0.9.0",
+ "html-loader": "^0.4.3",
+ "html-minifier": "^3.2.2",
+ "html-minifier-loader": "^1.3.4",
+ "html-webpack-plugin": "^2.22.0",
+ "img-loader": "^1.3.1",
+ "less": "^2.7.1",
+ "less-loader": "^2.2.3",
+ "ng-annotate-loader": "^0.1.1",
+ "ngtemplate-loader": "^1.3.1",
+ "node-sass": "^3.9.3",
+ "postcss-loader": "^0.13.0",
+ "react-hot-loader": "^3.0.0-beta.6",
+ "sass-loader": "^4.0.2",
+ "style-loader": "^0.13.1",
+ "url-loader": "^0.5.7",
+ "webpack": "^1.13.2",
+ "webpack-dev-middleware": "^1.6.1",
+ "webpack-dev-server": "^1.15.1",
+ "webpack-hot-middleware": "^2.12.2"
+ },
+ "engine": "node >= 5.9.0",
+ "nyc": {
+ "exclude": [
+ "test",
+ "__tests__",
+ "node_modules",
+ "target"
+ ]
+ }
+}
ui/pom.xml 142(+142 -0)
diff --git a/ui/pom.xml b/ui/pom.xml
new file mode 100644
index 0000000..c9e4c68
--- /dev/null
+++ b/ui/pom.xml
@@ -0,0 +1,142 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.thingsboard</groupId>
+ <version>0.0.1-SNAPSHOT</version>
+ <artifactId>server</artifactId>
+ </parent>
+ <groupId>org.thingsboard.server</groupId>
+ <artifactId>ui</artifactId>
+ <packaging>jar</packaging>
+
+ <name>Thingsboard Server UI</name>
+ <url>http://thingsboard.org</url>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <main.dir>${basedir}/..</main.dir>
+ </properties>
+
+ <build>
+ <resources>
+ <resource>
+ <directory>${project.build.directory}/generated-resources</directory>
+ </resource>
+ </resources>
+ <plugins>
+ <plugin>
+ <groupId>com.github.eirslett</groupId>
+ <artifactId>frontend-maven-plugin</artifactId>
+ <version>1.0</version>
+ <configuration>
+ <installDirectory>target</installDirectory>
+ <workingDirectory>${basedir}</workingDirectory>
+ </configuration>
+ <executions>
+ <execution>
+ <id>install node and npm</id>
+ <goals>
+ <goal>install-node-and-npm</goal>
+ </goals>
+ <configuration>
+ <nodeVersion>v6.9.1</nodeVersion>
+ <npmVersion>3.10.8</npmVersion>
+ </configuration>
+ </execution>
+ <execution>
+ <id>npm install</id>
+ <goals>
+ <goal>npm</goal>
+ </goals>
+ <configuration>
+ <arguments>install</arguments>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ <profiles>
+ <profile>
+ <id>npm-build</id>
+ <activation>
+ <activeByDefault>true</activeByDefault>
+ </activation>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>com.github.eirslett</groupId>
+ <artifactId>frontend-maven-plugin</artifactId>
+ <version>1.0</version>
+ <configuration>
+ <installDirectory>target</installDirectory>
+ <workingDirectory>${basedir}</workingDirectory>
+ </configuration>
+ <executions>
+ <execution>
+ <id>npm build</id>
+ <goals>
+ <goal>npm</goal>
+ </goals>
+ <configuration>
+ <arguments>run build</arguments>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ <profile>
+ <id>npm-start</id>
+ <activation>
+ <property>
+ <name>npm-start</name>
+ </property>
+ </activation>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>com.github.eirslett</groupId>
+ <artifactId>frontend-maven-plugin</artifactId>
+ <version>1.0</version>
+ <configuration>
+ <installDirectory>target</installDirectory>
+ <workingDirectory>${basedir}</workingDirectory>
+ </configuration>
+ <executions>
+ <execution>
+ <id>npm start</id>
+ <goals>
+ <goal>npm</goal>
+ </goals>
+
+ <configuration>
+ <arguments>start</arguments>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+</project>
ui/server.js 75(+75 -0)
diff --git a/ui/server.js b/ui/server.js
new file mode 100644
index 0000000..842eb9c
--- /dev/null
+++ b/ui/server.js
@@ -0,0 +1,75 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-commonjs */
+/* eslint-disable global-require */
+/* eslint-disable import/no-nodejs-modules */
+
+const path = require('path');
+const webpack = require('webpack');
+const historyApiFallback = require("connect-history-api-fallback");
+const webpackDevMiddleware = require('webpack-dev-middleware');
+const webpackHotMiddleware = require('webpack-hot-middleware');
+const config = require('./webpack.config');
+
+const express = require('express');
+const http = require('http');
+const httpProxy = require('http-proxy');
+const forwardHost = 'localhost';
+const forwardPort = 8080;
+
+const app = express();
+const server = http.createServer(app);
+
+const PORT = 3000;
+
+const compiler = webpack(config);
+
+app.use(historyApiFallback());
+app.use(webpackDevMiddleware(compiler, {noInfo: true, publicPath: config.output.publicPath}));
+app.use(webpackHotMiddleware(compiler));
+
+const root = path.join(__dirname, '/src');
+
+app.use('/static', express.static(root));
+
+const apiProxy = httpProxy.createProxyServer({
+ target: {
+ host: forwardHost,
+ port: forwardPort
+ }
+});
+
+console.info(`Forwarding API requests to http://${forwardHost}:${forwardPort}`);
+
+app.all('/api/*', (req, res) => {
+ apiProxy.web(req, res);
+});
+
+app.get('*', function(req, res) {
+ res.sendFile(path.join(__dirname, 'src/index.html'));
+});
+
+server.on('upgrade', (req, socket, head) => {
+ apiProxy.ws(req, socket, head);
+});
+
+server.listen(PORT, '0.0.0.0', (error) => {
+ if (error) {
+ console.error(error);
+ } else {
+ console.info(`==> 🌎 Listening on port ${PORT}. Open up http://localhost:${PORT}/ in your browser.`);
+ }
+});
ui/src/app/admin/admin.controller.js 54(+54 -0)
diff --git a/ui/src/app/admin/admin.controller.js b/ui/src/app/admin/admin.controller.js
new file mode 100644
index 0000000..f59ae80
--- /dev/null
+++ b/ui/src/app/admin/admin.controller.js
@@ -0,0 +1,54 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*@ngInject*/
+export default function AdminController(adminService, toast, $scope, $rootScope, $state, $translate) {
+
+ var vm = this;
+ vm.save = save;
+ vm.sendTestMail = sendTestMail;
+ vm.smtpProtocols = ('smtp smtps').split(' ').map(function (protocol) {
+ return protocol;
+ });
+
+ $translate('admin.test-mail-sent').then(function (translation) {
+ vm.testMailSent = translation;
+ }, function (translationId) {
+ vm.testMailSent = translationId;
+ });
+
+
+ loadSettings();
+
+ function loadSettings() {
+ adminService.getAdminSettings($state.$current.data.key).then(function success(settings) {
+ vm.settings = settings;
+ });
+ }
+
+ function save() {
+ adminService.saveAdminSettings(vm.settings).then(function success(settings) {
+ vm.settings = settings;
+ vm.settingsForm.$setPristine();
+ });
+ }
+
+ function sendTestMail() {
+ adminService.sendTestMail(vm.settings).then(function success() {
+ toast.showSuccess($translate.instant('admin.test-mail-sent'));
+ });
+ }
+
+}
ui/src/app/admin/admin.routes.js 73(+73 -0)
diff --git a/ui/src/app/admin/admin.routes.js b/ui/src/app/admin/admin.routes.js
new file mode 100644
index 0000000..8f7c71a
--- /dev/null
+++ b/ui/src/app/admin/admin.routes.js
@@ -0,0 +1,73 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import generalSettingsTemplate from '../admin/general-settings.tpl.html';
+import outgoingMailSettingsTemplate from '../admin/outgoing-mail-settings.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function AdminRoutes($stateProvider) {
+ $stateProvider
+ .state('home.settings', {
+ url: '/settings',
+ module: 'private',
+ auth: ['SYS_ADMIN'],
+ redirectTo: 'home.settings.general',
+ ncyBreadcrumb: {
+ label: '{"icon": "settings", "label": "admin.system-settings"}'
+ }
+ })
+ .state('home.settings.general', {
+ url: '/general',
+ module: 'private',
+ auth: ['SYS_ADMIN'],
+ views: {
+ "content@home": {
+ templateUrl: generalSettingsTemplate,
+ controllerAs: 'vm',
+ controller: 'AdminController'
+ }
+ },
+ data: {
+ key: 'general',
+ pageTitle: 'admin.general-settings'
+ },
+ ncyBreadcrumb: {
+ label: '{"icon": "settings_applications", "label": "admin.general"}'
+ }
+ })
+ .state('home.settings.outgoing-mail', {
+ url: '/outgoing-mail',
+ module: 'private',
+ auth: ['SYS_ADMIN'],
+ views: {
+ "content@home": {
+ templateUrl: outgoingMailSettingsTemplate,
+ controllerAs: 'vm',
+ controller: 'AdminController'
+ }
+ },
+ data: {
+ key: 'mail',
+ pageTitle: 'admin.outgoing-mail-settings'
+ },
+ ncyBreadcrumb: {
+ label: '{"icon": "mail", "label": "admin.outgoing-mail"}'
+ }
+ });
+}
ui/src/app/admin/general-settings.tpl.html 45(+45 -0)
diff --git a/ui/src/app/admin/general-settings.tpl.html b/ui/src/app/admin/general-settings.tpl.html
new file mode 100644
index 0000000..70cbfcf
--- /dev/null
+++ b/ui/src/app/admin/general-settings.tpl.html
@@ -0,0 +1,45 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div layout="row" width="100%" layout-wrap>
+ <md-card flex-gt-sm="60" flex="100" >
+ <md-card-title>
+ <md-card-title-text>
+ <span translate class="md-headline">admin.general-settings</span>
+ </md-card-title-text>
+ </md-card-title>
+ <md-progress-linear md-mode="indeterminate" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-card-content>
+ <form name="vm.settingsForm" ng-submit="vm.save()" tb-confirm-on-exit confirm-form="vm.settingsForm">
+ <fieldset ng-disabled="loading">
+ <md-input-container class="md-block">
+ <label translate>admin.base-url</label>
+ <input required name="baseUrl" ng-model="vm.settings.jsonValue.baseUrl">
+ <div ng-messages="vm.settingsForm.baseUrl.$error">
+ <div translate ng-message="required">admin.base-url-required</div>
+ </div>
+ </md-input-container>
+ <div layout="row" layout-align="end center" width="100%" layout-wrap>
+ <md-button ng-disabled="loading || vm.settingsForm.$invalid || !vm.settingsForm.$dirty" type="submit" class="md-raised md-primary">{{'action.save' | translate}}</md-button>
+ </div>
+ </fieldset>
+ </form>
+ </md-card-content>
+ </md-card>
+</div>
+
ui/src/app/admin/index.js 36(+36 -0)
diff --git a/ui/src/app/admin/index.js b/ui/src/app/admin/index.js
new file mode 100644
index 0000000..087d912
--- /dev/null
+++ b/ui/src/app/admin/index.js
@@ -0,0 +1,36 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import uiRouter from 'angular-ui-router';
+import ngMaterial from 'angular-material';
+import ngMessages from 'angular-messages';
+import thingsboardApiAdmin from '../api/admin.service';
+import thingsboardConfirmOnExit from '../components/confirm-on-exit.directive';
+import thingsboardToast from '../services/toast';
+
+import AdminRoutes from './admin.routes';
+import AdminController from './admin.controller';
+
+export default angular.module('thingsboard.admin', [
+ uiRouter,
+ ngMaterial,
+ ngMessages,
+ thingsboardApiAdmin,
+ thingsboardConfirmOnExit,
+ thingsboardToast
+])
+ .config(AdminRoutes)
+ .controller('AdminController', AdminController)
+ .name;
diff --git a/ui/src/app/admin/outgoing-mail-settings.tpl.html b/ui/src/app/admin/outgoing-mail-settings.tpl.html
new file mode 100644
index 0000000..6e0914f
--- /dev/null
+++ b/ui/src/app/admin/outgoing-mail-settings.tpl.html
@@ -0,0 +1,99 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div layout="row" width="100%" layout-wrap tb-help="'outgoingMailSettings'" help-container-id="help-container">
+ <md-card flex-gt-sm="60" flex="100" >
+ <md-card-title>
+ <md-card-title-text layout="row">
+ <span translate class="md-headline">admin.outgoing-mail-settings</span>
+ <span flex></span>
+ <div id="help-container"></div>
+ </md-card-title-text>
+ </md-card-title>
+ <md-progress-linear md-mode="indeterminate" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-card-content>
+ <form name="vm.settingsForm" ng-submit="vm.save()" tb-confirm-on-exit confirm-form="vm.settingsForm">
+ <fieldset ng-disabled="loading">
+ <md-input-container class="md-block">
+ <label translate>admin.mail-from</label>
+ <input required name="mailFrom" ng-model="vm.settings.jsonValue.mailFrom">
+ <div ng-messages="vm.settingsForm.mailFrom.$error">
+ <div translate ng-message="required">admin.mail-from-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container class="md-block">
+ <label translate>admin.smtp-protocol</label>
+ <md-select ng-disabled="loading" ng-model="vm.settings.jsonValue.smtpProtocol">
+ <md-option ng-repeat="smtpProtocol in vm.smtpProtocols" value="{{smtpProtocol}}">
+ {{smtpProtocol.toUpperCase()}}
+ </md-option>
+ </md-select>
+ </md-input-container>
+ <div layout-gt-sm="row">
+ <md-input-container class="md-block" flex="100" flex-gt-sm="60">
+ <label translate>admin.smtp-host</label>
+ <input required name="smtpHost" ng-model="vm.settings.jsonValue.smtpHost"
+ placeholder="localhost">
+ <div ng-messages="vm.settingsForm.smtpHost.$error">
+ <div translate ng-message="required">admin.smtp-host-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container class="md-block" flex="100" flex-gt-sm="40">
+ <label translate>admin.smtp-port</label>
+ <input required name="smtpPort" ng-model="vm.settings.jsonValue.smtpPort"
+ placeholder="25"
+ ng-pattern="/^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/"
+ md-maxlength="5">
+ <div ng-messages="vm.settingsForm.smtpPort.$error" role="alert" multiple>
+ <div translate ng-message="required">admin.smtp-port-required</div>
+ <div translate ng-message="pattern">admin.smtp-port-invalid</div>
+ <div translate ng-message="md-maxlength">admin.smtp-port-invalid</div>
+ </div>
+ </md-input-container>
+ </div>
+ <md-input-container class="md-block">
+ <label translate>admin.timeout-msec</label>
+ <input required name="timeout" ng-model="vm.settings.jsonValue.timeout"
+ placeholder="10000"
+ ng-pattern="/^[0-9]{1,6}$/"
+ md-maxlength="6">
+ <div ng-messages="vm.settingsForm.timeout.$error" role="alert" multiple>
+ <div translate ng-message="required">admin.timeout-required</div>
+ <div translate ng-message="pattern">admin.timeout-invalid</div>
+ <div translate ng-message="md-maxlength">admin.timeout-invalid</div>
+ </div>
+ </md-input-container>
+ <md-checkbox ng-disabled="loading" ng-true-value="'true'" ng-false-value="'false'"
+ aria-label="{{ 'admin.enable-tls' | translate }}" ng-model="vm.settings.jsonValue.enableTls">{{ 'admin.enable-tls' | translate }}</md-checkbox>
+ <md-input-container class="md-block">
+ <label translate>common.username</label>
+ <input name="username" placeholder="{{ 'common.enter-username' | translate }}" ng-model="vm.settings.jsonValue.username">
+ </md-input-container>
+ <md-input-container class="md-block">
+ <label translate>common.password</label>
+ <input name="password" placeholder="{{ 'common.enter-password' | translate }}" type="password" ng-model="vm.settings.jsonValue.password">
+ </md-input-container>
+ <div layout="row" layout-align="end center" width="100%" layout-wrap>
+ <md-button ng-disabled="loading || vm.settingsForm.$invalid" ng-click="vm.sendTestMail()" class="md-raised">{{'admin.send-test-mail' | translate}}</md-button>
+ <md-button ng-disabled="loading || vm.settingsForm.$invalid || !vm.settingsForm.$dirty" type="submit" class="md-raised md-primary">{{'action.save' | translate}}</md-button>
+ </div>
+ </fieldset>
+ </form>
+ </md-card-content>
+ </md-card>
+</div>
ui/src/app/api/admin.service.js 63(+63 -0)
diff --git a/ui/src/app/api/admin.service.js b/ui/src/app/api/admin.service.js
new file mode 100644
index 0000000..e49a017
--- /dev/null
+++ b/ui/src/app/api/admin.service.js
@@ -0,0 +1,63 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export default angular.module('thingsboard.api.admin', [])
+ .factory('adminService', AdminService)
+ .name;
+
+/*@ngInject*/
+function AdminService($http, $q) {
+
+ var service = {
+ getAdminSettings: getAdminSettings,
+ saveAdminSettings: saveAdminSettings,
+ sendTestMail: sendTestMail
+ }
+
+ return service;
+
+ function getAdminSettings(key) {
+ var deferred = $q.defer();
+ var url = '/api/admin/settings/' + key;
+ $http.get(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function saveAdminSettings(settings) {
+ var deferred = $q.defer();
+ var url = '/api/admin/settings';
+ $http.post(url, settings).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function sendTestMail(settings) {
+ var deferred = $q.defer();
+ var url = '/api/admin/settings/testMail';
+ $http.post(url, settings).then(function success() {
+ deferred.resolve();
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+}
diff --git a/ui/src/app/api/component-descriptor.service.js b/ui/src/app/api/component-descriptor.service.js
new file mode 100644
index 0000000..7f108ff
--- /dev/null
+++ b/ui/src/app/api/component-descriptor.service.js
@@ -0,0 +1,87 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export default angular.module('thingsboard.api.componentDescriptor', [])
+ .factory('componentDescriptorService', ComponentDescriptorService).name;
+
+/*@ngInject*/
+function ComponentDescriptorService($http, $q) {
+
+ var componentsByType = {};
+ var componentsByClazz = {};
+ var actionsByPlugin = {};
+
+ var service = {
+ getComponentDescriptorsByType: getComponentDescriptorsByType,
+ getComponentDescriptorByClazz: getComponentDescriptorByClazz,
+ getPluginActionsByPluginClazz: getPluginActionsByPluginClazz
+ }
+
+ return service;
+
+ function getComponentDescriptorsByType(componentType) {
+ var deferred = $q.defer();
+ if (componentsByType[componentType]) {
+ deferred.resolve(componentsByType[componentType]);
+ } else {
+ var url = '/api/components/' + componentType;
+ $http.get(url, null).then(function success(response) {
+ componentsByType[componentType] = response.data;
+ for (var i = 0; i < componentsByType[componentType].length; i++) {
+ var component = componentsByType[componentType][i];
+ componentsByClazz[component.clazz] = component;
+ }
+ deferred.resolve(componentsByType[componentType]);
+ }, function fail() {
+ deferred.reject();
+ });
+
+ }
+ return deferred.promise;
+ }
+
+ function getComponentDescriptorByClazz(componentDescriptorClazz) {
+ var deferred = $q.defer();
+ if (componentsByClazz[componentDescriptorClazz]) {
+ deferred.resolve(componentsByClazz[componentDescriptorClazz]);
+ } else {
+ var url = '/api/component/' + componentDescriptorClazz;
+ $http.get(url, null).then(function success(response) {
+ componentsByClazz[componentDescriptorClazz] = response.data;
+ deferred.resolve(componentsByClazz[componentDescriptorClazz]);
+ }, function fail() {
+ deferred.reject();
+ });
+ }
+ return deferred.promise;
+ }
+
+ function getPluginActionsByPluginClazz(pluginClazz) {
+ var deferred = $q.defer();
+ if (actionsByPlugin[pluginClazz]) {
+ deferred.resolve(actionsByPlugin[pluginClazz]);
+ } else {
+ var url = '/api/components/actions/' + pluginClazz;
+ $http.get(url, null).then(function success(response) {
+ actionsByPlugin[pluginClazz] = response.data;
+ deferred.resolve(actionsByPlugin[pluginClazz]);
+ }, function fail() {
+ deferred.reject();
+ });
+ }
+ return deferred.promise;
+ }
+
+}
ui/src/app/api/customer.service.js 85(+85 -0)
diff --git a/ui/src/app/api/customer.service.js b/ui/src/app/api/customer.service.js
new file mode 100644
index 0000000..b6570ef
--- /dev/null
+++ b/ui/src/app/api/customer.service.js
@@ -0,0 +1,85 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export default angular.module('thingsboard.api.customer', [])
+ .factory('customerService', CustomerService)
+ .name;
+
+/*@ngInject*/
+function CustomerService($http, $q) {
+
+ var service = {
+ getCustomers: getCustomers,
+ getCustomer: getCustomer,
+ deleteCustomer: deleteCustomer,
+ saveCustomer: saveCustomer
+ }
+
+ return service;
+
+ function getCustomers(pageLink) {
+ var deferred = $q.defer();
+ var url = '/api/customers?limit=' + pageLink.limit;
+ if (angular.isDefined(pageLink.textSearch)) {
+ url += '&textSearch=' + pageLink.textSearch;
+ }
+ if (angular.isDefined(pageLink.idOffset)) {
+ url += '&idOffset=' + pageLink.idOffset;
+ }
+ if (angular.isDefined(pageLink.textOffset)) {
+ url += '&textOffset=' + pageLink.textOffset;
+ }
+ $http.get(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function getCustomer(customerId) {
+ var deferred = $q.defer();
+ var url = '/api/customer/' + customerId;
+ $http.get(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function saveCustomer(customer) {
+ var deferred = $q.defer();
+ var url = '/api/customer';
+ $http.post(url, customer).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function deleteCustomer(customerId) {
+ var deferred = $q.defer();
+ var url = '/api/customer/' + customerId;
+ $http.delete(url).then(function success() {
+ deferred.resolve();
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+}
ui/src/app/api/dashboard.service.js 129(+129 -0)
diff --git a/ui/src/app/api/dashboard.service.js b/ui/src/app/api/dashboard.service.js
new file mode 100644
index 0000000..3709fc8
--- /dev/null
+++ b/ui/src/app/api/dashboard.service.js
@@ -0,0 +1,129 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export default angular.module('thingsboard.api.dashboard', [])
+ .factory('dashboardService', DashboardService).name;
+
+/*@ngInject*/
+function DashboardService($http, $q) {
+
+ var service = {
+ assignDashboardToCustomer: assignDashboardToCustomer,
+ getCustomerDashboards: getCustomerDashboards,
+ getDashboard: getDashboard,
+ getTenantDashboards: getTenantDashboards,
+ deleteDashboard: deleteDashboard,
+ saveDashboard: saveDashboard,
+ unassignDashboardFromCustomer: unassignDashboardFromCustomer
+ }
+
+ return service;
+
+ function getTenantDashboards(pageLink) {
+ var deferred = $q.defer();
+ var url = '/api/tenant/dashboards?limit=' + pageLink.limit;
+ if (angular.isDefined(pageLink.textSearch)) {
+ url += '&textSearch=' + pageLink.textSearch;
+ }
+ if (angular.isDefined(pageLink.idOffset)) {
+ url += '&idOffset=' + pageLink.idOffset;
+ }
+ if (angular.isDefined(pageLink.textOffset)) {
+ url += '&textOffset=' + pageLink.textOffset;
+ }
+ $http.get(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function getCustomerDashboards(customerId, pageLink) {
+ var deferred = $q.defer();
+ var url = '/api/customer/' + customerId + '/dashboards?limit=' + pageLink.limit;
+ if (angular.isDefined(pageLink.textSearch)) {
+ url += '&textSearch=' + pageLink.textSearch;
+ }
+ if (angular.isDefined(pageLink.idOffset)) {
+ url += '&idOffset=' + pageLink.idOffset;
+ }
+ if (angular.isDefined(pageLink.textOffset)) {
+ url += '&textOffset=' + pageLink.textOffset;
+ }
+ $http.get(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function getDashboard(dashboardId) {
+ var deferred = $q.defer();
+ var url = '/api/dashboard/' + dashboardId;
+ $http.get(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function saveDashboard(dashboard) {
+ var deferred = $q.defer();
+ var url = '/api/dashboard';
+ $http.post(url, dashboard).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function deleteDashboard(dashboardId) {
+ var deferred = $q.defer();
+ var url = '/api/dashboard/' + dashboardId;
+ $http.delete(url).then(function success() {
+ deferred.resolve();
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function assignDashboardToCustomer(customerId, dashboardId) {
+ var deferred = $q.defer();
+ var url = '/api/customer/' + customerId + '/dashboard/' + dashboardId;
+ $http.post(url, null).then(function success() {
+ deferred.resolve();
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function unassignDashboardFromCustomer(dashboardId) {
+ var deferred = $q.defer();
+ var url = '/api/customer/dashboard/' + dashboardId;
+ $http.delete(url).then(function success() {
+ deferred.resolve();
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+}
ui/src/app/api/datasource.service.js 490(+490 -0)
diff --git a/ui/src/app/api/datasource.service.js b/ui/src/app/api/datasource.service.js
new file mode 100644
index 0000000..495b555
--- /dev/null
+++ b/ui/src/app/api/datasource.service.js
@@ -0,0 +1,490 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import thingsboardApiDevice from './device.service';
+import thingsboardApiTelemetryWebsocket from './telemetry-websocket.service';
+import thingsboardTypes from '../common/types.constant';
+import thingsboardUtils from '../common/utils.service';
+
+export default angular.module('thingsboard.api.datasource', [thingsboardApiDevice, thingsboardApiTelemetryWebsocket, thingsboardTypes, thingsboardUtils])
+ .factory('datasourceService', DatasourceService)
+ .name;
+
+/*@ngInject*/
+function DatasourceService($timeout, $log, telemetryWebsocketService, types, utils) {
+
+ var subscriptions = {};
+
+ var service = {
+ subscribeToDatasource: subscribeToDatasource,
+ unsubscribeFromDatasource: unsubscribeFromDatasource
+ }
+
+ return service;
+
+
+ function subscribeToDatasource(listener) {
+ var datasource = listener.datasource;
+
+ if (datasource.type === types.datasourceType.device && !listener.deviceId) {
+ return;
+ }
+
+ var subscriptionDataKeys = [];
+ for (var d in datasource.dataKeys) {
+ var dataKey = datasource.dataKeys[d];
+ var subscriptionDataKey = {
+ name: dataKey.name,
+ type: dataKey.type,
+ funcBody: dataKey.funcBody,
+ postFuncBody: dataKey.postFuncBody
+ }
+ subscriptionDataKeys.push(subscriptionDataKey);
+ }
+
+ var datasourceSubscription = {
+ datasourceType: datasource.type,
+ dataKeys: subscriptionDataKeys,
+ type: listener.widget.type
+ };
+
+ if (listener.widget.type === types.widgetType.timeseries.value) {
+ datasourceSubscription.subscriptionTimewindow = listener.subscriptionTimewindow;
+ }
+ if (datasourceSubscription.datasourceType === types.datasourceType.device) {
+ datasourceSubscription.deviceId = listener.deviceId;
+ }
+
+ listener.datasourceSubscriptionKey = utils.objectHashCode(datasourceSubscription);
+ var subscription;
+ if (subscriptions[listener.datasourceSubscriptionKey]) {
+ subscription = subscriptions[listener.datasourceSubscriptionKey];
+ subscription.syncListener(listener);
+ } else {
+ subscription = new DatasourceSubscription(datasourceSubscription, telemetryWebsocketService, $timeout, $log, types, utils);
+ subscriptions[listener.datasourceSubscriptionKey] = subscription;
+ subscription.start();
+ }
+ subscription.addListener(listener);
+ }
+
+ function unsubscribeFromDatasource(listener) {
+ if (listener.datasourceSubscriptionKey) {
+ if (subscriptions[listener.datasourceSubscriptionKey]) {
+ var subscription = subscriptions[listener.datasourceSubscriptionKey];
+ subscription.removeListener(listener);
+ if (!subscription.hasListeners()) {
+ subscription.unsubscribe();
+ delete subscriptions[listener.datasourceSubscriptionKey];
+ }
+ }
+ listener.datasourceSubscriptionKey = null;
+ }
+ }
+
+}
+
+function DatasourceSubscription(datasourceSubscription, telemetryWebsocketService, $timeout, $log, types, utils) {
+
+ var listeners = [];
+ var datasourceType = datasourceSubscription.datasourceType;
+ var datasourceData = {};
+ var dataKeys = {};
+ var subscribers = {};
+ var history = datasourceSubscription.subscriptionTimewindow &&
+ datasourceSubscription.subscriptionTimewindow.fixedWindow;
+ var realtime = datasourceSubscription.subscriptionTimewindow &&
+ datasourceSubscription.subscriptionTimewindow.realtimeWindowMs;
+ var dataGenFunction = null;
+ var timer;
+ var frequency;
+
+ var subscription = {
+ addListener: addListener,
+ hasListeners: hasListeners,
+ removeListener: removeListener,
+ syncListener: syncListener,
+ start: start,
+ unsubscribe: unsubscribe
+ }
+
+ initializeSubscription();
+
+ return subscription;
+
+ function initializeSubscription() {
+ for (var i = 0; i < datasourceSubscription.dataKeys.length; i++) {
+ var dataKey = angular.copy(datasourceSubscription.dataKeys[i]);
+ dataKey.index = i;
+ var key;
+ if (datasourceType === types.datasourceType.function) {
+ key = utils.objectHashCode(dataKey);
+ if (!dataKey.func) {
+ dataKey.func = new Function("time", "prevValue", dataKey.funcBody);
+ }
+ datasourceData[key] = [];
+ dataKeys[key] = dataKey;
+ } else if (datasourceType === types.datasourceType.device) {
+ key = dataKey.name + '_' + dataKey.type;
+ if (dataKey.postFuncBody && !dataKey.postFunc) {
+ dataKey.postFunc = new Function("time", "value", "prevValue", dataKey.postFuncBody);
+ }
+ var dataKeysList = dataKeys[key];
+ if (!dataKeysList) {
+ dataKeysList = [];
+ dataKeys[key] = dataKeysList;
+ }
+ var index = dataKeysList.push(dataKey) - 1;
+ datasourceData[key + '_' + index] = [];
+ }
+ dataKey.key = key;
+ }
+ if (datasourceType === types.datasourceType.function) {
+ frequency = 1000;
+ if (datasourceSubscription.type === types.widgetType.timeseries.value) {
+ dataGenFunction = generateSeries;
+ var window;
+ if (realtime) {
+ window = datasourceSubscription.subscriptionTimewindow.realtimeWindowMs;
+ } else {
+ window = datasourceSubscription.subscriptionTimewindow.fixedWindow.endTimeMs -
+ datasourceSubscription.subscriptionTimewindow.fixedWindow.startTimeMs;
+ }
+ frequency = window / 1000 * 5;
+ } else if (datasourceSubscription.type === types.widgetType.latest.value) {
+ dataGenFunction = generateLatest;
+ frequency = 1000;
+ }
+ }
+ }
+
+ function addListener(listener) {
+ listeners.push(listener);
+ if (history) {
+ start();
+ }
+ }
+
+ function hasListeners() {
+ return listeners.length > 0;
+ }
+
+ function removeListener(listener) {
+ listeners.splice(listeners.indexOf(listener), 1);
+ }
+
+ function syncListener(listener) {
+ var key;
+ var dataKey;
+ if (datasourceType === types.datasourceType.function) {
+ for (key in dataKeys) {
+ dataKey = dataKeys[key];
+ listener.dataUpdated(datasourceData[key],
+ listener.datasourceIndex,
+ dataKey.index);
+ }
+ } else if (datasourceType === types.datasourceType.device) {
+ for (key in dataKeys) {
+ var dataKeysList = dataKeys[key];
+ for (var i = 0; i < dataKeysList.length; i++) {
+ dataKey = dataKeysList[i];
+ var datasourceKey = key + '_' + i;
+ listener.dataUpdated(datasourceData[datasourceKey],
+ listener.datasourceIndex,
+ dataKey.index);
+ }
+ }
+ }
+ }
+
+ function start() {
+ if (history && !hasListeners()) {
+ return;
+ }
+ //$log.debug("started!");
+ if (datasourceType === types.datasourceType.device) {
+
+ //send subscribe command
+
+ var tsKeys = '';
+ var attrKeys = '';
+
+ for (var key in dataKeys) {
+ var dataKeysList = dataKeys[key];
+ var dataKey = dataKeysList[0];
+ if (dataKey.type === types.dataKeyType.timeseries) {
+ if (tsKeys.length > 0) {
+ tsKeys += ',';
+ }
+ tsKeys += dataKey.name;
+ } else if (dataKey.type === types.dataKeyType.attribute) {
+ if (attrKeys.length > 0) {
+ attrKeys += ',';
+ }
+ attrKeys += dataKey.name;
+ }
+ }
+
+ if (tsKeys.length > 0) {
+
+ var subscriber;
+ var subscriptionCommand;
+
+ if (history) {
+
+ var historyCommand = {
+ deviceId: datasourceSubscription.deviceId,
+ keys: tsKeys,
+ startTs: datasourceSubscription.subscriptionTimewindow.fixedWindow.startTimeMs,
+ endTs: datasourceSubscription.subscriptionTimewindow.fixedWindow.endTimeMs
+ };
+
+ subscriber = {
+ historyCommand: historyCommand,
+ type: types.dataKeyType.timeseries,
+ onData: function (data) {
+ onData(data, types.dataKeyType.timeseries);
+ }
+ };
+
+ telemetryWebsocketService.subscribe(subscriber);
+ subscribers[subscriber.historyCommand.cmdId] = subscriber;
+
+ } else {
+
+ subscriptionCommand = {
+ deviceId: datasourceSubscription.deviceId,
+ keys: tsKeys
+ };
+
+ if (datasourceSubscription.type === types.widgetType.timeseries.value) {
+ subscriptionCommand.timeWindow = datasourceSubscription.subscriptionTimewindow.realtimeWindowMs;
+ }
+
+ subscriber = {
+ subscriptionCommand: subscriptionCommand,
+ type: types.dataKeyType.timeseries,
+ onData: function (data) {
+ onData(data, types.dataKeyType.timeseries);
+ }
+ };
+
+ telemetryWebsocketService.subscribe(subscriber);
+ subscribers[subscriber.subscriptionCommand.cmdId] = subscriber;
+
+ }
+ }
+
+ if (attrKeys.length > 0) {
+
+ subscriptionCommand = {
+ deviceId: datasourceSubscription.deviceId,
+ keys: attrKeys
+ };
+
+ subscriber = {
+ subscriptionCommand: subscriptionCommand,
+ type: types.dataKeyType.attribute,
+ onData: function (data) {
+ onData(data, types.dataKeyType.attribute);
+ }
+ };
+
+ telemetryWebsocketService.subscribe(subscriber);
+ subscribers[subscriber.cmdId] = subscriber;
+
+ }
+
+ } else if (dataGenFunction) {
+ if (history) {
+ onTick();
+ } else {
+ timer = $timeout(onTick, 0, false);
+ }
+ }
+
+ }
+
+ function unsubscribe() {
+ if (timer) {
+ $timeout.cancel(timer);
+ }
+ if (datasourceType === types.datasourceType.device) {
+ for (var cmdId in subscribers) {
+ telemetryWebsocketService.unsubscribe(subscribers[cmdId]);
+ }
+ subscribers = {};
+ }
+ //$log.debug("unsibscribed!");
+ }
+
+ function boundToInterval(data, timewindowMs) {
+ if (data.length > 1) {
+ var start = data[0][0];
+ var end = data[data.length - 1][0];
+ var i = 0;
+ var currentInterval = end - start;
+ while (currentInterval > timewindowMs && i < data.length - 2) {
+ i++;
+ start = data[i][0];
+ currentInterval = end - start;
+ }
+ if (i > 1) {
+ data.splice(0, i - 1);
+ }
+ }
+ return data;
+ }
+
+ function generateSeries(dataKey) {
+
+ var data = [];
+ var startTime;
+ var endTime;
+
+ if (realtime) {
+ endTime = (new Date).getTime();
+ if (dataKey.lastUpdateTime) {
+ startTime = dataKey.lastUpdateTime + frequency;
+ } else {
+ startTime = endTime - datasourceSubscription.subscriptionTimewindow.realtimeWindowMs;
+ }
+ } else {
+ startTime = datasourceSubscription.subscriptionTimewindow.fixedWindow.startTimeMs;
+ endTime = datasourceSubscription.subscriptionTimewindow.fixedWindow.endTimeMs;
+ }
+ var prevSeries;
+ var datasourceKeyData = datasourceData[dataKey.key];
+ if (datasourceKeyData.length > 0) {
+ prevSeries = datasourceKeyData[datasourceKeyData.length - 1];
+ } else {
+ prevSeries = [0, 0];
+ }
+ for (var time = startTime; time <= endTime; time += frequency) {
+ var series = [];
+ series.push(time);
+ var value = dataKey.func(time, prevSeries[1]);
+ series.push(value);
+ data.push(series);
+ prevSeries = series;
+ }
+ if (data.length > 0) {
+ dataKey.lastUpdateTime = data[data.length - 1][0];
+ }
+ if (realtime) {
+ datasourceData[dataKey.key] = boundToInterval(datasourceKeyData.concat(data),
+ datasourceSubscription.subscriptionTimewindow.realtimeWindowMs);
+ } else {
+ datasourceData[dataKey.key] = data;
+ }
+ for (var i in listeners) {
+ var listener = listeners[i];
+ listener.dataUpdated(datasourceData[dataKey.key],
+ listener.datasourceIndex,
+ dataKey.index);
+ }
+ }
+
+ function generateLatest(dataKey) {
+ var prevSeries;
+ var datasourceKeyData = datasourceData[dataKey.key];
+ if (datasourceKeyData.length > 0) {
+ prevSeries = datasourceKeyData[datasourceKeyData.length - 1];
+ } else {
+ prevSeries = [0, 0];
+ }
+ var series = [];
+ var time = (new Date).getTime();
+ series.push(time);
+ var value = dataKey.func(time, prevSeries[1]);
+ series.push(value);
+ datasourceData[dataKey.key] = [series];
+ for (var i in listeners) {
+ var listener = listeners[i];
+ listener.dataUpdated(datasourceData[dataKey.key],
+ listener.datasourceIndex,
+ dataKey.index);
+ }
+ }
+
+ function onTick() {
+ for (var key in dataKeys) {
+ dataGenFunction(dataKeys[key]);
+ }
+ if (!history) {
+ timer = $timeout(onTick, frequency / 2, false);
+ }
+ }
+
+ function onData(sourceData, type) {
+ for (var keyName in sourceData) {
+ var keyData = sourceData[keyName];
+ var key = keyName + '_' + type;
+ var dataKeyList = dataKeys[key];
+ for (var keyIndex = 0; keyIndex < dataKeyList.length; keyIndex++) {
+ var datasourceKey = key + "_" + keyIndex;
+ if (datasourceData[datasourceKey]) {
+ var dataKey = dataKeyList[keyIndex];
+ var data = [];
+ var prevSeries;
+ var datasourceKeyData = datasourceData[datasourceKey];
+ if (datasourceKeyData.length > 0) {
+ prevSeries = datasourceKeyData[datasourceKeyData.length - 1];
+ } else {
+ prevSeries = [0, 0];
+ }
+ if (datasourceSubscription.type === types.widgetType.timeseries.value) {
+ var series, time, value;
+ for (var i in keyData) {
+ series = keyData[i];
+ time = series[0];
+ value = Number(series[1]);
+ if (dataKey.postFunc) {
+ value = dataKey.postFunc(time, value, prevSeries[1]);
+ }
+ series = [time, value];
+ data.push(series);
+ prevSeries = series;
+ }
+ } else if (datasourceSubscription.type === types.widgetType.latest.value) {
+ if (keyData.length > 0) {
+ series = keyData[0];
+ time = series[0];
+ value = series[1];
+ if (dataKey.postFunc) {
+ value = dataKey.postFunc(time, value, prevSeries[1]);
+ }
+ series = [time, value];
+ data.push(series);
+ }
+ }
+ if (data.length > 0) {
+ if (realtime) {
+ datasourceData[datasourceKey] = boundToInterval(datasourceKeyData.concat(data), datasourceSubscription.subscriptionTimewindow.realtimeWindowMs);
+ } else {
+ datasourceData[datasourceKey] = data;
+ }
+ for (var i2 in listeners) {
+ var listener = listeners[i2];
+ listener.dataUpdated(datasourceData[datasourceKey],
+ listener.datasourceIndex,
+ dataKey.index);
+ }
+ }
+ }
+ }
+ }
+ }
+}
ui/src/app/api/device.service.js 381(+381 -0)
diff --git a/ui/src/app/api/device.service.js b/ui/src/app/api/device.service.js
new file mode 100644
index 0000000..cf38d12
--- /dev/null
+++ b/ui/src/app/api/device.service.js
@@ -0,0 +1,381 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import thingsboardTypes from '../common/types.constant';
+
+export default angular.module('thingsboard.api.device', [thingsboardTypes])
+ .factory('deviceService', DeviceService)
+ .name;
+
+/*@ngInject*/
+function DeviceService($http, $q, $filter, telemetryWebsocketService, types) {
+
+
+ var deviceAttributesSubscriptionMap = {};
+
+ var service = {
+ assignDeviceToCustomer: assignDeviceToCustomer,
+ deleteDevice: deleteDevice,
+ getCustomerDevices: getCustomerDevices,
+ getDevice: getDevice,
+ getDeviceCredentials: getDeviceCredentials,
+ getDeviceKeys: getDeviceKeys,
+ getDeviceTimeseriesValues: getDeviceTimeseriesValues,
+ getTenantDevices: getTenantDevices,
+ saveDevice: saveDevice,
+ saveDeviceCredentials: saveDeviceCredentials,
+ unassignDeviceFromCustomer: unassignDeviceFromCustomer,
+ getDeviceAttributes: getDeviceAttributes,
+ subscribeForDeviceAttributes: subscribeForDeviceAttributes,
+ unsubscribeForDeviceAttributes: unsubscribeForDeviceAttributes,
+ saveDeviceAttributes: saveDeviceAttributes,
+ deleteDeviceAttributes: deleteDeviceAttributes,
+ sendOneWayRpcCommand: sendOneWayRpcCommand,
+ sendTwoWayRpcCommand: sendTwoWayRpcCommand
+ }
+
+ return service;
+
+ function getTenantDevices(pageLink) {
+ var deferred = $q.defer();
+ var url = '/api/tenant/devices?limit=' + pageLink.limit;
+ if (angular.isDefined(pageLink.textSearch)) {
+ url += '&textSearch=' + pageLink.textSearch;
+ }
+ if (angular.isDefined(pageLink.idOffset)) {
+ url += '&idOffset=' + pageLink.idOffset;
+ }
+ if (angular.isDefined(pageLink.textOffset)) {
+ url += '&textOffset=' + pageLink.textOffset;
+ }
+ $http.get(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function getCustomerDevices(customerId, pageLink) {
+ var deferred = $q.defer();
+ var url = '/api/customer/' + customerId + '/devices?limit=' + pageLink.limit;
+ if (angular.isDefined(pageLink.textSearch)) {
+ url += '&textSearch=' + pageLink.textSearch;
+ }
+ if (angular.isDefined(pageLink.idOffset)) {
+ url += '&idOffset=' + pageLink.idOffset;
+ }
+ if (angular.isDefined(pageLink.textOffset)) {
+ url += '&textOffset=' + pageLink.textOffset;
+ }
+ $http.get(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function getDevice(deviceId) {
+ var deferred = $q.defer();
+ var url = '/api/device/' + deviceId;
+ $http.get(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function saveDevice(device) {
+ var deferred = $q.defer();
+ var url = '/api/device';
+ $http.post(url, device).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function deleteDevice(deviceId) {
+ var deferred = $q.defer();
+ var url = '/api/device/' + deviceId;
+ $http.delete(url).then(function success() {
+ deferred.resolve();
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function getDeviceCredentials(deviceId) {
+ var deferred = $q.defer();
+ var url = '/api/device/' + deviceId + '/credentials';
+ $http.get(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function saveDeviceCredentials(deviceCredentials) {
+ var deferred = $q.defer();
+ var url = '/api/device/credentials';
+ $http.post(url, deviceCredentials).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function assignDeviceToCustomer(customerId, deviceId) {
+ var deferred = $q.defer();
+ var url = '/api/customer/' + customerId + '/device/' + deviceId;
+ $http.post(url, null).then(function success() {
+ deferred.resolve();
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function unassignDeviceFromCustomer(deviceId) {
+ var deferred = $q.defer();
+ var url = '/api/customer/device/' + deviceId;
+ $http.delete(url).then(function success() {
+ deferred.resolve();
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function getDeviceKeys(deviceId, query, type) {
+ var deferred = $q.defer();
+ var url = '/api/plugins/telemetry/' + deviceId + '/keys/';
+ if (type === types.dataKeyType.timeseries) {
+ url += 'timeseries';
+ } else if (type === types.dataKeyType.attribute) {
+ url += 'attributes';
+ }
+ $http.get(url, null).then(function success(response) {
+ var result = [];
+ if (response.data) {
+ if (query) {
+ var dataKeys = response.data;
+ var lowercaseQuery = angular.lowercase(query);
+ for (var i in dataKeys) {
+ if (angular.lowercase(dataKeys[i]).indexOf(lowercaseQuery) === 0) {
+ result.push(dataKeys[i]);
+ }
+ }
+ } else {
+ result = response.data;
+ }
+ }
+ deferred.resolve(result);
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function getDeviceTimeseriesValues(deviceId, keys, startTs, endTs, limit) {
+ var deferred = $q.defer();
+ var url = '/api/plugins/telemetry/' + deviceId + '/values/timeseries';
+ url += '?keys=' + keys;
+ url += '&startTs=' + startTs;
+ url += '&endTs=' + endTs;
+ if (angular.isDefined(limit)) {
+ url += '&limit=' + limit;
+ }
+ $http.get(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function processDeviceAttributes(attributes, query, deferred, successCallback, update) {
+ attributes = $filter('orderBy')(attributes, query.order);
+ if (query.search != null) {
+ attributes = $filter('filter')(attributes, {key: query.search});
+ }
+ var responseData = {
+ count: attributes.length
+ }
+ var startIndex = query.limit * (query.page - 1);
+ responseData.data = attributes.slice(startIndex, startIndex + query.limit);
+ successCallback(responseData, update);
+ if (deferred) {
+ deferred.resolve();
+ }
+ }
+
+ function getDeviceAttributes(deviceId, attributeScope, query, successCallback) {
+ var deferred = $q.defer();
+ var subscriptionId = deviceId + attributeScope;
+ var das = deviceAttributesSubscriptionMap[subscriptionId];
+ if (das) {
+ if (das.attributes) {
+ processDeviceAttributes(das.attributes, query, deferred, successCallback);
+ das.subscriptionCallback = function(attributes) {
+ processDeviceAttributes(attributes, query, null, successCallback, true);
+ }
+ } else {
+ das.subscriptionCallback = function(attributes) {
+ processDeviceAttributes(attributes, query, deferred, successCallback);
+ das.subscriptionCallback = function(attributes) {
+ processDeviceAttributes(attributes, query, null, successCallback, true);
+ }
+ }
+ }
+ } else {
+ var url = '/api/plugins/telemetry/' + deviceId + '/values/attributes/' + attributeScope;
+ $http.get(url, null).then(function success(response) {
+ processDeviceAttributes(response.data, query, deferred, successCallback);
+ }, function fail() {
+ deferred.reject();
+ });
+ }
+ return deferred;
+ }
+
+ function onSubscriptionData(data, subscriptionId) {
+ var deviceAttributesSubscription = deviceAttributesSubscriptionMap[subscriptionId];
+ if (deviceAttributesSubscription) {
+ if (!deviceAttributesSubscription.attributes) {
+ deviceAttributesSubscription.attributes = [];
+ deviceAttributesSubscription.keys = {};
+ }
+ var attributes = deviceAttributesSubscription.attributes;
+ var keys = deviceAttributesSubscription.keys;
+ for (var key in data) {
+ var index = keys[key];
+ var attribute;
+ if (index > -1) {
+ attribute = attributes[index];
+ } else {
+ attribute = {
+ key: key
+ };
+ index = attributes.push(attribute)-1;
+ keys[key] = index;
+ }
+ var attrData = data[key][0];
+ attribute.lastUpdateTs = attrData[0];
+ attribute.value = attrData[1];
+ }
+ if (deviceAttributesSubscription.subscriptionCallback) {
+ deviceAttributesSubscription.subscriptionCallback(attributes);
+ }
+ }
+ }
+
+ function subscribeForDeviceAttributes(deviceId, attributeScope) {
+ var subscriptionId = deviceId + attributeScope;
+ var deviceAttributesSubscription = deviceAttributesSubscriptionMap[subscriptionId];
+ if (!deviceAttributesSubscription) {
+ var subscriptionCommand = {
+ deviceId: deviceId
+ };
+
+ var type = attributeScope === types.latestTelemetry.value ?
+ types.dataKeyType.timeseries : types.dataKeyType.attribute;
+
+ var subscriber = {
+ subscriptionCommand: subscriptionCommand,
+ type: type,
+ onData: function (data) {
+ onSubscriptionData(data, subscriptionId);
+ }
+ };
+ telemetryWebsocketService.subscribe(subscriber);
+ deviceAttributesSubscription = {
+ subscriber: subscriber,
+ attributes: null
+ }
+ deviceAttributesSubscriptionMap[subscriptionId] = deviceAttributesSubscription;
+ }
+ return subscriptionId;
+ }
+ function unsubscribeForDeviceAttributes(subscriptionId) {
+ var deviceAttributesSubscription = deviceAttributesSubscriptionMap[subscriptionId];
+ if (deviceAttributesSubscription) {
+ telemetryWebsocketService.unsubscribe(deviceAttributesSubscription.subscriber);
+ delete deviceAttributesSubscriptionMap[subscriptionId];
+ }
+ }
+
+ function saveDeviceAttributes(deviceId, attributeScope, attributes) {
+ var deferred = $q.defer();
+ var attributesData = {};
+ for (var a in attributes) {
+ attributesData[attributes[a].key] = attributes[a].value;
+ }
+ var url = '/api/plugins/telemetry/' + deviceId + '/' + attributeScope;
+ $http.post(url, attributesData).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function deleteDeviceAttributes(deviceId, attributeScope, attributes) {
+ var deferred = $q.defer();
+ var keys = '';
+ for (var i = 0; i < attributes.length; i++) {
+ if (i > 0) {
+ keys += ',';
+ }
+ keys += attributes[i].key;
+ }
+ var url = '/api/plugins/telemetry/' + deviceId + '/' + attributeScope + '?keys=' + keys;
+ $http.delete(url).then(function success() {
+ deferred.resolve();
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function sendOneWayRpcCommand(deviceId, requestBody) {
+ var deferred = $q.defer();
+ var url = '/api/plugins/rpc/oneway/' + deviceId;
+ $http.post(url, requestBody).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail(rejection) {
+ deferred.reject(rejection);
+ });
+ return deferred.promise;
+ }
+
+ function sendTwoWayRpcCommand(deviceId, requestBody) {
+ var deferred = $q.defer();
+ var url = '/api/plugins/rpc/twoway/' + deviceId;
+ $http.post(url, requestBody).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail(rejection) {
+ deferred.reject(rejection);
+ });
+ return deferred.promise;
+ }
+
+}
ui/src/app/api/event.service.js 50(+50 -0)
diff --git a/ui/src/app/api/event.service.js b/ui/src/app/api/event.service.js
new file mode 100644
index 0000000..39074e0
--- /dev/null
+++ b/ui/src/app/api/event.service.js
@@ -0,0 +1,50 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export default angular.module('thingsboard.api.event', [])
+ .factory('eventService', EventService)
+ .name;
+
+/*@ngInject*/
+function EventService($http, $q) {
+
+ var service = {
+ getEvents: getEvents
+ }
+
+ return service;
+
+ function getEvents (entityType, entityId, eventType, tenantId, pageLink) {
+ var deferred = $q.defer();
+ var url = '/api/events/'+entityType+'/'+entityId+'/'+eventType+'?tenantId=' + tenantId + '&limit=' + pageLink.limit;
+
+ if (angular.isDefined(pageLink.startTime)) {
+ url += '&startTime=' + pageLink.startTime;
+ }
+ if (angular.isDefined(pageLink.endTime)) {
+ url += '&endTime=' + pageLink.endTime;
+ }
+ if (angular.isDefined(pageLink.idOffset)) {
+ url += '&offset=' + pageLink.idOffset;
+ }
+ $http.get(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+}
ui/src/app/api/login.service.js 95(+95 -0)
diff --git a/ui/src/app/api/login.service.js b/ui/src/app/api/login.service.js
new file mode 100644
index 0000000..e904e43
--- /dev/null
+++ b/ui/src/app/api/login.service.js
@@ -0,0 +1,95 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export default angular.module('thingsboard.api.login', [])
+ .factory('loginService', LoginService)
+ .name;
+
+/*@ngInject*/
+function LoginService($http, $q) {
+
+ var service = {
+ activate: activate,
+ changePassword: changePassword,
+ hasUser: hasUser,
+ login: login,
+ resetPassword: resetPassword,
+ sendResetPasswordLink: sendResetPasswordLink,
+ }
+
+ return service;
+
+ function hasUser() {
+ return true;
+ }
+
+ function login(user) {
+ var deferred = $q.defer();
+ var loginRequest = {
+ username: user.name,
+ password: user.password
+ };
+ $http.post('/api/auth/login', loginRequest).then(function success(response) {
+ deferred.resolve(response);
+ }, function fail(response) {
+ deferred.reject(response);
+ });
+ return deferred.promise;
+ }
+
+ function sendResetPasswordLink(email) {
+ var deferred = $q.defer();
+ var url = '/api/noauth/resetPasswordByEmail?email=' + email;
+ $http.post(url, null).then(function success(response) {
+ deferred.resolve(response);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function resetPassword(resetToken, password) {
+ var deferred = $q.defer();
+ var url = '/api/noauth/resetPassword?resetToken=' + resetToken + '&password=' + password;
+ $http.post(url, null).then(function success(response) {
+ deferred.resolve(response);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function activate(activateToken, password) {
+ var deferred = $q.defer();
+ var url = '/api/noauth/activate?activateToken=' + activateToken + '&password=' + password;
+ $http.post(url, null).then(function success(response) {
+ deferred.resolve(response);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function changePassword(currentPassword, newPassword) {
+ var deferred = $q.defer();
+ var url = '/api/auth/changePassword?currentPassword=' + currentPassword + '&newPassword=' + newPassword;
+ $http.post(url, null).then(function success(response) {
+ deferred.resolve(response);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+}
ui/src/app/api/plugin.service.js 216(+216 -0)
diff --git a/ui/src/app/api/plugin.service.js b/ui/src/app/api/plugin.service.js
new file mode 100644
index 0000000..83a99a6
--- /dev/null
+++ b/ui/src/app/api/plugin.service.js
@@ -0,0 +1,216 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export default angular.module('thingsboard.api.plugin', [])
+ .factory('pluginService', PluginService).name;
+
+/*@ngInject*/
+function PluginService($http, $q, $rootScope, $filter, componentDescriptorService, types, utils) {
+
+ var allPlugins = undefined;
+ var allActionPlugins = undefined;
+ var systemPlugins = undefined;
+ var tenantPlugins = undefined;
+
+ $rootScope.pluginServiceStateChangeStartHandle = $rootScope.$on('$stateChangeStart', function () {
+ invalidatePluginsCache();
+ });
+
+ var service = {
+ getSystemPlugins: getSystemPlugins,
+ getTenantPlugins: getTenantPlugins,
+ getAllPlugins: getAllPlugins,
+ getAllActionPlugins: getAllActionPlugins,
+ getPluginByToken: getPluginByToken,
+ getPlugin: getPlugin,
+ deletePlugin: deletePlugin,
+ savePlugin: savePlugin,
+ activatePlugin: activatePlugin,
+ suspendPlugin: suspendPlugin
+ }
+
+ return service;
+
+ function invalidatePluginsCache() {
+ allPlugins = undefined;
+ allActionPlugins = undefined;
+ systemPlugins = undefined;
+ tenantPlugins = undefined;
+ }
+
+ function loadPluginsCache() {
+ var deferred = $q.defer();
+ if (!allPlugins) {
+ var url = '/api/plugins';
+ $http.get(url, null).then(function success(response) {
+ componentDescriptorService.getComponentDescriptorsByType(types.componentType.plugin).then(
+ function success(pluginComponents) {
+ allPlugins = response.data;
+ allActionPlugins = [];
+ systemPlugins = [];
+ tenantPlugins = [];
+ allPlugins = $filter('orderBy')(allPlugins, ['+name', '-createdTime']);
+ var pluginHasActionsByClazz = {};
+ for (var index in pluginComponents) {
+ pluginHasActionsByClazz[pluginComponents[index].clazz] =
+ (pluginComponents[index].actions != null && pluginComponents[index].actions.length > 0);
+ }
+ for (var i = 0; i < allPlugins.length; i++) {
+ var plugin = allPlugins[i];
+ if (pluginHasActionsByClazz[plugin.clazz] === true) {
+ allActionPlugins.push(plugin);
+ }
+ if (plugin.tenantId.id === types.id.nullUid) {
+ systemPlugins.push(plugin);
+ } else {
+ tenantPlugins.push(plugin);
+ }
+ }
+ deferred.resolve();
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ }, function fail() {
+ deferred.reject();
+ });
+ } else {
+ deferred.resolve();
+ }
+ return deferred.promise;
+ }
+
+ function getSystemPlugins(pageLink) {
+ var deferred = $q.defer();
+ loadPluginsCache().then(
+ function success() {
+ utils.filterSearchTextEntities(systemPlugins, 'name', pageLink, deferred);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ return deferred.promise;
+ }
+
+ function getTenantPlugins(pageLink) {
+ var deferred = $q.defer();
+ loadPluginsCache().then(
+ function success() {
+ utils.filterSearchTextEntities(tenantPlugins, 'name', pageLink, deferred);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ return deferred.promise;
+ }
+
+ function getAllActionPlugins(pageLink) {
+ var deferred = $q.defer();
+ loadPluginsCache().then(
+ function success() {
+ utils.filterSearchTextEntities(allActionPlugins, 'name', pageLink, deferred);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ return deferred.promise;
+ }
+
+ function getAllPlugins(pageLink) {
+ var deferred = $q.defer();
+ loadPluginsCache().then(
+ function success() {
+ utils.filterSearchTextEntities(allPlugins, 'name', pageLink, deferred);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ return deferred.promise;
+ }
+
+ function getPluginByToken(pluginToken) {
+ var deferred = $q.defer();
+ var url = '/api/plugin/token/' + pluginToken;
+ $http.get(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function getPlugin(pluginId) {
+ var deferred = $q.defer();
+ var url = '/api/plugin/' + pluginId;
+ $http.get(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function savePlugin(plugin) {
+ var deferred = $q.defer();
+ var url = '/api/plugin';
+ $http.post(url, plugin).then(function success(response) {
+ invalidatePluginsCache();
+ deferred.resolve(response.data);
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function deletePlugin(pluginId) {
+ var deferred = $q.defer();
+ var url = '/api/plugin/' + pluginId;
+ $http.delete(url).then(function success() {
+ invalidatePluginsCache();
+ deferred.resolve();
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function activatePlugin(pluginId) {
+ var deferred = $q.defer();
+ var url = '/api/plugin/' + pluginId + '/activate';
+ $http.post(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function suspendPlugin(pluginId) {
+ var deferred = $q.defer();
+ var url = '/api/plugin/' + pluginId + '/suspend';
+ $http.post(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+}
ui/src/app/api/rule.service.js 182(+182 -0)
diff --git a/ui/src/app/api/rule.service.js b/ui/src/app/api/rule.service.js
new file mode 100644
index 0000000..b27f405
--- /dev/null
+++ b/ui/src/app/api/rule.service.js
@@ -0,0 +1,182 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export default angular.module('thingsboard.api.rule', [])
+ .factory('ruleService', RuleService).name;
+
+/*@ngInject*/
+function RuleService($http, $q, $rootScope, $filter, types, utils) {
+
+ var allRules = undefined;
+ var systemRules = undefined;
+ var tenantRules = undefined;
+
+ $rootScope.ruleServiceStateChangeStartHandle = $rootScope.$on('$stateChangeStart', function () {
+ invalidateRulesCache();
+ });
+
+ var service = {
+ getSystemRules: getSystemRules,
+ getTenantRules: getTenantRules,
+ getAllRules: getAllRules,
+ getRulesByPluginToken: getRulesByPluginToken,
+ getRule: getRule,
+ deleteRule: deleteRule,
+ saveRule: saveRule,
+ activateRule: activateRule,
+ suspendRule: suspendRule
+ }
+
+ return service;
+
+ function invalidateRulesCache() {
+ allRules = undefined;
+ systemRules = undefined;
+ tenantRules = undefined;
+ }
+
+ function loadRulesCache() {
+ var deferred = $q.defer();
+ if (!allRules) {
+ var url = '/api/rules';
+ $http.get(url, null).then(function success(response) {
+ allRules = response.data;
+ systemRules = [];
+ tenantRules = [];
+ allRules = $filter('orderBy')(allRules, ['+name', '-createdTime']);
+ for (var i = 0; i < allRules.length; i++) {
+ var rule = allRules[i];
+ if (rule.tenantId.id === types.id.nullUid) {
+ systemRules.push(rule);
+ } else {
+ tenantRules.push(rule);
+ }
+ }
+ deferred.resolve();
+ }, function fail() {
+ deferred.reject();
+ });
+ } else {
+ deferred.resolve();
+ }
+ return deferred.promise;
+ }
+
+ function getSystemRules(pageLink) {
+ var deferred = $q.defer();
+ loadRulesCache().then(
+ function success() {
+ utils.filterSearchTextEntities(systemRules, 'name', pageLink, deferred);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ return deferred.promise;
+ }
+
+ function getTenantRules(pageLink) {
+ var deferred = $q.defer();
+ loadRulesCache().then(
+ function success() {
+ utils.filterSearchTextEntities(tenantRules, 'name', pageLink, deferred);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ return deferred.promise;
+ }
+
+ function getAllRules(pageLink) {
+ var deferred = $q.defer();
+ loadRulesCache().then(
+ function success() {
+ utils.filterSearchTextEntities(allRules, 'name', pageLink, deferred);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ return deferred.promise;
+ }
+
+ function getRulesByPluginToken(pluginToken) {
+ var deferred = $q.defer();
+ var url = '/api/rule/token/' + pluginToken;
+ $http.get(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function getRule(ruleId) {
+ var deferred = $q.defer();
+ var url = '/api/rule/' + ruleId;
+ $http.get(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function saveRule(rule) {
+ var deferred = $q.defer();
+ var url = '/api/rule';
+ $http.post(url, rule).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function deleteRule(ruleId) {
+ var deferred = $q.defer();
+ var url = '/api/rule/' + ruleId;
+ $http.delete(url).then(function success() {
+ deferred.resolve();
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function activateRule(ruleId) {
+ var deferred = $q.defer();
+ var url = '/api/rule/' + ruleId + '/activate';
+ $http.post(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function suspendRule(ruleId) {
+ var deferred = $q.defer();
+ var url = '/api/rule/' + ruleId + '/suspend';
+ $http.post(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+}
ui/src/app/api/telemetry-websocket.service.js 170(+170 -0)
diff --git a/ui/src/app/api/telemetry-websocket.service.js b/ui/src/app/api/telemetry-websocket.service.js
new file mode 100644
index 0000000..f7e35eb
--- /dev/null
+++ b/ui/src/app/api/telemetry-websocket.service.js
@@ -0,0 +1,170 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import 'angular-websocket';
+import thingsboardTypes from '../common/types.constant';
+
+export default angular.module('thingsboard.api.telemetryWebsocket', [thingsboardTypes])
+ .factory('telemetryWebsocketService', TelemetryWebsocketService)
+ .name;
+
+/*@ngInject*/
+function TelemetryWebsocketService($log, $websocket, $timeout, $window, types, userService) {
+
+ var isOpening = false,
+ isOpened = false,
+ lastCmdId = 0,
+ subscribers = {},
+ subscribersCount = 0,
+ cmdsWrapper = {
+ tsSubCmds: [],
+ historyCmds: [],
+ attrSubCmds: []
+ },
+ telemetryUri,
+ dataStream,
+ location = $window.location,
+ socketCloseTimer;
+
+ if (location.protocol === "https:") {
+ telemetryUri = "wss:";
+ } else {
+ telemetryUri = "ws:";
+ }
+ telemetryUri += "//" + location.hostname + ":" + location.port;
+ telemetryUri += "/api/ws/plugins/telemetry";
+
+ var service = {
+ subscribe: subscribe,
+ unsubscribe: unsubscribe
+ }
+
+ return service;
+
+ function publishCommands () {
+ if (isOpened && (cmdsWrapper.tsSubCmds.length > 0 ||
+ cmdsWrapper.historyCmds.length > 0 ||
+ cmdsWrapper.attrSubCmds.length > 0)) {
+ $log.debug("Sending subscription commands!");
+ dataStream.send(angular.copy(cmdsWrapper)).then(function () {
+ $log.debug("Subscription commands were sent!");
+ checkToClose();
+ });
+ cmdsWrapper.tsSubCmds = [];
+ cmdsWrapper.historyCmds = [];
+ cmdsWrapper.attrSubCmds = [];
+ }
+ tryOpenSocket();
+ }
+
+ function onError (message) {
+ $log.debug("Websocket error:");
+ $log.debug(message);
+ isOpening = false;
+ }
+
+ function onOpen () {
+ $log.debug("Websocket opened");
+ isOpening = false;
+ isOpened = true;
+ publishCommands();
+ }
+
+ function onClose () {
+ $log.debug("Websocket closed");
+ isOpening = false;
+ isOpened = false;
+ }
+
+ function onMessage (message) {
+ if (message.data) {
+ var data = angular.fromJson(message.data);
+ if (data.subscriptionId) {
+ var subscriber = subscribers[data.subscriptionId];
+ if (subscriber && data.data) {
+ subscriber.onData(data.data);
+ }
+ }
+ }
+ checkToClose();
+ }
+
+ function nextCmdId () {
+ lastCmdId++;
+ return lastCmdId;
+ }
+
+ function subscribe (subscriber) {
+ var cmdId = nextCmdId();
+ subscribers[cmdId] = subscriber;
+ subscribersCount++;
+ if (angular.isDefined(subscriber.subscriptionCommand)) {
+ subscriber.subscriptionCommand.cmdId = cmdId;
+ if (subscriber.type === types.dataKeyType.timeseries) {
+ cmdsWrapper.tsSubCmds.push(subscriber.subscriptionCommand);
+ } else if (subscriber.type === types.dataKeyType.attribute) {
+ cmdsWrapper.attrSubCmds.push(subscriber.subscriptionCommand);
+ }
+ } else if (angular.isDefined(subscriber.historyCommand)) {
+ subscriber.historyCommand.cmdId = cmdId;
+ cmdsWrapper.historyCmds.push(subscriber.historyCommand);
+ }
+ publishCommands();
+ }
+
+ function unsubscribe (subscriber) {
+ if (subscriber.subscriptionCommand) {
+ subscriber.subscriptionCommand.unsubscribe = true;
+ if (subscriber.type === types.dataKeyType.timeseries) {
+ cmdsWrapper.tsSubCmds.push(subscriber.subscriptionCommand);
+ } else if (subscriber.type === types.dataKeyType.attribute) {
+ cmdsWrapper.attrSubCmds.push(subscriber.subscriptionCommand);
+ }
+ delete subscribers[subscriber.subscriptionCommand.cmdId];
+ } else if (subscriber.historyCommand) {
+ delete subscribers[subscriber.historyCommand.cmdId];
+ }
+ subscribersCount--;
+ publishCommands();
+ }
+
+ function checkToClose () {
+ if (subscribersCount === 0 && isOpened) {
+ if (!socketCloseTimer) {
+ socketCloseTimer = $timeout(closeSocket, 90000, false);
+ }
+ }
+ }
+
+ function tryOpenSocket () {
+ if (!isOpened && !isOpening) {
+ isOpening = true;
+ dataStream = $websocket(telemetryUri + '?token=' + userService.getJwtToken());
+ dataStream.onError(onError);
+ dataStream.onOpen(onOpen);
+ dataStream.onClose(onClose);
+ dataStream.onMessage(onMessage);
+ }
+ if (socketCloseTimer) {
+ $timeout.cancel(socketCloseTimer);
+ }
+ }
+
+ function closeSocket() {
+ if (isOpened) {
+ dataStream.close();
+ }
+ }
+}
ui/src/app/api/tenant.service.js 85(+85 -0)
diff --git a/ui/src/app/api/tenant.service.js b/ui/src/app/api/tenant.service.js
new file mode 100644
index 0000000..ab87038
--- /dev/null
+++ b/ui/src/app/api/tenant.service.js
@@ -0,0 +1,85 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export default angular.module('thingsboard.api.tenant', [])
+ .factory('tenantService', TenantService)
+ .name;
+
+/*@ngInject*/
+function TenantService($http, $q) {
+
+ var service = {
+ deleteTenant: deleteTenant,
+ getTenant: getTenant,
+ getTenants: getTenants,
+ saveTenant: saveTenant,
+ }
+
+ return service;
+
+ function getTenants (pageLink) {
+ var deferred = $q.defer();
+ var url = '/api/tenants?limit=' + pageLink.limit;
+ if (angular.isDefined(pageLink.textSearch)) {
+ url += '&textSearch=' + pageLink.textSearch;
+ }
+ if (angular.isDefined(pageLink.idOffset)) {
+ url += '&idOffset=' + pageLink.idOffset;
+ }
+ if (angular.isDefined(pageLink.textOffset)) {
+ url += '&textOffset=' + pageLink.textOffset;
+ }
+ $http.get(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function getTenant (tenantId) {
+ var deferred = $q.defer();
+ var url = '/api/tenant/' + tenantId;
+ $http.get(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function saveTenant (tenant) {
+ var deferred = $q.defer();
+ var url = '/api/tenant';
+ $http.post(url, tenant).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function deleteTenant (tenantId) {
+ var deferred = $q.defer();
+ var url = '/api/tenant/' + tenantId;
+ $http.delete(url).then(function success() {
+ deferred.resolve();
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+}
ui/src/app/api/user.service.js 334(+334 -0)
diff --git a/ui/src/app/api/user.service.js b/ui/src/app/api/user.service.js
new file mode 100644
index 0000000..ee679c6
--- /dev/null
+++ b/ui/src/app/api/user.service.js
@@ -0,0 +1,334 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import thingsboardApiLogin from './login.service';
+import angularStorage from 'angular-storage';
+
+export default angular.module('thingsboard.api.user', [thingsboardApiLogin,
+ angularStorage])
+ .factory('userService', UserService)
+ .name;
+
+/*@ngInject*/
+function UserService($http, $q, $rootScope, store, jwtHelper, $translate) {
+ var currentUser = null,
+ userLoaded = false;
+
+ var refreshTokenQueue = [];
+
+ var service = {
+ deleteUser: deleteUser,
+ getAuthority: getAuthority,
+ isAuthenticated: isAuthenticated,
+ getCurrentUser: getCurrentUser,
+ getCustomerUsers: getCustomerUsers,
+ getUser: getUser,
+ getTenantAdmins: getTenantAdmins,
+ isUserLoaded: isUserLoaded,
+ saveUser: saveUser,
+ sendActivationEmail: sendActivationEmail,
+ setUserFromJwtToken: setUserFromJwtToken,
+ getJwtToken: getJwtToken,
+ clearJwtToken: clearJwtToken,
+ isJwtTokenValid : isJwtTokenValid,
+ validateJwtToken: validateJwtToken,
+ refreshJwtToken: refreshJwtToken,
+ refreshTokenPending: refreshTokenPending,
+ updateAuthorizationHeader: updateAuthorizationHeader,
+ logout: logout
+ }
+
+ loadUser(true).then(function success() {
+ notifyUserLoaded();
+ }, function fail() {
+ notifyUserLoaded();
+ });
+
+ return service;
+
+ function updateAndValidateToken(token, prefix) {
+ var valid = false;
+ var tokenData = jwtHelper.decodeToken(token);
+ var issuedAt = tokenData.iat;
+ var expTime = tokenData.exp;
+ if (issuedAt && expTime) {
+ var ttl = expTime - issuedAt;
+ if (ttl > 0) {
+ var clientExpiration = new Date().valueOf() + ttl*1000;
+ store.set(prefix, token);
+ store.set(prefix + '_expiration', clientExpiration);
+ valid = true;
+ }
+ }
+ if (!valid) {
+ $rootScope.$broadcast('unauthenticated');
+ }
+ }
+
+ function clearTokenData() {
+ store.remove('jwt_token');
+ store.remove('jwt_token_expiration');
+ store.remove('refresh_token');
+ store.remove('refresh_token_expiration');
+ }
+
+ function setUserFromJwtToken(jwtToken, refreshToken, notify, doLogout) {
+ currentUser = null;
+ if (!jwtToken) {
+ clearTokenData();
+ if (notify) {
+ $rootScope.$broadcast('unauthenticated', doLogout);
+ }
+ } else {
+ updateAndValidateToken(jwtToken, 'jwt_token');
+ updateAndValidateToken(refreshToken, 'refresh_token');
+ if (notify) {
+ loadUser(false).then(function success() {
+ $rootScope.$broadcast('authenticated');
+ }, function fail() {
+ $rootScope.$broadcast('unauthenticated');
+ });
+ } else {
+ loadUser(false);
+ }
+ }
+ }
+
+ function isAuthenticated() {
+ return store.get('jwt_token');
+ }
+
+ function getJwtToken() {
+ return store.get('jwt_token');
+ }
+
+ function logout() {
+ clearJwtToken(true);
+ }
+
+ function clearJwtToken(doLogout) {
+ setUserFromJwtToken(null, null, true, doLogout);
+ }
+
+ function isJwtTokenValid() {
+ return isTokenValid('jwt_token');
+ }
+
+ function isTokenValid(prefix) {
+ var clientExpiration = store.get(prefix + '_expiration');
+ return clientExpiration && clientExpiration > new Date().valueOf();
+ }
+
+ function validateJwtToken(doRefresh) {
+ var deferred = $q.defer();
+ if (!isTokenValid('jwt_token')) {
+ if (doRefresh) {
+ refreshJwtToken().then(function success() {
+ deferred.resolve();
+ }, function fail() {
+ deferred.reject();
+ });
+ } else {
+ clearJwtToken(false);
+ deferred.reject();
+ }
+ } else {
+ deferred.resolve();
+ }
+ return deferred.promise;
+ }
+
+ function resolveRefreshTokenQueue(data) {
+ for (var q in refreshTokenQueue) {
+ refreshTokenQueue[q].resolve(data);
+ }
+ refreshTokenQueue = [];
+ }
+
+ function rejectRefreshTokenQueue(message) {
+ for (var q in refreshTokenQueue) {
+ refreshTokenQueue[q].reject(message);
+ }
+ refreshTokenQueue = [];
+ }
+
+ function refreshTokenPending() {
+ return refreshTokenQueue.length > 0;
+ }
+
+ function refreshJwtToken() {
+ var deferred = $q.defer();
+ refreshTokenQueue.push(deferred);
+ if (refreshTokenQueue.length === 1) {
+ var refreshToken = store.get('refresh_token');
+ var refreshTokenValid = isTokenValid('refresh_token');
+ setUserFromJwtToken(null, null, false, false);
+ if (!refreshTokenValid) {
+ rejectRefreshTokenQueue($translate.instant('access.refresh-token-expired'));
+ } else {
+ var refreshTokenRequest = {
+ refreshToken: refreshToken
+ };
+ $http.post('/api/auth/token', refreshTokenRequest).then(function success(response) {
+ var token = response.data.token;
+ var refreshToken = response.data.refreshToken;
+ setUserFromJwtToken(token, refreshToken, false);
+ resolveRefreshTokenQueue(response.data);
+ }, function fail() {
+ clearJwtToken(false);
+ rejectRefreshTokenQueue($translate.instant('access.refresh-token-failed'));
+ });
+ }
+ }
+ return deferred.promise;
+ }
+
+ function getCurrentUser() {
+ return currentUser;
+ }
+
+ function getAuthority() {
+ if (currentUser) {
+ return currentUser.authority;
+ } else {
+ return '';
+ }
+ }
+
+ function isUserLoaded() {
+ return userLoaded;
+ }
+
+ function loadUser(doTokenRefresh) {
+ var deferred = $q.defer();
+ if (!currentUser) {
+ validateJwtToken(doTokenRefresh).then(function success() {
+ var jwtToken = store.get('jwt_token');
+ currentUser = jwtHelper.decodeToken(jwtToken);
+ if (currentUser && currentUser.scopes && currentUser.scopes.length > 0) {
+ currentUser.authority = currentUser.scopes[0];
+ } else if (currentUser) {
+ currentUser.authority = "ANONYMOUS";
+ }
+ deferred.resolve();
+ }, function fail() {
+ deferred.reject();
+ });
+ } else {
+ deferred.resolve();
+ }
+ return deferred.promise;
+ }
+
+ function notifyUserLoaded() {
+ if (!userLoaded) {
+ userLoaded = true;
+ $rootScope.$broadcast('userLoaded');
+ }
+ }
+
+ function updateAuthorizationHeader(headers) {
+ var jwtToken = store.get('jwt_token');
+ if (jwtToken) {
+ headers['X-Authorization'] = 'Bearer ' + jwtToken;
+ }
+ return jwtToken;
+ }
+
+ function getTenantAdmins(tenantId, pageLink) {
+ var deferred = $q.defer();
+ var url = '/api/tenant/' + tenantId + '/users?limit=' + pageLink.limit;
+ if (angular.isDefined(pageLink.textSearch)) {
+ url += '&textSearch=' + pageLink.textSearch;
+ }
+ if (angular.isDefined(pageLink.idOffset)) {
+ url += '&idOffset=' + pageLink.idOffset;
+ }
+ if (angular.isDefined(pageLink.textOffset)) {
+ url += '&textOffset=' + pageLink.textOffset;
+ }
+ $http.get(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function getCustomerUsers(customerId, pageLink) {
+ var deferred = $q.defer();
+ var url = '/api/customer/' + customerId + '/users?limit=' + pageLink.limit;
+ if (angular.isDefined(pageLink.textSearch)) {
+ url += '&textSearch=' + pageLink.textSearch;
+ }
+ if (angular.isDefined(pageLink.idOffset)) {
+ url += '&idOffset=' + pageLink.idOffset;
+ }
+ if (angular.isDefined(pageLink.textOffset)) {
+ url += '&textOffset=' + pageLink.textOffset;
+ }
+ $http.get(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function saveUser(user) {
+ var deferred = $q.defer();
+ var url = '/api/user';
+ $http.post(url, user).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function getUser(userId) {
+ var deferred = $q.defer();
+ var url = '/api/user/' + userId;
+ $http.get(url).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function deleteUser(userId) {
+ var deferred = $q.defer();
+ var url = '/api/user/' + userId;
+ $http.delete(url).then(function success() {
+ deferred.resolve();
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function sendActivationEmail(email) {
+ var deferred = $q.defer();
+ var url = '/api/user/sendActivationMail?email=' + email;
+ $http.post(url, null).then(function success(response) {
+ deferred.resolve(response);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+}
ui/src/app/api/widget.service.js 601(+601 -0)
diff --git a/ui/src/app/api/widget.service.js b/ui/src/app/api/widget.service.js
new file mode 100644
index 0000000..d7885b3
--- /dev/null
+++ b/ui/src/app/api/widget.service.js
@@ -0,0 +1,601 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import $ from 'jquery';
+import moment from 'moment';
+import tinycolor from 'tinycolor2';
+
+import thinsboardLedLight from '../components/led-light.directive';
+
+import TbAnalogueLinearGauge from '../widget/lib/analogue-linear-gauge';
+import TbAnalogueRadialGauge from '../widget/lib/analogue-radial-gauge';
+import TbDigitalGauge from '../widget/lib/digital-gauge';
+
+import 'oclazyload';
+import cssjs from '../../vendor/css.js/css';
+
+import thingsboardTypes from '../common/types.constant';
+import thingsboardUtils from '../common/utils.service';
+
+export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thinsboardLedLight, thingsboardTypes, thingsboardUtils])
+ .factory('widgetService', WidgetService)
+ .name;
+
+/*@ngInject*/
+function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, types, utils) {
+
+ $window.$ = $;
+ $window.moment = moment;
+ $window.tinycolor = tinycolor;
+ $window.lazyLoad = $ocLazyLoad;
+
+ $window.TbAnalogueLinearGauge = TbAnalogueLinearGauge;
+ $window.TbAnalogueRadialGauge = TbAnalogueRadialGauge;
+ $window.TbDigitalGauge = TbDigitalGauge;
+
+ var cssParser = new cssjs();
+ cssParser.testMode = false;
+
+ var missingWidgetType;
+ var errorWidgetType;
+
+ var editingWidgetType;
+
+ var widgetsInfoInMemoryCache = {};
+
+ var allWidgetsBundles = undefined;
+ var systemWidgetsBundles = undefined;
+ var tenantWidgetsBundles = undefined;
+
+ $rootScope.widgetServiceStateChangeStartHandle = $rootScope.$on('$stateChangeStart', function () {
+ invalidateWidgetsBundleCache();
+ });
+
+ initEditingWidgetType();
+ initWidgetPlaceholders();
+
+ var service = {
+ getWidgetTemplate: getWidgetTemplate,
+ getSystemWidgetsBundles: getSystemWidgetsBundles,
+ getTenantWidgetsBundles: getTenantWidgetsBundles,
+ getAllWidgetsBundles: getAllWidgetsBundles,
+ getSystemWidgetsBundlesByPageLink: getSystemWidgetsBundlesByPageLink,
+ getTenantWidgetsBundlesByPageLink: getTenantWidgetsBundlesByPageLink,
+ getAllWidgetsBundlesByPageLink: getAllWidgetsBundlesByPageLink,
+ getWidgetsBundleByAlias: getWidgetsBundleByAlias,
+ saveWidgetsBundle: saveWidgetsBundle,
+ getWidgetsBundle: getWidgetsBundle,
+ deleteWidgetsBundle: deleteWidgetsBundle,
+ getBundleWidgetTypes: getBundleWidgetTypes,
+ getWidgetInfo: getWidgetInfo,
+ getInstantWidgetInfo: getInstantWidgetInfo,
+ deleteWidgetType: deleteWidgetType,
+ saveWidgetType: saveWidgetType,
+ getWidgetType: getWidgetType,
+ getWidgetTypeById: getWidgetTypeById,
+ toWidgetInfo: toWidgetInfo
+ }
+
+ return service;
+
+ function initEditingWidgetType() {
+ if ($rootScope.widgetEditMode) {
+ editingWidgetType =
+ toWidgetType({
+ widgetName: $rootScope.editWidgetInfo.widgetName,
+ alias: 'customWidget',
+ type: $rootScope.editWidgetInfo.type,
+ sizeX: $rootScope.editWidgetInfo.sizeX,
+ sizeY: $rootScope.editWidgetInfo.sizeY,
+ resources: $rootScope.editWidgetInfo.resources,
+ templateHtml: $rootScope.editWidgetInfo.templateHtml,
+ templateCss: $rootScope.editWidgetInfo.templateCss,
+ controllerScript: $rootScope.editWidgetInfo.controllerScript,
+ settingsSchema: $rootScope.editWidgetInfo.settingsSchema,
+ dataKeySettingsSchema: $rootScope.editWidgetInfo.dataKeySettingsSchema,
+ defaultConfig: $rootScope.editWidgetInfo.defaultConfig
+ }, {id: '1'}, { id: types.id.nullUid }, 'customWidgetBundle');
+ }
+ }
+
+ function initWidgetPlaceholders() {
+
+ missingWidgetType = {
+ widgetName: 'Widget type not found',
+ alias: 'undefined',
+ sizeX: 8,
+ sizeY: 6,
+ resources: [],
+ templateHtml: '<div class="tb-widget-error-container"><div translate class="tb-widget-error-msg">widget.widget-type-not-found</div></div>',
+ templateCss: '',
+ controllerScript: 'fns.init = function(containerElement, settings, datasources,\n data) {}\n\n\nfns.redraw = function(containerElement, width, height, data) {};',
+ settingsSchema: '{}\n',
+ dataKeySettingsSchema: '{}\n',
+ defaultConfig: '{\n' +
+ '"title": "Widget type not found",\n' +
+ '"datasources": [],\n' +
+ '"settings": {}\n' +
+ '}\n'
+ };
+
+ errorWidgetType = {
+ widgetName: 'Error loading widget',
+ alias: 'error',
+ sizeX: 8,
+ sizeY: 6,
+ resources: [],
+ templateHtml: '<div class="tb-widget-error-container"><div translate class="tb-widget-error-msg">widget.widget-type-load-error</div>',
+ templateCss: '',
+ controllerScript: 'fns.init = function(containerElement, settings, datasources,\n data) {}\n\n\nfns.redraw = function(containerElement, width, height, data) {};',
+ settingsSchema: '{}\n',
+ dataKeySettingsSchema: '{}\n',
+ defaultConfig: '{\n' +
+ '"title": "Widget failed to load",\n' +
+ '"datasources": [],\n' +
+ '"settings": {}\n' +
+ '}\n'
+ };
+ }
+
+ function toWidgetInfo(widgetType) {
+
+ var widgetInfo = {
+ widgetName: widgetType.name,
+ alias: widgetType.alias
+ }
+
+ var descriptor = widgetType.descriptor;
+
+ widgetInfo.type = descriptor.type;
+ widgetInfo.sizeX = descriptor.sizeX;
+ widgetInfo.sizeY = descriptor.sizeY;
+ widgetInfo.resources = descriptor.resources;
+ widgetInfo.templateHtml = descriptor.templateHtml;
+ widgetInfo.templateCss = descriptor.templateCss;
+ widgetInfo.controllerScript = descriptor.controllerScript;
+ widgetInfo.settingsSchema = descriptor.settingsSchema;
+ widgetInfo.dataKeySettingsSchema = descriptor.dataKeySettingsSchema;
+ widgetInfo.defaultConfig = descriptor.defaultConfig;
+
+ return widgetInfo;
+ }
+
+ function toWidgetType(widgetInfo, id, tenantId, bundleAlias) {
+ var widgetType = {
+ id: id,
+ tenantId: tenantId,
+ bundleAlias: bundleAlias,
+ alias: widgetInfo.alias,
+ name: widgetInfo.widgetName
+ }
+
+ var descriptor = {
+ type: widgetInfo.type,
+ sizeX: widgetInfo.sizeX,
+ sizeY: widgetInfo.sizeY,
+ resources: widgetInfo.resources,
+ templateHtml: widgetInfo.templateHtml,
+ templateCss: widgetInfo.templateCss,
+ controllerScript: widgetInfo.controllerScript,
+ settingsSchema: widgetInfo.settingsSchema,
+ dataKeySettingsSchema: widgetInfo.dataKeySettingsSchema,
+ defaultConfig: widgetInfo.defaultConfig
+ }
+
+ widgetType.descriptor = descriptor;
+
+ return widgetType;
+ }
+
+ function getWidgetTemplate(type) {
+ var deferred = $q.defer();
+ var templateWidgetType = types.widgetType.timeseries;
+ for (var t in types.widgetType) {
+ var widgetType = types.widgetType[t];
+ if (widgetType.value === type) {
+ templateWidgetType = widgetType;
+ break;
+ }
+ }
+ getWidgetType(templateWidgetType.template.bundleAlias,
+ templateWidgetType.template.alias, true).then(
+ function success(widgetType) {
+ var widgetInfo = toWidgetInfo(widgetType);
+ widgetInfo.alias = undefined;
+ deferred.resolve(widgetInfo);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ return deferred.promise;
+ }
+
+ /** Cache functions **/
+
+ function getWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem) {
+ var key = (isSystem ? 'sys_' : '') + bundleAlias + '_' + widgetTypeAlias;
+ return widgetsInfoInMemoryCache[key];
+ }
+
+ function putWidgetInfoToCache(widgetInfo, bundleAlias, widgetTypeAlias, isSystem) {
+ var key = (isSystem ? 'sys_' : '') + bundleAlias + '_' + widgetTypeAlias;
+ widgetsInfoInMemoryCache[key] = widgetInfo;
+ }
+
+ function deleteWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem) {
+ var key = (isSystem ? 'sys_' : '') + bundleAlias + '_' + widgetTypeAlias;
+ delete widgetsInfoInMemoryCache[key];
+ }
+
+ function deleteWidgetsBundleFromCache(bundleAlias, isSystem) {
+ var key = (isSystem ? 'sys_' : '') + bundleAlias;
+ for (var cacheKey in widgetsInfoInMemoryCache) {
+ if (cacheKey.startsWith(key)) {
+ delete widgetsInfoInMemoryCache[cacheKey];
+ }
+ }
+ }
+
+ /** Bundle functions **/
+
+ function invalidateWidgetsBundleCache() {
+ allWidgetsBundles = undefined;
+ systemWidgetsBundles = undefined;
+ tenantWidgetsBundles = undefined;
+ }
+
+ function loadWidgetsBundleCache() {
+ var deferred = $q.defer();
+ if (!allWidgetsBundles) {
+ var url = '/api/widgetsBundles';
+ $http.get(url, null).then(function success(response) {
+ allWidgetsBundles = response.data;
+ systemWidgetsBundles = [];
+ tenantWidgetsBundles = [];
+ allWidgetsBundles = $filter('orderBy')(allWidgetsBundles, ['+title', '-createdTime']);
+ for (var i = 0; i < allWidgetsBundles.length; i++) {
+ var widgetsBundle = allWidgetsBundles[i];
+ if (widgetsBundle.tenantId.id === types.id.nullUid) {
+ systemWidgetsBundles.push(widgetsBundle);
+ } else {
+ tenantWidgetsBundles.push(widgetsBundle);
+ }
+ }
+ deferred.resolve();
+ }, function fail() {
+ deferred.reject();
+ });
+ } else {
+ deferred.resolve();
+ }
+ return deferred.promise;
+ }
+
+
+ function getSystemWidgetsBundles() {
+ var deferred = $q.defer();
+ loadWidgetsBundleCache().then(
+ function success() {
+ deferred.resolve(systemWidgetsBundles);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ return deferred.promise;
+ }
+
+ function getTenantWidgetsBundles() {
+ var deferred = $q.defer();
+ loadWidgetsBundleCache().then(
+ function success() {
+ deferred.resolve(tenantWidgetsBundles);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ return deferred.promise;
+ }
+
+ function getAllWidgetsBundles() {
+ var deferred = $q.defer();
+ loadWidgetsBundleCache().then(
+ function success() {
+ deferred.resolve(allWidgetsBundles);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ return deferred.promise;
+ }
+
+ function getSystemWidgetsBundlesByPageLink(pageLink) {
+ var deferred = $q.defer();
+ loadWidgetsBundleCache().then(
+ function success() {
+ utils.filterSearchTextEntities(systemWidgetsBundles, 'title', pageLink, deferred);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ return deferred.promise;
+ }
+
+ function getTenantWidgetsBundlesByPageLink(pageLink) {
+ var deferred = $q.defer();
+ loadWidgetsBundleCache().then(
+ function success() {
+ utils.filterSearchTextEntities(tenantWidgetsBundles, 'title', pageLink, deferred);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ return deferred.promise;
+ }
+
+ function getAllWidgetsBundlesByPageLink(pageLink) {
+ var deferred = $q.defer();
+ loadWidgetsBundleCache().then(
+ function success() {
+ utils.filterSearchTextEntities(allWidgetsBundles, 'title', pageLink, deferred);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ return deferred.promise;
+ }
+
+ function getWidgetsBundleByAlias(bundleAlias) {
+ var deferred = $q.defer();
+ loadWidgetsBundleCache().then(
+ function success() {
+ var widgetsBundles = $filter('filter')(allWidgetsBundles, {alias: bundleAlias});
+ if (widgetsBundles.length > 0) {
+ deferred.resolve(widgetsBundles[0]);
+ } else {
+ deferred.resolve();
+ }
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ return deferred.promise;
+ }
+
+ function saveWidgetsBundle(widgetsBundle) {
+ var deferred = $q.defer();
+ var url = '/api/widgetsBundle';
+ $http.post(url, widgetsBundle).then(function success(response) {
+ invalidateWidgetsBundleCache();
+ deferred.resolve(response.data);
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ function getWidgetsBundle(widgetsBundleId) {
+ var deferred = $q.defer();
+
+ var url = '/api/widgetsBundle/' + widgetsBundleId;
+ $http.get(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+
+ return deferred.promise;
+ }
+
+ function deleteWidgetsBundle(widgetsBundleId) {
+ var deferred = $q.defer();
+
+ getWidgetsBundle(widgetsBundleId).then(
+ function success(response) {
+ var widgetsBundle = response;
+ var url = '/api/widgetsBundle/' + widgetsBundleId;
+ $http.delete(url).then(function success() {
+ invalidateWidgetsBundleCache();
+ deleteWidgetsBundleFromCache(widgetsBundle.alias,
+ widgetsBundle.tenantId.id === types.id.nullUid);
+ deferred.resolve();
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+
+ return deferred.promise;
+ }
+
+ function getBundleWidgetTypes(bundleAlias, isSystem) {
+ var deferred = $q.defer();
+ var url = '/api/widgetTypes?isSystem=' + (isSystem ? 'true' : 'false') +
+ '&bundleAlias='+bundleAlias;
+ $http.get(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail(response) {
+ deferred.reject(response.data);
+ });
+ return deferred.promise;
+ }
+
+ /** Widget type functions **/
+
+ function getInstantWidgetInfo(widget) {
+ var widgetInfo = getWidgetInfoFromCache(widget.bundleAlias, widget.typeAlias, widget.isSystemType);
+ if (widgetInfo) {
+ return widgetInfo;
+ } else {
+ return {};
+ }
+ }
+
+ function getWidgetInfo(bundleAlias, widgetTypeAlias, isSystem) {
+ var deferred = $q.defer();
+ var widgetInfo = getWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem);
+ if (widgetInfo) {
+ deferred.resolve(widgetInfo);
+ } else {
+ if ($rootScope.widgetEditMode) {
+ loadWidget(editingWidgetType, bundleAlias, isSystem, deferred);
+ } else {
+ getWidgetType(bundleAlias, widgetTypeAlias, isSystem).then(
+ function success(widgetType) {
+ loadWidget(widgetType, bundleAlias, isSystem, deferred);
+ }, function fail() {
+ deferred.resolve(missingWidgetType);
+ }
+ );
+ }
+ }
+ return deferred.promise;
+ }
+
+ function loadWidget(widgetType, bundleAlias, isSystem, deferred) {
+ var widgetInfo = toWidgetInfo(widgetType);
+ loadWidgetResources(widgetInfo, bundleAlias, isSystem).then(
+ function success() {
+ putWidgetInfoToCache(widgetInfo, bundleAlias, widgetInfo.alias, isSystem);
+ deferred.resolve(widgetInfo);
+ }, function fail(errorMessages) {
+ widgetInfo = angular.copy(errorWidgetType);
+ for (var e in errorMessages) {
+ var error = errorMessages[e];
+ widgetInfo.templateHtml += '<div class="tb-widget-error-msg">' + error + '</div>';
+ }
+ widgetInfo.templateHtml += '</div>';
+ deferred.resolve(widgetInfo);
+ }
+ );
+ }
+
+ function getWidgetType(bundleAlias, widgetTypeAlias, isSystem) {
+ var deferred = $q.defer();
+ var url = '/api/widgetType?isSystem=' + (isSystem ? 'true' : 'false') +
+ '&bundleAlias='+bundleAlias+'&alias='+widgetTypeAlias;
+ $http.get(url, null).then(function success(response) {
+ var widgetType = response.data;
+ deferred.resolve(widgetType);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function getWidgetTypeById(widgetTypeId) {
+ var deferred = $q.defer();
+ var url = '/api/widgetType/' + widgetTypeId;
+ $http.get(url, null).then(function success(response) {
+ var widgetType = response.data;
+ deferred.resolve(widgetType);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function deleteWidgetType(bundleAlias, widgetTypeAlias, isSystem) {
+ var deferred = $q.defer();
+ getWidgetType(bundleAlias, widgetTypeAlias, isSystem).then(
+ function success(widgetType) {
+ var url = '/api/widgetType/' + widgetType.id.id;
+ $http.delete(url).then(function success() {
+ deleteWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem);
+ deferred.resolve();
+ }, function fail() {
+ deferred.reject();
+ });
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ return deferred.promise;
+ }
+
+ function saveWidgetType(widgetInfo, id, bundleAlias) {
+ var deferred = $q.defer();
+ var widgetType = toWidgetType(widgetInfo, id, undefined, bundleAlias);
+ var url = '/api/widgetType';
+ $http.post(url, widgetType).then(function success(response) {
+ var widgetType = response.data;
+ deleteWidgetInfoFromCache(widgetType.bundleAlias, widgetType.alias, widgetType.tenantId.id === types.id.nullUid);
+ deferred.resolve(widgetType);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ function loadWidgetResources(widgetInfo, bundleAlias, isSystem) {
+
+ var deferred = $q.defer();
+ var errors = [];
+
+ var widgetNamespace = "widget-type-" + (isSystem ? 'sys-' : '') + bundleAlias + '-' + widgetInfo.alias;
+ cssParser.cssPreviewNamespace = widgetNamespace;
+ cssParser.createStyleElement(widgetNamespace, widgetInfo.templateCss);
+
+ function loadNextOrComplete(i) {
+ i++;
+ if (i < widgetInfo.resources.length) {
+ loadNext(i);
+ } else {
+ if (errors.length > 0) {
+ deferred.reject(errors);
+ } else {
+ deferred.resolve();
+ }
+ }
+ }
+
+ function loadNext(i) {
+ var resourceUrl = widgetInfo.resources[i].url;
+ if (resourceUrl && resourceUrl.length > 0) {
+ $ocLazyLoad.load(resourceUrl).then(
+ function success() {
+ loadNextOrComplete(i);
+ },
+ function fail() {
+ errors.push('Failed to load widget resource: \'' + resourceUrl + '\'');
+ loadNextOrComplete(i);
+ }
+ );
+ } else {
+ loadNextOrComplete(i);
+ }
+ }
+
+ if (widgetInfo.resources.length > 0) {
+ loadNext(0);
+ } else {
+ deferred.resolve();
+ }
+
+ return deferred.promise;
+ }
+
+}
ui/src/app/app.config.js 147(+147 -0)
diff --git a/ui/src/app/app.config.js b/ui/src/app/app.config.js
new file mode 100644
index 0000000..12d60f6
--- /dev/null
+++ b/ui/src/app/app.config.js
@@ -0,0 +1,147 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import injectTapEventPlugin from 'react-tap-event-plugin';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import mdiIconSet from '../svg/mdi.svg';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+const PRIMARY_BACKGROUND_COLOR = "#305680";//#2856b6";//"#3f51b5";
+const SECONDARY_BACKGROUND_COLOR = "#527dad";
+const HUE3_COLOR = "#a7c1de";
+
+/*@ngInject*/
+export default function AppConfig($provide,
+ $urlRouterProvider,
+ $locationProvider,
+ $mdIconProvider,
+ $mdThemingProvider,
+ $httpProvider,
+ $translateProvider,
+ storeProvider) {
+
+ injectTapEventPlugin();
+ $locationProvider.html5Mode(true);
+ $urlRouterProvider.otherwise('/home');
+ storeProvider.setCaching(false);
+
+ $translateProvider.useStaticFilesLoader({
+ prefix: 'static/locale/',// path to translations files
+ suffix: '.json'// suffix, currently- extension of the translations
+ });
+
+ $translateProvider.useSanitizeValueStrategy('sanitize');
+ $translateProvider.preferredLanguage('en_US');
+ $translateProvider.useLocalStorage();
+ $translateProvider.useMissingTranslationHandlerLog();
+ $translateProvider.addInterpolation('$translateMessageFormatInterpolation');
+
+ $httpProvider.interceptors.push('globalInterceptor');
+
+ $provide.decorator("$exceptionHandler", ['$delegate', '$injector', function ($delegate, $injector) {
+ return function (exception, cause) {
+ var rootScope = $injector.get("$rootScope");
+ var $window = $injector.get("$window");
+ var utils = $injector.get("utils");
+ if (rootScope.widgetEditMode) {
+ var parentScope = $window.parent.angular.element($window.frameElement).scope();
+ var data = utils.parseException(exception);
+ parentScope.$emit('widgetException', data);
+ parentScope.$apply();
+ }
+ $delegate(exception, cause);
+ };
+ }]);
+
+ $mdIconProvider.iconSet('mdi', mdiIconSet);
+
+ configureTheme();
+
+ function blueGrayTheme() {
+ var tbPrimaryPalette = $mdThemingProvider.extendPalette('blue-grey');
+ var tbAccentPalette = $mdThemingProvider.extendPalette('orange', {
+ 'contrastDefaultColor': 'light'
+ });
+
+ $mdThemingProvider.definePalette('tb-primary', tbPrimaryPalette);
+ $mdThemingProvider.definePalette('tb-accent', tbAccentPalette);
+
+ $mdThemingProvider.theme('default')
+ .primaryPalette('tb-primary')
+ .accentPalette('tb-accent');
+
+ $mdThemingProvider.theme('tb-dark')
+ .primaryPalette('tb-primary')
+ .accentPalette('tb-accent')
+ .backgroundPalette('tb-primary')
+ .dark();
+ }
+
+ function indigoTheme() {
+ var tbPrimaryPalette = $mdThemingProvider.extendPalette('indigo', {
+ '500': PRIMARY_BACKGROUND_COLOR,
+ '600': SECONDARY_BACKGROUND_COLOR,
+ 'A100': HUE3_COLOR
+ });
+
+ var tbAccentPalette = $mdThemingProvider.extendPalette('deep-orange');
+
+ $mdThemingProvider.definePalette('tb-primary', tbPrimaryPalette);
+ $mdThemingProvider.definePalette('tb-accent', tbAccentPalette);
+
+ var tbDarkPrimaryPalette = $mdThemingProvider.extendPalette('tb-primary', {
+ '500': '#9fa8da'
+ });
+
+ var tbDarkPrimaryBackgroundPalette = $mdThemingProvider.extendPalette('tb-primary', {
+ '800': PRIMARY_BACKGROUND_COLOR
+ });
+
+ $mdThemingProvider.definePalette('tb-dark-primary', tbDarkPrimaryPalette);
+ $mdThemingProvider.definePalette('tb-dark-primary-background', tbDarkPrimaryBackgroundPalette);
+
+ $mdThemingProvider.theme('default')
+ .primaryPalette('tb-primary')
+ .accentPalette('tb-accent');
+
+ $mdThemingProvider.theme('tb-dark')
+ .primaryPalette('tb-dark-primary')
+ .accentPalette('tb-accent')
+ .backgroundPalette('tb-dark-primary-background')
+ .dark();
+ }
+
+ function configureTheme() {
+
+ var theme = 'indigo';
+
+ if (theme === 'blueGray') {
+ blueGrayTheme();
+ } else {
+ indigoTheme();
+ }
+
+ $mdThemingProvider.theme('tb-search-input', 'default')
+ .primaryPalette('tb-primary')
+ .backgroundPalette('tb-primary');
+
+ $mdThemingProvider.setDefaultTheme('default');
+ $mdThemingProvider.alwaysWatchTheme(true);
+ }
+
+}
\ No newline at end of file
ui/src/app/app.js 105(+105 -0)
diff --git a/ui/src/app/app.js b/ui/src/app/app.js
new file mode 100644
index 0000000..3d55ac1
--- /dev/null
+++ b/ui/src/app/app.js
@@ -0,0 +1,105 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import angular from 'angular';
+import ngMaterial from 'angular-material';
+import ngMdIcons from 'angular-material-icons';
+import ngCookies from 'angular-cookies';
+import 'angular-translate';
+import 'angular-translate-loader-static-files';
+import 'angular-translate-storage-local';
+import 'angular-translate-storage-cookie';
+import 'angular-translate-handler-log';
+import 'angular-translate-interpolation-messageformat';
+import 'md-color-picker';
+import mdPickers from 'mdPickers';
+import ngSanitize from 'angular-sanitize';
+import vAccordion from 'v-accordion';
+import ngAnimate from 'angular-animate';
+import 'angular-websocket';
+import uiRouter from 'angular-ui-router';
+import angularJwt from 'angular-jwt';
+import 'angular-drag-and-drop-lists';
+import mdDataTable from 'angular-material-data-table';
+import ngTouch from 'angular-touch';
+import 'angular-carousel';
+import 'clipboard';
+import 'ngclipboard';
+import 'react';
+import 'react-dom';
+import 'material-ui';
+import 'react-schema-form';
+import react from 'ngreact';
+
+import thingsboardLogin from './login';
+import thingsboardDialogs from './components/datakey-config-dialog.controller';
+import thingsboardMenu from './services/menu.service';
+import thingsboardUtils from './common/utils.service';
+import thingsboardTypes from './common/types.constant';
+import thingsboardHelp from './help/help.directive';
+import thingsboardToast from './services/toast';
+import thingsboardHome from './layout';
+import thingsboardApiLogin from './api/login.service';
+import thingsboardApiDevice from './api/device.service';
+import thingsboardApiUser from './api/user.service';
+
+import 'font-awesome/css/font-awesome.min.css';
+import 'angular-material/angular-material.min.css';
+import 'angular-material-icons/angular-material-icons.css';
+import 'angular-gridster/dist/angular-gridster.min.css';
+import 'v-accordion/dist/v-accordion.min.css'
+import 'md-color-picker/dist/mdColorPicker.min.css';
+import 'mdPickers/dist/mdPickers.min.css';
+import 'angular-hotkeys/build/hotkeys.min.css';
+import 'angular-carousel/dist/angular-carousel.min.css';
+import '../scss/main.scss';
+
+import AppConfig from './app.config';
+import GlobalInterceptor from './global-interceptor.service';
+import AppRun from './app.run';
+
+angular.module('thingsboard', [
+ ngMaterial,
+ ngMdIcons,
+ ngCookies,
+ 'pascalprecht.translate',
+ 'mdColorPicker',
+ mdPickers,
+ ngSanitize,
+ vAccordion,
+ ngAnimate,
+ 'ngWebSocket',
+ angularJwt,
+ 'dndLists',
+ mdDataTable,
+ ngTouch,
+ 'angular-carousel',
+ 'ngclipboard',
+ react.name,
+ thingsboardLogin,
+ thingsboardDialogs,
+ thingsboardMenu,
+ thingsboardUtils,
+ thingsboardTypes,
+ thingsboardHelp,
+ thingsboardToast,
+ thingsboardHome,
+ thingsboardApiLogin,
+ thingsboardApiDevice,
+ thingsboardApiUser,
+ uiRouter])
+ .config(AppConfig)
+ .factory('globalInterceptor', GlobalInterceptor)
+ .run(AppRun);
ui/src/app/app.run.js 166(+166 -0)
diff --git a/ui/src/app/app.run.js b/ui/src/app/app.run.js
new file mode 100644
index 0000000..934f021
--- /dev/null
+++ b/ui/src/app/app.run.js
@@ -0,0 +1,166 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*@ngInject*/
+export default function AppRun($rootScope, $window, $log, $state, $mdDialog, $filter, loginService, userService, $translate) {
+
+ var frame = $window.frameElement;
+ var unauthorizedDialog = null;
+ var forbiddenDialog = null;
+
+ if (frame) {
+ var dataWidgetAttr = angular.element(frame).attr('data-widget');
+ if (dataWidgetAttr) {
+ $rootScope.editWidgetInfo = angular.fromJson(dataWidgetAttr);
+ $rootScope.widgetEditMode = true;
+ }
+ }
+
+ initWatchers();
+
+ function initWatchers() {
+ $rootScope.unauthenticatedHandle = $rootScope.$on('unauthenticated', function (event, doLogout) {
+ if (doLogout) {
+ $state.go('login');
+ } else {
+ checkCurrentState();
+ }
+ });
+
+ $rootScope.authenticatedHandle = $rootScope.$on('authenticated', function () {
+ checkCurrentState();
+ });
+
+ $rootScope.forbiddenHandle = $rootScope.$on('forbidden', function () {
+ showForbiddenDialog();
+ });
+
+ $rootScope.stateChangeStartHandle = $rootScope.$on('$stateChangeStart', function (evt, to, params) {
+ if (userService.isUserLoaded() === true) {
+ if (userService.isAuthenticated()) {
+ var authority = userService.getAuthority();
+ if (to.module === 'public') {
+ evt.preventDefault();
+ $state.go('home', params);
+ } else if (angular.isDefined(to.auth) &&
+ to.auth.indexOf(authority) === -1) {
+ evt.preventDefault();
+ showForbiddenDialog();
+ } else if (to.redirectTo) {
+ evt.preventDefault();
+ $state.go(to.redirectTo, params)
+ }
+ } else {
+ if (to.module === 'private') {
+ evt.preventDefault();
+ if (to.url === '/home') {
+ $state.go('login', params);
+ } else {
+ showUnauthorizedDialog();
+ }
+ }
+ }
+ } else {
+ evt.preventDefault();
+ $rootScope.userLoadedHandle = $rootScope.$on('userLoaded', function () {
+ $rootScope.userLoadedHandle();
+ $state.go(to.name, params);
+ });
+ }
+ })
+
+ $rootScope.pageTitle = 'Thingsboard';
+
+ $rootScope.stateChangeSuccessHandle = $rootScope.$on('$stateChangeSuccess', function (evt, to) {
+ if (angular.isDefined(to.data.pageTitle)) {
+ $translate(to.data.pageTitle).then(function (translation) {
+ $rootScope.pageTitle = 'Thingsboard | ' + translation;
+ }, function (translationId) {
+ $rootScope.pageTitle = 'Thingsboard | ' + translationId;
+ });
+ }
+ })
+ }
+
+ function checkCurrentState() {
+ if (userService.isUserLoaded() === true) {
+ var module = $state.$current.module;
+ if (userService.isAuthenticated()) {
+ if ($state.$current.module === 'public') {
+ $state.go('home');
+ }
+ } else {
+ if (angular.isUndefined(module) || !module) {
+ //$state.go('login');
+ } else if ($state.$current.module === 'private') {
+ showUnauthorizedDialog();
+ }
+ }
+ } else {
+ showUnauthorizedDialog();
+ }
+ }
+
+ function showUnauthorizedDialog() {
+ if (unauthorizedDialog === null) {
+ $translate(['access.unauthorized-access',
+ 'access.unauthorized-access-text',
+ 'access.unauthorized',
+ 'action.cancel',
+ 'action.sign-in']).then(function (translations) {
+ if (unauthorizedDialog === null) {
+ unauthorizedDialog = $mdDialog.confirm()
+ .title(translations['access.unauthorized-access'])
+ .textContent(translations['access.unauthorized-access-text'])
+ .ariaLabel(translations['access.unauthorized'])
+ .cancel(translations['action.cancel'])
+ .ok(translations['action.sign-in']);
+ $mdDialog.show(unauthorizedDialog).then(function () {
+ unauthorizedDialog = null;
+ $state.go('login');
+ }, function () {
+ unauthorizedDialog = null;
+ });
+ }
+ });
+ }
+ }
+
+ function showForbiddenDialog() {
+ if (forbiddenDialog === null) {
+ $translate(['access.access-forbidden',
+ 'access.access-forbidden-text',
+ 'access.access-forbidden',
+ 'action.cancel',
+ 'action.sign-in']).then(function (translations) {
+ if (forbiddenDialog === null) {
+ forbiddenDialog = $mdDialog.confirm()
+ .title(translations['access.access-forbidden'])
+ .htmlContent(translations['access.access-forbidden-text'])
+ .ariaLabel(translations['access.access-forbidden'])
+ .cancel(translations['action.cancel'])
+ .ok(translations['action.sign-in']);
+ $mdDialog.show(forbiddenDialog).then(function () {
+ forbiddenDialog = null;
+ userService.logout();
+ }, function () {
+ forbiddenDialog = null;
+ });
+ }
+ });
+ }
+ }
+}
ui/src/app/common/types.constant.js 151(+151 -0)
diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js
new file mode 100644
index 0000000..54ca208
--- /dev/null
+++ b/ui/src/app/common/types.constant.js
@@ -0,0 +1,151 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export default angular.module('thingsboard.types', [])
+ .constant('types',
+ {
+ serverErrorCode: {
+ general: 2,
+ authentication: 10,
+ jwtTokenExpired: 11,
+ permissionDenied: 20,
+ invalidArguments: 30,
+ badRequestParams: 31,
+ itemNotFound: 32
+ },
+ entryPoints: {
+ login: "/api/auth/login",
+ tokenRefresh: "/api/auth/token",
+ nonTokenBased: "/api/noauth"
+ },
+ id: {
+ nullUid: "13814000-1dd2-11b2-8080-808080808080",
+ },
+ datasourceType: {
+ function: "function",
+ device: "device"
+ },
+ dataKeyType: {
+ timeseries: "timeseries",
+ attribute: "attribute",
+ function: "function"
+ },
+ componentType: {
+ filter: "FILTER",
+ processor: "PROCESSOR",
+ action: "ACTION",
+ plugin: "PLUGIN"
+ },
+ entityType: {
+ tenant: "TENANT",
+ device: "DEVICE",
+ customer: "CUSTOMER",
+ rule: "RULE",
+ plugin: "PLUGIN"
+ },
+ eventType: {
+ alarm: {
+ value: "ALARM",
+ name: "event.type-alarm"
+ },
+ error: {
+ value: "ERROR",
+ name: "event.type-error"
+ },
+ lcEvent: {
+ value: "LC_EVENT",
+ name: "event.type-lc-event"
+ },
+ stats: {
+ value: "STATS",
+ name: "event.type-stats"
+ }
+ },
+ latestTelemetry: {
+ value: "LATEST_TELEMETRY",
+ name: "attribute.scope-latest-telemetry",
+ clientSide: true
+ },
+ deviceAttributesScope: {
+ client: {
+ value: "CLIENT_SCOPE",
+ name: "attribute.scope-client",
+ clientSide: true
+ },
+ server: {
+ value: "SERVER_SCOPE",
+ name: "attribute.scope-server",
+ clientSide: false
+ },
+ shared: {
+ value: "SHARED_SCOPE",
+ name: "attribute.scope-shared",
+ clientSide: false
+ }
+ },
+ valueType: {
+ string: {
+ value: "string",
+ name: "value.string",
+ icon: "mdi:format-text"
+ },
+ integer: {
+ value: "integer",
+ name: "value.integer",
+ icon: "mdi:numeric"
+ },
+ double: {
+ value: "double",
+ name: "value.double",
+ icon: "mdi:numeric"
+ },
+ boolean: {
+ value: "boolean",
+ name: "value.boolean",
+ icon: "mdi:checkbox-marked-outline"
+ }
+ },
+ widgetType: {
+ timeseries: {
+ value: "timeseries",
+ name: "widget.timeseries",
+ template: {
+ bundleAlias: "charts",
+ alias: "basic_timeseries"
+ }
+ },
+ latest: {
+ value: "latest",
+ name: "widget.latest-values",
+ template: {
+ bundleAlias: "cards",
+ alias: "attributes_card"
+ }
+ },
+ rpc: {
+ value: "rpc",
+ name: "widget.rpc",
+ template: {
+ bundleAlias: "gpio_widgets",
+ alias: "basic_gpio_control"
+ }
+ }
+ },
+ systemBundleAlias: {
+ charts: "charts",
+ cards: "cards"
+ }
+ }
+ ).name;
ui/src/app/common/utils.service.js 265(+265 -0)
diff --git a/ui/src/app/common/utils.service.js b/ui/src/app/common/utils.service.js
new file mode 100644
index 0000000..d6dd0c0
--- /dev/null
+++ b/ui/src/app/common/utils.service.js
@@ -0,0 +1,265 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import tinycolor from "tinycolor2";
+import jsonSchemaDefaults from "json-schema-defaults";
+import thingsboardTypes from "./types.constant";
+
+export default angular.module('thingsboard.utils', [thingsboardTypes])
+ .factory('utils', Utils)
+ .name;
+
+/*@ngInject*/
+function Utils($mdColorPalette, types) {
+
+ var predefinedFunctions = {},
+ predefinedFunctionsList = [],
+ materialColors = [];
+
+ predefinedFunctions['Sin'] = "return Math.round(1000*Math.sin(time/5000));";
+ predefinedFunctions['Cos'] = "return Math.round(1000*Math.cos(time/5000));";
+ predefinedFunctions['Random'] =
+ "var value = prevValue + Math.random() * 100 - 50;\n" +
+ "var multiplier = Math.pow(10, 2 || 0);\n" +
+ "var value = Math.round(value * multiplier) / multiplier;\n" +
+ "if (value < -1000) {\n" +
+ " value = -1000;\n" +
+ "} else if (value > 1000) {\n" +
+ " value = 1000;\n" +
+ "}\n" +
+ "return value;";
+
+ for (var func in predefinedFunctions) {
+ predefinedFunctionsList.push(func);
+ }
+
+ var colorPalettes = ['blue', 'green', 'red', 'amber', 'blue-grey', 'purple', 'light-green', 'indigo', 'pink', 'yellow', 'light-blue', 'orange', 'deep-purple', 'lime', 'teal', 'brown', 'cyan', 'deep-orange', 'grey'];
+ var colorSpectrum = ['500', 'A700', '600', '700', '800', '900', '300', '400', 'A200', 'A400'];
+
+ angular.forEach($mdColorPalette, function (value, key) {
+ angular.forEach(value, function (color, label) {
+ if (colorSpectrum.indexOf(label) > -1) {
+ var rgb = 'rgb(' + color.value[0] + ',' + color.value[1] + ',' + color.value[2] + ')';
+ color = tinycolor(rgb);
+ var isDark = color.isDark();
+ var colorItem = {
+ value: color.toHexString(),
+ group: key,
+ label: label,
+ isDark: isDark
+ };
+ materialColors.push(colorItem);
+ }
+ });
+ });
+
+ materialColors.sort(function (colorItem1, colorItem2) {
+ var spectrumIndex1 = colorSpectrum.indexOf(colorItem1.label);
+ var spectrumIndex2 = colorSpectrum.indexOf(colorItem2.label);
+ var result = spectrumIndex1 - spectrumIndex2;
+ if (result === 0) {
+ var paletteIndex1 = colorPalettes.indexOf(colorItem1.group);
+ var paletteIndex2 = colorPalettes.indexOf(colorItem2.group);
+ result = paletteIndex1 - paletteIndex2;
+ }
+ return result;
+ });
+
+ var defaultDataKey = {
+ name: 'f(x)',
+ type: types.dataKeyType.function,
+ label: 'Sin',
+ color: getMaterialColor(0),
+ funcBody: getPredefinedFunctionBody('Sin'),
+ settings: {},
+ _hash: Math.random()
+ };
+
+ var defaultDatasource = {
+ type: types.datasourceType.function,
+ name: types.datasourceType.function,
+ dataKeys: [angular.copy(defaultDataKey)]
+ };
+
+ var service = {
+ getDefaultDatasource: getDefaultDatasource,
+ getDefaultDatasourceJson: getDefaultDatasourceJson,
+ getMaterialColor: getMaterialColor,
+ getPredefinedFunctionBody: getPredefinedFunctionBody,
+ getPredefinedFunctionsList: getPredefinedFunctionsList,
+ genMaterialColor: genMaterialColor,
+ objectHashCode: objectHashCode,
+ parseException: parseException,
+ isDescriptorSchemaNotEmpty: isDescriptorSchemaNotEmpty,
+ filterSearchTextEntities: filterSearchTextEntities
+ }
+
+ return service;
+
+ function getPredefinedFunctionsList() {
+ return predefinedFunctionsList;
+ }
+
+ function getPredefinedFunctionBody(func) {
+ return predefinedFunctions[func];
+ }
+
+ function getMaterialColor(index) {
+ var colorIndex = index % materialColors.length;
+ return materialColors[colorIndex].value;
+ }
+
+ function genMaterialColor(str) {
+ var hash = Math.abs(hashCode(str));
+ return getMaterialColor(hash);
+ }
+
+ function hashCode(str) {
+ var hash = 0;
+ var i, char;
+ if (str.length == 0) return hash;
+ for (i = 0; i < str.length; i++) {
+ char = str.charCodeAt(i);
+ hash = ((hash << 5) - hash) + char;
+ hash = hash & hash; // Convert to 32bit integer
+ }
+ return hash;
+ }
+
+ function objectHashCode(obj) {
+ var hash = 0;
+ if (obj) {
+ var str = angular.toJson(obj);
+ hash = hashCode(str);
+ }
+ return hash;
+ }
+
+ function parseException(exception) {
+ var data = {};
+ if (exception) {
+ if (angular.isString(exception) || exception instanceof String) {
+ data.message = exception;
+ } else {
+ if (exception.name) {
+ data.name = exception.name;
+ } else {
+ data.name = 'UnknownError';
+ }
+ if (exception.message) {
+ data.message = exception.message;
+ }
+ if (exception.lineNumber) {
+ data.lineNumber = exception.lineNumber;
+ if (exception.columnNumber) {
+ data.columnNumber = exception.columnNumber;
+ }
+ } else if (exception.stack) {
+ var lineInfoRegexp = /(.*<anonymous>):(\d*)(:)?(\d*)?/g;
+ var lineInfoGroups = lineInfoRegexp.exec(exception.stack);
+ if (lineInfoGroups != null && lineInfoGroups.length >= 3) {
+ data.lineNumber = lineInfoGroups[2] - 2;
+ if (lineInfoGroups.length >= 5) {
+ data.columnNumber = lineInfoGroups[4];
+ }
+ }
+ }
+ }
+ }
+ return data;
+ }
+
+ function getDefaultDatasource(dataKeySchema) {
+ var datasource = angular.copy(defaultDatasource);
+ if (angular.isDefined(dataKeySchema)) {
+ datasource.dataKeys[0].settings = jsonSchemaDefaults(dataKeySchema);
+ }
+ return datasource;
+ }
+
+ function getDefaultDatasourceJson(dataKeySchema) {
+ return angular.toJson(getDefaultDatasource(dataKeySchema));
+ }
+
+ function isDescriptorSchemaNotEmpty(descriptor) {
+ if (descriptor && descriptor.schema && descriptor.schema.properties) {
+ for(var prop in descriptor.schema.properties) {
+ if (descriptor.schema.properties.hasOwnProperty(prop)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ function filterSearchTextEntities(entities, searchTextField, pageLink, deferred) {
+ var response = {
+ data: [],
+ hasNext: false,
+ nextPageLink: null
+ };
+ var limit = pageLink.limit;
+ var textSearch = '';
+ if (pageLink.textSearch) {
+ textSearch = pageLink.textSearch.toLowerCase();
+ }
+
+ for (var i=0;i<entities.length;i++) {
+ var entity = entities[i];
+ var text = entity[searchTextField].toLowerCase();
+ var createdTime = entity.createdTime;
+ if (pageLink.textOffset && pageLink.textOffset.length > 0) {
+ var comparison = text.localeCompare(pageLink.textOffset);
+ if (comparison === 0
+ && createdTime < pageLink.createdTimeOffset) {
+ response.data.push(entity);
+ if (response.data.length === limit) {
+ break;
+ }
+ } else if (comparison > 0 && text.startsWith(textSearch)) {
+ response.data.push(entity);
+ if (response.data.length === limit) {
+ break;
+ }
+ }
+ } else if (textSearch.length > 0) {
+ if (text.startsWith(textSearch)) {
+ response.data.push(entity);
+ if (response.data.length === limit) {
+ break;
+ }
+ }
+ } else {
+ response.data.push(entity);
+ if (response.data.length === limit) {
+ break;
+ }
+ }
+ }
+ if (response.data.length === limit) {
+ var lastEntity = response.data[limit-1];
+ response.nextPageLink = {
+ limit: pageLink.limit,
+ textSearch: textSearch,
+ idOffset: lastEntity.id.id,
+ createdTimeOffset: lastEntity.createdTime,
+ textOffset: lastEntity[searchTextField].toLowerCase()
+ };
+ response.hasNext = true;
+ }
+ deferred.resolve(response);
+ }
+
+}
ui/src/app/component/component.directive.js 75(+75 -0)
diff --git a/ui/src/app/component/component.directive.js b/ui/src/app/component/component.directive.js
new file mode 100644
index 0000000..2fe6c83
--- /dev/null
+++ b/ui/src/app/component/component.directive.js
@@ -0,0 +1,75 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import componentTemplate from './component.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function ComponentDirective($compile, $templateCache, $document, $mdDialog, componentDialogService, componentDescriptorService) {
+
+ var linker = function (scope, element) {
+
+ var template = $templateCache.get(componentTemplate);
+
+ element.html(template);
+
+ scope.componentTypeName = '';
+
+ scope.loadComponentTypeName = function () {
+ componentDescriptorService.getComponentDescriptorByClazz(scope.component.clazz).then(
+ function success(component) {
+ scope.componentTypeName = component.name;
+ },
+ function fail() {}
+ );
+ }
+
+ scope.$watch('component', function(newVal) {
+ if (newVal) {
+ scope.loadComponentTypeName();
+ }
+ }
+ );
+
+ scope.openComponent = function($event) {
+ componentDialogService.openComponentDialog($event, false,
+ scope.readOnly, scope.title, scope.type, scope.pluginClazz,
+ angular.copy(scope.component)).then(
+ function success(component) {
+ scope.component = component;
+ },
+ function fail() {}
+ );
+ }
+
+ $compile(element.contents())(scope);
+ }
+
+ return {
+ restrict: "E",
+ link: linker,
+ scope: {
+ component: '=',
+ type: '=',
+ pluginClazz: '=',
+ title: '@',
+ readOnly: '=',
+ onRemoveComponent: '&'
+ }
+ };
+}
ui/src/app/component/component.tpl.html 58(+58 -0)
diff --git a/ui/src/app/component/component.tpl.html b/ui/src/app/component/component.tpl.html
new file mode 100644
index 0000000..cc55059
--- /dev/null
+++ b/ui/src/app/component/component.tpl.html
@@ -0,0 +1,58 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div class="md-whiteframe-4dp" flex layout="row" layout-align="start center"
+ style="padding: 0 0 0 10px; margin: 5px;">
+ <span flex="50">
+ {{ component.name }}
+ </span>
+ <span flex="50">
+ {{ componentTypeName }}
+ </span>
+ <span ng-if="readOnly" style="min-width: 40px; min-height: 40px; margin: 0 6px;"></br></span>
+ <md-button ng-disabled="loading" class="md-icon-button md-primary"
+ style="min-width: 40px;"
+ ng-click="openComponent($event)"
+ aria-label="{{ (readOnly ? 'action.view' : 'action.edit') | translate }}">
+ <md-tooltip ng-if="readOnly" md-direction="top">
+ {{ 'action.view' | translate }}
+ </md-tooltip>
+ <md-tooltip ng-if="!readOnly" md-direction="top">
+ {{ 'action.edit' | translate }}
+ </md-tooltip>
+ <md-icon ng-if="readOnly" aria-label="{{ 'action.view' | translate }}"
+ class="material-icons">
+ more_horiz
+ </md-icon>
+ <md-icon ng-if="!readOnly" aria-label="{{ 'action.edit' | translate }}"
+ class="material-icons">
+ edit
+ </md-icon>
+ </md-button>
+ <md-button ng-if="!readOnly" ng-disabled="loading" class="md-icon-button md-primary"
+ style="min-width: 40px;"
+ ng-click="onRemoveComponent({event: $event})"
+ aria-label="{{ 'action.remove' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'action.remove' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.delete' | translate }}"
+ class="material-icons">
+ close
+ </md-icon>
+ </md-button>
+</div>
\ No newline at end of file
ui/src/app/component/component-dialog.controller.js 105(+105 -0)
diff --git a/ui/src/app/component/component-dialog.controller.js b/ui/src/app/component/component-dialog.controller.js
new file mode 100644
index 0000000..fb1d1da
--- /dev/null
+++ b/ui/src/app/component/component-dialog.controller.js
@@ -0,0 +1,105 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*@ngInject*/
+export default function ComponentDialogController($mdDialog, $q, $scope, componentDescriptorService, types, utils, helpLinks, isAdd, isReadOnly, componentInfo) {
+
+ var vm = this;
+
+ vm.isReadOnly = isReadOnly;
+ vm.isAdd = isAdd;
+ vm.componentInfo = componentInfo;
+ if (isAdd) {
+ vm.componentInfo.component = {};
+ }
+
+ vm.componentHasSchema = false;
+ vm.componentDescriptors = [];
+
+ if (vm.componentInfo.component && !vm.componentInfo.component.configuration) {
+ vm.componentInfo.component.configuration = {};
+ }
+
+ vm.helpLinkIdForComponent = helpLinkIdForComponent;
+ vm.save = save;
+ vm.cancel = cancel;
+
+ $scope.$watch("vm.componentInfo.component.clazz", function (newValue, prevValue) {
+ if (newValue != prevValue) {
+ if (newValue && prevValue) {
+ vm.componentInfo.component.configuration = {};
+ }
+ loadComponentDescriptor();
+ }
+ });
+
+ var componentDescriptorsPromise =
+ vm.componentInfo.type === types.componentType.action
+ ? componentDescriptorService.getPluginActionsByPluginClazz(vm.componentInfo.pluginClazz)
+ : componentDescriptorService.getComponentDescriptorsByType(vm.componentInfo.type);
+
+ componentDescriptorsPromise.then(
+ function success(componentDescriptors) {
+ vm.componentDescriptors = componentDescriptors;
+ if (vm.componentDescriptors.length === 1 && isAdd && !vm.componentInfo.component.clazz) {
+ vm.componentInfo.component.clazz = vm.componentDescriptors[0].clazz;
+ }
+ },
+ function fail() {
+ }
+ );
+
+ loadComponentDescriptor();
+
+ function loadComponentDescriptor () {
+ if (vm.componentInfo.component.clazz) {
+ componentDescriptorService.getComponentDescriptorByClazz(vm.componentInfo.component.clazz).then(
+ function success(componentDescriptor) {
+ vm.componentDescriptor = componentDescriptor;
+ vm.componentHasSchema = utils.isDescriptorSchemaNotEmpty(vm.componentDescriptor.configurationDescriptor);
+ },
+ function fail() {
+ }
+ );
+ } else {
+ vm.componentHasSchema = false;
+ }
+ }
+
+ function helpLinkIdForComponent() {
+ switch (vm.componentInfo.type) {
+ case types.componentType.filter: {
+ return helpLinks.getFilterLink(vm.componentInfo.component);
+ }
+ case types.componentType.processor: {
+ return helpLinks.getProcessorLink(vm.componentInfo.component);
+ }
+ case types.componentType.action: {
+ return helpLinks.getPluginActionLink(vm.componentInfo.component);
+ }
+
+ }
+ }
+
+
+ function cancel () {
+ $mdDialog.cancel();
+ }
+
+ function save () {
+ $mdDialog.hide(vm.componentInfo.component);
+ }
+
+}
diff --git a/ui/src/app/component/component-dialog.service.js b/ui/src/app/component/component-dialog.service.js
new file mode 100644
index 0000000..a9ad10f
--- /dev/null
+++ b/ui/src/app/component/component-dialog.service.js
@@ -0,0 +1,60 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import componentDialogTemplate from './component-dialog.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function ComponentDialogService($mdDialog, $document, $q) {
+
+ var service = {
+ openComponentDialog: openComponentDialog
+ }
+
+ return service;
+
+ function openComponentDialog($event, isAdd, readOnly, title, type, pluginClazz, component) {
+ var deferred = $q.defer();
+ var componentInfo = {
+ title: title,
+ type: type,
+ pluginClazz: pluginClazz
+ };
+ if (component) {
+ componentInfo.component = angular.copy(component);
+ }
+ $mdDialog.show({
+ controller: 'ComponentDialogController',
+ controllerAs: 'vm',
+ templateUrl: componentDialogTemplate,
+ locals: {isAdd: isAdd,
+ isReadOnly: readOnly,
+ componentInfo: componentInfo},
+ parent: angular.element($document[0].body),
+ fullscreen: true,
+ targetEvent: $event,
+ skipHide: true
+ }).then(function (component) {
+ deferred.resolve(component);
+ }, function () {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+}
\ No newline at end of file
diff --git a/ui/src/app/component/component-dialog.tpl.html b/ui/src/app/component/component-dialog.tpl.html
new file mode 100644
index 0000000..1720b92
--- /dev/null
+++ b/ui/src/app/component/component-dialog.tpl.html
@@ -0,0 +1,79 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog aria-label="{{ vm.componentInfo.title | translate }}" tb-help="vm.helpLinkIdForComponent()" help-container-id="help-container">
+ <form name="theForm" ng-submit="vm.save()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>{{ vm.componentInfo.title }}</h2>
+ <span flex></span>
+ <div id="help-container"></div>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content tb-filter">
+ <fieldset ng-disabled="loading || vm.isReadOnly">
+ <section flex layout="row">
+ <md-input-container flex class="md-block">
+ <label translate>rule.component-name</label>
+ <input required name="componentName" ng-model="vm.componentInfo.component.name">
+ <div ng-messages="theForm.componentName.$error">
+ <div translate ng-message="required">rule.component-name-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container flex class="md-block">
+ <label translate>rule.component-type</label>
+ <md-select required name="componentType" ng-model="vm.componentInfo.component.clazz" ng-disabled="loading || vm.isReadOnly">
+ <md-option ng-repeat="componentDescriptor in vm.componentDescriptors" ng-value="componentDescriptor.clazz">
+ {{componentDescriptor.name}}
+ </md-option>
+ </md-select>
+ <div ng-messages="theForm.componentType.$error">
+ <div translate ng-message="required">rule.component-type-required</div>
+ </div>
+ </md-input-container>
+ </section>
+ <md-card flex class="plugin-config" ng-if="vm.componentHasSchema">
+ <md-card-content>
+ <tb-json-form schema="vm.componentDescriptor.configurationDescriptor.schema"
+ form="vm.componentDescriptor.configurationDescriptor.form"
+ model="vm.componentInfo.component.configuration"
+ readonly="loading || vm.isReadOnly"
+ form-control="theForm">
+ </tb-json-form>
+ </md-card-content>
+ </md-card>
+ </fieldset>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-if="!vm.isReadOnly" ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit"
+ class="md-raised md-primary">
+ {{ (vm.isAdd ? 'action.add' : 'action.save') | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+ translate }}
+ </md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
ui/src/app/component/index.js 28(+28 -0)
diff --git a/ui/src/app/component/index.js b/ui/src/app/component/index.js
new file mode 100644
index 0000000..306f7fb
--- /dev/null
+++ b/ui/src/app/component/index.js
@@ -0,0 +1,28 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import thingsboardApiComponentDescriptor from '../api/component-descriptor.service';
+
+import ComponentDialogService from './component-dialog.service';
+import ComponentDialogController from './component-dialog.controller';
+import ComponentDirective from './component.directive';
+
+export default angular.module('thingsboard.component', [
+ thingsboardApiComponentDescriptor
+])
+ .factory('componentDialogService', ComponentDialogService)
+ .controller('ComponentDialogController', ComponentDialogController)
+ .directive('tbComponent', ComponentDirective)
+ .name;
diff --git a/ui/src/app/components/circular-progress.directive.js b/ui/src/app/components/circular-progress.directive.js
new file mode 100644
index 0000000..e384056
--- /dev/null
+++ b/ui/src/app/components/circular-progress.directive.js
@@ -0,0 +1,74 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import $ from 'jquery';
+
+export default angular.module('thingsboard.directives.circularProgress', [])
+ .directive('tbCircularProgress', CircularProgress)
+ .name;
+
+/* eslint-disable angular/angularelement */
+
+/*@ngInject*/
+function CircularProgress($compile) {
+
+ var linker = function (scope, element) {
+
+ var circularProgressElement = angular.element('<md-progress-circular style="margin: auto;" md-mode="indeterminate" md-diameter="20"></md-progress-circular>');
+
+ $compile(circularProgressElement)(scope);
+
+ var children = null;
+ var cssWidth = element.prop('style')['width'];
+ var width = null;
+ if (!cssWidth) {
+ $(element).css('width', width + 'px');
+ }
+
+ scope.$watch('circularProgress', function (newCircularProgress, prevCircularProgress) {
+ if (newCircularProgress != prevCircularProgress) {
+ if (newCircularProgress) {
+ if (!cssWidth) {
+ $(element).css('width', '');
+ width = element.prop('offsetWidth');
+ $(element).css('width', width + 'px');
+ }
+ children = $(element).children();
+ $(element).empty();
+ $(element).append($(circularProgressElement));
+ } else {
+ $(element).empty();
+ $(element).append(children);
+ if (cssWidth) {
+ $(element).css('width', cssWidth);
+ } else {
+ $(element).css('width', '');
+ }
+ }
+ }
+ });
+
+ }
+
+ return {
+ restrict: "A",
+ link: linker,
+ scope: {
+ circularProgress: "=tbCircularProgress"
+ }
+ };
+}
+
+/* eslint-enable angular/angularelement */
diff --git a/ui/src/app/components/confirm-on-exit.directive.js b/ui/src/app/components/confirm-on-exit.directive.js
new file mode 100644
index 0000000..d346b38
--- /dev/null
+++ b/ui/src/app/components/confirm-on-exit.directive.js
@@ -0,0 +1,56 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export default angular.module('thingsboard.directives.confirmOnExit', [])
+ .directive('tbConfirmOnExit', ConfirmOnExit)
+ .name;
+
+/*@ngInject*/
+function ConfirmOnExit($state, $mdDialog, $window, $filter) {
+ return {
+ link: function ($scope) {
+
+ $window.onbeforeunload = function () {
+ if (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty) {
+ return $filter('translate')('confirm-on-exit.message');
+ }
+ }
+ $scope.$on('$stateChangeStart', function (event, next, current, params) {
+ if (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty) {
+ event.preventDefault();
+ var confirm = $mdDialog.confirm()
+ .title($filter('translate')('confirm-on-exit.title'))
+ .htmlContent($filter('translate')('confirm-on-exit.html-message'))
+ .ariaLabel($filter('translate')('confirm-on-exit.title'))
+ .cancel($filter('translate')('action.cancel'))
+ .ok($filter('translate')('action.ok'));
+ $mdDialog.show(confirm).then(function () {
+ if ($scope.confirmForm) {
+ $scope.confirmForm.$setPristine();
+ } else {
+ $scope.isDirty = false;
+ }
+ $state.go(next.name, params);
+ }, function () {
+ });
+ }
+ });
+ },
+ scope: {
+ confirmForm: '=',
+ isDirty: '='
+ }
+ };
+}
\ No newline at end of file
ui/src/app/components/contact.directive.js 300(+300 -0)
diff --git a/ui/src/app/components/contact.directive.js b/ui/src/app/components/contact.directive.js
new file mode 100644
index 0000000..c88f844
--- /dev/null
+++ b/ui/src/app/components/contact.directive.js
@@ -0,0 +1,300 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import contactTemplate from './contact.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+export default angular.module('thingsboard.directives.contact', [])
+ .directive('tbContact', Contact)
+ .name;
+
+/*@ngInject*/
+function Contact($compile, $templateCache) {
+ var countries = [
+ "Afghanistan",
+ "Åland Islands",
+ "Albania",
+ "Algeria",
+ "American Samoa",
+ "Andorra",
+ "Angola",
+ "Anguilla",
+ "Antarctica",
+ "Antigua and Barbuda",
+ "Argentina",
+ "Armenia",
+ "Aruba",
+ "Australia",
+ "Austria",
+ "Azerbaijan",
+ "Bahamas",
+ "Bahrain",
+ "Bangladesh",
+ "Barbados",
+ "Belarus",
+ "Belgium",
+ "Belize",
+ "Benin",
+ "Bermuda",
+ "Bhutan",
+ "Bolivia",
+ "Bonaire, Sint Eustatius and Saba",
+ "Bosnia and Herzegovina",
+ "Botswana",
+ "Bouvet Island",
+ "Brazil",
+ "British Indian Ocean Territory",
+ "Brunei Darussalam",
+ "Bulgaria",
+ "Burkina Faso",
+ "Burundi",
+ "Cambodia",
+ "Cameroon",
+ "Canada",
+ "Cape Verde",
+ "Cayman Islands",
+ "Central African Republic",
+ "Chad",
+ "Chile",
+ "China",
+ "Christmas Island",
+ "Cocos (Keeling) Islands",
+ "Colombia",
+ "Comoros",
+ "Congo",
+ "Congo, The Democratic Republic of the",
+ "Cook Islands",
+ "Costa Rica",
+ "Côte d'Ivoire",
+ "Croatia",
+ "Cuba",
+ "Curaçao",
+ "Cyprus",
+ "Czech Republic",
+ "Denmark",
+ "Djibouti",
+ "Dominica",
+ "Dominican Republic",
+ "Ecuador",
+ "Egypt",
+ "El Salvador",
+ "Equatorial Guinea",
+ "Eritrea",
+ "Estonia",
+ "Ethiopia",
+ "Falkland Islands (Malvinas)",
+ "Faroe Islands",
+ "Fiji",
+ "Finland",
+ "France",
+ "French Guiana",
+ "French Polynesia",
+ "French Southern Territories",
+ "Gabon",
+ "Gambia",
+ "Georgia",
+ "Germany",
+ "Ghana",
+ "Gibraltar",
+ "Greece",
+ "Greenland",
+ "Grenada",
+ "Guadeloupe",
+ "Guam",
+ "Guatemala",
+ "Guernsey",
+ "Guinea",
+ "Guinea-Bissau",
+ "Guyana",
+ "Haiti",
+ "Heard Island and McDonald Islands",
+ "Holy See (Vatican City State)",
+ "Honduras",
+ "Hong Kong",
+ "Hungary",
+ "Iceland",
+ "India",
+ "Indonesia",
+ "Iran, Islamic Republic of",
+ "Iraq",
+ "Ireland",
+ "Isle of Man",
+ "Israel",
+ "Italy",
+ "Jamaica",
+ "Japan",
+ "Jersey",
+ "Jordan",
+ "Kazakhstan",
+ "Kenya",
+ "Kiribati",
+ "Korea, Democratic People's Republic of",
+ "Korea, Republic of",
+ "Kuwait",
+ "Kyrgyzstan",
+ "Lao People's Democratic Republic",
+ "Latvia",
+ "Lebanon",
+ "Lesotho",
+ "Liberia",
+ "Libya",
+ "Liechtenstein",
+ "Lithuania",
+ "Luxembourg",
+ "Macao",
+ "Macedonia, Republic Of",
+ "Madagascar",
+ "Malawi",
+ "Malaysia",
+ "Maldives",
+ "Mali",
+ "Malta",
+ "Marshall Islands",
+ "Martinique",
+ "Mauritania",
+ "Mauritius",
+ "Mayotte",
+ "Mexico",
+ "Micronesia, Federated States of",
+ "Moldova, Republic of",
+ "Monaco",
+ "Mongolia",
+ "Montenegro",
+ "Montserrat",
+ "Morocco",
+ "Mozambique",
+ "Myanmar",
+ "Namibia",
+ "Nauru",
+ "Nepal",
+ "Netherlands",
+ "New Caledonia",
+ "New Zealand",
+ "Nicaragua",
+ "Niger",
+ "Nigeria",
+ "Niue",
+ "Norfolk Island",
+ "Northern Mariana Islands",
+ "Norway",
+ "Oman",
+ "Pakistan",
+ "Palau",
+ "Palestinian Territory, Occupied",
+ "Panama",
+ "Papua New Guinea",
+ "Paraguay",
+ "Peru",
+ "Philippines",
+ "Pitcairn",
+ "Poland",
+ "Portugal",
+ "Puerto Rico",
+ "Qatar",
+ "Reunion",
+ "Romania",
+ "Russian Federation",
+ "Rwanda",
+ "Saint Barthélemy",
+ "Saint Helena, Ascension and Tristan da Cunha",
+ "Saint Kitts and Nevis",
+ "Saint Lucia",
+ "Saint Martin (French Part)",
+ "Saint Pierre and Miquelon",
+ "Saint Vincent and the Grenadines",
+ "Samoa",
+ "San Marino",
+ "Sao Tome and Principe",
+ "Saudi Arabia",
+ "Senegal",
+ "Serbia",
+ "Seychelles",
+ "Sierra Leone",
+ "Singapore",
+ "Sint Maarten (Dutch Part)",
+ "Slovakia",
+ "Slovenia",
+ "Solomon Islands",
+ "Somalia",
+ "South Africa",
+ "South Georgia and the South Sandwich Islands",
+ "South Sudan",
+ "Spain",
+ "Sri Lanka",
+ "Sudan",
+ "Suriname",
+ "Svalbard and Jan Mayen",
+ "Swaziland",
+ "Sweden",
+ "Switzerland",
+ "Syrian Arab Republic",
+ "Taiwan",
+ "Tajikistan",
+ "Tanzania, United Republic of",
+ "Thailand",
+ "Timor-Leste",
+ "Togo",
+ "Tokelau",
+ "Tonga",
+ "Trinidad and Tobago",
+ "Tunisia",
+ "Turkey",
+ "Turkmenistan",
+ "Turks and Caicos Islands",
+ "Tuvalu",
+ "Uganda",
+ "Ukraine",
+ "United Arab Emirates",
+ "United Kingdom",
+ "United States",
+ "United States Minor Outlying Islands",
+ "Uruguay",
+ "Uzbekistan",
+ "Vanuatu",
+ "Venezuela",
+ "Viet Nam",
+ "Virgin Islands, British",
+ "Virgin Islands, U.S.",
+ "Wallis and Futuna",
+ "Western Sahara",
+ "Yemen",
+ "Zambia",
+ "Zimbabwe"
+ ];
+
+ var linker = function (scope, element) {
+
+ scope.countries = countries;
+
+ var template = $templateCache.get(contactTemplate);
+
+ element.html(template);
+
+ $compile(element.contents())(scope);
+ }
+
+ return {
+ restrict: "E",
+ link: linker,
+ scope: {
+ contact: '=',
+ isEdit: '=',
+ theForm: '='
+ }
+ };
+}
ui/src/app/components/contact.tpl.html 59(+59 -0)
diff --git a/ui/src/app/components/contact.tpl.html b/ui/src/app/components/contact.tpl.html
new file mode 100644
index 0000000..8eb0fdd
--- /dev/null
+++ b/ui/src/app/components/contact.tpl.html
@@ -0,0 +1,59 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-input-container class="md-block">
+ <label translate>contact.country</label>
+ <md-select ng-disabled="!isEdit" name="country" ng-model="contact.country">
+ <md-option ng-repeat="country in countries" value="{{country}}">
+ {{country}}
+ </md-option>
+ </md-select>
+</md-input-container>
+<div layout-gt-sm="row">
+ <md-input-container class="md-block">
+ <label translate>contact.city</label>
+ <input name="city" ng-model="contact.city">
+ </md-input-container>
+ <md-input-container class="md-block">
+ <label translate>contact.state</label>
+ <input name="state" ng-model="contact.state">
+ </md-input-container>
+ <md-input-container class="md-block">
+ <label translate>contact.postal-code</label>
+ <input name="zip" ng-model="contact.zip" ng-pattern="/^([0-9]*)$/">
+ <div ng-messages="theForm.zip.$error" role="alert" multiple>
+ <div translate ng-message="pattern">contact.postal-code-invalid</div>
+ </div>
+ </md-input-container>
+</div>
+<md-input-container class="md-block">
+ <label translate>contact.address</label>
+ <input name="address" ng-model="contact.address">
+</md-input-container>
+<md-input-container class="md-block">
+ <label translate>contact.address2</label>
+ <input name="address2" ng-model="contact.address2">
+</md-input-container>
+<md-input-container class="md-block">
+ <label translate>contact.phone</label>
+ <input name="phone" ng-model="contact.phone">
+</md-input-container>
+<md-input-container class="md-block">
+ <label translate>contact.email</label>
+ <input name="email" type="email" ng-model="contact.email">
+</md-input-container>
+
\ No newline at end of file
diff --git a/ui/src/app/components/contact-short.filter.js b/ui/src/app/components/contact-short.filter.js
new file mode 100644
index 0000000..80c2c93
--- /dev/null
+++ b/ui/src/app/components/contact-short.filter.js
@@ -0,0 +1,54 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export default angular.module('thingsboard.filters.contactShort', [])
+ .filter('contactShort', ContactShort)
+ .name;
+
+/*@ngInject*/
+function ContactShort($filter) {
+ return function (contact) {
+ var contactShort = '';
+ if (contact) {
+ if (contact.address) {
+ contactShort += contact.address;
+ contactShort += ' ';
+ }
+ if (contact.address2) {
+ contactShort += contact.address2;
+ contactShort += ' ';
+ }
+ if (contact.city) {
+ contactShort += contact.city;
+ contactShort += ' ';
+ }
+ if (contact.state) {
+ contactShort += contact.state;
+ contactShort += ' ';
+ }
+ if (contact.zip) {
+ contactShort += contact.zip;
+ contactShort += ' ';
+ }
+ if (contact.country) {
+ contactShort += contact.country;
+ }
+ }
+ if (contactShort === '') {
+ contactShort = $filter('translate')('contact.no-address');
+ }
+ return contactShort;
+ };
+}
ui/src/app/components/dashboard.directive.js 427(+427 -0)
diff --git a/ui/src/app/components/dashboard.directive.js b/ui/src/app/components/dashboard.directive.js
new file mode 100644
index 0000000..ce7668f
--- /dev/null
+++ b/ui/src/app/components/dashboard.directive.js
@@ -0,0 +1,427 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './dashboard.scss';
+
+import $ from 'jquery';
+import gridster from 'angular-gridster';
+import thingsboardTypes from '../common/types.constant';
+import thingsboardApiWidget from '../api/widget.service';
+import thingsboardWidget from './widget.directive';
+import thingsboardToast from '../services/toast';
+import thingsboardTimewindow from './timewindow.directive';
+import thingsboardEvents from './tb-event-directives';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import dashboardTemplate from './dashboard.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/* eslint-disable angular/angularelement */
+
+export default angular.module('thingsboard.directives.dashboard', [thingsboardTypes,
+ thingsboardToast,
+ thingsboardApiWidget,
+ thingsboardWidget,
+ thingsboardTimewindow,
+ thingsboardEvents,
+ gridster.name])
+ .directive('tbDashboard', Dashboard)
+ .name;
+
+/*@ngInject*/
+function Dashboard() {
+ return {
+ restrict: "E",
+ scope: true,
+ bindToController: {
+ widgets: '=',
+ deviceAliasList: '=',
+ columns: '=',
+ isEdit: '=',
+ isMobile: '=',
+ isMobileDisabled: '=?',
+ isEditActionEnabled: '=',
+ isRemoveActionEnabled: '=',
+ onEditWidget: '&?',
+ onRemoveWidget: '&?',
+ onWidgetClicked: '&?',
+ loadWidgets: '&?',
+ onInit: '&?',
+ onInitFailed: '&?'
+ },
+ controller: DashboardController,
+ controllerAs: 'vm',
+ templateUrl: dashboardTemplate
+ };
+}
+
+/*@ngInject*/
+function DashboardController($scope, $rootScope, $element, $timeout, $log, toast, types) {
+
+ var highlightedMode = false;
+ var highlightedIndex = -1;
+ var mouseDownIndex = -1;
+ var widgetMouseMoved = false;
+
+ var gridsterParent = null;
+ var gridsterElement = null;
+ var gridster = null;
+
+ var vm = this;
+
+ vm.isMobileDisabled = angular.isDefined(vm.isMobileDisabled) ? vm.isMobileDisabled : false;
+
+ vm.dashboardLoading = true;
+ vm.visibleRect = {
+ top: 0,
+ bottom: 0,
+ left: 0,
+ right: 0
+ };
+ vm.gridsterOpts = {
+ floating: false,
+ maxRows: 100,
+ columns: vm.columns ? vm.columns : 24,
+ minSizeX: 2,
+ minSizeY: 2,
+ defaultSizeX: 8,
+ defaultSizeY: 6,
+ resizable: {
+ enabled: vm.isEdit
+ },
+ draggable: {
+ enabled: vm.isEdit
+ },
+ isMobile: vm.isMobileDisabled ? false : vm.isMobile,
+ mobileBreakPoint: vm.isMobileDisabled ? 0 : (vm.isMobile ? 20000 : 960),
+ margins: [10, 10],
+ saveGridItemCalculatedHeightInMobile: true
+ };
+
+ vm.isWidgetExpanded = false;
+ vm.isHighlighted = isHighlighted;
+ vm.isNotHighlighted = isNotHighlighted;
+ vm.highlightWidget = highlightWidget;
+ vm.resetHighlight = resetHighlight;
+
+ vm.onWidgetFullscreenChanged = onWidgetFullscreenChanged;
+ vm.widgetMouseDown = widgetMouseDown;
+ vm.widgetMouseMove = widgetMouseMove;
+ vm.widgetMouseUp = widgetMouseUp;
+
+ vm.widgetColor = widgetColor;
+ vm.widgetBackgroundColor = widgetBackgroundColor;
+ vm.widgetPadding = widgetPadding;
+ vm.showWidgetTitle = showWidgetTitle;
+ vm.hasTimewindow = hasTimewindow;
+ vm.editWidget = editWidget;
+ vm.removeWidget = removeWidget;
+ vm.loading = loading;
+
+ //$element[0].onmousemove=function(){
+ // widgetMouseMove();
+ // }
+
+ gridsterParent = $('#gridster-parent', $element);
+ gridsterElement = angular.element($('#gridster-child', gridsterParent));
+
+ gridsterParent.scroll(function () {
+ updateVisibleRect();
+ });
+
+ gridsterParent.resize(function () {
+ updateVisibleRect();
+ });
+
+ $scope.$watch('vm.isMobile', function () {
+ vm.gridsterOpts.isMobile = vm.isMobileDisabled ? false : vm.isMobile;
+ vm.gridsterOpts.mobileBreakPoint = vm.isMobileDisabled ? 0 : (vm.isMobile ? 20000 : 960);
+ });
+
+ $scope.$watch('vm.isMobileDisabled', function () {
+ vm.gridsterOpts.isMobile = vm.isMobileDisabled ? false : vm.isMobile;
+ vm.gridsterOpts.mobileBreakPoint = vm.isMobileDisabled ? 0 : (vm.isMobile ? 20000 : 960);
+ });
+
+ $scope.$watch('vm.columns', function () {
+ vm.gridsterOpts.columns = vm.columns ? vm.columns : 24;
+ });
+
+ $scope.$watch('vm.isEdit', function () {
+ vm.gridsterOpts.resizable.enabled = vm.isEdit;
+ vm.gridsterOpts.draggable.enabled = vm.isEdit;
+ $scope.$broadcast('toggleDashboardEditMode', vm.isEdit);
+ });
+
+ $scope.$watch('vm.deviceAliasList', function () {
+ $scope.$broadcast('deviceAliasListChanged', vm.deviceAliasList);
+ }, true);
+
+ $scope.$on('gridster-resized', function (event, sizes, theGridster) {
+ if (checkIsLocalGridsterElement(theGridster)) {
+ gridster = theGridster;
+ updateVisibleRect(false, true);
+ }
+ });
+
+ $scope.$on('gridster-mobile-changed', function (event, theGridster) {
+ if (checkIsLocalGridsterElement(theGridster)) {
+ gridster = theGridster;
+ if (gridster.isMobile) {
+ vm.gridsterOpts.rowHeight = 70;
+ } else {
+ vm.gridsterOpts.rowHeight = 'match';
+ }
+ $timeout(function () {
+ updateVisibleRect(true);
+ }, 500, false);
+ }
+ });
+
+ $scope.$on('widgetPositionChanged', function () {
+ vm.widgets.sort(function (widget1, widget2) {
+ var res = widget1.row - widget2.row;
+ if (res === 0) {
+ res = widget1.col - widget2.col;
+ }
+ return res;
+ });
+ });
+
+ loadDashboard();
+
+ function loadDashboard() {
+ resetWidgetClick();
+ $timeout(function () {
+ if (vm.loadWidgets) {
+ var promise = vm.loadWidgets();
+ if (promise) {
+ promise.then(function () {
+ dashboardLoaded();
+ }, function () {
+ dashboardLoaded();
+ });
+ } else {
+ dashboardLoaded();
+ }
+ } else {
+ dashboardLoaded();
+ }
+ }, 0, false);
+ }
+
+ function updateVisibleRect (force, containerResized) {
+ if (gridster) {
+ var position = $(gridster.$element).position()
+ if (position) {
+ var viewportWidth = gridsterParent.width();
+ var viewportHeight = gridsterParent.height();
+ var top = -position.top;
+ var bottom = top + viewportHeight;
+ var left = -position.left;
+ var right = left + viewportWidth;
+
+ var newVisibleRect = {
+ top: gridster.pixelsToRows(top),
+ topPx: top,
+ bottom: gridster.pixelsToRows(bottom),
+ bottomPx: bottom,
+ left: gridster.pixelsToColumns(left),
+ right: gridster.pixelsToColumns(right),
+ isMobile: gridster.isMobile,
+ curRowHeight: gridster.curRowHeight,
+ containerResized: containerResized
+ };
+
+ if (force ||
+ newVisibleRect.top != vm.visibleRect.top ||
+ newVisibleRect.topPx != vm.visibleRect.topPx ||
+ newVisibleRect.bottom != vm.visibleRect.bottom ||
+ newVisibleRect.bottomPx != vm.visibleRect.bottomPx ||
+ newVisibleRect.left != vm.visibleRect.left ||
+ newVisibleRect.right != vm.visibleRect.right ||
+ newVisibleRect.isMobile != vm.visibleRect.isMobile ||
+ newVisibleRect.curRowHeight != vm.visibleRect.curRowHeight ||
+ newVisibleRect.containerResized != vm.visibleRect.containerResized) {
+ vm.visibleRect = newVisibleRect;
+ $scope.$broadcast('visibleRectChanged', vm.visibleRect);
+ }
+ }
+ }
+ }
+
+ function checkIsLocalGridsterElement (gridster) {
+ return gridsterElement[0] == gridster.$element[0];
+ }
+
+ function resetWidgetClick () {
+ mouseDownIndex = -1;
+ widgetMouseMoved = false;
+ }
+
+ function onWidgetFullscreenChanged(expanded, widget) {
+ vm.isWidgetExpanded = expanded;
+ $scope.$broadcast('onWidgetFullscreenChanged', vm.isWidgetExpanded, widget);
+ }
+
+ function widgetMouseDown ($event, widget) {
+ mouseDownIndex = vm.widgets.indexOf(widget);
+ widgetMouseMoved = false;
+ }
+
+ function widgetMouseMove () {
+ if (mouseDownIndex > -1) {
+ widgetMouseMoved = true;
+ }
+ }
+
+ function widgetMouseUp ($event, widget) {
+ $timeout(function () {
+ if (!widgetMouseMoved && mouseDownIndex > -1) {
+ var index = vm.widgets.indexOf(widget);
+ if (index === mouseDownIndex) {
+ widgetClicked($event, widget);
+ }
+ }
+ mouseDownIndex = -1;
+ widgetMouseMoved = false;
+ }, 0);
+ }
+
+ function widgetClicked ($event, widget) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ if (vm.onWidgetClicked) {
+ vm.onWidgetClicked({event: $event, widget: widget});
+ }
+ }
+
+ function editWidget ($event, widget) {
+ resetWidgetClick();
+ if ($event) {
+ $event.stopPropagation();
+ }
+ if (vm.isEditActionEnabled && vm.onEditWidget) {
+ vm.onEditWidget({event: $event, widget: widget});
+ }
+ }
+
+ function removeWidget($event, widget) {
+ resetWidgetClick();
+ if ($event) {
+ $event.stopPropagation();
+ }
+ if (vm.isRemoveActionEnabled && vm.onRemoveWidget) {
+ vm.onRemoveWidget({event: $event, widget: widget});
+ }
+ }
+
+ function highlightWidget(widgetIndex, delay) {
+ highlightedMode = true;
+ highlightedIndex = widgetIndex;
+ var item = $('.gridster-item', gridster.$element)[widgetIndex];
+ if (item) {
+ var height = $(item).outerHeight(true);
+ var rectHeight = gridsterParent.height();
+ var offset = (rectHeight - height) / 2;
+ var scrollTop = item.offsetTop;
+ if (offset > 0) {
+ scrollTop -= offset;
+ }
+ gridsterParent.animate({
+ scrollTop: scrollTop
+ }, delay);
+ }
+ }
+
+ function resetHighlight() {
+ highlightedMode = false;
+ highlightedIndex = -1;
+ }
+
+ function isHighlighted(widget) {
+ return highlightedMode && vm.widgets.indexOf(widget) === highlightedIndex;
+ }
+
+ function isNotHighlighted(widget) {
+ return highlightedMode && vm.widgets.indexOf(widget) != highlightedIndex;
+ }
+
+ function widgetColor(widget) {
+ if (widget.config.color) {
+ return widget.config.color;
+ } else {
+ return 'rgba(0, 0, 0, 0.87)';
+ }
+ }
+
+ function widgetBackgroundColor(widget) {
+ if (widget.config.backgroundColor) {
+ return widget.config.backgroundColor;
+ } else {
+ return '#fff';
+ }
+ }
+
+ function widgetPadding(widget) {
+ if (widget.config.padding) {
+ return widget.config.padding;
+ } else {
+ return '8px';
+ }
+ }
+
+ function showWidgetTitle(widget) {
+ if (angular.isDefined(widget.config.showTitle)) {
+ return widget.config.showTitle;
+ } else {
+ return true;
+ }
+ }
+
+ function hasTimewindow(widget) {
+ return widget.type === types.widgetType.timeseries.value;
+ }
+
+ function adoptMaxRows() {
+ if (vm.widgets) {
+ var maxRows = vm.gridsterOpts.maxRows;
+ for (var i = 0; i < vm.widgets.length; i++) {
+ var w = vm.widgets[i];
+ var bottom = w.row + w.sizeY;
+ maxRows = Math.max(maxRows, bottom);
+ }
+ vm.gridsterOpts.maxRows = Math.max(maxRows, vm.gridsterOpts.maxRows);
+ }
+ }
+
+ function dashboardLoaded() {
+ adoptMaxRows();
+ vm.dashboardLoading = false;
+ if (vm.onInit) {
+ vm.onInit({dashboard: vm});
+ }
+ }
+
+ function loading() {
+ return $rootScope.loading;
+ }
+
+}
+
+/* eslint-enable angular/angularelement */
ui/src/app/components/dashboard.scss 108(+108 -0)
diff --git a/ui/src/app/components/dashboard.scss b/ui/src/app/components/dashboard.scss
new file mode 100644
index 0000000..1b08cdd
--- /dev/null
+++ b/ui/src/app/components/dashboard.scss
@@ -0,0 +1,108 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import "~compass-sass-mixins/lib/compass";
+
+div.tb-widget {
+ position: relative;
+ height: 100%;
+ margin: 0;
+ overflow: hidden;
+ @include transition(all .2s ease-in-out);
+
+ .tb-widget-title {
+ max-height: 60px;
+
+ padding-top: 5px;
+ padding-left: 5px;
+ overflow: hidden;
+
+ tb-timewindow {
+ font-size: 14px;
+ opacity: 0.85;
+ }
+ }
+
+ .tb-widget-actions {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ z-index: 1;
+ margin: 0px;
+
+ .md-button.md-icon-button {
+ margin: 0px !important;
+ padding: 0px !important;
+ line-height: 20px;
+ width: 32px;
+ height: 32px;
+ min-width: 32px;
+ min-height: 32px;
+ md-icon {
+ width: 20px;
+ height: 20px;
+ min-width: 20px;
+ min-height: 20px;
+ font-size: 20px;
+ }
+ }
+ }
+
+ .tb-widget-content {
+ tb-widget {
+ width: 100%;
+ position: relative;
+ }
+ }
+}
+
+div.tb-widget.tb-highlighted {
+ border: 1px solid #039be5;
+ box-shadow: 0 0 20px #039be5;
+}
+
+div.tb-widget.tb-not-highlighted {
+ opacity: 0.5;
+}
+
+tb-dashboard {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+}
+
+md-content.tb-dashboard-content {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+}
+
+.tb-widget-error-container {
+ position: absolute;
+ background-color: #fff;
+ width: 100%;
+ height: 100%;
+}
+
+.tb-widget-error-msg {
+ color: red;
+ font-size: 16px;
+ word-wrap: break-word;
+ padding: 5px;
+}
ui/src/app/components/dashboard.tpl.html 80(+80 -0)
diff --git a/ui/src/app/components/dashboard.tpl.html b/ui/src/app/components/dashboard.tpl.html
new file mode 100644
index 0000000..1127cb0
--- /dev/null
+++ b/ui/src/app/components/dashboard.tpl.html
@@ -0,0 +1,80 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-content flex layout="column" class="tb-progress-cover" layout-align="center center"
+ ng-show="(vm.loading() || vm.dashboardLoading) && !vm.isEdit">
+ <md-progress-circular md-mode="indeterminate" class="md-warn" md-diameter="100"></md-progress-circular>
+</md-content>
+<md-content id="gridster-parent" class="tb-dashboard-content" flex layout-wrap>
+ <div id="gridster-child" gridster="vm.gridsterOpts">
+ <ul>
+<!-- ng-click="widgetClicked($event, widget)" -->
+ <li gridster-item="widget" ng-repeat="widget in vm.widgets">
+ <div tb-expand-fullscreen expand-button-id="expand-button" on-fullscreen-changed="vm.onWidgetFullscreenChanged(expanded, widget)" layout="column" class="tb-widget md-whiteframe-4dp"
+ ng-class="{'tb-highlighted': vm.isHighlighted(widget), 'tb-not-highlighted': vm.isNotHighlighted(widget)}"
+ tb-mousedown="vm.widgetMouseDown($event, widget)"
+ tb-mousemove="vm.widgetMouseMove($event, widget)"
+ tb-mouseup="vm.widgetMouseUp($event, widget)"
+ style="
+ cursor: pointer;
+ color: {{vm.widgetColor(widget)}};
+ background-color: {{vm.widgetBackgroundColor(widget)}};
+ padding: {{vm.widgetPadding(widget)}}
+ ">
+ <div class="tb-widget-title" layout="column" ng-show="vm.showWidgetTitle(widget) || vm.hasTimewindow(widget)">
+ <span ng-show="vm.showWidgetTitle(widget)" class="md-subhead">{{widget.config.title}}</span>
+ <tb-timewindow ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow>
+ </div>
+ <div class="tb-widget-actions" layout="row" layout-align="start center">
+ <md-button id="expand-button"
+ aria-label="{{ 'fullscreen.fullscreen' | translate }}"
+ class="md-icon-button md-primary"></md-button>
+ <md-button ng-show="vm.isEditActionEnabled && !vm.isWidgetExpanded"
+ ng-disabled="vm.loading()"
+ class="md-icon-button md-primary"
+ ng-click="vm.editWidget($event, widget)"
+ aria-label="{{ 'widget.edit' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'widget.edit' | translate }}
+ </md-tooltip>
+ <md-icon class="material-icons">
+ edit
+ </md-icon>
+ </md-button>
+ <md-button ng-show="vm.isRemoveActionEnabled && !vm.isWidgetExpanded"
+ ng-disabled="vm.loading()"
+ class="md-icon-button md-primary"
+ ng-click="vm.removeWidget($event, widget)"
+ aria-label="{{ 'widget.remove' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'widget.remove' | translate }}
+ </md-tooltip>
+ <md-icon class="material-icons">
+ close
+ </md-icon>
+ </md-button>
+ </div>
+ <div flex layout="column" class="tb-widget-content">
+ <div flex tb-widget
+ locals="{ visibleRect: vm.visibleRect, widget: widget, deviceAliasList: vm.deviceAliasList, isPreview: vm.isEdit }">
+ </div>
+ </div>
+ </div>
+ </li>
+ </ul>
+ </div>
+</md-content>
\ No newline at end of file
ui/src/app/components/dashboard-select.directive.js 114(+114 -0)
diff --git a/ui/src/app/components/dashboard-select.directive.js b/ui/src/app/components/dashboard-select.directive.js
new file mode 100644
index 0000000..8f007a7
--- /dev/null
+++ b/ui/src/app/components/dashboard-select.directive.js
@@ -0,0 +1,114 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './dashboard-select.scss';
+
+import thingsboardApiDashboard from '../api/dashboard.service';
+import thingsboardApiUser from '../api/user.service';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import dashboardSelectTemplate from './dashboard-select.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+
+export default angular.module('thingsboard.directives.dashboardSelect', [thingsboardApiDashboard, thingsboardApiUser])
+ .directive('tbDashboardSelect', DashboardSelect)
+ .name;
+
+/*@ngInject*/
+function DashboardSelect($compile, $templateCache, $q, dashboardService, userService) {
+
+ var linker = function (scope, element, attrs, ngModelCtrl) {
+ var template = $templateCache.get(dashboardSelectTemplate);
+ element.html(template);
+
+ scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false;
+ scope.dashboard = null;
+ scope.dashboardSearchText = '';
+
+ scope.dashboardFetchFunction = dashboardService.getTenantDashboards;
+ if (angular.isDefined(scope.dashboardsScope)) {
+ if (scope.dashboardsScope === 'customer') {
+ scope.dashboardFetchFunction = dashboardService.getCustomerDashboards;
+ } else {
+ scope.dashboardFetchFunction = dashboardService.getTenantDashboards;
+ }
+ } else {
+ if (userService.getAuthority() === 'TENANT_ADMIN') {
+ scope.dashboardFetchFunction = dashboardService.getTenantDashboards;
+ } else if (userService.getAuthority() === 'CUSTOMER_USER') {
+ scope.dashboardFetchFunction = dashboardService.getCustomerDashboards;
+ }
+ }
+
+ scope.fetchDashboards = function(searchText) {
+ var pageLink = {limit: 10, textSearch: searchText};
+
+ var deferred = $q.defer();
+
+ scope.dashboardFetchFunction(pageLink).then(function success(result) {
+ deferred.resolve(result.data);
+ }, function fail() {
+ deferred.reject();
+ });
+
+ return deferred.promise;
+ }
+
+ scope.dashboardSearchTextChanged = function() {
+ }
+
+ scope.updateView = function () {
+ ngModelCtrl.$setViewValue(scope.dashboard);
+ }
+
+ ngModelCtrl.$render = function () {
+ if (ngModelCtrl.$viewValue) {
+ scope.dashboard = ngModelCtrl.$viewValue;
+ }
+ }
+
+ scope.$watch('dashboard', function () {
+ scope.updateView();
+ });
+
+ if (scope.selectFirstDashboard) {
+ var pageLink = {limit: 1, textSearch: ''};
+ scope.dashboardFetchFunction(pageLink).then(function success(result) {
+ var dashboards = result.data;
+ if (dashboards.length > 0) {
+ scope.dashboard = dashboards[0];
+ }
+ }, function fail() {
+ });
+ }
+
+ $compile(element.contents())(scope);
+ }
+
+ return {
+ restrict: "E",
+ require: "^ngModel",
+ link: linker,
+ scope: {
+ dashboardsScope: '@',
+ theForm: '=?',
+ tbRequired: '=?',
+ selectFirstDashboard: '='
+ }
+ };
+}
ui/src/app/components/dashboard-select.scss 30(+30 -0)
diff --git a/ui/src/app/components/dashboard-select.scss b/ui/src/app/components/dashboard-select.scss
new file mode 100644
index 0000000..63ad8ce
--- /dev/null
+++ b/ui/src/app/components/dashboard-select.scss
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+.tb-dashboard-autocomplete {
+ .tb-not-found {
+ display: block;
+ line-height: 1.5;
+ height: 48px;
+ }
+ .tb-dashboard-item {
+ display: block;
+ height: 48px;
+ }
+ li {
+ height: auto !important;
+ white-space: normal !important;
+ }
+}
diff --git a/ui/src/app/components/dashboard-select.tpl.html b/ui/src/app/components/dashboard-select.tpl.html
new file mode 100644
index 0000000..32eb12f
--- /dev/null
+++ b/ui/src/app/components/dashboard-select.tpl.html
@@ -0,0 +1,42 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-autocomplete ng-required="tbRequired"
+ md-input-name="dashboard"
+ ng-model="dashboard"
+ md-selected-item="dashboard"
+ md-search-text="dashboardSearchText"
+ md-search-text-change="dashboardSearchTextChanged()"
+ md-items="item in fetchDashboards(dashboardSearchText)"
+ md-item-text="item.title"
+ md-min-length="0"
+ placeholder="{{ 'dashboard.select-dashboard' | translate }}"
+ md-menu-class="tb-dashboard-autocomplete">
+ <md-item-template>
+ <div class="tb-dashboard-item">
+ <span md-highlight-text="dashboardSearchText" md-highlight-flags="^i">{{item.title}}</span>
+ </div>
+ </md-item-template>
+ <md-not-found>
+ <div class="tb-not-found">
+ <span translate translate-values='{ dashboard: dashboardSearchText }'>dashboard.no-dashboards-matching</span>
+ </div>
+ </md-not-found>
+ <div ng-messages="theForm.dashboard.$error">
+ <div translate ng-message="required">dashboard.dashboard-required</div>
+ </div>
+</md-autocomplete>
ui/src/app/components/datakey-config.directive.js 139(+139 -0)
diff --git a/ui/src/app/components/datakey-config.directive.js b/ui/src/app/components/datakey-config.directive.js
new file mode 100644
index 0000000..0ae91ae
--- /dev/null
+++ b/ui/src/app/components/datakey-config.directive.js
@@ -0,0 +1,139 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './datakey-config.scss';
+
+import thingsboardJsonForm from "./json-form.directive";
+import thingsboardTypes from '../common/types.constant';
+import thingsboardJsFunc from './js-func.directive';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import datakeyConfigTemplate from './datakey-config.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+export default angular.module('thingsboard.directives.datakeyConfig', [thingsboardTypes, thingsboardJsFunc, thingsboardJsonForm])
+ .directive('tbDatakeyConfig', DatakeyConfig)
+ .name;
+
+/*@ngInject*/
+function DatakeyConfig($compile, $templateCache, $q, types) {
+
+ var linker = function (scope, element, attrs, ngModelCtrl) {
+ var template = $templateCache.get(datakeyConfigTemplate);
+
+ if (scope.datakeySettingsSchema.schema) {
+ scope.dataKeySchema = scope.datakeySettingsSchema.schema;
+ scope.dataKeyForm = scope.datakeySettingsSchema.form || ['*'];
+ template = '<md-tabs md-border-bottom class="tb-datakey-config">\n' +
+ '<md-tab label="{{\'datakey.settings\' | translate}}">\n' +
+ template +
+ '</md-tab>\n' +
+ '<md-tab label="{{\'datakey.advanced\' | translate}}">\n' +
+ '<md-content class="md-padding" layout="column">\n' +
+ '<div style="overflow: auto;">\n' +
+ '<ng-form name="ngform" ' +
+ 'layout="column" ' +
+ 'layout-padding>' +
+ '<tb-json-form schema="dataKeySchema"' +
+ 'form="dataKeyForm"' +
+ 'model="model.settings"' +
+ 'form-control="ngform">' +
+ '</tb-json-form>' +
+ '</ng-form>\n' +
+ '</div>\n' +
+ '</md-content>\n' +
+ '</md-tab>\n' +
+ '</md-tabs>';
+ }
+
+ element.html(template);
+
+ scope.types = types;
+ scope.selectedKey = null;
+ scope.keySearchText = null;
+ scope.usePostProcessing = false;
+
+ scope.functions = {};
+
+ ngModelCtrl.$render = function () {
+ scope.model = {};
+ if (ngModelCtrl.$viewValue) {
+ scope.model.type = ngModelCtrl.$viewValue.type;
+ scope.model.name = ngModelCtrl.$viewValue.name;
+ scope.model.label = ngModelCtrl.$viewValue.label;
+ scope.model.color = ngModelCtrl.$viewValue.color;
+ scope.model.funcBody = ngModelCtrl.$viewValue.funcBody;
+ scope.model.postFuncBody = ngModelCtrl.$viewValue.postFuncBody;
+ scope.model.usePostProcessing = scope.model.postFuncBody ? true : false;
+ scope.model.settings = ngModelCtrl.$viewValue.settings;
+ }
+ };
+
+ scope.$watch('model', function (newVal, oldVal) {
+ if (newVal.usePostProcessing != oldVal.usePostProcessing) {
+ if (scope.model.usePostProcessing && !scope.model.postFuncBody) {
+ scope.model.postFuncBody = "return value;";
+ } else if (!scope.model.usePostProcessing && scope.model.postFuncBody) {
+ delete scope.model.postFuncBody;
+ }
+ }
+ if (ngModelCtrl.$viewValue) {
+ var value = ngModelCtrl.$viewValue;
+ value.type = scope.model.type;
+ value.name = scope.model.name;
+ value.label = scope.model.label;
+ value.color = scope.model.color;
+ value.funcBody = scope.model.funcBody;
+ if (!scope.model.postFuncBody) {
+ delete value.postFuncBody;
+ } else {
+ value.postFuncBody = scope.model.postFuncBody;
+ }
+ ngModelCtrl.$setViewValue(value);
+ }
+ }, true);
+
+ scope.keysSearch = function (searchText) {
+ if (scope.deviceAlias) {
+ var deferred = $q.defer();
+ scope.fetchDeviceKeys({deviceAliasId: scope.deviceAlias.id, query: searchText, type: scope.model.type})
+ .then(function (keys) {
+ keys.push(searchText);
+ deferred.resolve(keys);
+ }, function (e) {
+ deferred.reject(e);
+ });
+ return deferred.promise;
+ } else {
+ return $q.when([]);
+ }
+ };
+
+ $compile(element.contents())(scope);
+ }
+
+ return {
+ restrict: 'E',
+ require: '^ngModel',
+ scope: {
+ deviceAlias: '=',
+ fetchDeviceKeys: '&',
+ datakeySettingsSchema: '='
+ },
+ link: linker
+ };
+}
\ No newline at end of file
ui/src/app/components/datakey-config.scss 28(+28 -0)
diff --git a/ui/src/app/components/datakey-config.scss b/ui/src/app/components/datakey-config.scss
new file mode 100644
index 0000000..2fe24fe
--- /dev/null
+++ b/ui/src/app/components/datakey-config.scss
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+.tb-datakey-config {
+ min-width: 500px !important;
+ min-height: 500px !important;
+ md-content {
+ background-color: #fff;
+ }
+}
+
+tb-datakey-config {
+ md-content {
+ background-color: #fff;
+ }
+}
diff --git a/ui/src/app/components/datakey-config.tpl.html b/ui/src/app/components/datakey-config.tpl.html
new file mode 100644
index 0000000..eecaa5a
--- /dev/null
+++ b/ui/src/app/components/datakey-config.tpl.html
@@ -0,0 +1,71 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-content class="md-padding" layout="column">
+ <md-autocomplete ng-if="model.type === types.dataKeyType.timeseries || model.type === types.dataKeyType.attribute"
+ style="padding-bottom: 8px;"
+ ng-required="true"
+ md-no-cache="true"
+ id="key"
+ ng-model="model.name"
+ md-selected-item="model.name"
+ md-search-text="keySearchText"
+ md-items="item in keysSearch(keySearchText)"
+ md-item-text="item"
+ md-min-length="0"
+ placeholder="Key name"
+ md-floating-label="Key">
+ <span md-highlight-text="keySearchText" md-highlight-flags="^i">{{item}}</span>
+ </md-autocomplete>
+ <div layout="row" layout-align="start center">
+ <md-input-container flex class="md-block">
+ <label translate>datakey.label</label>
+ <input ng-required="true" name="label" ng-model="model.label">
+ </md-input-container>
+ <div flex md-color-picker
+ ng-required="true"
+ ng-model="model.color"
+ label="{{ 'datakey.color' | translate }}"
+ icon="format_color_fill"
+ default="#999"
+ md-color-clear-button="false"
+ open-on-input="true"
+ md-color-generic-palette="false"
+ md-color-history="false">
+ </div>
+ </div>
+ <section layout="column" ng-if="model.type === types.dataKeyType.function">
+ <span translate>datakey.data-generation-func</span>
+ <br/>
+ <tb-js-func ng-model="model.funcBody"
+ function-args="{{ ['time', 'prevValue'] }}"
+ validation-args="{{ [1, 1] }}"
+ result-type="any">
+ </tb-js-func>
+ </section>
+ <section layout="column" ng-if="model.type === types.dataKeyType.timeseries || model.type === types.dataKeyType.attribute">
+ <md-checkbox ng-model="model.usePostProcessing" aria-label="{{ 'datakey.use-data-post-processing-func' | translate }}">
+ {{ 'datakey.use-data-post-processing-func' | translate }}
+ </md-checkbox>
+ <tb-js-func ng-if="model.usePostProcessing"
+ ng-model="model.postFuncBody"
+ function-args="{{ ['time', 'value', 'prevValue'] }}"
+ validation-args="{{ [1, 1, 1] }}"
+ result-type="any">
+ </tb-js-func>
+ </section>
+</md-content>
\ No newline at end of file
diff --git a/ui/src/app/components/datakey-config-dialog.controller.js b/ui/src/app/components/datakey-config-dialog.controller.js
new file mode 100644
index 0000000..a041ed9
--- /dev/null
+++ b/ui/src/app/components/datakey-config-dialog.controller.js
@@ -0,0 +1,56 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import thingsboardDatakeyConfig from './datakey-config.directive';
+
+export default angular.module('thingsboard.dialogs.datakeyConfigDialog', [thingsboardDatakeyConfig])
+ .controller('DatakeyConfigDialogController', DatakeyConfigDialogController)
+ .name;
+
+/*@ngInject*/
+function DatakeyConfigDialogController($scope, $mdDialog, deviceService, dataKey, dataKeySettingsSchema, deviceAlias, deviceAliases) {
+
+ var vm = this;
+
+ vm.dataKey = dataKey;
+ vm.dataKeySettingsSchema = dataKeySettingsSchema;
+ vm.deviceAlias = deviceAlias;
+ vm.deviceAliases = deviceAliases;
+
+ vm.hide = function () {
+ $mdDialog.hide();
+ };
+
+ vm.cancel = function () {
+ $mdDialog.cancel();
+ };
+
+ vm.fetchDeviceKeys = function (deviceAliasId, query, type) {
+ var deviceId = vm.deviceAliases[deviceAliasId];
+ if (deviceId) {
+ return deviceService.getDeviceKeys(deviceId, query, type);
+ } else {
+ return [];
+ }
+ };
+
+ vm.save = function () {
+ $scope.$broadcast('form-submit');
+ if ($scope.theForm.$valid) {
+ $scope.theForm.$setPristine();
+ $mdDialog.hide(vm.dataKey);
+ }
+ };
+}
\ No newline at end of file
diff --git a/ui/src/app/components/datakey-config-dialog.tpl.html b/ui/src/app/components/datakey-config-dialog.tpl.html
new file mode 100644
index 0000000..89d04c5
--- /dev/null
+++ b/ui/src/app/components/datakey-config-dialog.tpl.html
@@ -0,0 +1,46 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'datakey.configuration' | translate }}" style="width: 600px;">
+ <form name="theForm" ng-submit="vm.save()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>datakey.configuration</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <tb-datakey-config ng-model="vm.dataKey"
+ fetch-device-keys="vm.fetchDeviceKeys(deviceAliasId, query, type)"
+ device-alias="vm.deviceAlias"
+ datakey-settings-schema="vm.dataKeySettingsSchema">
+ </tb-datakey-config>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit" class="md-raised md-primary">
+ {{ 'action.save' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
diff --git a/ui/src/app/components/datasource.directive.js b/ui/src/app/components/datasource.directive.js
new file mode 100644
index 0000000..0a6fd2c
--- /dev/null
+++ b/ui/src/app/components/datasource.directive.js
@@ -0,0 +1,89 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './datasource.scss';
+
+import thingsboardTypes from '../common/types.constant';
+import thingsboardDatasourceFunc from './datasource-func.directive'
+import thingsboardDatasourceDevice from './datasource-device.directive';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import datasourceTemplate from './datasource.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+export default angular.module('thingsboard.directives.datasource', [thingsboardTypes, thingsboardDatasourceFunc, thingsboardDatasourceDevice])
+ .directive('tbDatasource', Datasource)
+ .name;
+
+/*@ngInject*/
+function Datasource($compile, $templateCache, types) {
+
+ var linker = function (scope, element, attrs, ngModelCtrl) {
+
+ var template = $templateCache.get(datasourceTemplate);
+ element.html(template);
+
+ scope.types = types;
+
+ if (scope.functionsOnly) {
+ scope.datasourceTypes = [types.datasourceType.function];
+ } else{
+ scope.datasourceTypes = [types.datasourceType.device, types.datasourceType.function];
+ }
+
+ scope.updateView = function () {
+ if (!scope.model.dataKeys) {
+ scope.model.dataKeys = [];
+ }
+ ngModelCtrl.$setViewValue(scope.model);
+ }
+
+ scope.$watch('model.type', function (newType, prevType) {
+ if (newType != prevType) {
+ scope.model.dataKeys = [];
+ }
+ });
+
+ scope.$watch('model', function () {
+ scope.updateView();
+ }, true);
+
+ ngModelCtrl.$render = function () {
+ scope.model = {};
+ if (ngModelCtrl.$viewValue) {
+ scope.model = ngModelCtrl.$viewValue;
+ }
+ };
+
+ $compile(element.contents())(scope);
+ }
+
+ return {
+ restrict: "E",
+ require: "^ngModel",
+ scope: {
+ deviceAliases: '=',
+ widgetType: '=',
+ functionsOnly: '=',
+ datakeySettingsSchema: '=',
+ generateDataKey: '&',
+ fetchDeviceKeys: '&',
+ onCreateDeviceAlias: '&'
+ },
+ link: linker
+ };
+}
ui/src/app/components/datasource.scss 54(+54 -0)
diff --git a/ui/src/app/components/datasource.scss b/ui/src/app/components/datasource.scss
new file mode 100644
index 0000000..0b22134
--- /dev/null
+++ b/ui/src/app/components/datasource.scss
@@ -0,0 +1,54 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+.tb-datasource {
+ #device-autocomplete {
+ height: 30px;
+ margin-top: 18px;
+ md-autocomplete-wrap {
+ height: 30px;
+ }
+ input, input:not(.md-input) {
+ height: 30px;
+ }
+ }
+ #datasourceType {
+ }
+}
+
+@mixin tb-checkered-bg() {
+ background-color: #fff;
+ background-image: linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%, #ddd),
+ linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%, #ddd);
+ background-size: 8px 8px;
+ background-position: 0 0, 4px 4px;
+}
+
+.tb-color-preview {
+ content: '';
+ width: 24px;
+ height: 24px;
+ border: 2px solid #fff;
+ border-radius: 50%;
+ box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .14), 0 2px 2px 0 rgba(0, 0, 0, .098), 0 1px 5px 0 rgba(0, 0, 0, .084);
+ position: relative;
+ overflow: hidden;
+ @include tb-checkered-bg();
+
+ .tb-color-result {
+ width: 100%;
+ height: 100%;
+ }
+}
ui/src/app/components/datasource.tpl.html 46(+46 -0)
diff --git a/ui/src/app/components/datasource.tpl.html b/ui/src/app/components/datasource.tpl.html
new file mode 100644
index 0000000..e698cc3
--- /dev/null
+++ b/ui/src/app/components/datasource.tpl.html
@@ -0,0 +1,46 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<section flex layout='row' layout-align="start center" class="tb-datasource">
+ <md-input-container style="min-width: 110px;">
+ <md-select placeholder="{{ 'datasource.type' | translate }}" required id="datasourceType" ng-model="model.type">
+ <md-option ng-repeat="datasourceType in datasourceTypes" value="{{datasourceType}}">
+ {{ datasourceType | translate }}
+ </md-option>
+ </md-select>
+ </md-input-container>
+ <section flex layout='row' layout-align="start center" class="datasource" ng-switch on="model.type">
+ <tb-datasource-func flex style="padding-left: 8px;"
+ ng-switch-default
+ ng-model="model"
+ datakey-settings-schema="datakeySettingsSchema"
+ ng-required="model.type === types.datasourceType.function"
+ generate-data-key="generateDataKey({chip: chip, type: type})">
+ </tb-datasource-func>
+ <tb-datasource-device flex style="padding-left: 4px; padding-right: 4px;"
+ ng-model="model"
+ datakey-settings-schema="datakeySettingsSchema"
+ ng-switch-when="device"
+ ng-required="model.type === types.datasourceType.device"
+ widget-type="widgetType"
+ device-aliases="deviceAliases"
+ generate-data-key="generateDataKey({chip: chip, type: type})"
+ fetch-device-keys="fetchDeviceKeys({deviceAliasId: deviceAliasId, query: query, type: type})"
+ on-create-device-alias="onCreateDeviceAlias({event: event, alias: alias})">
+ </tb-datasource-device>
+ </section>
+</section>
ui/src/app/components/datasource-device.directive.js 248(+248 -0)
diff --git a/ui/src/app/components/datasource-device.directive.js b/ui/src/app/components/datasource-device.directive.js
new file mode 100644
index 0000000..889a886
--- /dev/null
+++ b/ui/src/app/components/datasource-device.directive.js
@@ -0,0 +1,248 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './datasource-device.scss';
+
+import 'md-color-picker';
+import tinycolor from 'tinycolor2';
+import $ from 'jquery';
+import thingsboardTypes from '../common/types.constant';
+import thingsboardDatakeyConfigDialog from './datakey-config-dialog.controller';
+import thingsboardTruncate from './truncate.filter';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import datasourceDeviceTemplate from './datasource-device.tpl.html';
+import datakeyConfigDialogTemplate from './datakey-config-dialog.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/* eslint-disable angular/angularelement */
+
+export default angular.module('thingsboard.directives.datasourceDevice', [thingsboardTruncate, thingsboardTypes, thingsboardDatakeyConfigDialog])
+ .directive('tbDatasourceDevice', DatasourceDevice)
+ .name;
+
+/*@ngInject*/
+function DatasourceDevice($compile, $templateCache, $q, $mdDialog, $window, $document, $mdColorPicker, $mdConstant, types) {
+
+ var linker = function (scope, element, attrs, ngModelCtrl) {
+ var template = $templateCache.get(datasourceDeviceTemplate);
+ element.html(template);
+
+ scope.ngModelCtrl = ngModelCtrl;
+ scope.types = types;
+
+ scope.selectedTimeseriesDataKey = null;
+ scope.timeseriesDataKeySearchText = null;
+
+ scope.selectedAttributeDataKey = null;
+ scope.attributeDataKeySearchText = null;
+
+ scope.updateValidity = function () {
+ if (ngModelCtrl.$viewValue) {
+ var value = ngModelCtrl.$viewValue;
+ var dataValid = angular.isDefined(value) && value != null;
+ ngModelCtrl.$setValidity('deviceData', dataValid);
+ if (dataValid) {
+ ngModelCtrl.$setValidity('deviceAlias',
+ angular.isDefined(value.deviceAliasId) &&
+ value.deviceAliasId != null);
+ ngModelCtrl.$setValidity('deviceKeys',
+ angular.isDefined(value.dataKeys) &&
+ value.dataKeys != null &&
+ value.dataKeys.length > 0);
+ }
+ }
+ };
+
+ scope.$watch('deviceAlias', function () {
+ if (ngModelCtrl.$viewValue) {
+ if (scope.deviceAlias) {
+ ngModelCtrl.$viewValue.deviceAliasId = scope.deviceAlias.id;
+ } else {
+ ngModelCtrl.$viewValue.deviceAliasId = null;
+ }
+ scope.updateValidity();
+ scope.selectedDeviceAliasChange();
+ }
+ });
+
+ scope.$watch('timeseriesDataKeys', function () {
+ if (ngModelCtrl.$viewValue) {
+ var dataKeys = [];
+ dataKeys = dataKeys.concat(scope.timeseriesDataKeys);
+ dataKeys = dataKeys.concat(scope.attributeDataKeys);
+ ngModelCtrl.$viewValue.dataKeys = dataKeys;
+ scope.updateValidity();
+ }
+ }, true);
+
+ scope.$watch('attributeDataKeys', function () {
+ if (ngModelCtrl.$viewValue) {
+ var dataKeys = [];
+ dataKeys = dataKeys.concat(scope.timeseriesDataKeys);
+ dataKeys = dataKeys.concat(scope.attributeDataKeys);
+ ngModelCtrl.$viewValue.dataKeys = dataKeys;
+ scope.updateValidity();
+ }
+ }, true);
+
+ ngModelCtrl.$render = function () {
+ if (ngModelCtrl.$viewValue) {
+ var deviceAliasId = ngModelCtrl.$viewValue.deviceAliasId;
+ if (scope.deviceAliases[deviceAliasId]) {
+ scope.deviceAlias = {id: deviceAliasId, alias: scope.deviceAliases[deviceAliasId].alias,
+ deviceId: scope.deviceAliases[deviceAliasId].deviceId};
+ } else {
+ scope.deviceAlias = null;
+ }
+ var timeseriesDataKeys = [];
+ var attributeDataKeys = [];
+ for (var d in ngModelCtrl.$viewValue.dataKeys) {
+ var dataKey = ngModelCtrl.$viewValue.dataKeys[d];
+ if (dataKey.type === types.dataKeyType.timeseries) {
+ timeseriesDataKeys.push(dataKey);
+ } else if (dataKey.type === types.dataKeyType.attribute) {
+ attributeDataKeys.push(dataKey);
+ }
+ }
+ scope.timeseriesDataKeys = timeseriesDataKeys;
+ scope.attributeDataKeys = attributeDataKeys;
+ }
+ };
+
+ scope.textIsNotEmpty = function(text) {
+ return (text && text != null && text.length > 0) ? true : false;
+ }
+
+ scope.selectedDeviceAliasChange = function () {
+ if (!scope.timeseriesDataKeySearchText || scope.timeseriesDataKeySearchText === '') {
+ scope.timeseriesDataKeySearchText = scope.timeseriesDataKeySearchText === '' ? null : '';
+ }
+ if (!scope.attributeDataKeySearchText || scope.attributeDataKeySearchText === '') {
+ scope.attributeDataKeySearchText = scope.attributeDataKeySearchText === '' ? null : '';
+ }
+ };
+
+ scope.transformTimeseriesDataKeyChip = function (chip) {
+ return scope.generateDataKey({chip: chip, type: types.dataKeyType.timeseries});
+ };
+
+ scope.transformAttributeDataKeyChip = function (chip) {
+ return scope.generateDataKey({chip: chip, type: types.dataKeyType.attribute});
+ };
+
+ scope.showColorPicker = function (event, dataKey) {
+ $mdColorPicker.show({
+ value: dataKey.color,
+ defaultValue: '#fff',
+ random: tinycolor.random(),
+ clickOutsideToClose: false,
+ hasBackdrop: false,
+ skipHide: true,
+ preserveScope: false,
+
+ mdColorAlphaChannel: true,
+ mdColorSpectrum: true,
+ mdColorSliders: true,
+ mdColorGenericPalette: false,
+ mdColorMaterialPalette: true,
+ mdColorHistory: false,
+ mdColorDefaultTab: 2,
+
+ $event: event
+
+ }).then(function (color) {
+ dataKey.color = color;
+ ngModelCtrl.$setDirty();
+ });
+ }
+
+ scope.editDataKey = function (event, dataKey, index) {
+
+ $mdDialog.show({
+ controller: 'DatakeyConfigDialogController',
+ controllerAs: 'vm',
+ templateUrl: datakeyConfigDialogTemplate,
+ locals: {
+ dataKey: angular.copy(dataKey),
+ dataKeySettingsSchema: scope.datakeySettingsSchema,
+ deviceAlias: scope.deviceAlias,
+ deviceAliases: scope.deviceAliases
+ },
+ parent: angular.element($document[0].body),
+ fullscreen: true,
+ targetEvent: event,
+ skipHide: true,
+ onComplete: function () {
+ var w = angular.element($window);
+ w.triggerHandler('resize');
+ }
+ }).then(function (dataKey) {
+ if (dataKey.type === types.dataKeyType.timeseries) {
+ scope.timeseriesDataKeys[index] = dataKey;
+ } else if (dataKey.type === types.dataKeyType.attribute) {
+ scope.attributeDataKeys[index] = dataKey;
+ }
+ ngModelCtrl.$setDirty();
+ }, function () {
+ });
+ };
+
+ scope.dataKeysSearch = function (searchText, type) {
+ if (scope.deviceAlias) {
+ var deferred = $q.defer();
+ scope.fetchDeviceKeys({deviceAliasId: scope.deviceAlias.id, query: searchText, type: type})
+ .then(function (dataKeys) {
+ deferred.resolve(dataKeys);
+ }, function (e) {
+ deferred.reject(e);
+ });
+ return deferred.promise;
+ } else {
+ return $q.when([]);
+ }
+ };
+
+ scope.createKey = function (event, chipsId) {
+ var chipsChild = $(chipsId, element)[0].firstElementChild;
+ var el = angular.element(chipsChild);
+ var chipBuffer = el.scope().$mdChipsCtrl.getChipBuffer();
+ event.preventDefault();
+ event.stopPropagation();
+ el.scope().$mdChipsCtrl.appendChip(chipBuffer.trim());
+ el.scope().$mdChipsCtrl.resetChipBuffer();
+ }
+
+ $compile(element.contents())(scope);
+ }
+
+ return {
+ restrict: "E",
+ require: "^ngModel",
+ scope: {
+ widgetType: '=',
+ deviceAliases: '=',
+ datakeySettingsSchema: '=',
+ generateDataKey: '&',
+ fetchDeviceKeys: '&',
+ onCreateDeviceAlias: '&'
+ },
+ link: linker
+ };
+}
+
+/* eslint-enable angular/angularelement */
ui/src/app/components/datasource-device.scss 29(+29 -0)
diff --git a/ui/src/app/components/datasource-device.scss b/ui/src/app/components/datasource-device.scss
new file mode 100644
index 0000000..5e01d1f
--- /dev/null
+++ b/ui/src/app/components/datasource-device.scss
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+.tb-device-alias-autocomplete, .tb-timeseries-datakey-autocomplete, .tb-attribute-datakey-autocomplete {
+ .tb-not-found {
+ display: block;
+ line-height: 1.5;
+ height: 48px;
+ .tb-no-entries {
+ line-height: 48px;
+ }
+ }
+ li {
+ height: auto !important;
+ white-space: normal !important;
+ }
+}
ui/src/app/components/datasource-device.tpl.html 127(+127 -0)
diff --git a/ui/src/app/components/datasource-device.tpl.html b/ui/src/app/components/datasource-device.tpl.html
new file mode 100644
index 0000000..c9f4b10
--- /dev/null
+++ b/ui/src/app/components/datasource-device.tpl.html
@@ -0,0 +1,127 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<section flex layout='row' layout-align="start center">
+ <tb-device-alias-select flex="40"
+ tb-required="true"
+ device-aliases="deviceAliases"
+ ng-model="deviceAlias"
+ on-create-device-alias="onCreateDeviceAlias({event: event, alias: alias})">
+ </tb-device-alias-select>
+ <section flex="120" layout='column'>
+ <section flex layout='row' layout-align="start center">
+ <md-chips flex style="padding-left: 4px;"
+ id="timeseries_datakey_chips"
+ ng-required="true"
+ ng-model="timeseriesDataKeys" md-autocomplete-snap
+ md-transform-chip="transformTimeseriesDataKeyChip($chip)"
+ md-require-match="false">
+ <md-autocomplete
+ md-no-cache="true"
+ id="timeseries_datakey"
+ md-selected-item="selectedTimeseriesDataKey"
+ md-search-text="timeseriesDataKeySearchText"
+ md-items="item in dataKeysSearch(timeseriesDataKeySearchText, types.dataKeyType.timeseries)"
+ md-item-text="item.name"
+ md-min-length="0"
+ placeholder="{{'datakey.timeseries' | translate }}"
+ md-menu-class="tb-timeseries-datakey-autocomplete">
+ <span md-highlight-text="timeseriesDataKeySearchText" md-highlight-flags="^i">{{item}}</span>
+ <md-not-found>
+ <div class="tb-not-found">
+ <div class="tb-no-entries" ng-if="!textIsNotEmpty(timeseriesDataKeySearchText)">
+ <span translate>device.no-keys-found</span>
+ </div>
+ <div ng-if="textIsNotEmpty(timeseriesDataKeySearchText)">
+ <span translate translate-values='{ key: "{{timeseriesDataKeySearchText | truncate:true:6:'...'}}" }'>device.no-key-matching</span>
+ <span>
+ <a translate ng-click="createKey($event, '#timeseries_datakey_chips')">device.create-new-key</a>
+ </span>
+ </div>
+ </div>
+ </md-not-found>
+ </md-autocomplete>
+ <md-chip-template>
+ <div layout="row" layout-align="start center">
+ <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
+ <div class="tb-color-result" ng-style="{background: $chip.color}"></div>
+ </div>
+ <div>
+ {{$chip.label}}:
+ <strong ng-if="!$chip.postFuncBody">{{$chip.name}}</strong>
+ <strong ng-if="$chip.postFuncBody">f({{$chip.name}})</strong>
+ </div>
+ <md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
+ <md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
+ </md-button>
+ </div>
+ </md-chip-template>
+ </md-chips>
+ <md-chips flex ng-if="widgetType === types.widgetType.latest.value" style="padding-left: 4px;"
+ id="attribute_datakey_chips"
+ ng-required="true"
+ ng-model="attributeDataKeys" md-autocomplete-snap
+ md-transform-chip="transformAttributeDataKeyChip($chip)"
+ md-require-match="false">
+ <md-autocomplete
+ md-no-cache="true"
+ id="attribute_datakey"
+ md-selected-item="selectedAttributeDataKey"
+ md-search-text="attributeDataKeySearchText"
+ md-items="item in dataKeysSearch(attributeDataKeySearchText, types.dataKeyType.attribute)"
+ md-item-text="item.name"
+ md-min-length="0"
+ placeholder="{{'datakey.attributes' | translate }}"
+ md-menu-class="tb-attribute-datakey-autocomplete">
+ <span md-highlight-text="attributeDataKeySearchText" md-highlight-flags="^i">{{item}}</span>
+ <md-not-found>
+ <div class="tb-not-found">
+ <div class="tb-no-entries" ng-if="!textIsNotEmpty(attributeDataKeySearchText)">
+ <span translate>device.no-keys-found</span>
+ </div>
+ <div ng-if="textIsNotEmpty(attributeDataKeySearchText)">
+ <span translate translate-values='{ key: "{{attributeDataKeySearchText | truncate:true:6:'...'}}" }'>device.no-key-matching</span>
+ <span>
+ <a translate ng-click="createKey($event, '#attribute_datakey_chips')">device.create-new-key</a>
+ </span>
+ </div>
+ </div>
+ </md-not-found>
+ </md-autocomplete>
+ <md-chip-template>
+ <div layout="row" layout-align="start center">
+ <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
+ <div class="tb-color-result" ng-style="{background: $chip.color}"></div>
+ </div>
+ <div>
+ {{$chip.label}}:
+ <strong ng-if="!$chip.postFuncBody">{{$chip.name}}</strong>
+ <strong ng-if="$chip.postFuncBody">f({{$chip.name}})</strong>
+ </div>
+ <md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
+ <md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
+ </md-button>
+ </div>
+ </md-chip-template>
+ </md-chips>
+ </section>
+ <div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert">
+ <div translate ng-message="deviceKeys" ng-if="widgetType === types.widgetType.timeseries.value" class="tb-error-message">datakey.timeseries-required</div>
+ <div translate ng-message="deviceKeys" ng-if="widgetType === types.widgetType.latest.value" class="tb-error-message">datakey.timeseries-or-attributes-required</div>
+ </div>
+ </section>
+</section>
ui/src/app/components/datasource-func.directive.js 182(+182 -0)
diff --git a/ui/src/app/components/datasource-func.directive.js b/ui/src/app/components/datasource-func.directive.js
new file mode 100644
index 0000000..37f48d3
--- /dev/null
+++ b/ui/src/app/components/datasource-func.directive.js
@@ -0,0 +1,182 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './datasource-func.scss';
+
+import 'md-color-picker';
+import tinycolor from 'tinycolor2';
+import $ from 'jquery';
+import thingsboardTypes from '../common/types.constant';
+import thingsboardUtils from '../common/utils.service';
+import thingsboardDatakeyConfigDialog from './datakey-config-dialog.controller';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import datasourceFuncTemplate from './datasource-func.tpl.html';
+import datakeyConfigDialogTemplate from './datakey-config-dialog.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/* eslint-disable angular/angularelement */
+
+export default angular.module('thingsboard.directives.datasourceFunc', [thingsboardTypes, thingsboardUtils, thingsboardDatakeyConfigDialog])
+ .directive('tbDatasourceFunc', DatasourceFunc)
+ .name;
+
+/*@ngInject*/
+function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document, $mdColorPicker, types, utils) {
+
+ var linker = function (scope, element, attrs, ngModelCtrl) {
+ var template = $templateCache.get(datasourceFuncTemplate);
+ element.html(template);
+
+ scope.ngModelCtrl = ngModelCtrl;
+ scope.functionTypes = utils.getPredefinedFunctionsList();
+
+ scope.selectedDataKey = null;
+ scope.dataKeySearchText = null;
+
+ scope.updateValidity = function () {
+ if (ngModelCtrl.$viewValue) {
+ var value = ngModelCtrl.$viewValue;
+ var dataValid = angular.isDefined(value) && value != null;
+ ngModelCtrl.$setValidity('deviceData', dataValid);
+ if (dataValid) {
+ ngModelCtrl.$setValidity('funcTypes',
+ angular.isDefined(value.dataKeys) &&
+ value.dataKeys != null &&
+ value.dataKeys.length > 0);
+ }
+ }
+ };
+
+ scope.$watch('funcDataKeys', function () {
+ if (ngModelCtrl.$viewValue) {
+ var dataKeys = [];
+ dataKeys = dataKeys.concat(scope.funcDataKeys);
+ ngModelCtrl.$viewValue.dataKeys = dataKeys;
+ scope.updateValidity();
+ }
+ }, true);
+
+ ngModelCtrl.$render = function () {
+ if (ngModelCtrl.$viewValue) {
+ var funcDataKeys = [];
+ if (ngModelCtrl.$viewValue.dataKeys) {
+ funcDataKeys = funcDataKeys.concat(ngModelCtrl.$viewValue.dataKeys);
+ }
+ scope.funcDataKeys = funcDataKeys;
+ }
+ };
+
+ scope.transformDataKeyChip = function (chip) {
+ return scope.generateDataKey({chip: chip, type: types.dataKeyType.function});
+ };
+
+ scope.showColorPicker = function (event, dataKey) {
+ $mdColorPicker.show({
+ value: dataKey.color,
+ defaultValue: '#fff',
+ random: tinycolor.random(),
+ clickOutsideToClose: false,
+ hasBackdrop: false,
+ skipHide: true,
+ preserveScope: false,
+
+ mdColorAlphaChannel: true,
+ mdColorSpectrum: true,
+ mdColorSliders: true,
+ mdColorGenericPalette: false,
+ mdColorMaterialPalette: true,
+ mdColorHistory: false,
+ mdColorDefaultTab: 2,
+
+ $event: event
+
+ }).then(function (color) {
+ dataKey.color = color;
+ ngModelCtrl.$setDirty();
+ });
+ }
+
+ scope.editDataKey = function (event, dataKey, index) {
+
+ $mdDialog.show({
+ controller: 'DatakeyConfigDialogController',
+ controllerAs: 'vm',
+ templateUrl: datakeyConfigDialogTemplate,
+ locals: {
+ dataKey: angular.copy(dataKey),
+ dataKeySettingsSchema: scope.datakeySettingsSchema,
+ deviceAlias: null,
+ deviceAliases: null
+ },
+ parent: angular.element($document[0].body),
+ fullscreen: true,
+ targetEvent: event,
+ skipHide: true,
+ onComplete: function () {
+ var w = angular.element($window);
+ w.triggerHandler('resize');
+ }
+ }).then(function (dataKey) {
+ scope.funcDataKeys[index] = dataKey;
+ ngModelCtrl.$setDirty();
+ }, function () {
+ });
+ };
+
+ scope.textIsNotEmpty = function(text) {
+ return (text && text != null && text.length > 0) ? true : false;
+ }
+
+ scope.dataKeysSearch = function (dataKeySearchText) {
+ var dataKeys = dataKeySearchText ? scope.functionTypes.filter(
+ scope.createFilterForDataKey(dataKeySearchText)) : scope.functionTypes;
+ return dataKeys;
+ };
+
+ scope.createFilterForDataKey = function (query) {
+ var lowercaseQuery = angular.lowercase(query);
+ return function filterFn(dataKey) {
+ return (angular.lowercase(dataKey).indexOf(lowercaseQuery) === 0);
+ };
+ };
+
+ scope.createKey = function (event, chipsId) {
+ var chipsChild = $(chipsId, element)[0].firstElementChild;
+ var el = angular.element(chipsChild);
+ var chipBuffer = el.scope().$mdChipsCtrl.getChipBuffer();
+ event.preventDefault();
+ event.stopPropagation();
+ el.scope().$mdChipsCtrl.appendChip(chipBuffer.trim());
+ el.scope().$mdChipsCtrl.resetChipBuffer();
+ }
+
+ $compile(element.contents())(scope);
+ }
+
+ return {
+ restrict: "E",
+ require: "^ngModel",
+ scope: {
+ generateDataKey: '&',
+ datakeySettingsSchema: '='
+ },
+ link: linker
+ };
+}
+
+/* eslint-enable angular/angularelement */
ui/src/app/components/datasource-func.scss 29(+29 -0)
diff --git a/ui/src/app/components/datasource-func.scss b/ui/src/app/components/datasource-func.scss
new file mode 100644
index 0000000..08dbd4e
--- /dev/null
+++ b/ui/src/app/components/datasource-func.scss
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+.tb-func-datakey-autocomplete {
+ .tb-not-found {
+ display: block;
+ line-height: 1.5;
+ height: 48px;
+ .tb-no-entries {
+ line-height: 48px;
+ }
+ }
+ li {
+ height: auto !important;
+ white-space: normal !important;
+ }
+}
diff --git a/ui/src/app/components/datasource-func.tpl.html b/ui/src/app/components/datasource-func.tpl.html
new file mode 100644
index 0000000..2b509f5
--- /dev/null
+++ b/ui/src/app/components/datasource-func.tpl.html
@@ -0,0 +1,68 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<section flex layout='column'>
+ <md-chips flex style="padding-left: 4px;"
+ id="function_datakey_chips"
+ ng-required="true"
+ ng-model="funcDataKeys" md-autocomplete-snap
+ md-transform-chip="transformDataKeyChip($chip)"
+ md-require-match="false">
+ <md-autocomplete
+ md-no-cache="false"
+ id="dataKey"
+ md-selected-item="selectedDataKey"
+ md-search-text="dataKeySearchText"
+ md-items="item in dataKeysSearch(dataKeySearchText)"
+ md-item-text="item.name"
+ md-min-length="0"
+ placeholder="{{ 'datakey.function-types' | translate }}"
+ md-menu-class="tb-func-datakey-autocomplete">
+ <span md-highlight-text="dataKeySearchText" md-highlight-flags="^i">{{item}}</span>
+ <md-not-found>
+ <div class="tb-not-found">
+ <div class="tb-no-entries" ng-if="!textIsNotEmpty(dataKeySearchText)">
+ <span translate>device.no-keys-found</span>
+ </div>
+ <div ng-if="textIsNotEmpty(dataKeySearchText)">
+ <span translate translate-values='{ key: "{{dataKeySearchText | truncate:true:6:'...'}}" }'>device.no-key-matching</span>
+ <span>
+ <a translate ng-click="createKey($event, '#function_datakey_chips')">device.create-new-key</a>
+ </span>
+ </div>
+ </div>
+ </md-not-found>
+ </md-autocomplete>
+ <md-chip-template>
+ <div layout="row" layout-align="start center">
+ <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
+ <div class="tb-color-result" ng-style="{background: $chip.color}"></div>
+ </div>
+ <div>
+ {{$chip.label}}:
+ <strong>{{$chip.name}}</strong>
+ </div>
+ <md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
+ <md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
+ </md-button>
+ </div>
+ </md-chip-template>
+ </md-chips>
+ <div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert">
+ <div translate ng-message="funcTypes" class="tb-error-message">datakey.function-types-required</div>
+ </div>
+</section>
ui/src/app/components/datetime-period.directive.js 108(+108 -0)
diff --git a/ui/src/app/components/datetime-period.directive.js b/ui/src/app/components/datetime-period.directive.js
new file mode 100644
index 0000000..00c6cf0
--- /dev/null
+++ b/ui/src/app/components/datetime-period.directive.js
@@ -0,0 +1,108 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './datetime-period.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import datetimePeriodTemplate from './datetime-period.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+export default angular.module('thingsboard.directives.datetimePeriod', [])
+ .directive('tbDatetimePeriod', DatetimePeriod)
+ .name;
+
+/*@ngInject*/
+function DatetimePeriod($compile, $templateCache) {
+
+ var linker = function (scope, element, attrs, ngModelCtrl) {
+
+ var template = $templateCache.get(datetimePeriodTemplate);
+ element.html(template);
+
+ ngModelCtrl.$render = function () {
+ var date = new Date();
+ scope.startDate = new Date(
+ date.getFullYear(),
+ date.getMonth(),
+ date.getDate() - 1);
+ scope.endDate = date;
+ if (ngModelCtrl.$viewValue) {
+ var value = ngModelCtrl.$viewValue;
+ scope.startDate = new Date(value.startTimeMs);
+ scope.endDate = new Date(value.endTimeMs);
+ }
+ }
+
+ scope.updateMinMaxDates = function () {
+ scope.maxStartDate = angular.copy(new Date(scope.endDate.getTime() - 1000));
+ scope.minEndDate = angular.copy(new Date(scope.startDate.getTime() + 1000));
+ scope.maxEndDate = new Date();
+ }
+
+ scope.updateView = function () {
+ var value = null;
+ if (scope.startDate && scope.endDate) {
+ value = {
+ startTimeMs: scope.startDate.getTime(),
+ endTimeMs: scope.endDate.getTime()
+ };
+ ngModelCtrl.$setValidity('datetimePeriod', true);
+ } else {
+ ngModelCtrl.$setValidity('datetimePeriod', !scope.required);
+ }
+ ngModelCtrl.$setViewValue(value);
+ }
+
+ scope.$watch('required', function () {
+ scope.updateView();
+ });
+
+ scope.$watch('startDate', function (newDate) {
+ if (newDate) {
+ if (newDate.getTime() > scope.maxStartDate) {
+ scope.startDate = angular.copy(scope.maxStartDate);
+ }
+ scope.updateMinMaxDates();
+ }
+ scope.updateView();
+ });
+
+ scope.$watch('endDate', function (newDate) {
+ if (newDate) {
+ if (newDate.getTime() < scope.minEndDate) {
+ scope.endDate = angular.copy(scope.minEndDate);
+ } else if (newDate.getTime() > scope.maxEndDate) {
+ scope.endDate = angular.copy(scope.maxEndDate);
+ }
+ scope.updateMinMaxDates();
+ }
+ scope.updateView();
+ });
+
+ $compile(element.contents())(scope);
+
+ }
+
+ return {
+ restrict: "E",
+ require: "^ngModel",
+ scope: {
+ required: '=ngRequired'
+ },
+ link: linker
+ };
+}
ui/src/app/components/datetime-period.scss 28(+28 -0)
diff --git a/ui/src/app/components/datetime-period.scss b/ui/src/app/components/datetime-period.scss
new file mode 100644
index 0000000..e4b3b45
--- /dev/null
+++ b/ui/src/app/components/datetime-period.scss
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+tb-datetime-period {
+ md-input-container {
+ margin-bottom: 0px;
+ .md-errors-spacer {
+ min-height: 0px;
+ }
+ }
+ mdp-date-picker {
+ .md-input {
+ width: 150px !important;
+ }
+ }
+}
diff --git a/ui/src/app/components/datetime-period.tpl.html b/ui/src/app/components/datetime-period.tpl.html
new file mode 100644
index 0000000..39938d4
--- /dev/null
+++ b/ui/src/app/components/datetime-period.tpl.html
@@ -0,0 +1,31 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<section layout="column" layout-align="start start">
+ <section layout="row" layout-align="start start">
+ <mdp-date-picker ng-model="startDate" mdp-placeholder="{{ 'datetime.date-from' | translate }}"
+ mdp-max-date="maxStartDate"></mdp-date-picker>
+ <mdp-time-picker ng-model="startDate" mdp-placeholder="{{ 'datetime.time-from' | translate }}"
+ mdp-max-date="maxStartDate" mdp-auto-switch="true"></mdp-time-picker>
+ </section>
+ <section layout="row" layout-align="start start">
+ <mdp-date-picker ng-model="endDate" mdp-placeholder="{{ 'datetime.date-to' | translate }}"
+ mdp-min-date="minEndDate" mdp-max-date="maxEndDate"></mdp-date-picker>
+ <mdp-time-picker ng-model="endDate" mdp-placeholder="{{ 'datetime.time-to' | translate }}"
+ mdp-min-date="minEndDate" mdp-max-date="maxEndDate" mdp-auto-switch="true"></mdp-time-picker>
+ </section>
+</section>
\ No newline at end of file
diff --git a/ui/src/app/components/details-sidenav.directive.js b/ui/src/app/components/details-sidenav.directive.js
new file mode 100644
index 0000000..71beab7
--- /dev/null
+++ b/ui/src/app/components/details-sidenav.directive.js
@@ -0,0 +1,94 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './details-sidenav.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import detailsSidenavTemplate from './details-sidenav.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+export default angular.module('thingsboard.directives.detailsSidenav', [])
+ .directive('tbDetailsSidenav', DetailsSidenav)
+ .name;
+
+/*@ngInject*/
+function DetailsSidenav($timeout) {
+
+ var linker = function (scope, element, attrs) {
+
+ if (angular.isUndefined(attrs.isReadOnly)) {
+ attrs.isReadOnly = false;
+ }
+
+ if (angular.isUndefined(scope.headerHeightPx)) {
+ scope.headerHeightPx = 100;
+ }
+
+ if (angular.isDefined(attrs.isAlwaysEdit) && attrs.isAlwaysEdit) {
+ scope.isEdit = true;
+ }
+
+ scope.toggleDetailsEditMode = function () {
+ if (!scope.isAlwaysEdit) {
+ if (!scope.isEdit) {
+ scope.isEdit = true;
+ } else {
+ scope.isEdit = false;
+ }
+ }
+ $timeout(function () {
+ scope.onToggleDetailsEditMode();
+ });
+ };
+
+ scope.detailsApply = function () {
+ $timeout(function () {
+ scope.onApplyDetails();
+ });
+ }
+
+ scope.closeDetails = function () {
+ scope.isOpen = false;
+ $timeout(function () {
+ scope.onCloseDetails();
+ });
+ };
+ }
+
+ return {
+ restrict: "E",
+ transclude: {
+ headerPane: '?headerPane',
+ detailsButtons: '?detailsButtons'
+ },
+ scope: {
+ headerTitle: '=',
+ headerSubtitle: '@',
+ headerHeightPx: '@',
+ isReadOnly: '=',
+ isOpen: '=',
+ isEdit: '=?',
+ isAlwaysEdit: '=?',
+ theForm: '=',
+ onCloseDetails: '&',
+ onToggleDetailsEditMode: '&',
+ onApplyDetails: '&'
+ },
+ link: linker,
+ templateUrl: detailsSidenavTemplate
+ };
+}
\ No newline at end of file
ui/src/app/components/details-sidenav.scss 49(+49 -0)
diff --git a/ui/src/app/components/details-sidenav.scss b/ui/src/app/components/details-sidenav.scss
new file mode 100644
index 0000000..2dc2b51
--- /dev/null
+++ b/ui/src/app/components/details-sidenav.scss
@@ -0,0 +1,49 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import '../../scss/constants';
+
+.tb-details-title {
+ font-size: 1.600rem;
+ font-weight: 400;
+ text-transform: uppercase;
+ margin: 20px 8px 0 0;
+}
+
+.tb-details-subtitle {
+ font-size: 1.000rem;
+ margin: 10px 0;
+ opacity: 0.8;
+}
+
+md-sidenav.tb-sidenav-details {
+ width: 100% !important;
+ max-width: 100% !important;
+ z-index: 59 !important;
+ @media (min-width: $layout-breakpoint-gt-sm) {
+ width: 80% !important;
+ }
+ @media (min-width: $layout-breakpoint-gt-md) {
+ width: 65% !important;
+ }
+ @media (min-width: $layout-breakpoint-lg) {
+ width: 45% !important;
+ }
+ tb-dashboard {
+ md-content {
+ background-color: $primary-hue-3;
+ }
+ }
+}
diff --git a/ui/src/app/components/details-sidenav.tpl.html b/ui/src/app/components/details-sidenav.tpl.html
new file mode 100644
index 0000000..a5e84e0
--- /dev/null
+++ b/ui/src/app/components/details-sidenav.tpl.html
@@ -0,0 +1,62 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-sidenav class="md-sidenav-right md-whiteframe-4dp tb-sidenav-details"
+ md-disable-backdrop="true"
+ md-is-open="isOpen"
+ md-component-id="right"
+ layout="column">
+ <header>
+ <md-toolbar class="md-theme-light" ng-style="{'height':headerHeightPx+'px'}">
+ <div class="md-toolbar-tools">
+ <div class="md-toolbar-tools" layout="column" layout-align="start start">
+ <span class="tb-details-title">{{headerTitle}}</span>
+ <span class="tb-details-subtitle">{{headerSubtitle}}</span>
+ <span style="width: 100%;" ng-transclude="headerPane"></span>
+ </div>
+ <span flex></span>
+ <div ng-transclude="detailsButtons"></div>
+ <md-button class="md-icon-button" ng-click="closeDetails()">
+ <md-icon aria-label="close" class="material-icons">close</md-icon>
+ </md-button>
+ </div>
+ <section ng-if="!isReadOnly" layout="row" layout-wrap
+ class="tb-header-buttons md-fab">
+ <md-button ng-show="isEdit" ng-disabled="loading || theForm.$invalid || !theForm.$dirty"
+ class="tb-btn-header md-accent md-hue-2 md-fab md-fab-bottom-right"
+ aria-label="{{ 'action.apply' | translate }}"
+ ng-click="detailsApply()">
+ <md-tooltip md-direction="top">
+ {{ 'action.apply-changes' | translate }}
+ </md-tooltip>
+ <ng-md-icon icon="done"></ng-md-icon>
+ </md-button>
+ <md-button ng-disabled="loading || (isAlwaysEdit && !theForm.$dirty)" class="tb-btn-header md-accent md-hue-2 md-fab md-fab-bottom-right"
+ aria-label="{{ 'details.edit-mode' | translate }}"
+ ng-click="toggleDetailsEditMode()">
+ <md-tooltip md-direction="top">
+ {{ (isAlwaysEdit ? 'action.decline-changes' : 'details.toggle-edit-mode') | translate }}
+ </md-tooltip>
+ <ng-md-icon icon="{{isEdit ? 'close' : 'edit'}}" options='{"easing": "circ-in-out", "duration": 375, "rotation": "none"}'></ng-md-icon>
+ </md-button>
+ </section>
+ </md-toolbar>
+ </header>
+ <md-content flex>
+ <div ng-transclude></div>
+ </md-content>
+</md-sidenav>
\ No newline at end of file
diff --git a/ui/src/app/components/device-alias-select.directive.js b/ui/src/app/components/device-alias-select.directive.js
new file mode 100644
index 0000000..52a31cc
--- /dev/null
+++ b/ui/src/app/components/device-alias-select.directive.js
@@ -0,0 +1,145 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import $ from 'jquery';
+
+import './device-alias-select.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import deviceAliasSelectTemplate from './device-alias-select.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+
+/* eslint-disable angular/angularelement */
+
+export default angular.module('thingsboard.directives.deviceAliasSelect', [])
+ .directive('tbDeviceAliasSelect', DeviceAliasSelect)
+ .name;
+
+/*@ngInject*/
+function DeviceAliasSelect($compile, $templateCache, $mdConstant) {
+
+ var linker = function (scope, element, attrs, ngModelCtrl) {
+ var template = $templateCache.get(deviceAliasSelectTemplate);
+ element.html(template);
+
+ scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false;
+
+ scope.ngModelCtrl = ngModelCtrl;
+ scope.deviceAliasList = [];
+ scope.deviceAlias = null;
+
+ scope.updateValidity = function () {
+ var value = ngModelCtrl.$viewValue;
+ var valid = angular.isDefined(value) && value != null || !scope.tbRequired;
+ ngModelCtrl.$setValidity('deviceAlias', valid);
+ };
+
+ scope.$watch('deviceAliases', function () {
+ scope.deviceAliasList = [];
+ for (var aliasId in scope.deviceAliases) {
+ var deviceAlias = {id: aliasId, alias: scope.deviceAliases[aliasId].alias, deviceId: scope.deviceAliases[aliasId].deviceId};
+ scope.deviceAliasList.push(deviceAlias);
+ }
+ }, true);
+
+ scope.$watch('deviceAlias', function () {
+ scope.updateView();
+ });
+
+ scope.deviceAliasSearch = function (deviceAliasSearchText) {
+ return deviceAliasSearchText ? scope.deviceAliasList.filter(
+ scope.createFilterForDeviceAlias(deviceAliasSearchText)) : scope.deviceAliasList;
+ };
+
+ scope.createFilterForDeviceAlias = function (query) {
+ var lowercaseQuery = angular.lowercase(query);
+ return function filterFn(deviceAlias) {
+ return (angular.lowercase(deviceAlias.alias).indexOf(lowercaseQuery) === 0);
+ };
+ };
+
+ scope.updateView = function () {
+ ngModelCtrl.$setViewValue(scope.deviceAlias);
+ scope.updateValidity();
+ }
+
+ ngModelCtrl.$render = function () {
+ if (ngModelCtrl.$viewValue) {
+ scope.deviceAlias = ngModelCtrl.$viewValue;
+ }
+ }
+
+ scope.textIsNotEmpty = function(text) {
+ return (text && text != null && text.length > 0) ? true : false;
+ }
+
+ scope.deviceAliasEnter = function($event) {
+ if ($event.keyCode === $mdConstant.KEY_CODE.ENTER) {
+ $event.preventDefault();
+ if (!scope.deviceAlias) {
+ var found = scope.deviceAliasSearch(scope.deviceAliasSearchText);
+ found = found.length > 0;
+ if (!found) {
+ scope.createDeviceAlias($event, scope.deviceAliasSearchText);
+ }
+ }
+ }
+ }
+
+ scope.createDeviceAlias = function (event, alias) {
+ var autoChild = $('#device-autocomplete', element)[0].firstElementChild;
+ var el = angular.element(autoChild);
+ el.scope().$mdAutocompleteCtrl.hidden = true;
+ el.scope().$mdAutocompleteCtrl.hasNotFound = false;
+ event.preventDefault();
+ var promise = scope.onCreateDeviceAlias({event: event, alias: alias});
+ if (promise) {
+ promise.then(
+ function success(newAlias) {
+ el.scope().$mdAutocompleteCtrl.hasNotFound = true;
+ if (newAlias) {
+ scope.deviceAliasList.push(newAlias);
+ scope.deviceAlias = newAlias;
+ }
+ },
+ function fail() {
+ el.scope().$mdAutocompleteCtrl.hasNotFound = true;
+ }
+ );
+ } else {
+ el.scope().$mdAutocompleteCtrl.hasNotFound = true;
+ }
+ };
+
+ $compile(element.contents())(scope);
+ }
+
+ return {
+ restrict: "E",
+ require: "^ngModel",
+ link: linker,
+ scope: {
+ tbRequired: '=?',
+ deviceAliases: '=',
+ onCreateDeviceAlias: '&'
+ }
+ };
+}
+
+/* eslint-enable angular/angularelement */
diff --git a/ui/src/app/components/device-alias-select.scss b/ui/src/app/components/device-alias-select.scss
new file mode 100644
index 0000000..2ce533c
--- /dev/null
+++ b/ui/src/app/components/device-alias-select.scss
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.tb-device-alias-autocomplete {
+ .tb-not-found {
+ display: block;
+ line-height: 1.5;
+ height: 48px;
+ .tb-no-entries {
+ line-height: 48px;
+ }
+ }
+ li {
+ height: auto !important;
+ white-space: normal !important;
+ }
+}
diff --git a/ui/src/app/components/device-alias-select.tpl.html b/ui/src/app/components/device-alias-select.tpl.html
new file mode 100644
index 0000000..2fdd5d5
--- /dev/null
+++ b/ui/src/app/components/device-alias-select.tpl.html
@@ -0,0 +1,52 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<section layout='column'>
+ <md-autocomplete id="device-autocomplete"
+ md-input-name="device_alias"
+ ng-required="tbRequired"
+ ng-model="deviceAlias"
+ md-selected-item="deviceAlias"
+ md-search-text="deviceAliasSearchText"
+ md-items="item in deviceAliasSearch(deviceAliasSearchText)"
+ md-item-text="item.alias"
+ tb-keydown="deviceAliasEnter($event)"
+ tb-keypress="deviceAliasEnter($event)"
+ md-min-length="0"
+ placeholder="{{ 'device.device-alias' | translate }}"
+ md-menu-class="tb-device-alias-autocomplete">
+ <md-item-template>
+ <span md-highlight-text="deviceAliasSearchText" md-highlight-flags="^i">{{item.alias}}</span>
+ </md-item-template>
+ <md-not-found>
+ <div class="tb-not-found">
+ <div class="tb-no-entries" ng-if="!textIsNotEmpty(deviceAliasSearchText)">
+ <span translate>device.no-aliases-found</span>
+ </div>
+ <div ng-if="textIsNotEmpty(deviceAliasSearchText)">
+ <span translate translate-values='{ alias: "{{deviceAliasSearchText | truncate:true:6:'...'}}" }'>device.no-alias-matching</span>
+ <span>
+ <a translate ng-click="createDeviceAlias($event, deviceAliasSearchText)">device.create-new-alias</a>
+ </span>
+ </div>
+ </div>
+ </md-not-found>
+ </md-autocomplete>
+ <div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert">
+ <div translate ng-message="deviceAlias" class="tb-error-message">device.alias-required</div>
+ </div>
+</section>
ui/src/app/components/expand-fullscreen.directive.js 140(+140 -0)
diff --git a/ui/src/app/components/expand-fullscreen.directive.js b/ui/src/app/components/expand-fullscreen.directive.js
new file mode 100644
index 0000000..f5a078c
--- /dev/null
+++ b/ui/src/app/components/expand-fullscreen.directive.js
@@ -0,0 +1,140 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './expand-fullscreen.scss';
+
+import $ from 'jquery';
+
+export default angular.module('thingsboard.directives.expandFullscreen', [])
+ .directive('tbExpandFullscreen', ExpandFullscreen)
+ .name;
+
+/* eslint-disable angular/angularelement */
+
+/*@ngInject*/
+function ExpandFullscreen($compile, $document) {
+
+ var uniqueId = 1;
+ var linker = function (scope, element, attrs) {
+
+ scope.body = angular.element($document.find('body').eq(0));
+ scope.fullscreenParentId = 'fullscreen-parent' + uniqueId;
+ scope.fullscreenParent = $('#' + scope.fullscreenParentId, scope.body)[0];
+ if (!scope.fullscreenParent) {
+ uniqueId++;
+ var fullscreenParent = angular.element('<div id=\'' + scope.fullscreenParentId + '\' class=\'tb-fullscreen-parent\'></div>');
+ scope.body.append(fullscreenParent);
+ scope.fullscreenParent = $('#' + scope.fullscreenParentId, scope.body)[0];
+ scope.fullscreenParent = angular.element(scope.fullscreenParent);
+ scope.fullscreenParent.css('display', 'none');
+ } else {
+ scope.fullscreenParent = angular.element(scope.fullscreenParent);
+ }
+
+ scope.$on('$destroy', function () {
+ scope.fullscreenParent.remove();
+ });
+
+ scope.elementParent = null;
+ scope.expanded = false;
+ scope.fullscreenZindex = scope.fullscreenZindex();
+ if (!scope.fullscreenZindex) {
+ scope.fullscreenZindex = '70';
+ }
+
+ scope.$watch('expanded', function (newExpanded, prevExpanded) {
+ if (newExpanded != prevExpanded) {
+ if (scope.expanded) {
+ scope.elementParent = element.parent();
+ element.detach();
+ scope.fullscreenParent.append(element);
+ scope.fullscreenParent.css('display', '');
+ scope.fullscreenParent.css('z-index', scope.fullscreenZindex);
+ element.addClass('tb-fullscreen');
+ } else {
+ if (scope.elementParent) {
+ element.detach();
+ scope.elementParent.append(element);
+ scope.elementParent = null;
+ }
+ element.removeClass('tb-fullscreen');
+ scope.fullscreenParent.css('display', 'none');
+ scope.fullscreenParent.css('z-index', '');
+ }
+ if (scope.onFullscreenChanged) {
+ scope.onFullscreenChanged({expanded: scope.expanded});
+ }
+ }
+ });
+
+ scope.$watch(function () {
+ return scope.expand();
+ }, function (newExpanded) {
+ scope.expanded = newExpanded;
+ });
+
+ scope.toggleExpand = function ($event) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ scope.expanded = !scope.expanded;
+ }
+
+ var expandButton = null;
+ if (attrs.expandButtonId) {
+ expandButton = $('#' + attrs.expandButtonId, element)[0];
+ }
+
+ var html = '<md-tooltip md-direction="{{expanded ? \'bottom\' : \'top\'}}">' +
+ '{{(expanded ? \'fullscreen.exit\' : \'fullscreen.expand\') | translate}}' +
+ '</md-tooltip>' +
+ '<ng-md-icon icon="{{expanded ? \'fullscreen_exit\' : \'fullscreen\'}}" ' +
+ 'options=\'{"easing": "circ-in-out", "duration": 375, "rotation": "none"}\'>' +
+ '</ng-md-icon>';
+
+ if (expandButton) {
+ expandButton = angular.element(expandButton);
+ expandButton.attr('md-ink-ripple', 'false');
+ expandButton.append(html);
+
+ $compile(expandButton.contents())(scope);
+
+ expandButton.on("click", scope.toggleExpand);
+
+ } else if (!scope.hideExpandButton()) {
+ var button = angular.element('<md-button class="tb-fullscreen-button-style tb-fullscreen-button-pos md-icon-button" ' +
+ 'md-ink-ripple="false" ng-click="toggleExpand($event)">' +
+ html +
+ '</md-button>');
+
+ $compile(button)(scope);
+
+ element.prepend(button);
+ }
+ }
+
+ return {
+ restrict: "A",
+ link: linker,
+ scope: {
+ expand: "&tbExpandFullscreen",
+ hideExpandButton: "&hideExpandButton",
+ onFullscreenChanged: "&onFullscreenChanged",
+ fullscreenZindex: "&fullscreenZindex"
+ }
+ };
+}
+
+/* eslint-enable angular/angularelement */
ui/src/app/components/expand-fullscreen.scss 53(+53 -0)
diff --git a/ui/src/app/components/expand-fullscreen.scss b/ui/src/app/components/expand-fullscreen.scss
new file mode 100644
index 0000000..1ff154f
--- /dev/null
+++ b/ui/src/app/components/expand-fullscreen.scss
@@ -0,0 +1,53 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import "../../scss/constants";
+
+.tb-fullscreen {
+ section.header-buttons {
+ top: 25px;
+ }
+}
+
+.tb-fullscreen {
+ width: 100% !important;
+ height: 100% !important;
+ position: fixed !important;
+ top: 0;
+ left: 0;
+}
+
+.tb-fullscreen-parent {
+ background-color: $gray;
+ width: 100%;
+ height: 100%;
+ position: fixed;
+ top: 0;
+ left: 0;
+}
+
+.md-button.tb-fullscreen-button-style, .tb-fullscreen-button-style {
+ background: #ccc;
+ opacity: 0.85;
+ ng-md-icon {
+ color: #666;
+ }
+}
+
+.md-button.tb-fullscreen-button-pos, .tb-fullscreen-button-pos {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+}
ui/src/app/components/grid.directive.js 607(+607 -0)
diff --git a/ui/src/app/components/grid.directive.js b/ui/src/app/components/grid.directive.js
new file mode 100644
index 0000000..ba8cdc1
--- /dev/null
+++ b/ui/src/app/components/grid.directive.js
@@ -0,0 +1,607 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './grid.scss';
+
+import thingsboardScopeElement from './scope-element.directive';
+import thingsboardDetailsSidenav from './details-sidenav.directive';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import gridTemplate from './grid.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+export default angular.module('thingsboard.directives.grid', [thingsboardScopeElement, thingsboardDetailsSidenav])
+ .directive('tbGrid', Grid)
+ .directive('tbGridCardContent', GridCardContent)
+ .filter('range', RangeFilter)
+ .name;
+
+/*@ngInject*/
+function RangeFilter() {
+ return function(input, total) {
+ total = parseInt(total);
+
+ for (var i=0; i<total; i++) {
+ input.push(i);
+ }
+
+ return input;
+ };
+}
+
+/*@ngInject*/
+function GridCardContent($compile) {
+ var linker = function(scope, element) {
+ scope.$watch('itemTemplate',
+ function(value) {
+ element.html(value);
+ $compile(element.contents())(scope);
+ }
+ );
+ };
+
+ return {
+ restrict: "E",
+ link: linker,
+ scope: {
+ parentCtl: "=parentCtl",
+ gridCtl: "=gridCtl",
+ itemTemplate: "=itemTemplate",
+ item: "=item"
+ }
+ };
+}
+
+/*@ngInject*/
+function Grid() {
+ return {
+ restrict: "E",
+ scope: true,
+ transclude: {
+ detailsButtons: '?detailsButtons'
+ },
+ bindToController: {
+ gridConfiguration: '&?'
+ },
+ controller: GridController,
+ controllerAs: 'vm',
+ templateUrl: gridTemplate
+ }
+}
+
+/*@ngInject*/
+function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $translate, $mdMedia, $templateCache) {
+
+ var vm = this;
+
+ var columns = 1;
+ if ($mdMedia('md')) {
+ columns = 2;
+ } else if ($mdMedia('lg')) {
+ columns = 3;
+ } else if ($mdMedia('gt-lg')) {
+ columns = 4;
+ }
+
+ var pageSize = 10 * columns;
+
+ vm.columns = columns;
+
+ vm.addItem = addItem;
+ vm.deleteItem = deleteItem;
+ vm.deleteItems = deleteItems;
+ vm.hasData = hasData;
+ vm.isCurrentItem = isCurrentItem;
+ vm.moveToTop = moveToTop;
+ vm.noData = noData;
+ vm.onCloseDetails = onCloseDetails;
+ vm.onToggleDetailsEditMode = onToggleDetailsEditMode;
+ vm.openItem = openItem;
+ vm.operatingItem = operatingItem;
+ vm.refreshList = refreshList;
+ vm.saveItem = saveItem;
+ vm.toggleItemSelection = toggleItemSelection;
+
+ $scope.$watch(function () {
+ return $mdMedia('sm');
+ }, function (sm) {
+ if (sm) {
+ columnsUpdated(1);
+ }
+ });
+ $scope.$watch(function () {
+ return $mdMedia('md');
+ }, function (md) {
+ if (md) {
+ columnsUpdated(2);
+ }
+ });
+ $scope.$watch(function () {
+ return $mdMedia('lg');
+ }, function (lg) {
+ if (lg) {
+ columnsUpdated(3);
+ }
+ });
+ $scope.$watch(function () {
+ return $mdMedia('gt-lg');
+ }, function (gtLg) {
+ if (gtLg) {
+ columnsUpdated(4);
+ }
+ });
+
+ initGridConfiguration();
+
+ vm.itemRows = {
+ getItemAtIndex: function (index) {
+ if (index >= vm.items.rowData.length) {
+ vm.itemRows.fetchMoreItems_(index);
+ return null;
+ }
+ return vm.items.rowData[index];
+ },
+
+ getLength: function () {
+ if (vm.items.hasNext) {
+ return vm.items.rowData.length + pageSize;
+ } else {
+ return vm.items.rowData.length;
+ }
+ },
+
+ fetchMoreItems_: function () {
+ if (vm.items.hasNext && !vm.items.pending) {
+ var promise = vm.fetchItemsFunc(vm.items.nextPageLink);
+ if (promise) {
+ vm.items.pending = true;
+ promise.then(
+ function success(items) {
+ vm.items.data = vm.items.data.concat(items.data);
+ var startIndex = vm.items.data.length - items.data.length;
+ var endIndex = vm.items.data.length;
+ for (var i = startIndex; i < endIndex; i++) {
+ var item = vm.items.data[i];
+ item.index = i;
+ var row = Math.floor(i / vm.columns);
+ var itemRow = vm.items.rowData[row];
+ if (!itemRow) {
+ itemRow = [];
+ vm.items.rowData.push(itemRow);
+ }
+ itemRow.push(item);
+ }
+ vm.items.nextPageLink = items.nextPageLink;
+ vm.items.hasNext = items.hasNext;
+ if (vm.items.hasNext) {
+ vm.items.nextPageLink.limit = pageSize;
+ }
+ vm.items.pending = false;
+ },
+ function fail() {
+ vm.items.hasNext = false;
+ vm.items.pending = false;
+ });
+ } else {
+ vm.items.hasNext = false;
+ }
+ }
+ }
+ };
+
+ function columnsUpdated(newColumns) {
+ if (vm.columns !== newColumns) {
+ var newTopIndex = Math.ceil(vm.columns * vm.topIndex / newColumns);
+ pageSize = 10 * newColumns;
+ vm.items.rowData = [];
+ if (vm.items.nextPageLink) {
+ vm.items.nextPageLink.limit = pageSize;
+ }
+
+ for (var i = 0; i < vm.items.data.length; i++) {
+ var item = vm.items.data[i];
+ var row = Math.floor(i / newColumns);
+ var itemRow = vm.items.rowData[row];
+ if (!itemRow) {
+ itemRow = [];
+ vm.items.rowData.push(itemRow);
+ }
+ itemRow.push(item);
+ }
+
+ vm.columns = newColumns;
+ vm.topIndex = newTopIndex;
+ vm.itemRows.getItemAtIndex(newTopIndex+pageSize);
+ $timeout(function() {
+ moveToIndex(newTopIndex);
+ }, 500);
+ }
+ }
+
+ function initGridConfiguration() {
+ vm.gridConfiguration = vm.gridConfiguration || function () {
+ return {};
+ };
+
+ vm.config = vm.gridConfiguration();
+
+ vm.itemHeight = vm.config.itemHeight || 199;
+
+ vm.refreshParamsFunc = vm.config.refreshParamsFunc || function () {
+ return {"topIndex": vm.topIndex};
+ };
+
+ vm.deleteItemTitleFunc = vm.config.deleteItemTitleFunc || function () {
+ return $translate.instant('grid.delete-item-title');
+ };
+
+ vm.deleteItemContentFunc = vm.config.deleteItemContentFunc || function () {
+ return $translate.instant('grid.delete-item-text');
+ };
+
+ vm.deleteItemsTitleFunc = vm.config.deleteItemsTitleFunc || function () {
+ return $translate.instant('grid.delete-items-title', {count: vm.items.selectedCount}, 'messageformat');
+ };
+
+ vm.deleteItemsActionTitleFunc = vm.config.deleteItemsActionTitleFunc || function (selectedCount) {
+ return $translate.instant('grid.delete-items-action-title', {count: selectedCount}, 'messageformat');
+ };
+
+ vm.deleteItemsContentFunc = vm.config.deleteItemsContentFunc || function () {
+ return $translate.instant('grid.delete-items-text');
+ };
+
+ vm.fetchItemsFunc = vm.config.fetchItemsFunc || function () {
+ return $q.when([]);
+ };
+
+ vm.saveItemFunc = vm.config.saveItemFunc || function (item) {
+ return $q.when(item);
+ };
+
+ vm.deleteItemFunc = vm.config.deleteItemFunc || function () {
+ return $q.when();
+ };
+
+ vm.clickItemFunc = vm.config.clickItemFunc || function ($event, item) {
+ vm.openItem($event, item);
+ };
+
+ vm.itemCardTemplate = '<span></span>';
+ if (vm.config.itemCardTemplate) {
+ vm.itemCardTemplate = vm.config.itemCardTemplate;
+ } else if (vm.config.itemCardTemplateUrl) {
+ vm.itemCardTemplate = $templateCache.get(vm.config.itemCardTemplateUrl);
+ }
+
+ vm.parentCtl = vm.config.parentCtl || vm;
+
+ vm.getItemTitleFunc = vm.config.getItemTitleFunc || function () {
+ return '';
+ };
+
+ vm.actionsList = vm.config.actionsList || [];
+
+ for (var i = 0; i < vm.actionsList.length; i++) {
+ var action = vm.actionsList[i];
+ action.isEnabled = action.isEnabled || function() {
+ return true;
+ };
+ }
+
+ vm.groupActionsList = vm.config.groupActionsList || [
+ {
+ onAction: function ($event) {
+ deleteItems($event);
+ },
+ name: function() { return $translate.instant('action.delete') },
+ details: vm.deleteItemsActionTitleFunc,
+ icon: "delete"
+ }
+ ];
+
+ vm.addItemText = vm.config.addItemText || function () {
+ return $translate.instant('grid.add-item-text');
+ };
+
+ vm.addItemAction = vm.config.addItemAction || {
+ onAction: function ($event) {
+ addItem($event);
+ },
+ name: function() { return $translate.instant('action.add') },
+ details: function() { return vm.addItemText() },
+ icon: "add"
+ };
+
+ vm.onGridInited = vm.config.onGridInited || function () {
+ };
+
+ vm.addItemTemplateUrl = vm.config.addItemTemplateUrl;
+
+ vm.noItemsText = vm.config.noItemsText || function () {
+ return $translate.instant('grid.no-items-text');
+ };
+
+ vm.itemDetailsText = vm.config.itemDetailsText || function () {
+ return $translate.instant('grid.item-details');
+ };
+
+ vm.isDetailsReadOnly = vm.config.isDetailsReadOnly || function () {
+ return false;
+ };
+
+ vm.isSelectionEnabled = vm.config.isSelectionEnabled || function () {
+ return true;
+ };
+
+ vm.topIndex = vm.config.topIndex || 0;
+
+ vm.items = vm.config.items || {
+ data: [],
+ rowData: [],
+ nextPageLink: {
+ limit: vm.topIndex + pageSize,
+ textSearch: $scope.searchConfig.searchText
+ },
+ selections: {},
+ selectedCount: 0,
+ hasNext: true,
+ pending: false
+ };
+
+ vm.detailsConfig = {
+ isDetailsOpen: false,
+ isDetailsEditMode: false,
+ currentItem: null,
+ editingItem: null
+ };
+ }
+
+ $scope.$on('searchTextUpdated', function () {
+ vm.items = {
+ data: [],
+ rowData: [],
+ nextPageLink: {
+ limit: pageSize,
+ textSearch: $scope.searchConfig.searchText
+ },
+ selections: {},
+ selectedCount: 0,
+ hasNext: true,
+ pending: false
+ };
+ vm.detailsConfig.isDetailsOpen = false;
+ vm.itemRows.getItemAtIndex(pageSize);
+ });
+
+ vm.onGridInited(vm);
+
+ vm.itemRows.getItemAtIndex(pageSize);
+
+ function refreshList() {
+ $state.go($state.current, vm.refreshParamsFunc(), {reload: true});
+ }
+
+ function addItem($event) {
+ $mdDialog.show({
+ controller: AddItemController,
+ controllerAs: 'vm',
+ templateUrl: vm.addItemTemplateUrl,
+ parent: angular.element($document[0].body),
+ locals: {saveItemFunction: vm.saveItemFunc},
+ fullscreen: true,
+ targetEvent: $event
+ }).then(function () {
+ refreshList();
+ }, function () {
+ });
+ }
+
+ function openItem($event, item) {
+ $event.stopPropagation();
+ if (vm.detailsConfig.currentItem != null && vm.detailsConfig.currentItem.id.id === item.id.id) {
+ if (vm.detailsConfig.isDetailsOpen) {
+ vm.detailsConfig.isDetailsOpen = false;
+ return;
+ }
+ }
+ vm.detailsConfig.currentItem = item;
+ vm.detailsConfig.isDetailsEditMode = false;
+ vm.detailsConfig.isDetailsOpen = true;
+ }
+
+ function isCurrentItem(item) {
+ if (item != null && vm.detailsConfig.currentItem != null &&
+ vm.detailsConfig.currentItem.id.id === item.id.id) {
+ return vm.detailsConfig.isDetailsOpen;
+ } else {
+ return false;
+ }
+ }
+
+ function onToggleDetailsEditMode(theForm) {
+ if (!vm.detailsConfig.isDetailsEditMode) {
+ theForm.$setPristine();
+ }
+ }
+
+ function onCloseDetails() {
+ vm.detailsConfig.currentItem = null;
+ }
+
+ function operatingItem() {
+ if (!vm.detailsConfig.isDetailsEditMode) {
+ if (vm.detailsConfig.editingItem) {
+ vm.detailsConfig.editingItem = null;
+ }
+ return vm.detailsConfig.currentItem;
+ } else {
+ if (!vm.detailsConfig.editingItem) {
+ vm.detailsConfig.editingItem = angular.copy(vm.detailsConfig.currentItem);
+ }
+ return vm.detailsConfig.editingItem;
+ }
+ }
+
+ function saveItem(theForm) {
+ vm.saveItemFunc(vm.detailsConfig.editingItem).then(function success(item) {
+ theForm.$setPristine();
+ vm.detailsConfig.isDetailsEditMode = false;
+ var index = vm.detailsConfig.currentItem.index;
+ item.index = index;
+ vm.detailsConfig.currentItem = item;
+ vm.items.data[index] = item;
+ var row = Math.floor(index / vm.columns);
+ var itemRow = vm.items.rowData[row];
+ var column = index % vm.columns;
+ itemRow[column] = item;
+ });
+ }
+
+ function deleteItem($event, item) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ var confirm = $mdDialog.confirm()
+ .targetEvent($event)
+ .title(vm.deleteItemTitleFunc(item))
+ .htmlContent(vm.deleteItemContentFunc(item))
+ .ariaLabel($translate.instant('grid.delete-item'))
+ .cancel($translate.instant('action.no'))
+ .ok($translate.instant('action.yes'));
+ $mdDialog.show(confirm).then(function () {
+ vm.deleteItemFunc(item.id.id).then(function success() {
+ refreshList();
+ });
+ },
+ function () {
+ });
+
+ }
+
+ function deleteItems($event) {
+ var confirm = $mdDialog.confirm()
+ .targetEvent($event)
+ .title(vm.deleteItemsTitleFunc(vm.items.selectedCount))
+ .htmlContent(vm.deleteItemsContentFunc())
+ .ariaLabel($translate.instant('grid.delete-items'))
+ .cancel($translate.instant('action.no'))
+ .ok($translate.instant('action.yes'));
+ $mdDialog.show(confirm).then(function () {
+ var tasks = [];
+ for (var id in vm.items.selections) {
+ tasks.push(vm.deleteItemFunc(id));
+ }
+ $q.all(tasks).then(function () {
+ refreshList();
+ });
+ },
+ function () {
+ });
+ }
+
+
+ function toggleItemSelection($event, item) {
+ $event.stopPropagation();
+ var selected = angular.isDefined(item.selected) && item.selected;
+ item.selected = !selected;
+ if (item.selected) {
+ vm.items.selections[item.id.id] = true;
+ vm.items.selectedCount++;
+ } else {
+ delete vm.items.selections[item.id.id];
+ vm.items.selectedCount--;
+ }
+ }
+
+ function moveToTop() {
+ moveToIndex(0, true);
+ }
+
+ function moveToIndex(index, animate) {
+ var repeatContainer = $scope.repeatContainer[0];
+ var scrollElement = repeatContainer.children[0];
+ var startY = scrollElement.scrollTop;
+ var stopY = index * vm.itemHeight;
+ if (stopY > 0) {
+ stopY+= 16;
+ }
+ var distance = Math.abs(startY - stopY);
+ if (distance < 100 || !animate) {
+ scrollElement.scrollTop = stopY;
+ return;
+ }
+ var upElseDown = stopY < startY;
+ var speed = Math.round(distance / 100);
+ if (speed >= 20) speed = 20;
+ var step = Math.round(distance / 25);
+
+ var leapY = upElseDown ? startY - step : startY + step;
+ var timer = 0;
+ for (var i = startY; upElseDown ? (i > stopY) : (i < stopY); upElseDown ? (i -= step) : (i += step)) {
+ $timeout(function (topY) {
+ scrollElement.scrollTop = topY;
+ }, timer * speed, true, leapY);
+ if (upElseDown) {
+ leapY -= step;
+ if (leapY < stopY) {
+ leapY = stopY;
+ }
+ } else {
+ leapY += step;
+ if (leapY > stopY) {
+ leapY = stopY;
+ }
+ }
+ timer++;
+ }
+
+ }
+
+ function noData() {
+ return vm.items.data.length == 0 && !vm.items.hasNext;
+ }
+
+ function hasData() {
+ return vm.items.data.length > 0;
+ }
+
+}
+
+/*@ngInject*/
+function AddItemController($scope, $mdDialog, saveItemFunction, helpLinks) {
+
+ var vm = this;
+
+ vm.helpLinks = helpLinks;
+ vm.item = {};
+
+ vm.add = add;
+ vm.cancel = cancel;
+
+ function cancel() {
+ $mdDialog.cancel();
+ }
+
+ function add() {
+ saveItemFunction(vm.item).then(function success(item) {
+ vm.item = item;
+ $scope.theForm.$setPristine();
+ $mdDialog.hide();
+ });
+ }
+}
\ No newline at end of file
ui/src/app/components/grid.scss 37(+37 -0)
diff --git a/ui/src/app/components/grid.scss b/ui/src/app/components/grid.scss
new file mode 100644
index 0000000..549b8eb
--- /dev/null
+++ b/ui/src/app/components/grid.scss
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import "~compass-sass-mixins/lib/animate";
+
+.tb-uppercase {
+ text-transform: uppercase;
+}
+
+.tb-card-item {
+ @include transition(all .2s ease-in-out);
+}
+
+.tb-current-item {
+ opacity: 0.5;
+ @include transform(scale(1.05));
+}
+
+#tb-vertical-container {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+}
ui/src/app/components/grid.tpl.html 102(+102 -0)
diff --git a/ui/src/app/components/grid.tpl.html b/ui/src/app/components/grid.tpl.html
new file mode 100644
index 0000000..16d38f3
--- /dev/null
+++ b/ui/src/app/components/grid.tpl.html
@@ -0,0 +1,102 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<span layout-align="center center"
+ style="text-transform: uppercase; display: flex;"
+ class="md-headline tb-absolute-fill"
+ ng-show="vm.noData()">{{vm.noItemsText()}}</span>
+<section layout="row" flex style="height: 100%;" flex layout-wrap>
+ <div layout="column" style="height: 100%;" flex layout-wrap>
+ <md-virtual-repeat-container ng-show="vm.hasData()" tb-scope-element="repeatContainer" id="tb-vertical-container" md-top-index="vm.topIndex" flex>
+ <div class="md-padding" layout="column">
+ <section layout="row" md-virtual-repeat="rowItem in vm.itemRows" md-on-demand md-item-size="vm.itemHeight">
+ <div flex ng-repeat="n in [] | range:vm.columns" ng-style="{'height':vm.itemHeight+'px'}">
+ <md-card ng-if="rowItem[n]"
+ ng-class="{'tb-current-item': vm.isCurrentItem(rowItem[n])}"
+ class="repeated-item tb-card-item" ng-style="{'height':(vm.itemHeight-16)+'px','cursor':'pointer'}"
+ ng-click="vm.clickItemFunc($event, rowItem[n])">
+ <section layout="row" layout-wrap>
+ <md-card-title layout="row">
+ <section layout="column" layout-wrap>
+ <md-checkbox ng-if="vm.isSelectionEnabled(rowItem[n])" ng-click="vm.toggleItemSelection($event, rowItem[n])"
+ layout-align="start start" aria-label="{{ 'item.selected' | translate }}" ng-checked="rowItem[n].selected">
+ </md-checkbox>
+ <span flex></span>
+ </section>
+ <md-card-title-text>
+ <span class="md-headline">{{vm.getItemTitleFunc(rowItem[n])}}</span>
+ </md-card-title-text>
+ </md-card-title>
+ </section>
+ <md-card-content flex>
+ <tb-grid-card-content grid-ctl="vm" parent-ctl="vm.parentCtl" item-template="vm.itemCardTemplate" item="rowItem[n]"></tb-grid-card-content>
+ </md-card-content>
+ <md-card-actions layout="row" layout-align="end end">
+ <md-button ng-if="action.isEnabled(rowItem[n])" ng-disabled="loading" class="md-icon-button md-primary" ng-repeat="action in vm.actionsList"
+ ng-click="action.onAction($event, rowItem[n])" aria-label="{{ action.name() }}">
+ <md-tooltip md-direction="top">
+ {{ action.details() }}
+ </md-tooltip>
+ <ng-md-icon icon="{{action.icon}}"></ng-md-icon>
+ </md-button>
+ </md-card-actions>
+ </md-card>
+ </div>
+ </section>
+ </div>
+ </md-virtual-repeat-container>
+ </div>
+ <tb-details-sidenav
+ header-title="vm.getItemTitleFunc(vm.operatingItem())"
+ header-subtitle="{{vm.itemDetailsText()}}"
+ is-read-only="vm.isDetailsReadOnly(vm.operatingItem())"
+ is-open="vm.detailsConfig.isDetailsOpen"
+ is-edit="vm.detailsConfig.isDetailsEditMode"
+ on-close-details="vm.onCloseDetails()"
+ on-toggle-details-edit-mode="vm.onToggleDetailsEditMode(vm.detailsForm)"
+ on-apply-details="vm.saveItem(vm.detailsForm)"
+ the-form="vm.detailsForm">
+ <details-buttons>
+ <div ng-transclude="detailsButtons"></div>
+ </details-buttons>
+ <form name="vm.detailsForm">
+ <div ng-transclude></div>
+ </form>
+ </tb-details-sidenav>
+</section>
+
+<section layout="row" layout-wrap class="tb-footer-buttons md-fab ">
+ <md-button ng-disabled="loading" ng-show="vm.items.selectedCount > 0" class="tb-btn-footer md-accent md-hue-2 md-fab" ng-repeat="groupAction in vm.groupActionsList"
+ ng-click="groupAction.onAction($event, vm.items)" aria-label="{{ groupAction.name() }}">
+ <md-tooltip md-direction="top">
+ {{ groupAction.details(vm.items.selectedCount) }}
+ </md-tooltip>
+ <ng-md-icon icon="{{groupAction.icon}}"></ng-md-icon>
+ </md-button>
+ <md-button ng-disabled="loading" ng-show="vm.topIndex > 0" class="tb-btn-footer md-primary md-hue-1 md-fab" ng-click="vm.moveToTop()" aria-label="{{'grid.scroll-to-top' | translate}}" >
+ <md-tooltip md-direction="top">
+ {{'grid.scroll-to-top' | translate}}
+ </md-tooltip>
+ <ng-md-icon icon="arrow_drop_up"></ng-md-icon>
+ </md-button>
+ <md-button ng-disabled="loading" ng-if="vm.addItemAction.name()" class="tb-btn-footer md-accent md-hue-2 md-fab" ng-click="vm.addItemAction.onAction($event)" aria-label="{{ vm.addItemAction.name() }}" >
+ <md-tooltip md-direction="top">
+ {{ vm.addItemAction.details() }}
+ </md-tooltip>
+ <ng-md-icon icon="{{ vm.addItemAction.icon }}"></ng-md-icon>
+ </md-button>
+</section>
\ No newline at end of file
ui/src/app/components/js-func.directive.js 203(+203 -0)
diff --git a/ui/src/app/components/js-func.directive.js b/ui/src/app/components/js-func.directive.js
new file mode 100644
index 0000000..8247b2b
--- /dev/null
+++ b/ui/src/app/components/js-func.directive.js
@@ -0,0 +1,203 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './js-func.scss';
+
+import ace from 'brace';
+import 'brace/ext/language_tools';
+import $ from 'jquery';
+import thingsboardToast from '../services/toast';
+import thingsboardUtils from '../common/utils.service';
+import thingsboardExpandFullscreen from './expand-fullscreen.directive';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import jsFuncTemplate from './js-func.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/* eslint-disable angular/angularelement */
+
+export default angular.module('thingsboard.directives.jsFunc', [thingsboardToast, thingsboardUtils, thingsboardExpandFullscreen])
+ .directive('tbJsFunc', JsFunc)
+ .name;
+
+/*@ngInject*/
+function JsFunc($compile, $templateCache, toast, utils, $translate) {
+
+ var linker = function (scope, element, attrs, ngModelCtrl) {
+ var template = $templateCache.get(jsFuncTemplate);
+ element.html(template);
+
+ scope.functionArgs = scope.$eval(attrs.functionArgs);
+ scope.validationArgs = scope.$eval(attrs.validationArgs);
+ scope.resultType = attrs.resultType;
+ if (!scope.resultType || scope.resultType.length === 0) {
+ scope.resultType = "nocheck";
+ }
+
+ scope.functionValid = true;
+
+ var Range = ace.acequire("ace/range").Range;
+ scope.js_editor;
+ scope.errorMarkers = [];
+
+
+ scope.functionArgsString = '';
+ for (var i in scope.functionArgs) {
+ if (scope.functionArgsString.length > 0) {
+ scope.functionArgsString += ', ';
+ }
+ scope.functionArgsString += scope.functionArgs[i];
+ }
+
+ scope.onFullscreenChanged = function () {
+ if (scope.js_editor) {
+ scope.js_editor.resize();
+ scope.js_editor.renderer.updateFull();
+ }
+ };
+
+ scope.jsEditorOptions = {
+ useWrapMode: true,
+ mode: 'javascript',
+ advanced: {
+ enableSnippets: true,
+ enableBasicAutocompletion: true,
+ enableLiveAutocompletion: true
+ },
+ onLoad: function (_ace) {
+ scope.js_editor = _ace;
+ scope.js_editor.session.on("change", function () {
+ scope.cleanupJsErrors();
+ });
+ }
+ };
+
+ scope.cleanupJsErrors = function () {
+ toast.hide();
+ for (var i = 0; i < scope.errorMarkers.length; i++) {
+ scope.js_editor.session.removeMarker(scope.errorMarkers[i]);
+ }
+ scope.errorMarkers = [];
+ if (scope.errorAnnotationId && scope.errorAnnotationId > -1) {
+ var annotations = scope.js_editor.session.getAnnotations();
+ annotations.splice(scope.errorAnnotationId, 1);
+ scope.js_editor.session.setAnnotations(annotations);
+ scope.errorAnnotationId = -1;
+ }
+ }
+
+ scope.updateValidity = function () {
+ ngModelCtrl.$setValidity('functionBody', scope.functionValid);
+ };
+
+ scope.$watch('functionBody', function (newFunctionBody, oldFunctionBody) {
+ ngModelCtrl.$setViewValue(scope.functionBody);
+ if (!angular.equals(newFunctionBody, oldFunctionBody)) {
+ scope.functionValid = true;
+ }
+ scope.updateValidity();
+ });
+
+ ngModelCtrl.$render = function () {
+ scope.functionBody = ngModelCtrl.$viewValue;
+ };
+
+ scope.showError = function (error) {
+ var toastParent = $('#tb-javascript-panel', element);
+ var dialogContent = toastParent.closest('md-dialog-content');
+ if (dialogContent.length > 0) {
+ toastParent = dialogContent;
+ }
+ toast.showError(error, toastParent, 'bottom left');
+ }
+
+ scope.validate = function () {
+ try {
+ var toValidate = new Function(scope.functionArgsString, scope.functionBody);
+ var res = toValidate.apply(this, scope.validationArgs);
+ if (scope.resultType != 'nocheck') {
+ if (scope.resultType === 'any') {
+ if (angular.isUndefined(res)) {
+ scope.showError($translate.instant('js-func.no-return-error'));
+ return false;
+ }
+ } else {
+ var resType = typeof res;
+ if (resType != scope.resultType) {
+ scope.showError($translate.instant('js-func.return-type-mismatch', {type: scope.resultType}));
+ return false;
+ }
+ }
+ }
+ return true;
+ } catch (e) {
+ var details = utils.parseException(e);
+ var errorInfo = 'Error:';
+ if (details.name) {
+ errorInfo += ' ' + details.name + ':';
+ }
+ if (details.message) {
+ errorInfo += ' ' + details.message;
+ }
+ if (details.lineNumber) {
+ errorInfo += '<br>Line ' + details.lineNumber;
+ if (details.columnNumber) {
+ errorInfo += ' column ' + details.columnNumber;
+ }
+ errorInfo += ' of script.';
+ }
+ scope.showError(errorInfo);
+ if (scope.js_editor && details.lineNumber) {
+ var line = details.lineNumber - 1;
+ var column = 0;
+ if (details.columnNumber) {
+ column = details.columnNumber;
+ }
+
+ var errorMarkerId = scope.js_editor.session.addMarker(new Range(line, 0, line, Infinity), "ace_active-line", "screenLine");
+ scope.errorMarkers.push(errorMarkerId);
+ var annotations = scope.js_editor.session.getAnnotations();
+ var errorAnnotation = {
+ row: line,
+ column: column,
+ text: details.message,
+ type: "error"
+ };
+ scope.errorAnnotationId = annotations.push(errorAnnotation) - 1;
+ scope.js_editor.session.setAnnotations(annotations);
+ }
+ return false;
+ }
+ };
+
+ scope.$on('form-submit', function () {
+ scope.functionValid = scope.validate();
+ scope.updateValidity();
+ });
+
+ $compile(element.contents())(scope);
+ }
+
+ return {
+ restrict: "E",
+ require: "^ngModel",
+ scope: {},
+ link: linker
+ };
+}
+
+/* eslint-enable angular/angularelement */
ui/src/app/components/js-func.scss 31(+31 -0)
diff --git a/ui/src/app/components/js-func.scss b/ui/src/app/components/js-func.scss
new file mode 100644
index 0000000..b251e33
--- /dev/null
+++ b/ui/src/app/components/js-func.scss
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+tb-js-func {
+ position: relative;
+}
+
+.tb-js-func-panel {
+ margin-left: 15px;
+ border: 1px solid #C0C0C0;
+ height: 100%;
+ #tb-javascript-input {
+ min-width: 400px;
+ min-height: 200px;
+ width: 100%;
+ height: 100%;
+ }
+}
ui/src/app/components/js-func.tpl.html 33(+33 -0)
diff --git a/ui/src/app/components/js-func.tpl.html b/ui/src/app/components/js-func.tpl.html
new file mode 100644
index 0000000..2c52232
--- /dev/null
+++ b/ui/src/app/components/js-func.tpl.html
@@ -0,0 +1,33 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div style="background: #fff;" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()" layout="column">
+ <div layout="row" layout-align="start center" style="height: 40px;">
+ <span style="font-style: italic;">function({{ functionArgsString }}) {</span>
+ <span flex></span>
+ <md-button id="expand-button" aria-label="Fullscreen" class="md-icon-button tb-md-32 tb-fullscreen-button-style"></md-button>
+ </div>
+ <div flex id="tb-javascript-panel" class="tb-js-func-panel" layout="column">
+ <div flex id="tb-javascript-input"
+ ui-ace="jsEditorOptions"
+ ng-model="functionBody">
+ </div>
+ </div>
+ <div layout="row" layout-align="start center" style="height: 40px;">
+ <span style="font-style: italic;">}</span>
+ </div>
+</div>
\ No newline at end of file
ui/src/app/components/json-form.directive.js 243(+243 -0)
diff --git a/ui/src/app/components/json-form.directive.js b/ui/src/app/components/json-form.directive.js
new file mode 100644
index 0000000..a7f187d
--- /dev/null
+++ b/ui/src/app/components/json-form.directive.js
@@ -0,0 +1,243 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './json-form.scss';
+
+import tinycolor from 'tinycolor2';
+import ObjectPath from 'objectpath';
+import inspector from 'schema-inspector';
+import ReactSchemaForm from './react/json-form-react.jsx';
+import jsonFormTemplate from './json-form.tpl.html';
+import { utils } from 'react-schema-form';
+
+export default angular.module('thingsboard.directives.jsonForm', [])
+ .directive('tbJsonForm', JsonForm)
+ .value('ReactSchemaForm', ReactSchemaForm)
+ .name;
+
+/*@ngInject*/
+function JsonForm($compile, $templateCache, $mdColorPicker) {
+
+ var linker = function (scope, element) {
+
+ var template = $templateCache.get(jsonFormTemplate);
+
+ element.html(template);
+
+ var childScope;
+
+ var destroyModelChangeWatches = function() {
+ if (scope.modelWatchHandle) {
+ scope.modelWatchHandle();
+ }
+ if (scope.modelRefWatchHandle) {
+ scope.modelRefWatchHandle();
+ }
+ }
+
+ var initModelChangeWatches = function() {
+ scope.modelWatchHandle = scope.$watch('model',function(newValue, prevValue) {
+ if (newValue && prevValue && !angular.equals(newValue,prevValue)) {
+ scope.validate();
+ if (scope.formControl) {
+ scope.formControl.$setDirty();
+ }
+ }
+ }, true);
+ scope.modelRefWatchHandle = scope.$watch('model',function(newValue, prevValue) {
+ if (newValue && newValue != prevValue) {
+ scope.updateValues();
+ }
+ });
+ };
+
+ var recompile = function() {
+ if (childScope) {
+ childScope.$destroy();
+ }
+ childScope = scope.$new();
+ $compile(element.contents())(childScope);
+ }
+
+ scope.formProps = {
+ option: {
+ formDefaults: {
+ startEmpty: true
+ }
+ },
+ onModelChange: function(key, val) {
+ if (angular.isString(val) && val === '') {
+ val = undefined;
+ }
+ selectOrSet(key, scope.model, val);
+ },
+ onColorClick: function(event, key, val) {
+ scope.showColorPicker(event, val);
+ }
+ };
+
+ scope.showColorPicker = function (event, color) {
+ $mdColorPicker.show({
+ value: tinycolor(color).toRgbString(),
+ defaultValue: '#fff',
+ random: tinycolor.random(),
+ clickOutsideToClose: false,
+ hasBackdrop: false,
+ skipHide: true,
+ preserveScope: false,
+
+ mdColorAlphaChannel: true,
+ mdColorSpectrum: true,
+ mdColorSliders: true,
+ mdColorGenericPalette: false,
+ mdColorMaterialPalette: true,
+ mdColorHistory: false,
+ mdColorDefaultTab: 2,
+
+ $event: event
+
+ }).then(function (color) {
+ if (event.data && event.data.onValueChanged) {
+ event.data.onValueChanged(tinycolor(color).toRgb());
+ }
+ });
+ }
+
+ scope.validate = function(){
+ if (scope.schema && scope.model) {
+ var result = utils.validateBySchema(scope.schema, scope.model);
+ if (scope.formControl) {
+ scope.formControl.$setValidity('jsonForm', result.valid);
+ }
+ }
+ }
+
+ scope.updateValues = function(skipRerender) {
+ destroyModelChangeWatches();
+ if (!skipRerender) {
+ element.html(template);
+ }
+ var readonly = (scope.readonly && scope.readonly === true) ? true : false;
+ var schema = scope.schema ? angular.copy(scope.schema) : {
+ type: 'object'
+ };
+ schema.strict = true;
+ var form = scope.form ? angular.copy(scope.form) : [ "*" ];
+ var model = scope.model || {};
+ scope.model = inspector.sanitize(schema, model).data;
+ scope.formProps.option.formDefaults.readonly = readonly;
+ scope.formProps.schema = schema;
+ scope.formProps.form = form;
+ scope.formProps.model = angular.copy(scope.model);
+ if (!skipRerender) {
+ recompile();
+ }
+ initModelChangeWatches();
+ }
+
+ scope.updateValues(true);
+
+ scope.$watch('readonly',function() {
+ scope.updateValues();
+ });
+
+ scope.$watch('schema',function(newValue, prevValue) {
+ if (newValue && newValue != prevValue) {
+ scope.updateValues();
+ scope.validate();
+ }
+ });
+
+ scope.$watch('form',function(newValue, prevValue) {
+ if (newValue && newValue != prevValue) {
+ scope.updateValues();
+ }
+ });
+
+ scope.validate();
+
+ recompile();
+
+ }
+
+ return {
+ restrict: "E",
+ scope: {
+ schema: '=',
+ form: '=',
+ model: '=',
+ formControl: '=',
+ readonly: '='
+ },
+ link: linker
+ };
+
+}
+
+function setValue(obj, key, val) {
+ var changed = false;
+ if (obj) {
+ if (angular.isUndefined(val)) {
+ if (angular.isDefined(obj[key])) {
+ delete obj[key];
+ changed = true;
+ }
+ } else {
+ changed = !angular.equals(obj[key], val);
+ obj[key] = val;
+ }
+ }
+ return changed;
+}
+
+function selectOrSet(projection, obj, valueToSet) {
+ var numRe = /^\d+$/;
+
+ if (!obj) {
+ obj = this;
+ }
+
+ if (!obj) {
+ return false;
+ }
+
+ var parts = angular.isString(projection) ? ObjectPath.parse(projection) : projection;
+
+ if (parts.length === 1) {
+ return setValue(obj, parts[0], valueToSet);
+ }
+
+ if (angular.isUndefined(obj[parts[0]])) {
+ obj[parts[0]] = parts.length > 2 && numRe.test(parts[1]) ? [] : {};
+ }
+
+ var value = obj[parts[0]];
+ for (var i = 1; i < parts.length; i++) {
+ if (parts[i] === '') {
+ return false;
+ }
+ if (i === parts.length - 1) {
+ return setValue(value, parts[i], valueToSet);
+ } else {
+ var tmp = value[parts[i]];
+ if (angular.isUndefined(tmp) || tmp === null) {
+ tmp = numRe.test(parts[i + 1]) ? [] : {};
+ value[parts[i]] = tmp;
+ }
+ value = tmp;
+ }
+ }
+ return value;
+}
ui/src/app/components/json-form.scss 19(+19 -0)
diff --git a/ui/src/app/components/json-form.scss b/ui/src/app/components/json-form.scss
new file mode 100644
index 0000000..79cdde9
--- /dev/null
+++ b/ui/src/app/components/json-form.scss
@@ -0,0 +1,19 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+tb-json-form {
+ overflow: auto;
+ padding-bottom: 14px !important;
+}
\ No newline at end of file
ui/src/app/components/json-form.tpl.html 18(+18 -0)
diff --git a/ui/src/app/components/json-form.tpl.html b/ui/src/app/components/json-form.tpl.html
new file mode 100644
index 0000000..fb79640
--- /dev/null
+++ b/ui/src/app/components/json-form.tpl.html
@@ -0,0 +1,18 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<react-component name="ReactSchemaForm" props="formProps" watch-depth="value"></react-component>
\ No newline at end of file
ui/src/app/components/led-light.directive.js 110(+110 -0)
diff --git a/ui/src/app/components/led-light.directive.js b/ui/src/app/components/led-light.directive.js
new file mode 100644
index 0000000..edd430c
--- /dev/null
+++ b/ui/src/app/components/led-light.directive.js
@@ -0,0 +1,110 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import Raphael from 'raphael';
+import tinycolor from 'tinycolor2';
+import $ from 'jquery';
+
+/* eslint-disable angular/angularelement */
+
+export default angular.module('thingsboard.directives.ledLight', [])
+ .directive('tbLedLight', LedLight).name;
+
+/*@ngInject*/
+function LedLight($compile) {
+
+ var linker = function (scope, element) {
+ scope.offOpacity = scope.offOpacity || "0.4";
+ scope.glowColor = tinycolor(scope.colorOn).lighten().toHexString();
+
+ scope.$watch('tbEnabled',function() {
+ scope.draw();
+ });
+
+ scope.$watch('size',function() {
+ scope.update();
+ });
+
+ scope.draw = function () {
+ if (scope.tbEnabled) {
+ scope.circleElement.attr("fill", scope.colorOn);
+ scope.circleElement.attr("stroke", scope.colorOn);
+ scope.circleElement.attr("opacity", "1");
+
+ if (scope.circleElement.theGlow) {
+ scope.circleElement.theGlow.remove();
+ }
+
+ scope.circleElement.theGlow = scope.circleElement.glow(
+ {
+ color: scope.glowColor,
+ width: scope.radius + scope.glowSize,
+ opacity: 0.8,
+ fill: true
+ });
+ } else {
+ if (scope.circleElement.theGlow) {
+ scope.circleElement.theGlow.remove();
+ }
+
+ /*scope.circleElement.theGlow = scope.circleElement.glow(
+ {
+ color: scope.glowColor,
+ width: scope.radius + scope.glowSize,
+ opacity: 0.4,
+ fill: true
+ });*/
+
+ scope.circleElement.attr("fill", scope.colorOff);
+ scope.circleElement.attr("stroke", scope.colorOff);
+ scope.circleElement.attr("opacity", scope.offOpacity);
+ }
+ }
+
+ scope.update = function() {
+ scope.size = scope.size || 50;
+ scope.canvasSize = scope.size;
+ scope.radius = scope.canvasSize / 4;
+ scope.glowSize = scope.radius / 5;
+
+ var template = '<div id="canvas_container" style="width: ' + scope.size + 'px; height: ' + scope.size + 'px;"></div>';
+ element.html(template);
+ $compile(element.contents())(scope);
+ scope.paper = new Raphael($('#canvas_container', element)[0], scope.canvasSize, scope.canvasSize);
+ var center = scope.canvasSize / 2;
+ scope.circleElement = scope.paper.circle(center, center, scope.radius);
+ scope.draw();
+ }
+
+ scope.update();
+ }
+
+
+ return {
+ restrict: "E",
+ link: linker,
+ scope: {
+ size: '=?',
+ colorOn: '=',
+ colorOff: '=',
+ offOpacity: '=?',
+ //glowColor: '=',
+ tbEnabled: '='
+ }
+ };
+
+}
+
+/* eslint-enable angular/angularelement */
ui/src/app/components/menu-link.directive.js 76(+76 -0)
diff --git a/ui/src/app/components/menu-link.directive.js b/ui/src/app/components/menu-link.directive.js
new file mode 100644
index 0000000..a8f20ff
--- /dev/null
+++ b/ui/src/app/components/menu-link.directive.js
@@ -0,0 +1,76 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './menu-link.scss';
+
+import thingsboardMenu from '../services/menu.service';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import menulinkTemplate from './menu-link.tpl.html';
+import menutoggleTemplate from './menu-toggle.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+export default angular.module('thingsboard.directives.menuLink', [thingsboardMenu])
+ .directive('tbMenuLink', MenuLink)
+ .filter('nospace', NoSpace)
+ .name;
+
+/*@ngInject*/
+function MenuLink($compile, $templateCache, menu) {
+
+ var linker = function (scope, element) {
+ var template;
+
+ if (scope.section.type === 'link') {
+ template = $templateCache.get(menulinkTemplate);
+ } else {
+ template = $templateCache.get(menutoggleTemplate);
+
+ var parentNode = element[0].parentNode.parentNode.parentNode;
+ if (parentNode.classList.contains('parent-list-item')) {
+ var heading = parentNode.querySelector('h2');
+ element[0].firstChild.setAttribute('aria-describedby', heading.id);
+ }
+
+ scope.sectionActive = function () {
+ return menu.sectionActive(scope.section);
+ };
+
+ scope.sectionHeight = function () {
+ return menu.sectionHeight(scope.section);
+ };
+ }
+
+ element.html(template);
+
+ $compile(element.contents())(scope);
+ }
+
+ return {
+ restrict: "E",
+ link: linker,
+ scope: {
+ section: '='
+ }
+ };
+}
+
+function NoSpace() {
+ return function (value) {
+ return (!value) ? '' : value.replace(/ /g, '');
+ }
+}
ui/src/app/components/menu-link.scss 32(+32 -0)
diff --git a/ui/src/app/components/menu-link.scss b/ui/src/app/components/menu-link.scss
new file mode 100644
index 0000000..5d04761
--- /dev/null
+++ b/ui/src/app/components/menu-link.scss
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import "~compass-sass-mixins/lib/compass";
+
+.md-button-toggle .md-toggle-icon.tb-toggled {
+ @include transform(rotateZ(180deg));
+}
+
+.tb-menu-toggle-list.ng-hide {
+ max-height: 0;
+}
+
+.tb-menu-toggle-list {
+ overflow: hidden;
+ position: relative;
+ z-index: 1;
+ @include transition(0.75s cubic-bezier(0.35, 0, 0.25, 1));
+ @include transition-property(height);
+}
ui/src/app/components/menu-link.tpl.html 22(+22 -0)
diff --git a/ui/src/app/components/menu-link.tpl.html b/ui/src/app/components/menu-link.tpl.html
new file mode 100644
index 0000000..48a93af
--- /dev/null
+++ b/ui/src/app/components/menu-link.tpl.html
@@ -0,0 +1,22 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-button
+ ui-sref-active-eq="tb-active" ui-sref="{{section.state}}">
+ <md-icon ng-show="{{section.icon != null}}" aria-label="{{section.icon}}" class="material-icons">{{section.icon}}</md-icon>
+ {{section.name | translate}}
+</md-button>
\ No newline at end of file
ui/src/app/components/menu-toggle.tpl.html 33(+33 -0)
diff --git a/ui/src/app/components/menu-toggle.tpl.html b/ui/src/app/components/menu-toggle.tpl.html
new file mode 100644
index 0000000..3691521
--- /dev/null
+++ b/ui/src/app/components/menu-toggle.tpl.html
@@ -0,0 +1,33 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-button
+ ui-sref-active-eq="tb-active" ui-sref="{{section.state}}"
+ class="md-button-toggle"
+ aria-controls="docs-menu-{{section.name | nospace}}" layout="row"
+ aria-expanded="{{sectionActive()}}">
+ <md-icon ng-show="{{section.icon != null}}" aria-label="{{section.icon}}" class="material-icons">{{section.icon}}</md-icon>
+ {{section.name | translate}}
+ <span aria-hidden="true"
+ class=" pull-right fa fa-chevron-down md-toggle-icon"
+ ng-class="{'tb-toggled' : sectionActive()}"></span>
+</md-button>
+<ul id="docs-menu-{{section.name | nospace}}" class="tb-menu-toggle-list" style="height: {{sectionHeight()}};">
+ <li ng-repeat="page in section.pages">
+ <tb-menu-link section="page"></tb-menu-link>
+ </li>
+</ul>
diff --git a/ui/src/app/components/no-animate.directive.js b/ui/src/app/components/no-animate.directive.js
new file mode 100644
index 0000000..deceaa8
--- /dev/null
+++ b/ui/src/app/components/no-animate.directive.js
@@ -0,0 +1,33 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import ngAnimate from 'angular-animate';
+
+export default angular.module('thingsboard.directives.noAnimate', [ngAnimate])
+ .directive('tbNoAnimate', NoAnimate)
+ .name;
+
+/*@ngInject*/
+function NoAnimate($animate) {
+ return {
+ restrict: 'A',
+ link: function (scope, element) {
+ $animate.enabled(element, false)
+ scope.$watch(function () {
+ $animate.enabled(element, false)
+ })
+ }
+ };
+}
ui/src/app/components/plugin-select.directive.js 115(+115 -0)
diff --git a/ui/src/app/components/plugin-select.directive.js b/ui/src/app/components/plugin-select.directive.js
new file mode 100644
index 0000000..5ec14dd
--- /dev/null
+++ b/ui/src/app/components/plugin-select.directive.js
@@ -0,0 +1,115 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './plugin-select.scss';
+
+import thingsboardApiPlugin from '../api/plugin.service';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import pluginSelectTemplate from './plugin-select.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+
+export default angular.module('thingsboard.directives.pluginSelect', [thingsboardApiPlugin])
+ .directive('tbPluginSelect', PluginSelect)
+ .name;
+
+/*@ngInject*/
+function PluginSelect($compile, $templateCache, $q, pluginService, types) {
+
+ var linker = function (scope, element, attrs, ngModelCtrl) {
+ var template = $templateCache.get(pluginSelectTemplate);
+ element.html(template);
+
+ scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false;
+ scope.plugin = null;
+ scope.pluginSearchText = '';
+ scope.searchTextChanged = false;
+
+ scope.pluginFetchFunction = pluginService.getAllPlugins;
+ if (angular.isDefined(scope.pluginsScope)) {
+ if (scope.pluginsScope === 'action') {
+ scope.pluginFetchFunction = pluginService.getAllActionPlugins;
+ } else if (scope.pluginsScope === 'system') {
+ scope.pluginFetchFunction = pluginService.getSystemPlugins;
+ } else if (scope.pluginsScope === 'tenant') {
+ scope.pluginFetchFunction = pluginService.getTenantPlugins;
+ }
+ }
+
+ scope.fetchPlugins = function(searchText) {
+ var pageLink = {limit: 10, textSearch: searchText};
+
+ var deferred = $q.defer();
+
+ scope.pluginFetchFunction(pageLink).then(function success(result) {
+ deferred.resolve(result.data);
+ }, function fail() {
+ deferred.reject();
+ });
+
+ return deferred.promise;
+ }
+
+ scope.pluginSearchTextChanged = function() {
+ scope.searchTextChanged = true;
+ }
+
+ scope.isSystem = function(item) {
+ return item && item.tenantId.id === types.id.nullUid;
+ }
+
+ scope.updateView = function () {
+ ngModelCtrl.$setViewValue(scope.plugin);
+ }
+
+ ngModelCtrl.$render = function () {
+ if (ngModelCtrl.$viewValue) {
+ scope.plugin = ngModelCtrl.$viewValue;
+ }
+ }
+
+ scope.$watch('plugin', function () {
+ scope.updateView();
+ })
+
+ if (scope.selectFirstPlugin) {
+ var pageLink = {limit: 1, textSearch: ''};
+ scope.pluginFetchFunction(pageLink).then(function success(result) {
+ var plugins = result.data;
+ if (plugins.length > 0) {
+ scope.plugin = plugins[0];
+ }
+ }, function fail() {
+ });
+ }
+
+ $compile(element.contents())(scope);
+ }
+
+ return {
+ restrict: "E",
+ require: "^ngModel",
+ link: linker,
+ scope: {
+ pluginsScope: '@',
+ theForm: '=?',
+ tbRequired: '=?',
+ selectFirstPlugin: '='
+ }
+ };
+}
ui/src/app/components/plugin-select.scss 37(+37 -0)
diff --git a/ui/src/app/components/plugin-select.scss b/ui/src/app/components/plugin-select.scss
new file mode 100644
index 0000000..6adcc82
--- /dev/null
+++ b/ui/src/app/components/plugin-select.scss
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import "../../scss/mixins";
+
+.tb-plugin-autocomplete {
+ .tb-not-found {
+ display: block;
+ line-height: 1.5;
+ height: 48px;
+ }
+ .tb-plugin-item {
+ display: block;
+ height: 48px;
+ .tb-plugin-system {
+ font-size: 0.8rem;
+ opacity: 0.8;
+ float: right;
+ }
+ }
+ li {
+ height: auto !important;
+ white-space: normal !important;
+ }
+}
ui/src/app/components/plugin-select.tpl.html 44(+44 -0)
diff --git a/ui/src/app/components/plugin-select.tpl.html b/ui/src/app/components/plugin-select.tpl.html
new file mode 100644
index 0000000..2beb2f6
--- /dev/null
+++ b/ui/src/app/components/plugin-select.tpl.html
@@ -0,0 +1,44 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-autocomplete ng-required="tbRequired"
+ md-input-name="plugin"
+ ng-model="plugin"
+ md-selected-item="plugin"
+ md-search-text="pluginSearchText"
+ md-search-text-change="pluginSearchTextChanged()"
+ md-items="item in fetchPlugins(pluginSearchText)"
+ md-item-text="item.name"
+ md-min-length="0"
+ placeholder="{{ 'plugin.select-plugin' | translate }}"
+ md-menu-class="tb-plugin-autocomplete">
+ <md-item-template>
+ <div class="tb-plugin-item">
+ <span md-highlight-text="pluginSearchText" md-highlight-flags="^i">{{item.name}}</span>
+ <span translate class="tb-plugin-system" ng-if="isSystem(item)">plugin.system</span>
+ </div>
+ </md-item-template>
+ <md-not-found>
+ <div class="tb-not-found">
+ <span translate translate-values='{ plugin: pluginSearchText }'>plugin.no-plugins-matching</span>
+ </div>
+ </md-not-found>
+</md-autocomplete>
+<div ng-if="searchTextChanged" class="tb-error-messages" ng-messages="theForm.plugin.$error" role="alert">
+ <div translate ng-message="required" class="tb-error-message">plugin.plugin-required</div>
+ <div translate ng-message="md-require-match" class="tb-error-message">plugin.plugin-require-match</div>
+</div>
ui/src/app/components/react/json-form.scss 101(+101 -0)
diff --git a/ui/src/app/components/react/json-form.scss b/ui/src/app/components/react/json-form.scss
new file mode 100644
index 0000000..889eca5
--- /dev/null
+++ b/ui/src/app/components/react/json-form.scss
@@ -0,0 +1,101 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import "~compass-sass-mixins/lib/compass";
+
+$swift-ease-out-duration: 0.4s !default;
+$swift-ease-out-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !default;
+
+$input-label-float-offset: 6px !default;
+$input-label-float-scale: 0.75 !default;
+
+.json-form-error {
+ position: relative;
+ bottom: -5px;
+ font-size: 12px;
+ line-height: 12px;
+ color: rgb(244, 67, 54);
+ @include transition(all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms);
+}
+
+.tb-container {
+ position: relative;
+ margin-top: 32px;
+ padding: 10px 0;
+}
+
+.tb-field {
+ &.tb-required {
+ label:after {
+ content: ' *';
+ font-size: 13px;
+ vertical-align: top;
+ color: rgba(0,0,0,0.54);
+ }
+ }
+ &.tb-focused:not(.tb-readonly) {
+ label:after {
+ color: rgb(221,44,0);
+ }
+ }
+}
+
+.tb-date-field {
+ &.tb-required {
+ div>div:first-child:after {
+ content: ' *';
+ font-size: 13px;
+ vertical-align: top;
+ color: rgba(0,0,0,0.54);
+ }
+ }
+ &.tb-focused:not(.tb-readonly) {
+ div>div:first-child:after {
+ color: rgb(221,44,0);
+ }
+ }
+}
+
+label.tb-label {
+ color: rgba(0,0,0,0.54);
+ -webkit-font-smoothing: antialiased;
+ position: absolute;
+ bottom: 100%;
+ left: 0;
+ right: auto;
+ @include transform(translate3d(0, $input-label-float-offset, 0) scale($input-label-float-scale));
+ @include transition(transform $swift-ease-out-timing-function $swift-ease-out-duration,
+ width $swift-ease-out-timing-function $swift-ease-out-duration);
+ transform-origin: left top;
+
+ &.tb-focused {
+ color: rgb(96,125,139);
+ }
+
+ &.tb-required:after {
+ content: ' *';
+ font-size: 13px;
+ vertical-align: top;
+ color: rgba(0,0,0,0.54);
+ }
+
+ &.tb-focused:not(.tb-readonly):after {
+ color: rgb(221,44,0);
+ }
+}
+
+.tb-head-label {
+ color: rgba(0,0,0,0.54);
+}
ui/src/app/components/react/json-form-ace-editor.jsx 136(+136 -0)
diff --git a/ui/src/app/components/react/json-form-ace-editor.jsx b/ui/src/app/components/react/json-form-ace-editor.jsx
new file mode 100644
index 0000000..0bfc77f
--- /dev/null
+++ b/ui/src/app/components/react/json-form-ace-editor.jsx
@@ -0,0 +1,136 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './json-form-ace-editor.scss';
+
+import React from 'react';
+import ThingsboardBaseComponent from './json-form-base-component.jsx';
+import reactCSS from 'reactcss';
+import AceEditor from 'react-ace';
+import FlatButton from 'material-ui/FlatButton';
+import 'brace/ext/language_tools';
+import 'brace/theme/github';
+
+class ThingsboardAceEditor extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.onValueChanged = this.onValueChanged.bind(this);
+ this.onBlur = this.onBlur.bind(this);
+ this.onFocus = this.onFocus.bind(this);
+ this.onTidy = this.onTidy.bind(this);
+ var value = props.value ? props.value + '' : '';
+ this.state = {
+ value: value,
+ focused: false
+ };
+ }
+
+ onValueChanged(value) {
+ this.setState({
+ value: value
+ });
+ this.props.onChangeValidate({
+ target: {
+ value: value
+ }
+ });
+ }
+
+ onBlur() {
+ this.setState({ focused: false })
+ }
+
+ onFocus() {
+ this.setState({ focused: true })
+ }
+
+ onTidy() {
+ if (!this.props.form.readonly) {
+ var value = this.state.value;
+ value = this.props.onTidy(value);
+ this.setState({
+ value: value
+ })
+ this.props.onChangeValidate({
+ target: {
+ value: value
+ }
+ });
+ }
+ }
+
+ render() {
+
+ const styles = reactCSS({
+ 'default': {
+ tidyButtonStyle: {
+ color: '#7B7B7B',
+ minWidth: '32px',
+ minHeight: '15px',
+ lineHeight: '15px',
+ fontSize: '0.800rem',
+ margin: '0',
+ padding: '4px',
+ height: '23px',
+ borderRadius: '5px',
+ marginLeft: '5px'
+ }
+ }
+ });
+
+ var labelClass = "tb-label";
+ if (this.props.form.required) {
+ labelClass += " tb-required";
+ }
+ if (this.props.form.readonly) {
+ labelClass += " tb-readonly";
+ }
+ if (this.state.focused) {
+ labelClass += " tb-focused";
+ }
+
+ return (
+ <div className="tb-container">
+ <label className={labelClass}>{this.props.form.title}</label>
+ <div className="json-form-ace-editor">
+ <div className="title-panel">
+ <label>{this.props.mode}</label>
+ <FlatButton style={ styles.tidyButtonStyle } className="tidy-button" label={'Tidy'} onTouchTap={this.onTidy}/>
+ </div>
+ <AceEditor mode={this.props.mode}
+ height="150px"
+ width="300px"
+ theme="github"
+ onChange={this.onValueChanged}
+ onFocus={this.onFocus}
+ onBlur={this.onBlur}
+ name={this.props.form.title}
+ value={this.state.value}
+ readOnly={this.props.form.readonly}
+ editorProps={{$blockScrolling: true}}
+ enableBasicAutocompletion={true}
+ enableSnippets={true}
+ enableLiveAutocompletion={true}
+ style={this.props.form.style || {width: '100%'}}/>
+ </div>
+ <div className="json-form-error"
+ style={{opacity: this.props.valid ? '0' : '1'}}>{this.props.error}</div>
+ </div>
+ );
+ }
+}
+
+export default ThingsboardBaseComponent(ThingsboardAceEditor);
diff --git a/ui/src/app/components/react/json-form-ace-editor.scss b/ui/src/app/components/react/json-form-ace-editor.scss
new file mode 100644
index 0000000..8ee48a3
--- /dev/null
+++ b/ui/src/app/components/react/json-form-ace-editor.scss
@@ -0,0 +1,42 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+.json-form-ace-editor {
+ position: relative;
+ border: 1px solid #C0C0C0;
+ height: 100%;
+ .title-panel {
+ position: absolute;
+ font-size: 0.800rem;
+ font-weight: 500;
+ top: 10px;
+ right: 20px;
+ z-index: 5;
+ label {
+ color: #00acc1;
+ background: rgba(220, 220, 220, 0.35);
+ border-radius: 5px;
+ padding: 4px;
+ text-transform: uppercase;
+ }
+ button.tidy-button {
+ background: rgba(220, 220, 220, 0.35) !important;
+ span {
+ padding: 0px !important;
+ font-size: 12px !important;
+ }
+ }
+ }
+}
\ No newline at end of file
ui/src/app/components/react/json-form-array.jsx 165(+165 -0)
diff --git a/ui/src/app/components/react/json-form-array.jsx b/ui/src/app/components/react/json-form-array.jsx
new file mode 100644
index 0000000..6b380ef
--- /dev/null
+++ b/ui/src/app/components/react/json-form-array.jsx
@@ -0,0 +1,165 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import { utils } from 'react-schema-form';
+import ThingsboardBaseComponent from './json-form-base-component.jsx';
+import RaisedButton from 'material-ui/RaisedButton';
+import FontIcon from 'material-ui/FontIcon';
+import _ from 'lodash';
+import IconButton from 'material-ui/IconButton';
+
+class ThingsboardArray extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.onAppend = this.onAppend.bind(this);
+ this.onDelete = this.onDelete.bind(this);
+ var model = utils.selectOrSet(this.props.form.key, this.props.model) || [];
+ var keys = [];
+ for (var i=0;i<model.length;i++) {
+ keys.push(i);
+ }
+ this.state = {
+ model: model,
+ keys: keys
+ };
+ }
+
+ componentDidMount() {
+ if(this.props.form.startEmpty !== true && this.state.model.length === 0) {
+ this.onAppend();
+ }
+ }
+
+ onAppend() {
+ var empty;
+ if(this.props.form && this.props.form.schema && this.props.form.schema.items) {
+ var items = this.props.form.schema.items;
+ if (items.type && items.type.indexOf('object') !== -1) {
+ empty = {};
+ if (!this.props.options || this.props.options.setSchemaDefaults !== false) {
+ empty = typeof items['default'] !== 'undefined' ? items['default'] : empty;
+ if (empty) {
+ utils.traverseSchema(items, function(prop, path) {
+ if (typeof prop['default'] !== 'undefined') {
+ utils.selectOrSet(path, empty, prop['default']);
+ }
+ });
+ }
+ }
+
+ } else if (items.type && items.type.indexOf('array') !== -1) {
+ empty = [];
+ if (!this.props.options || this.props.options.setSchemaDefaults !== false) {
+ empty = items['default'] || empty;
+ }
+ } else {
+ if (!this.props.options || this.props.options.setSchemaDefaults !== false) {
+ empty = items['default'] || empty;
+ }
+ }
+ }
+ var newModel = this.state.model;
+ newModel.push(empty);
+ var newKeys = this.state.keys;
+ var key = 0;
+ if (newKeys.length > 0) {
+ key = newKeys[newKeys.length-1]+1;
+ }
+ newKeys.push(key);
+ this.setState({
+ model: newModel,
+ keys: newKeys
+ }
+ );
+ this.props.onChangeValidate(this.state.model);
+ }
+
+ onDelete(index) {
+ var newModel = this.state.model;
+ newModel.splice(index, 1);
+ var newKeys = this.state.keys;
+ newKeys.splice(index, 1);
+ this.setState(
+ {
+ model: newModel,
+ keys: newKeys
+ }
+ );
+ this.props.onChangeValidate(this.state.model);
+ }
+
+ setIndex(index) {
+ return function(form) {
+ if (form.key) {
+ form.key[form.key.indexOf('')] = index;
+ }
+ };
+ };
+
+ copyWithIndex(form, index) {
+ var copy = _.cloneDeep(form);
+ copy.arrayIndex = index;
+ utils.traverseForm(copy, this.setIndex(index));
+ return copy;
+ };
+
+ render() {
+ var arrays = [];
+ var fields = [];
+ var model = this.state.model;
+ var keys = this.state.keys;
+ var items = this.props.form.items;
+ for(var i = 0; i < model.length; i++ ) {
+ let removeButton = '';
+ if (!this.props.form.readonly) {
+ let boundOnDelete = this.onDelete.bind(this, i)
+ removeButton = <IconButton iconClassName="material-icons" tooltip="Remove" onTouchTap={boundOnDelete}>clear</IconButton>
+ }
+ let forms = this.props.form.items.map(function(form, index){
+ var copy = this.copyWithIndex(form, i);
+ return this.props.builder(copy, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.mapper, this.props.builder);
+ }.bind(this));
+ arrays.push(
+ <li key={keys[i]} className="list-group-item">
+ {removeButton}
+ {forms}
+ </li>
+ );
+ }
+ let addButton = '';
+ if (!this.props.form.readonly) {
+ addButton = <RaisedButton label={this.props.form.add || 'New'}
+ primary={true}
+ icon={<FontIcon className="material-icons">add</FontIcon>}
+ onTouchTap={this.onAppend}></RaisedButton>;
+ }
+
+ return (
+ <div>
+ <div className="tb-container">
+ <div className="tb-head-label">{this.props.form.title}</div>
+ <ol className="list-group">
+ {arrays}
+ </ol>
+ </div>
+ {addButton}
+ </div>
+ );
+ }
+}
+
+export default ThingsboardBaseComponent(ThingsboardArray);
diff --git a/ui/src/app/components/react/json-form-base-component.jsx b/ui/src/app/components/react/json-form-base-component.jsx
new file mode 100644
index 0000000..8afbe68
--- /dev/null
+++ b/ui/src/app/components/react/json-form-base-component.jsx
@@ -0,0 +1,117 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import { utils } from 'react-schema-form';
+
+export default ThingsboardBaseComponent => class extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.onChangeValidate = this.onChangeValidate.bind(this);
+ let value = this.defaultValue();
+ let validationResult = utils.validate(this.props.form, value);
+ this.state = {
+ value: value,
+ valid: !!(validationResult.valid || !value),
+ error: !validationResult.valid && value ? validationResult.error.message : null
+ };
+ }
+
+ componentDidMount() {
+ if (typeof this.state.value !== 'undefined') {
+ this.props.onChange(this.props.form.key, this.state.value);
+ }
+ }
+
+ onChangeValidate(e) {
+ let value = null;
+ if (this.props.form.schema.type === 'integer' || this.props.form.schema.type === 'number') {
+ if (e.target.value === null || e.target.value === '') {
+ value = undefined;
+ } else if (e.target.value.indexOf('.') == -1) {
+ value = parseInt(e.target.value);
+ } else {
+ value = parseFloat(e.target.value);
+ }
+ } else if(this.props.form.schema.type === 'boolean') {
+ value = e.target.checked;
+ } else if(this.props.form.schema.type === 'date' || this.props.form.schema.type === 'array') {
+ value = e;
+ } else { // string
+ value = e.target.value;
+ }
+ let validationResult = utils.validate(this.props.form, value);
+ this.setState({
+ value: value,
+ valid: validationResult.valid,
+ error: validationResult.valid ? null : validationResult.error.message
+ });
+ this.props.onChange(this.props.form.key, value);
+ }
+
+ defaultValue() {
+ let value = utils.selectOrSet(this.props.form.key, this.props.model);
+ if (this.props.form.schema.type === 'boolean') {
+ if (typeof value !== 'boolean' && this.props.form['default']) {
+ value = this.props.form['default'];
+ }
+ if (typeof value !== 'boolean' && this.props.form.schema && this.props.form.schema['default']) {
+ value = this.props.form.schema['default'];
+ }
+ if (typeof value !== 'boolean' &&
+ this.props.form.schema &&
+ this.props.form.required) {
+ value = false;
+ }
+ } else if (this.props.form.schema.type === 'integer' || this.props.form.schema.type === 'number') {
+ if (typeof value !== 'number' && this.props.form['default']) {
+ value = this.props.form['default'];
+ }
+ if (typeof value !== 'number' && this.props.form.schema && this.props.form.schema['default']) {
+ value = this.props.form.schema['default'];
+ }
+ if (typeof value !== 'number' && this.props.form.titleMap && this.props.form.titleMap[0].value) {
+ value = this.props.form.titleMap[0].value;
+ }
+ if (value && typeof value === 'string') {
+ if (value.indexOf('.') == -1) {
+ value = parseInt(value);
+ } else {
+ value = parseFloat(value);
+ }
+ }
+ } else {
+ if(!value && this.props.form['default']) {
+ value = this.props.form['default'];
+ }
+ if(!value && this.props.form.schema && this.props.form.schema['default']) {
+ value = this.props.form.schema['default'];
+ }
+ if(!value && this.props.form.titleMap && this.props.form.titleMap[0].value) {
+ value = this.props.form.titleMap[0].value;
+ }
+ }
+ return value;
+ }
+
+ render() {
+ if (this.props.form && this.props.form.schema) {
+ return <ThingsboardBaseComponent {...this.props} {...this.state} onChangeValidate={this.onChangeValidate}/>;
+ } else {
+ return <div></div>;
+ }
+ }
+};
diff --git a/ui/src/app/components/react/json-form-checkbox.jsx b/ui/src/app/components/react/json-form-checkbox.jsx
new file mode 100644
index 0000000..748b0a2
--- /dev/null
+++ b/ui/src/app/components/react/json-form-checkbox.jsx
@@ -0,0 +1,36 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import ThingsboardBaseComponent from './json-form-base-component.jsx';
+import Checkbox from 'material-ui/Checkbox';
+
+class ThingsboardCheckbox extends React.Component {
+ render() {
+ return (
+ <Checkbox
+ name={this.props.form.key.slice(-1)[0]}
+ value={this.props.form.key.slice(-1)[0]}
+ defaultChecked={this.props.value || false}
+ label={this.props.form.title}
+ disabled={this.props.form.readonly}
+ onCheck={(e, checked) => {this.props.onChangeValidate(e)}}
+ style={{paddingTop: '20px'}}
+ />
+ );
+ }
+}
+
+export default ThingsboardBaseComponent(ThingsboardCheckbox);
\ No newline at end of file
ui/src/app/components/react/json-form-color.jsx 161(+161 -0)
diff --git a/ui/src/app/components/react/json-form-color.jsx b/ui/src/app/components/react/json-form-color.jsx
new file mode 100644
index 0000000..7980a54
--- /dev/null
+++ b/ui/src/app/components/react/json-form-color.jsx
@@ -0,0 +1,161 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './json-form-color.scss';
+
+import $ from 'jquery';
+import React from 'react';
+import ReactDOM from 'react-dom';
+import ThingsboardBaseComponent from './json-form-base-component.jsx';
+import reactCSS from 'reactcss';
+import tinycolor from 'tinycolor2';
+import TextField from 'material-ui/TextField';
+import IconButton from 'material-ui/IconButton';
+
+class ThingsboardColor extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.onValueChanged = this.onValueChanged.bind(this);
+ this.onSwatchClick = this.onSwatchClick.bind(this);
+ this.onClear = this.onClear.bind(this);
+ var value = props.value ? props.value + '' : null;
+ var color = value != null ? tinycolor(value).toRgb() : null;
+ this.state = {
+ color: color
+ };
+ }
+
+ componentDidMount() {
+ var node = ReactDOM.findDOMNode(this);
+ var colContainer = $(node).children('#color-container');
+ colContainer.click(this, function(event) {
+ if (!event.data.props.form.readonly) {
+ event.data.onSwatchClick(event);
+ }
+ });
+ }
+
+ componentWillUnmount () {
+ var node = ReactDOM.findDOMNode(this);
+ var colContainer = $(node).children('#color-container');
+ colContainer.off( "click" );
+ }
+
+ onValueChanged(value) {
+ var color = null;
+ if (value != null) {
+ color = tinycolor(value);
+ }
+ this.setState({
+ color: value
+ })
+ var colorValue = '';
+ if (color != null && color.getAlpha() != 1) {
+ colorValue = color.toRgbString();
+ } else if (color != null) {
+ colorValue = color.toHexString();
+ }
+ this.props.onChangeValidate({
+ target: {
+ value: colorValue
+ }
+ });
+ }
+
+ onSwatchClick(event) {
+ this.props.onColorClick(event, this.props.form.key, this.state.color);
+ }
+
+ onClear(event) {
+ if (event) {
+ event.stopPropagation();
+ }
+ this.onValueChanged(null);
+ }
+
+ render() {
+
+ var background = 'rgba(0,0,0,0)';
+ if (this.state.color != null) {
+ background = `rgba(${ this.state.color.r }, ${ this.state.color.g }, ${ this.state.color.b }, ${ this.state.color.a })`;
+ }
+
+ const styles = reactCSS({
+ 'default': {
+ color: {
+ background: `${ background }`
+ },
+ swatch: {
+ display: 'inline-block',
+ marginRight: '10px',
+ marginTop: 'auto',
+ marginBottom: 'auto',
+ cursor: 'pointer',
+ opacity: `${ this.props.form.readonly ? '0.6' : '1' }`
+ },
+ swatchText: {
+ display: 'inline-block',
+ width: '100%'
+ },
+ container: {
+ display: 'flex'
+ },
+ colorContainer: {
+ display: 'flex',
+ width: '100%'
+ }
+ },
+ });
+
+ var fieldClass = "tb-field";
+ if (this.props.form.required) {
+ fieldClass += " tb-required";
+ }
+ if (this.props.form.readonly) {
+ fieldClass += " tb-readonly";
+ }
+ if (this.state.focused) {
+ fieldClass += " tb-focused";
+ }
+
+ var stringColor = '';
+ if (this.state.color != null) {
+ var color = tinycolor(this.state.color);
+ stringColor = color.toRgbString();
+ }
+
+ return (
+ <div style={ styles.container }>
+ <div id="color-container" style={ styles.colorContainer }>
+ <div className="tb-color-preview" style={ styles.swatch }>
+ <div className="tb-color-result" style={ styles.color }/>
+ </div>
+ <TextField
+ className={fieldClass}
+ floatingLabelText={this.props.form.title}
+ hintText={this.props.form.placeholder}
+ errorText={this.props.error}
+ value={stringColor}
+ disabled={this.props.form.readonly}
+ style={ styles.swatchText } />
+ </div>
+ <IconButton iconClassName="material-icons" tooltip="Clear" onTouchTap={this.onClear}>clear</IconButton>
+ </div>
+ );
+ }
+}
+
+export default ThingsboardBaseComponent(ThingsboardColor);
diff --git a/ui/src/app/components/react/json-form-color.scss b/ui/src/app/components/react/json-form-color.scss
new file mode 100644
index 0000000..60af66f
--- /dev/null
+++ b/ui/src/app/components/react/json-form-color.scss
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@mixin tb-checkered-bg() {
+ background-color: #fff;
+ background-image: linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%, #ddd),
+ linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%, #ddd);
+ background-size: 8px 8px;
+ background-position: 0 0, 4px 4px;
+}
+
+.tb-color-preview {
+ content: '';
+ width: 24px;
+ height: 24px;
+ border: 2px solid #fff;
+ border-radius: 50%;
+ box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .14), 0 2px 2px 0 rgba(0, 0, 0, .098), 0 1px 5px 0 rgba(0, 0, 0, .084);
+ position: relative;
+ overflow: hidden;
+ @include tb-checkered-bg();
+
+ .tb-color-result {
+ width: 100%;
+ height: 100%;
+ }
+}
diff --git a/ui/src/app/components/react/json-form-date.jsx b/ui/src/app/components/react/json-form-date.jsx
new file mode 100644
index 0000000..ff8fc70
--- /dev/null
+++ b/ui/src/app/components/react/json-form-date.jsx
@@ -0,0 +1,60 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import ThingsboardBaseComponent from './json-form-base-component.jsx';
+import DatePicker from 'material-ui/DatePicker/DatePicker';
+
+class ThingsboardDate extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.onDatePicked = this.onDatePicked.bind(this);
+ }
+
+
+ onDatePicked(empty, date) {
+ this.props.onChangeValidate(date);
+ }
+
+ render() {
+
+ var fieldClass = "tb-date-field";
+ if (this.props.form.required) {
+ fieldClass += " tb-required";
+ }
+ if (this.props.form.readonly) {
+ fieldClass += " tb-readonly";
+ }
+
+ return (
+ <div style={{width: '100%', display: 'block'}}>
+ <DatePicker
+ className={fieldClass}
+ mode={'landscape'}
+ autoOk={true}
+ hintText={this.props.form.title}
+ onChange={this.onDatePicked}
+ onShow={null}
+ onDismiss={null}
+ disabled={this.props.form.readonly}
+ style={this.props.form.style || {width: '100%'}}/>
+
+ </div>
+ );
+ }
+}
+
+export default ThingsboardBaseComponent(ThingsboardDate);
\ No newline at end of file
diff --git a/ui/src/app/components/react/json-form-fieldset.jsx b/ui/src/app/components/react/json-form-fieldset.jsx
new file mode 100644
index 0000000..c12098c
--- /dev/null
+++ b/ui/src/app/components/react/json-form-fieldset.jsx
@@ -0,0 +1,38 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+
+class ThingsboardFieldSet extends React.Component {
+
+ render() {
+ let forms = this.props.form.items.map(function(form, index){
+ return this.props.builder(form, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.mapper, this.props.builder);
+ }.bind(this));
+
+ return (
+ <div style={{paddingTop: '20px'}}>
+ <div className="tb-head-label">
+ {this.props.form.title}
+ </div>
+ <div>
+ {forms}
+ </div>
+ </div>
+ );
+ }
+}
+
+export default ThingsboardFieldSet;
diff --git a/ui/src/app/components/react/json-form-javascript.jsx b/ui/src/app/components/react/json-form-javascript.jsx
new file mode 100644
index 0000000..b808fc6
--- /dev/null
+++ b/ui/src/app/components/react/json-form-javascript.jsx
@@ -0,0 +1,41 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import ThingsboardAceEditor from './json-form-ace-editor.jsx';
+import 'brace/mode/javascript';
+import beautify from 'js-beautify';
+
+const js_beautify = beautify.js;
+
+class ThingsboardJavaScript extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.onTidyJavascript = this.onTidyJavascript.bind(this);
+ }
+
+ onTidyJavascript(javascript) {
+ return js_beautify(javascript, {indent_size: 4, wrap_line_length: 60});
+ }
+
+ render() {
+ return (
+ <ThingsboardAceEditor {...this.props} mode='javascript' onTidy={this.onTidyJavascript} {...this.state}></ThingsboardAceEditor>
+ );
+ }
+}
+
+export default ThingsboardJavaScript;
diff --git a/ui/src/app/components/react/json-form-json.jsx b/ui/src/app/components/react/json-form-json.jsx
new file mode 100644
index 0000000..91be72e
--- /dev/null
+++ b/ui/src/app/components/react/json-form-json.jsx
@@ -0,0 +1,41 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import ThingsboardAceEditor from './json-form-ace-editor.jsx';
+import 'brace/mode/json';
+import beautify from 'js-beautify';
+
+const js_beautify = beautify.js;
+
+class ThingsboardJson extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.onTidyJson = this.onTidyJson.bind(this);
+ }
+
+ onTidyJson(json) {
+ return js_beautify(json, {indent_size: 4});
+ }
+
+ render() {
+ return (
+ <ThingsboardAceEditor {...this.props} mode='json' onTidy={this.onTidyJson} {...this.state}></ThingsboardAceEditor>
+ );
+ }
+}
+
+export default ThingsboardJson;
diff --git a/ui/src/app/components/react/json-form-number.jsx b/ui/src/app/components/react/json-form-number.jsx
new file mode 100644
index 0000000..1922ca2
--- /dev/null
+++ b/ui/src/app/components/react/json-form-number.jsx
@@ -0,0 +1,85 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import ThingsboardBaseComponent from './json-form-base-component.jsx';
+import TextField from 'material-ui/TextField';
+
+class ThingsboardNumber extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.preValidationCheck = this.preValidationCheck.bind(this);
+ this.onBlur = this.onBlur.bind(this);
+ this.onFocus = this.onFocus.bind(this);
+ this.state = {
+ lastSuccessfulValue : this.props.value,
+ focused: false
+ }
+ }
+
+ isNumeric(n) {
+ return n === null || n === '' || !isNaN(n) && isFinite(n);
+ }
+
+ onBlur() {
+ this.setState({ focused: false })
+ }
+
+ onFocus() {
+ this.setState({ focused: true })
+ }
+
+ preValidationCheck(e) {
+ if (this.isNumeric(e.target.value)) {
+ this.setState({
+ lastSuccessfulValue: e.target.value
+ });
+ this.props.onChangeValidate(e);
+ }
+ }
+
+ render() {
+
+ var fieldClass = "tb-field";
+ if (this.props.form.required) {
+ fieldClass += " tb-required";
+ }
+ if (this.props.form.readonly) {
+ fieldClass += " tb-readonly";
+ }
+ if (this.state.focused) {
+ fieldClass += " tb-focused";
+ }
+
+ return (
+ <TextField
+ className={fieldClass}
+ type={this.props.form.type}
+ floatingLabelText={this.props.form.title}
+ hintText={this.props.form.placeholder}
+ errorText={this.props.error}
+ onChange={this.preValidationCheck}
+ defaultValue={this.state.lastSuccessfulValue}
+ ref="numberField"
+ disabled={this.props.form.readonly}
+ onFocus={this.onFocus}
+ onBlur={this.onBlur}
+ style={this.props.form.style || {width: '100%'}}/>
+ );
+ }
+}
+
+export default ThingsboardBaseComponent(ThingsboardNumber);
\ No newline at end of file
ui/src/app/components/react/json-form-rc-select.jsx 123(+123 -0)
diff --git a/ui/src/app/components/react/json-form-rc-select.jsx b/ui/src/app/components/react/json-form-rc-select.jsx
new file mode 100644
index 0000000..d9eb09f
--- /dev/null
+++ b/ui/src/app/components/react/json-form-rc-select.jsx
@@ -0,0 +1,123 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import 'rc-select/assets/index.css';
+
+import React from 'react';
+import ThingsboardBaseComponent from './json-form-base-component.jsx';
+import Select, {Option} from 'rc-select';
+
+class ThingsboardRcSelect extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.onSelect = this.onSelect.bind(this);
+ this.onDeselect = this.onDeselect.bind(this);
+ this.onBlur = this.onBlur.bind(this);
+ this.onFocus = this.onFocus.bind(this);
+ let emptyValue = this.props.form.schema.type === 'array'? [] : null;
+ this.state = {
+ currentValue: this.props.value || emptyValue,
+ items: this.props.form.items,
+ focused: false
+ };
+ }
+
+ onSelect(value, option) {
+ if(this.props.form.schema.type === 'array') {
+ let v = this.state.currentValue;
+ v.push(value);
+ this.setState({
+ currentValue: v
+ });
+ this.props.onChangeValidate(v);
+ } else {
+ this.setState({currentValue: value});
+ this.props.onChangeValidate({target: {value: value}});
+ }
+ }
+
+ onDeselect(value, option) {
+ if (this.props.form.schema.type === 'array') {
+ let v = this.state.currentValue;
+ let index = v.indexOf(value);
+ if (index > -1) {
+ v.splice(index, 1);
+ }
+ this.setState({
+ currentValue: v
+ });
+ this.props.onChangeValidate(v);
+ }
+ }
+
+ onBlur() {
+ this.setState({ focused: false })
+ }
+
+ onFocus() {
+ this.setState({ focused: true })
+ }
+
+ render() {
+ let options = [];
+ if(this.state.items && this.state.items.length > 0) {
+ options = this.state.items.map((item, idx) => (
+ <Option key={idx} value={item.value}>{item.label}</Option>
+ ));
+ }
+
+ var labelClass = "tb-label";
+ if (this.props.form.required) {
+ labelClass += " tb-required";
+ }
+ if (this.props.form.readonly) {
+ labelClass += " tb-readonly";
+ }
+ if (this.state.focused) {
+ labelClass += " tb-focused";
+ }
+
+ return (
+ <div className="tb-container">
+ <label className={labelClass}>{this.props.form.title}</label>
+ <Select
+ className={this.props.form.className}
+ dropdownClassName={this.props.form.dropdownClassName}
+ dropdownStyle={this.props.form.dropdownStyle}
+ dropdownMenuStyle={this.props.form.dropdownMenuStyle}
+ allowClear={this.props.form.allowClear}
+ tags={this.props.form.tags}
+ maxTagTextLength={this.props.form.maxTagTextLength}
+ multiple={this.props.form.multiple}
+ combobox={this.props.form.combobox}
+ disabled={this.props.form.readonly}
+ value={this.state.currentValue}
+ onSelect={this.onSelect}
+ onDeselect={this.onDeselect}
+ onFocus={this.onFocus}
+ onBlur={this.onBlur}
+ style={this.props.form.style || {width: "100%"}}>
+ {options}
+ </Select>
+ <div className="json-form-error"
+ style={{opacity: this.props.valid ? '0' : '1',
+ bottom: '-5px'}}>{this.props.error}</div>
+ </div>
+ );
+ }
+}
+
+export default ThingsboardBaseComponent(ThingsboardRcSelect);
diff --git a/ui/src/app/components/react/json-form-react.jsx b/ui/src/app/components/react/json-form-react.jsx
new file mode 100644
index 0000000..fbccd8e
--- /dev/null
+++ b/ui/src/app/components/react/json-form-react.jsx
@@ -0,0 +1,65 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './json-form.scss';
+
+import React from 'react';
+import getMuiTheme from 'material-ui/styles/getMuiTheme';
+import thingsboardTheme from './styles/thingsboardTheme';
+import ThingsboardSchemaForm from './json-form-schema-form.jsx';
+
+class ReactSchemaForm extends React.Component {
+
+ getChildContext() {
+ return {
+ muiTheme: this.state.muiTheme
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ muiTheme: getMuiTheme(thingsboardTheme)
+ };
+ }
+
+ render () {
+ if (this.props.form.length > 0) {
+ return <ThingsboardSchemaForm {...this.props} />;
+ } else {
+ return <div></div>;
+ }
+ }
+}
+
+ReactSchemaForm.propTypes = {
+ schema: React.PropTypes.object,
+ form: React.PropTypes.array,
+ model: React.PropTypes.object,
+ option: React.PropTypes.object,
+ onModelChange: React.PropTypes.func,
+ onColorClick: React.PropTypes.func
+}
+
+ReactSchemaForm.defaultProps = {
+ schema: {},
+ form: [ "*" ]
+}
+
+ReactSchemaForm.childContextTypes = {
+ muiTheme: React.PropTypes.object
+}
+
+export default ReactSchemaForm;
diff --git a/ui/src/app/components/react/json-form-schema-form.jsx b/ui/src/app/components/react/json-form-schema-form.jsx
new file mode 100644
index 0000000..3e1c046
--- /dev/null
+++ b/ui/src/app/components/react/json-form-schema-form.jsx
@@ -0,0 +1,99 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import { utils } from 'react-schema-form';
+
+import ThingsboardArray from './json-form-array.jsx';
+import ThingsboardJavaScript from './json-form-javascript.jsx';
+import ThingsboardJson from './json-form-json.jsx';
+import ThingsboardColor from './json-form-color.jsx'
+import ThingsboardRcSelect from './json-form-rc-select.jsx';
+import ThingsboardNumber from './json-form-number.jsx';
+import ThingsboardText from './json-form-text.jsx';
+import Select from 'react-schema-form/lib/Select';
+import Radios from 'react-schema-form/lib/Radios';
+import ThingsboardDate from './json-form-date.jsx';
+import ThingsboardCheckbox from './json-form-checkbox.jsx';
+import Help from 'react-schema-form/lib/Help';
+import ThingsboardFieldSet from './json-form-fieldset.jsx';
+
+import _ from 'lodash';
+
+class ThingsboardSchemaForm extends React.Component {
+
+ constructor(props) {
+ super(props);
+
+ this.mapper = {
+ 'number': ThingsboardNumber,
+ 'text': ThingsboardText,
+ 'password': ThingsboardText,
+ 'textarea': ThingsboardText,
+ 'select': Select,
+ 'radios': Radios,
+ 'date': ThingsboardDate,
+ 'checkbox': ThingsboardCheckbox,
+ 'help': Help,
+ 'array': ThingsboardArray,
+ 'javascript': ThingsboardJavaScript,
+ 'json': ThingsboardJson,
+ 'color': ThingsboardColor,
+ 'rc-select': ThingsboardRcSelect,
+ 'fieldset': ThingsboardFieldSet
+ };
+
+ this.onChange = this.onChange.bind(this);
+ this.onColorClick = this.onColorClick.bind(this);
+ }
+
+ onChange(key, val) {
+ //console.log('SchemaForm.onChange', key, val);
+ this.props.onModelChange(key, val);
+ }
+
+ onColorClick(event, key, val) {
+ this.props.onColorClick(event, key, val);
+ }
+
+ builder(form, model, index, onChange, onColorClick, mapper) {
+ var type = form.type;
+ let Field = this.mapper[type];
+ if(!Field) {
+ console.log('Invalid field: \"' + form.key[0] + '\"!');
+ return null;
+ }
+ if(form.condition && eval(form.condition) === false) {
+ return null;
+ }
+ return <Field model={model} form={form} key={index} onChange={onChange} onColorClick={onColorClick} mapper={mapper} builder={this.builder}/>
+ }
+
+ render() {
+ let merged = utils.merge(this.props.schema, this.props.form, this.props.ignore, this.props.option);
+ let mapper = this.mapper;
+ if(this.props.mapper) {
+ mapper = _.merge(this.mapper, this.props.mapper);
+ }
+ let forms = merged.map(function(form, index) {
+ return this.builder(form, this.props.model, index, this.onChange, this.onColorClick, mapper);
+ }.bind(this));
+
+ return (
+ <div style={{width: '100%'}} className='SchemaForm'>{forms}</div>
+ );
+ }
+}
+export default ThingsboardSchemaForm;
diff --git a/ui/src/app/components/react/json-form-text.jsx b/ui/src/app/components/react/json-form-text.jsx
new file mode 100644
index 0000000..6037dc2
--- /dev/null
+++ b/ui/src/app/components/react/json-form-text.jsx
@@ -0,0 +1,79 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import ThingsboardBaseComponent from './json-form-base-component.jsx';
+import TextField from 'material-ui/TextField';
+
+class ThingsboardText extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.onBlur = this.onBlur.bind(this);
+ this.onFocus = this.onFocus.bind(this);
+ this.state = {
+ focused: false
+ };
+ }
+
+ onBlur() {
+ this.setState({ focused: false })
+ }
+
+ onFocus() {
+ this.setState({ focused: true })
+ }
+
+ render() {
+
+ var fieldClass = "tb-field";
+ if (this.props.form.required) {
+ fieldClass += " tb-required";
+ }
+ if (this.props.form.readonly) {
+ fieldClass += " tb-readonly";
+ }
+ if (this.state.focused) {
+ fieldClass += " tb-focused";
+ }
+
+ var multiline = this.props.form.type === 'textarea';
+ var rows = multiline ? this.props.form.rows : 1;
+ var rowsMax = multiline ? this.props.form.rowsMax : 1;
+
+ return (
+ <div>
+ <TextField
+ className={fieldClass}
+ type={this.props.form.type}
+ floatingLabelText={this.props.form.title}
+ hintText={this.props.form.placeholder}
+ errorText={this.props.error}
+ onChange={this.props.onChangeValidate}
+ defaultValue={this.props.value}
+ disabled={this.props.form.readonly}
+ multiLine={multiline}
+ rows={rows}
+ rowsMax={rowsMax}
+ onFocus={this.onFocus}
+ onBlur={this.onBlur}
+ style={this.props.form.style || {width: '100%'}} />
+ </div>
+ );
+ }
+}
+
+export default ThingsboardBaseComponent(ThingsboardText);
\ No newline at end of file
diff --git a/ui/src/app/components/react/styles/thingsboardTheme.js b/ui/src/app/components/react/styles/thingsboardTheme.js
new file mode 100644
index 0000000..7ed06f4
--- /dev/null
+++ b/ui/src/app/components/react/styles/thingsboardTheme.js
@@ -0,0 +1,63 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { /*blueGrey500, blueGrey700, blueGrey100, orange500,*/
+ grey100, grey500, grey900, grey600, white, grey400, darkBlack, cyan500, fullBlack/*, indigo500*/, indigo700, indigo100, deepOrange500 } from 'material-ui/styles/colors';
+import {fade} from 'material-ui/utils/colorManipulator';
+import spacing from 'material-ui/styles/spacing';
+
+const PRIMARY_BACKGROUND_COLOR = "#305680";//"#3f51b5";
+
+/*var blueGrayPalette = {
+ primary1Color: blueGrey500,
+ primary2Color: blueGrey700,
+ primary3Color: blueGrey100,
+ accent1Color: orange500,
+ accent2Color: grey100,
+ accent3Color: grey500,
+ textColor: grey900,
+ secondaryTextColor: grey600,
+ alternateTextColor: white,
+ canvasColor: white,
+ borderColor: grey400,
+ disabledColor: fade(darkBlack, 0.3),
+ pickerHeaderColor: cyan500,
+ clockCircleColor: fade(darkBlack, 0.07),
+ shadowColor: fullBlack,
+};*/
+
+var indigoPalette = {
+ primary1Color: PRIMARY_BACKGROUND_COLOR,
+ primary2Color: indigo700,
+ primary3Color: indigo100,
+ accent1Color: deepOrange500,
+ accent2Color: grey100,
+ accent3Color: grey500,
+ textColor: grey900,
+ secondaryTextColor: grey600,
+ alternateTextColor: white,
+ canvasColor: white,
+ borderColor: grey400,
+ disabledColor: fade(darkBlack, 0.3),
+ pickerHeaderColor: cyan500,
+ clockCircleColor: fade(darkBlack, 0.07),
+ shadowColor: fullBlack,
+};
+
+export default {
+ spacing: spacing,
+ fontFamily: 'RobotoDraft, Roboto, \'Helvetica Neue\', sans-serif',
+ palette: indigoPalette,
+};
\ No newline at end of file
diff --git a/ui/src/app/components/scope-element.directive.js b/ui/src/app/components/scope-element.directive.js
new file mode 100644
index 0000000..73f11c3
--- /dev/null
+++ b/ui/src/app/components/scope-element.directive.js
@@ -0,0 +1,33 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export default angular.module('thingsboard.directives.scopeElement', [])
+ .directive('tbScopeElement', ScopeElement)
+ .name;
+
+/*@ngInject*/
+function ScopeElement() {
+ var directiveDefinitionObject = {
+ restrict: "A",
+ compile: function compile() {
+ return {
+ pre: function preLink(scope, iElement, iAttrs) {
+ scope[iAttrs.tbScopeElement] = iElement;
+ }
+ };
+ }
+ };
+ return directiveDefinitionObject;
+}
ui/src/app/components/side-menu.directive.js 50(+50 -0)
diff --git a/ui/src/app/components/side-menu.directive.js b/ui/src/app/components/side-menu.directive.js
new file mode 100644
index 0000000..9127198
--- /dev/null
+++ b/ui/src/app/components/side-menu.directive.js
@@ -0,0 +1,50 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './side-menu.scss';
+
+import thingsboardMenu from '../services/menu.service';
+import thingsboardMenuLink from './menu-link.directive';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import sidemenuTemplate from './side-menu.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+export default angular.module('thingsboard.directives.sideMenu', [thingsboardMenu, thingsboardMenuLink])
+ .directive('tbSideMenu', SideMenu)
+ .name;
+
+/*@ngInject*/
+function SideMenu($compile, $templateCache, menu) {
+
+ var linker = function (scope, element) {
+
+ scope.sections = menu.getSections();
+
+ var template = $templateCache.get(sidemenuTemplate);
+
+ element.html(template);
+
+ $compile(element.contents())(scope);
+ }
+
+ return {
+ restrict: "E",
+ link: linker,
+ scope: {}
+ };
+}
ui/src/app/components/side-menu.scss 84(+84 -0)
diff --git a/ui/src/app/components/side-menu.scss b/ui/src/app/components/side-menu.scss
new file mode 100644
index 0000000..9705523
--- /dev/null
+++ b/ui/src/app/components/side-menu.scss
@@ -0,0 +1,84 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import "~compass-sass-mixins/lib/compass";
+
+.tb-side-menu .md-button.tb-active {
+ background-color: rgba(255, 255, 255, 0.15);
+ font-weight: 500;
+}
+
+.tb-side-menu, .tb-side-menu ul {
+ list-style: none;
+ padding: 0;
+ margin-top: 0;
+}
+
+.tb-side-menu .tb-menu-toggle-list a.md-button {
+ padding: 0 16px 0 32px;
+ text-transform: none;
+ text-rendering: optimizeLegibility;
+ font-weight: 500;
+}
+
+.tb-side-menu .tb-menu-toggle-list .md-button {
+ padding: 0 16px 0 32px;
+ text-transform: none;
+}
+
+.tb-side-menu .tb-menu-toggle-list .tb-menu-toggle-list a.md-button {
+ padding: 0 16px 0 48px;
+}
+
+.tb-side-menu > li {
+ border-bottom: 1px solid rgba(0, 0, 0, 0.12);
+}
+
+.tb-side-menu .md-button-toggle .md-toggle-icon {
+ background-size: 100% auto;
+ display: inline-block;
+ margin: auto 0 auto auto;
+ width: 15px;
+ @include transition(transform .3s, ease-in-out);
+}
+
+.tb-side-menu .md-button {
+ display: block;
+ border-radius: 0;
+ color: inherit;
+ cursor: pointer;
+ line-height: 40px;
+ margin: 0;
+ max-height: 40px;
+ overflow: hidden;
+ padding: 0px 16px;
+ text-align: left;
+ text-decoration: none;
+ white-space: normal;
+ width: 100%;
+}
+
+.tb-side-menu tb-menu-link span.md-toggle-icon {
+ padding-top: 12px;
+ padding-bottom: 12px;
+}
+
+.tb-side-menu ng-md-icon {
+ margin-right: 8px;
+}
+
+.tb-side-menu md-icon {
+ margin-right: 8px;
+}
ui/src/app/components/side-menu.tpl.html 23(+23 -0)
diff --git a/ui/src/app/components/side-menu.tpl.html b/ui/src/app/components/side-menu.tpl.html
new file mode 100644
index 0000000..200fe4e
--- /dev/null
+++ b/ui/src/app/components/side-menu.tpl.html
@@ -0,0 +1,23 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<ul flex layout="column" layout-align="start stretch" class="tb-side-menu">
+ <li ng-repeat="section in sections" class="parent-list-item"
+ ng-class="{'parentActive' : isOpen(section)}">
+ <tb-menu-link section="section"></tb-menu-link>
+ </li>
+</ul>
\ No newline at end of file
ui/src/app/components/tb-event-directives.js 59(+59 -0)
diff --git a/ui/src/app/components/tb-event-directives.js b/ui/src/app/components/tb-event-directives.js
new file mode 100644
index 0000000..1fefd51
--- /dev/null
+++ b/ui/src/app/components/tb-event-directives.js
@@ -0,0 +1,59 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+const SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g;
+const MOZ_HACK_REGEXP = /^moz([A-Z])/;
+const PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i;
+
+var tbEventDirectives = {};
+
+angular.forEach(
+ 'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '),
+ function(eventName) {
+ var directiveName = directiveNormalize('tb-' + eventName);
+ tbEventDirectives[directiveName] = ['$parse', '$rootScope', function($parse) {
+ return {
+ restrict: 'A',
+ compile: function($element, attr) {
+ var fn = $parse(attr[directiveName], /* interceptorFn */ null, /* expensiveChecks */ true);
+ return function ngEventHandler(scope, element) {
+ element.on(eventName, function(event) {
+ var callback = function() {
+ fn(scope, {$event:event});
+ };
+ callback();
+ });
+ };
+ }
+ };
+ }];
+ }
+);
+
+export default angular.module('thingsboard.directives.event', [])
+ .directive(tbEventDirectives)
+ .name;
+
+function camelCase(name) {
+ return name.
+ replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) {
+ return offset ? letter.toUpperCase() : letter;
+ }).
+ replace(MOZ_HACK_REGEXP, 'Moz$1');
+}
+
+function directiveNormalize(name) {
+ return camelCase(name.replace(PREFIX_REGEXP, ''));
+}
\ No newline at end of file
ui/src/app/components/timeinterval.directive.js 213(+213 -0)
diff --git a/ui/src/app/components/timeinterval.directive.js b/ui/src/app/components/timeinterval.directive.js
new file mode 100644
index 0000000..1381b2c
--- /dev/null
+++ b/ui/src/app/components/timeinterval.directive.js
@@ -0,0 +1,213 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './timeinterval.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import timeintervalTemplate from './timeinterval.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+export default angular.module('thingsboard.directives.timeinterval', [])
+ .directive('tbTimeinterval', Timeinterval)
+ .name;
+
+/*@ngInject*/
+function Timeinterval($compile, $templateCache, $translate) {
+
+ var linker = function (scope, element, attrs, ngModelCtrl) {
+
+ var template = $templateCache.get(timeintervalTemplate);
+ element.html(template);
+
+ scope.rendered = false;
+ scope.days = 0;
+ scope.hours = 0;
+ scope.mins = 1;
+ scope.secs = 0;
+
+ scope.predefIntervals = [
+ {
+ name: $translate.instant('timeinterval.seconds-interval', {seconds: 10}, 'messageformat'),
+ value: 10 * 1000
+ },
+ {
+ name: $translate.instant('timeinterval.seconds-interval', {seconds: 30}, 'messageformat'),
+ value: 30 * 1000
+ },
+ {
+ name: $translate.instant('timeinterval.minutes-interval', {minutes: 1}, 'messageformat'),
+ value: 60 * 1000
+ },
+ {
+ name: $translate.instant('timeinterval.minutes-interval', {minutes: 2}, 'messageformat'),
+ value: 2 * 60 * 1000
+ },
+ {
+ name: $translate.instant('timeinterval.minutes-interval', {minutes: 5}, 'messageformat'),
+ value: 5 * 60 * 1000
+ },
+ {
+ name: $translate.instant('timeinterval.minutes-interval', {minutes: 10}, 'messageformat'),
+ value: 10 * 60 * 1000
+ },
+ {
+ name: $translate.instant('timeinterval.minutes-interval', {minutes: 30}, 'messageformat'),
+ value: 30 * 60 * 1000
+ },
+ {
+ name: $translate.instant('timeinterval.hours-interval', {hours: 1}, 'messageformat'),
+ value: 60 * 60 * 1000
+ },
+ {
+ name: $translate.instant('timeinterval.hours-interval', {hours: 2}, 'messageformat'),
+ value: 2 * 60 * 60 * 1000
+ },
+ {
+ name: $translate.instant('timeinterval.hours-interval', {hours: 10}, 'messageformat'),
+ value: 10 * 60 * 60 * 1000
+ },
+ {
+ name: $translate.instant('timeinterval.days-interval', {days: 1}, 'messageformat'),
+ value: 24 * 60 * 60 * 1000
+ },
+ {
+ name: $translate.instant('timeinterval.days-interval', {days: 7}, 'messageformat'),
+ value: 7 * 24 * 60 * 60 * 1000
+ },
+ {
+ name: $translate.instant('timeinterval.days-interval', {days: 30}, 'messageformat'),
+ value: 30 * 24 * 60 * 60 * 1000
+ }
+ ];
+
+ scope.setIntervalMs = function (intervalMs) {
+ var intervalSeconds = Math.floor(intervalMs / 1000);
+ scope.days = Math.floor(intervalSeconds / 86400);
+ scope.hours = Math.floor((intervalSeconds % 86400) / 3600);
+ scope.mins = Math.floor(((intervalSeconds % 86400) % 3600) / 60);
+ scope.secs = intervalSeconds % 60;
+ }
+
+ ngModelCtrl.$render = function () {
+ if (ngModelCtrl.$viewValue) {
+ var intervalMs = ngModelCtrl.$viewValue;
+ scope.setIntervalMs(intervalMs);
+ }
+ scope.rendered = true;
+ }
+
+ scope.updateView = function () {
+ if (!scope.rendered) {
+ return;
+ }
+ var value = null;
+ var intervalMs = (scope.days * 86400 +
+ scope.hours * 3600 +
+ scope.mins * 60 +
+ scope.secs) * 1000;
+ if (!isNaN(intervalMs) && intervalMs > 0) {
+ value = intervalMs;
+ ngModelCtrl.$setValidity('tb-timeinterval', true);
+ } else {
+ ngModelCtrl.$setValidity('tb-timeinterval', !scope.required);
+ }
+ ngModelCtrl.$setViewValue(value);
+ }
+
+ scope.$watch('required', function (newRequired, prevRequired) {
+ if (angular.isDefined(newRequired) && newRequired !== prevRequired) {
+ scope.updateView();
+ }
+ });
+
+ scope.$watch('secs', function (newSecs) {
+ if (angular.isUndefined(newSecs)) {
+ return;
+ }
+ if (newSecs < 0) {
+ if ((scope.days + scope.hours + scope.mins) > 0) {
+ scope.secs = newSecs + 60;
+ scope.mins--;
+ } else {
+ scope.secs = 0;
+ }
+ } else if (newSecs >= 60) {
+ scope.secs = newSecs - 60;
+ scope.mins++;
+ }
+ scope.updateView();
+ });
+
+ scope.$watch('mins', function (newMins) {
+ if (angular.isUndefined(newMins)) {
+ return;
+ }
+ if (newMins < 0) {
+ if ((scope.days + scope.hours) > 0) {
+ scope.mins = newMins + 60;
+ scope.hours--;
+ } else {
+ scope.mins = 0;
+ }
+ } else if (newMins >= 60) {
+ scope.mins = newMins - 60;
+ scope.hours++;
+ }
+ scope.updateView();
+ });
+
+ scope.$watch('hours', function (newHours) {
+ if (angular.isUndefined(newHours)) {
+ return;
+ }
+ if (newHours < 0) {
+ if (scope.days > 0) {
+ scope.hours = newHours + 24;
+ scope.days--;
+ } else {
+ scope.hours = 0;
+ }
+ } else if (newHours >= 24) {
+ scope.hours = newHours - 24;
+ scope.days++;
+ }
+ scope.updateView();
+ });
+
+ scope.$watch('days', function (newDays) {
+ if (angular.isUndefined(newDays)) {
+ return;
+ }
+ if (newDays < 0) {
+ scope.days = 0;
+ }
+ scope.updateView();
+ });
+
+ $compile(element.contents())(scope);
+
+ }
+
+ return {
+ restrict: "E",
+ require: "^ngModel",
+ scope: {
+ required: '=ngRequired'
+ },
+ link: linker
+ };
+}
ui/src/app/components/timeinterval.scss 34(+34 -0)
diff --git a/ui/src/app/components/timeinterval.scss b/ui/src/app/components/timeinterval.scss
new file mode 100644
index 0000000..015ca18
--- /dev/null
+++ b/ui/src/app/components/timeinterval.scss
@@ -0,0 +1,34 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+tb-timeinterval {
+ md-input-container {
+ margin-bottom: 0px;
+ .md-errors-spacer {
+ min-height: 0px;
+ }
+ }
+ mdp-date-picker {
+ .md-input {
+ width: 150px;
+ }
+ }
+}
+
+tb-timeinterval {
+ .md-input {
+ width: 70px !important;
+ }
+}
ui/src/app/components/timeinterval.tpl.html 47(+47 -0)
diff --git a/ui/src/app/components/timeinterval.tpl.html b/ui/src/app/components/timeinterval.tpl.html
new file mode 100644
index 0000000..89c1dcd
--- /dev/null
+++ b/ui/src/app/components/timeinterval.tpl.html
@@ -0,0 +1,47 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<section layout="row" layout-align="start start">
+ <md-input-container>
+ <label translate>timeinterval.days</label>
+ <input type="number" ng-model="days" step="1" aria-label="{{ 'timeinterval.days' | translate }}">
+ </md-input-container>
+ <md-input-container>
+ <label translate>timeinterval.hours</label>
+ <input type="number" ng-model="hours" step="1" aria-label="{{ 'timeinterval.hours' | translate }}">
+ </md-input-container>
+ <md-input-container>
+ <label translate>timeinterval.minutes</label>
+ <input type="number" ng-model="mins" step="1" aria-label="{{ 'timeinterval.minutes' | translate }}">
+ </md-input-container>
+ <md-input-container>
+ <label translate>timeinterval.seconds</label>
+ <input type="number" ng-model="secs" step="1" aria-label="{{ 'timeinterval.seconds' | translate }}">
+ </md-input-container>
+ <md-menu md-position-mode="target-right target">
+ <md-button class="md-icon-button" aria-label="Open intervals" ng-click="$mdOpenMenu($event)">
+ <md-icon md-menu-origin aria-label="arrow_drop_down" class="material-icons">arrow_drop_down</md-icon>
+ </md-button>
+ <md-menu-content width="4">
+ <md-menu-item ng-repeat="interval in predefIntervals" >
+ <md-button ng-click="setIntervalMs(interval.value)">
+ <span>{{interval.name}}</span>
+ </md-button>
+ </md-menu-item>
+ </md-menu-content>
+ </md-menu>
+</section>
ui/src/app/components/timewindow.directive.js 242(+242 -0)
diff --git a/ui/src/app/components/timewindow.directive.js b/ui/src/app/components/timewindow.directive.js
new file mode 100644
index 0000000..b3da4c8
--- /dev/null
+++ b/ui/src/app/components/timewindow.directive.js
@@ -0,0 +1,242 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './timewindow.scss';
+
+import thingsboardTimeinterval from './timeinterval.directive';
+import thingsboardDatetimePeriod from './datetime-period.directive';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import timewindowButtonTemplate from './timewindow-button.tpl.html';
+import timewindowTemplate from './timewindow.tpl.html';
+import timewindowPanelTemplate from './timewindow-panel.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+import TimewindowPanelController from './timewindow-panel.controller';
+
+export default angular.module('thingsboard.directives.timewindow', [thingsboardTimeinterval, thingsboardDatetimePeriod])
+ .controller('TimewindowPanelController', TimewindowPanelController)
+ .directive('tbTimewindow', Timewindow)
+ .filter('milliSecondsToTimeString', MillisecondsToTimeString)
+ .name;
+
+/*@ngInject*/
+function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $translate) {
+
+ var linker = function (scope, element, attrs, ngModelCtrl) {
+
+ /* tbTimewindow (ng-model)
+ * {
+ * realtime: {
+ * timewindowMs: 0
+ * },
+ * history: {
+ * timewindowMs: 0,
+ * fixedTimewindow: {
+ * startTimeMs: 0,
+ * endTimeMs: 0
+ * }
+ * }
+ * }
+ */
+
+ scope.historyOnly = angular.isDefined(attrs.historyOnly);
+
+ var translationPending = false;
+
+ $translate.onReady(function() {
+ if (translationPending) {
+ scope.updateDisplayValue();
+ translationPending = false;
+ }
+ });
+
+ var template;
+ if (scope.asButton) {
+ template = $templateCache.get(timewindowButtonTemplate);
+ } else {
+ template = $templateCache.get(timewindowTemplate);
+ }
+ element.html(template);
+
+ scope.isHovered = false;
+
+ scope.onHoverIn = function () {
+ scope.isHovered = true;
+ }
+
+ scope.onHoverOut = function () {
+ scope.isHovered = false;
+ }
+
+ scope.openEditMode = function (event) {
+ var position = $mdPanel.newPanelPosition()
+ .relativeTo(element)
+ .addPanelPosition($mdPanel.xPosition.ALIGN_START, $mdPanel.yPosition.BELOW);
+ var config = {
+ attachTo: angular.element($document[0].body),
+ controller: 'TimewindowPanelController',
+ controllerAs: 'vm',
+ templateUrl: timewindowPanelTemplate,
+ panelClass: 'tb-timewindow-panel',
+ position: position,
+ locals: {
+ 'timewindow': angular.copy(scope.model),
+ 'historyOnly': scope.historyOnly,
+ 'onTimewindowUpdate': function (timewindow) {
+ scope.model = timewindow;
+ scope.updateView();
+ }
+ },
+ openFrom: event,
+ clickOutsideToClose: true,
+ escapeToClose: true,
+ focusOnOpen: false
+ };
+ $mdPanel.open(config);
+ }
+
+ scope.updateView = function () {
+ var value = {};
+ var model = scope.model;
+ if (model.selectedTab === 0) {
+ value.realtime = {
+ timewindowMs: model.realtime.timewindowMs
+ };
+ } else {
+ if (model.history.historyType === 0) {
+ value.history = {
+ timewindowMs: model.history.timewindowMs
+ };
+ } else {
+ value.history = {
+ fixedTimewindow: {
+ startTimeMs: model.history.fixedTimewindow.startTimeMs,
+ endTimeMs: model.history.fixedTimewindow.endTimeMs
+ }
+ };
+ }
+ }
+
+ ngModelCtrl.$setViewValue(value);
+ scope.updateDisplayValue();
+ }
+
+ scope.updateDisplayValue = function () {
+ if ($translate.isReady()) {
+ if (scope.model.selectedTab === 0 && !scope.historyOnly) {
+ scope.model.displayValue = $translate.instant('timewindow.realtime') + ' - ' +
+ $translate.instant('timewindow.last-prefix') + ' ' +
+ $filter('milliSecondsToTimeString')(scope.model.realtime.timewindowMs);
+ } else {
+ scope.model.displayValue = !scope.historyOnly ? ($translate.instant('timewindow.history') + ' - ') : '';
+ if (scope.model.history.historyType === 0) {
+ scope.model.displayValue += $translate.instant('timewindow.last-prefix') + ' ' +
+ $filter('milliSecondsToTimeString')(scope.model.history.timewindowMs);
+ } else {
+ var startString = $filter('date')(scope.model.history.fixedTimewindow.startTimeMs, 'yyyy-MM-dd HH:mm:ss');
+ var endString = $filter('date')(scope.model.history.fixedTimewindow.endTimeMs, 'yyyy-MM-dd HH:mm:ss');
+ scope.model.displayValue += $translate.instant('timewindow.period', {startTime: startString, endTime: endString});
+ }
+ }
+ } else {
+ translationPending = true;
+ }
+ }
+
+ ngModelCtrl.$render = function () {
+ var currentTime = (new Date).getTime();
+ scope.model = {
+ displayValue: "",
+ selectedTab: 0,
+ realtime: {
+ timewindowMs: 60000 // 1 min by default
+ },
+ history: {
+ historyType: 0,
+ timewindowMs: 60000, // 1 min by default
+ fixedTimewindow: {
+ startTimeMs: currentTime - 24 * 60 * 60 * 1000, // 1 day by default
+ endTimeMs: currentTime
+ }
+ }
+ };
+ if (ngModelCtrl.$viewValue) {
+ var value = ngModelCtrl.$viewValue;
+ var model = scope.model;
+ if (angular.isDefined(value.realtime)) {
+ model.selectedTab = 0;
+ model.realtime.timewindowMs = value.realtime.timewindowMs;
+ } else {
+ model.selectedTab = 1;
+ if (angular.isDefined(value.history.timewindowMs)) {
+ model.history.historyType = 0;
+ model.history.timewindowMs = value.history.timewindowMs;
+ } else {
+ model.history.historyType = 1;
+ model.history.fixedTimewindow.startTimeMs = value.history.fixedTimewindow.startTimeMs;
+ model.history.fixedTimewindow.endTimeMs = value.history.fixedTimewindow.endTimeMs;
+ }
+ }
+ }
+ scope.updateDisplayValue();
+ };
+
+ $compile(element.contents())(scope);
+ }
+
+ return {
+ restrict: "E",
+ require: "^ngModel",
+ scope: {
+ asButton: '=asButton'
+ },
+ link: linker
+ };
+}
+
+/*@ngInject*/
+function MillisecondsToTimeString($translate) {
+ return function (millseconds) {
+ var seconds = Math.floor(millseconds / 1000);
+ var days = Math.floor(seconds / 86400);
+ var hours = Math.floor((seconds % 86400) / 3600);
+ var minutes = Math.floor(((seconds % 86400) % 3600) / 60);
+ seconds = seconds % 60;
+ var timeString = '';
+ if (days > 0) timeString += $translate.instant('timewindow.days', {days: days}, 'messageformat');
+ if (hours > 0) {
+ if (timeString.length === 0 && hours === 1) {
+ hours = 0;
+ }
+ timeString += $translate.instant('timewindow.hours', {hours: hours}, 'messageformat');
+ }
+ if (minutes > 0) {
+ if (timeString.length === 0 && minutes === 1) {
+ minutes = 0;
+ }
+ timeString += $translate.instant('timewindow.minutes', {minutes: minutes}, 'messageformat');
+ }
+ if (seconds > 0) {
+ if (timeString.length === 0 && seconds === 1) {
+ seconds = 0;
+ }
+ timeString += $translate.instant('timewindow.seconds', {seconds: seconds}, 'messageformat');
+ }
+ return timeString;
+ }
+}
\ No newline at end of file
ui/src/app/components/timewindow.scss 27(+27 -0)
diff --git a/ui/src/app/components/timewindow.scss b/ui/src/app/components/timewindow.scss
new file mode 100644
index 0000000..1d2c738
--- /dev/null
+++ b/ui/src/app/components/timewindow.scss
@@ -0,0 +1,27 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+.tb-timewindow-panel {
+ position: absolute;
+ background: white;
+ border-radius: 4px;
+ box-shadow: 0 7px 8px -4px rgba(0, 0, 0, 0.2),
+ 0 13px 19px 2px rgba(0, 0, 0, 0.14),
+ 0 5px 24px 4px rgba(0, 0, 0, 0.12);
+ overflow: hidden;
+ md-content {
+ background-color: #fff;
+ }
+}
ui/src/app/components/timewindow.tpl.html 23(+23 -0)
diff --git a/ui/src/app/components/timewindow.tpl.html b/ui/src/app/components/timewindow.tpl.html
new file mode 100644
index 0000000..d618e94
--- /dev/null
+++ b/ui/src/app/components/timewindow.tpl.html
@@ -0,0 +1,23 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<section ng-mouseover="onHoverIn()" ng-mouseleave="onHoverOut()" layout='row' layout-align="start center" style="min-height: 32px;">
+ <span ng-click="openEditMode($event)">{{model.displayValue}}</span>
+ <md-button class="md-icon-button tb-md-32" aria-label="{{ 'timewindow.edit' | translate }}" ng-show="isHovered" ng-click="openEditMode($event)">
+ <md-icon aria-label="{{ 'timewindow.date-range' | translate }}" class="material-icons">date_range</md-icon>
+ </md-button>
+</section>
\ No newline at end of file
diff --git a/ui/src/app/components/timewindow-button.tpl.html b/ui/src/app/components/timewindow-button.tpl.html
new file mode 100644
index 0000000..4bed410
--- /dev/null
+++ b/ui/src/app/components/timewindow-button.tpl.html
@@ -0,0 +1,21 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-button class="md-raised md-primary" ng-click="openEditMode($event)">
+ <md-icon class="material-icons">date_range</md-icon>
+ <span>{{model.displayValue}}</span>
+</md-button>
\ No newline at end of file
diff --git a/ui/src/app/components/timewindow-panel.controller.js b/ui/src/app/components/timewindow-panel.controller.js
new file mode 100644
index 0000000..005c3c8
--- /dev/null
+++ b/ui/src/app/components/timewindow-panel.controller.js
@@ -0,0 +1,49 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*@ngInject*/
+export default function TimewindowPanelController(mdPanelRef, $scope, timewindow, historyOnly, onTimewindowUpdate) {
+
+ var vm = this;
+
+ vm._mdPanelRef = mdPanelRef;
+ vm.timewindow = timewindow;
+ vm.historyOnly = historyOnly;
+ vm.onTimewindowUpdate = onTimewindowUpdate;
+
+ if (vm.historyOnly) {
+ vm.timewindow.selectedTab = 1;
+ }
+
+ vm._mdPanelRef.config.onOpenComplete = function () {
+ $scope.theForm.$setPristine();
+ }
+
+ $scope.$watch('vm.timewindow.selectedTab', function (newSelection, prevSelection) {
+ if (newSelection !== prevSelection) {
+ $scope.theForm.$setDirty();
+ }
+ });
+
+ vm.cancel = function () {
+ vm._mdPanelRef && vm._mdPanelRef.close();
+ };
+
+ vm.update = function () {
+ vm._mdPanelRef && vm._mdPanelRef.close().then(function () {
+ vm.onTimewindowUpdate && vm.onTimewindowUpdate(vm.timewindow);
+ });
+ };
+}
diff --git a/ui/src/app/components/timewindow-panel.tpl.html b/ui/src/app/components/timewindow-panel.tpl.html
new file mode 100644
index 0000000..fa039c0
--- /dev/null
+++ b/ui/src/app/components/timewindow-panel.tpl.html
@@ -0,0 +1,66 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<form name="theForm" ng-submit="vm.update()">
+ <fieldset ng-disabled="loading">
+ <section layout="column">
+ <md-tabs ng-class="{'tb-headless': vm.historyOnly}" flex md-dynamic-height md-selected="vm.timewindow.selectedTab" md-border-bottom>
+ <md-tab label="{{ 'timewindow.realtime' | translate }}">
+ <md-content class="md-padding" layout="column">
+ <span translate>timewindow.last</span>
+ <tb-timeinterval
+ ng-required="vm.timewindow.selectedTab === 0"
+ ng-model="vm.timewindow.realtime.timewindowMs" style="padding-top: 8px;"></tb-timeinterval>
+ </md-content>
+ </md-tab>
+ <md-tab label="{{ 'timewindow.history' | translate }}">
+ <md-content class="md-padding" layout="column">
+ <md-radio-group ng-model="vm.timewindow.history.historyType" class="md-primary">
+ <md-radio-button ng-value=0 class="md-primary md-align-top-left md-radio-interactive">
+ <section layout="column">
+ <span translate>timewindow.last</span>
+ <tb-timeinterval
+ ng-required="vm.timewindow.selectedTab === 1 && vm.timewindow.history.historyType === 0"
+ ng-show="vm.timewindow.history.historyType === 0"
+ ng-model="vm.timewindow.history.timewindowMs" style="padding-top: 8px;"></tb-timeinterval>
+ </section>
+ </md-radio-button>
+ <md-radio-button ng-value=1 class="md-primary md-align-top-left md-radio-interactive">
+ <section layout="column">
+ <span translate>timewindow.time-period</span>
+ <tb-datetime-period
+ ng-required="vm.timewindow.selectedTab === 1 && vm.timewindow.history.historyType === 1"
+ ng-show="vm.timewindow.history.historyType === 1"
+ ng-model="vm.timewindow.history.fixedTimewindow" style="padding-top: 8px;"></tb-datetime-period>
+ </section>
+ </md-radio-button>
+ </md-radio-group>
+ </md-content>
+ </md-tab>
+ </md-tabs>
+ <section layout="row" layout-alignment="start center">
+ <span flex></span>
+ <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit" class="md-raised md-primary">
+ {{ 'action.update' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">
+ {{ 'action.cancel' | translate }}
+ </md-button>
+ </section>
+ </section>
+ </fieldset>
+</form>
\ No newline at end of file
ui/src/app/components/truncate.filter.js 43(+43 -0)
diff --git a/ui/src/app/components/truncate.filter.js b/ui/src/app/components/truncate.filter.js
new file mode 100644
index 0000000..d56db6d
--- /dev/null
+++ b/ui/src/app/components/truncate.filter.js
@@ -0,0 +1,43 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export default angular.module('thingsboard.filters.truncate', [])
+ .filter('truncate', Truncate)
+ .name;
+
+/*@ngInject*/
+function Truncate () {
+ return function (value, wordwise, max, tail) {
+ if (!value) return '';
+
+ max = parseInt(max, 10);
+ if (!max) return value;
+ if (value.length <= max) return value;
+
+ value = value.substr(0, max);
+ if (wordwise) {
+ var lastspace = value.lastIndexOf(' ');
+ if (lastspace != -1) {
+ //Also remove . and , so its gives a cleaner result.
+ if (value.charAt(lastspace - 1) == '.' || value.charAt(lastspace - 1) == ',') {
+ lastspace = lastspace - 1;
+ }
+ value = value.substr(0, lastspace);
+ }
+ }
+
+ return value + (tail || ' …');
+ };
+}
ui/src/app/components/widget.controller.js 478(+478 -0)
diff --git a/ui/src/app/components/widget.controller.js b/ui/src/app/components/widget.controller.js
new file mode 100644
index 0000000..565c8a1
--- /dev/null
+++ b/ui/src/app/components/widget.controller.js
@@ -0,0 +1,478 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import $ from 'jquery';
+
+/* eslint-disable angular/angularelement */
+
+/*@ngInject*/
+export default function WidgetController($scope, $timeout, $window, $element, $q, $log, types, visibleRect,
+ datasourceService, deviceService, isPreview, widget, deviceAliasList, fns) {
+
+ var vm = this;
+
+ var timeWindow = {};
+ var subscriptionTimewindow = {
+ fixedWindow: null,
+ realtimeWindowMs: null
+ };
+
+ /*
+ * data = array of datasourceData
+ * datasourceData = {
+ * tbDatasource,
+ * dataKey, { name, config }
+ * data = array of [time, value]
+ * }
+ *
+ *
+ */
+ var data = [];
+ var datasourceListeners = [];
+ var targetDeviceAliasId = null;
+ var targetDeviceId = null;
+
+ var visible = false;
+
+ var bounds = {top: 0, left: 0, bottom: 0, right: 0};
+ var lastWidth, lastHeight;
+ var containerParent = $($element);
+ var container = $('#container', $element);
+ var containerElement = container[0];
+ var inited = false;
+
+ var gridsterItemElement;
+ var timer;
+
+ var init = fns.init || function () {
+ };
+
+ var redraw = fns.redraw || function () {
+ };
+
+ var destroy = fns.destroy || function () {
+ };
+
+ $scope.$timeout = $timeout;
+ $scope.$q = $q;
+
+ $scope.rpcRejection = null;
+ $scope.rpcErrorText = null;
+ $scope.rpcEnabled = false;
+ $scope.executingRpcRequest = false;
+ $scope.executingPromises = [];
+
+ function sendCommand(oneWayElseTwoWay, method, params, timeout) {
+ if (!$scope.rpcEnabled) {
+ return $q.reject();
+ }
+
+ if ($scope.rpcRejection && $scope.rpcRejection.status !== 408) {
+ $scope.rpcRejection = null;
+ $scope.rpcErrorText = null;
+ }
+
+ var requestBody = {
+ method: method,
+ params: params
+ };
+
+ if (timeout && timeout > 0) {
+ requestBody.timeout = timeout;
+ }
+
+ var deferred = $q.defer();
+ $scope.executingRpcRequest = true;
+ if ($scope.widgetEditMode) {
+ $timeout(function() {
+ $scope.executingRpcRequest = false;
+ if (oneWayElseTwoWay) {
+ deferred.resolve();
+ } else {
+ deferred.resolve(requestBody);
+ }
+ }, 500);
+ } else {
+ $scope.executingPromises.push(deferred.promise);
+ var targetSendFunction = oneWayElseTwoWay ? deviceService.sendOneWayRpcCommand : deviceService.sendTwoWayRpcCommand;
+ targetSendFunction(targetDeviceId, requestBody).then(
+ function success(responseBody) {
+ $scope.rpcRejection = null;
+ $scope.rpcErrorText = null;
+ var index = $scope.executingPromises.indexOf(deferred.promise);
+ if (index >= 0) {
+ $scope.executingPromises.splice( index, 1 );
+ }
+ $scope.executingRpcRequest = $scope.executingPromises.length > 0;
+ deferred.resolve(responseBody);
+ },
+ function fail(rejection) {
+ var index = $scope.executingPromises.indexOf(deferred.promise);
+ if (index >= 0) {
+ $scope.executingPromises.splice( index, 1 );
+ }
+ $scope.executingRpcRequest = $scope.executingPromises.length > 0;
+ if (!$scope.executingRpcRequest || rejection.status === 408) {
+ $scope.rpcRejection = rejection;
+ if (rejection.status === 408) {
+ $scope.rpcErrorText = 'Device is offline.';
+ } else {
+ $scope.rpcErrorText = 'Error : ' + rejection.status + ' - ' + rejection.statusText;
+ if (rejection.data && rejection.data.length > 0) {
+ $scope.rpcErrorText += '</br>';
+ $scope.rpcErrorText += rejection.data;
+ }
+ }
+ }
+ deferred.reject(rejection);
+ }
+ );
+ }
+ return deferred.promise;
+ }
+
+ $scope.clearRpcError = function() {
+ $scope.rpcRejection = null;
+ $scope.rpcErrorText = null;
+ }
+
+ var controlApi = {};
+
+ controlApi.sendOneWayCommand = function(method, params, timeout) {
+ return sendCommand(true, method, params, timeout);
+ };
+
+ controlApi.sendTwoWayCommand = function(method, params, timeout) {
+ return sendCommand(false, method, params, timeout);
+ };
+
+ vm.gridsterItemInitialized = gridsterItemInitialized;
+
+ function gridsterItemInitialized(item) {
+ if (item) {
+ gridsterItemElement = $(item.$element);
+ updateVisibility();
+ }
+ }
+
+ initWidget();
+
+ function initWidget() {
+ if (widget.type !== types.widgetType.rpc.value) {
+ for (var i in widget.config.datasources) {
+ var datasource = angular.copy(widget.config.datasources[i]);
+ for (var a in datasource.dataKeys) {
+ var dataKey = datasource.dataKeys[a];
+ var datasourceData = {
+ datasource: datasource,
+ dataKey: dataKey,
+ data: []
+ };
+ data.push(datasourceData);
+ }
+ }
+ } else {
+ if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length > 0) {
+ targetDeviceAliasId = widget.config.targetDeviceAliasIds[0];
+ if (deviceAliasList[targetDeviceAliasId]) {
+ targetDeviceId = deviceAliasList[targetDeviceAliasId].deviceId;
+ }
+ }
+ if (targetDeviceId) {
+ $scope.rpcEnabled = true;
+ } else {
+ $scope.rpcEnabled = $scope.widgetEditMode ? true : false;
+ }
+ }
+
+ $scope.$on('toggleDashboardEditMode', function (event, isEdit) {
+ isPreview = isEdit;
+ onRedraw();
+ });
+
+ $scope.$on('gridster-item-resized', function (event, item) {
+ if (item) {
+ updateBounds();
+ }
+ });
+
+ $scope.$on('gridster-item-transition-end', function (event, item) {
+ if (item) {
+ updateBounds();
+ }
+ });
+
+ $scope.$watch(function () {
+ return widget.row + ',' + widget.col;
+ }, function () {
+ updateBounds();
+ $scope.$emit("widgetPositionChanged", widget);
+ });
+
+ $scope.$on('visibleRectChanged', function (event, newVisibleRect) {
+ visibleRect = newVisibleRect;
+ updateVisibility();
+ });
+
+ $scope.$on('onWidgetFullscreenChanged', function(event, isWidgetExpanded, fullscreenWidget) {
+ if (widget === fullscreenWidget) {
+ onRedraw(0);
+ }
+ });
+
+ $scope.$on('deviceAliasListChanged', function (event, newDeviceAliasList) {
+ deviceAliasList = newDeviceAliasList;
+ if (widget.type === types.widgetType.rpc.value) {
+ if (targetDeviceAliasId) {
+ var deviceId = null;
+ if (deviceAliasList[targetDeviceAliasId]) {
+ deviceId = deviceAliasList[targetDeviceAliasId].deviceId;
+ }
+ if (!angular.equals(deviceId, targetDeviceId)) {
+ targetDeviceId = deviceId;
+ if (targetDeviceId) {
+ $scope.rpcEnabled = true;
+ } else {
+ $scope.rpcEnabled = $scope.widgetEditMode ? true : false;
+ }
+ inited = false;
+ onRedraw();
+ }
+ }
+ } else {
+ checkSubscriptions();
+ }
+ });
+
+ $scope.$on("$destroy", function () {
+ unsubscribe();
+ destroy();
+ });
+
+ subscribe();
+
+ if (widget.type === types.widgetType.timeseries.value) {
+ $scope.$watch(function () {
+ return widget.config.timewindow;
+ }, function (newTimewindow, prevTimewindow) {
+ if (!angular.equals(newTimewindow, prevTimewindow)) {
+ unsubscribe();
+ subscribe();
+ }
+ });
+ } else if (widget.type === types.widgetType.rpc.value) {
+ if (!inited) {
+ init(containerElement, widget.config.settings, widget.config.datasources, data, $scope, controlApi);
+ inited = true;
+ }
+ }
+ }
+
+ function updateVisibility(forceRedraw) {
+ if (visibleRect) {
+ forceRedraw = forceRedraw || visibleRect.containerResized;
+ var newVisible = false;
+ if (visibleRect.isMobile && gridsterItemElement) {
+ var topPx = gridsterItemElement.position().top;
+ var bottomPx = topPx + widget.sizeY * visibleRect.curRowHeight;
+ newVisible = !(topPx > visibleRect.bottomPx ||
+ bottomPx < visibleRect.topPx);
+ } else {
+ newVisible = !(bounds.left > visibleRect.right ||
+ bounds.right < visibleRect.left ||
+ bounds.top > visibleRect.bottom ||
+ bounds.bottom < visibleRect.top);
+ }
+ if (visible != newVisible) {
+ visible = newVisible;
+ if (visible) {
+ onRedraw(50);
+ }
+ } else if (forceRedraw && visible) {
+ onRedraw(50);
+ }
+ }
+ }
+
+ function updateBounds() {
+ bounds = {
+ top: widget.row,
+ left: widget.col,
+ bottom: widget.row + widget.sizeY,
+ right: widget.col + widget.sizeX
+ };
+ updateVisibility(true);
+ }
+
+
+ function onRedraw(delay, dataUpdate) {
+ if (!visible) {
+ return;
+ }
+ if (angular.isUndefined(delay)) {
+ delay = 0;
+ }
+ $timeout(function () {
+ var width = containerParent.width();
+ var height = containerParent.height();
+ var sizeChanged = false;
+
+ if (!lastWidth || lastWidth != width || !lastHeight || lastHeight != height) {
+ if (width > 0 && height > 0) {
+ container.css('height', height + 'px');
+ container.css('width', width + 'px');
+ lastWidth = width;
+ lastHeight = height;
+ sizeChanged = true;
+ }
+ }
+
+ if (width > 20 && height > 20) {
+ if (!inited) {
+ init(containerElement, widget.config.settings, widget.config.datasources, data, $scope, controlApi);
+ inited = true;
+ }
+ if (widget.type === types.widgetType.timeseries.value) {
+ if (dataUpdate && timer) {
+ $timeout.cancel(timer);
+ timer = $timeout(onTick, 1500, false);
+ }
+ if (subscriptionTimewindow.realtimeWindowMs) {
+ timeWindow.maxTime = (new Date).getTime();
+ timeWindow.minTime = timeWindow.maxTime - subscriptionTimewindow.realtimeWindowMs;
+ } else if (subscriptionTimewindow.fixedWindow) {
+ timeWindow.maxTime = subscriptionTimewindow.fixedWindow.endTimeMs;
+ timeWindow.minTime = subscriptionTimewindow.fixedWindow.startTimeMs;
+ }
+ }
+ redraw(containerElement, width, height, data, timeWindow, sizeChanged, $scope);
+ }
+ }, delay, false);
+ }
+
+ function onDataUpdated(sourceData, datasourceIndex, dataKeyIndex) {
+ data[datasourceIndex + dataKeyIndex].data = sourceData;
+ onRedraw(0, true);
+ }
+
+ function checkSubscriptions() {
+ if (widget.type !== types.widgetType.rpc.value) {
+ var subscriptionsChanged = false;
+ for (var i in datasourceListeners) {
+ var listener = datasourceListeners[i];
+ var deviceId = null;
+ var aliasName = null;
+ if (listener.datasource.type === types.datasourceType.device) {
+ if (deviceAliasList[listener.datasource.deviceAliasId]) {
+ deviceId = deviceAliasList[listener.datasource.deviceAliasId].deviceId;
+ aliasName = deviceAliasList[listener.datasource.deviceAliasId].alias;
+ }
+ if (!angular.equals(deviceId, listener.deviceId) ||
+ !angular.equals(aliasName, listener.datasource.name)) {
+ subscriptionsChanged = true;
+ break;
+ }
+ }
+ }
+ if (subscriptionsChanged) {
+ unsubscribe();
+ subscribe();
+ }
+ }
+ }
+
+ function unsubscribe() {
+ if (widget.type !== types.widgetType.rpc.value) {
+ if (timer) {
+ $timeout.cancel(timer);
+ timer = null;
+ }
+ for (var i in datasourceListeners) {
+ var listener = datasourceListeners[i];
+ datasourceService.unsubscribeFromDatasource(listener);
+ }
+ }
+ }
+
+ function onTick() {
+ onRedraw();
+ timer = $timeout(onTick, 1000, false);
+ }
+
+ function subscribe() {
+ if (widget.type !== types.widgetType.rpc.value) {
+ var index = 0;
+ subscriptionTimewindow.fixedWindow = null;
+ subscriptionTimewindow.realtimeWindowMs = null;
+ if (widget.type === types.widgetType.timeseries.value &&
+ angular.isDefined(widget.config.timewindow)) {
+ if (angular.isDefined(widget.config.timewindow.realtime)) {
+ subscriptionTimewindow.realtimeWindowMs = widget.config.timewindow.realtime.timewindowMs;
+ } else if (angular.isDefined(widget.config.timewindow.history)) {
+ if (angular.isDefined(widget.config.timewindow.history.timewindowMs)) {
+ var currentTime = (new Date).getTime();
+ subscriptionTimewindow.fixedWindow = {
+ startTimeMs: currentTime - widget.config.timewindow.history.timewindowMs,
+ endTimeMs: currentTime
+ }
+ } else {
+ subscriptionTimewindow.fixedWindow = {
+ startTimeMs: widget.config.timewindow.history.fixedTimewindow.startTimeMs,
+ endTimeMs: widget.config.timewindow.history.fixedTimewindow.endTimeMs
+ }
+ }
+ }
+ }
+ for (var i in widget.config.datasources) {
+ var datasource = widget.config.datasources[i];
+ var deviceId = null;
+ if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) {
+ if (deviceAliasList[datasource.deviceAliasId]) {
+ deviceId = deviceAliasList[datasource.deviceAliasId].deviceId;
+ datasource.name = deviceAliasList[datasource.deviceAliasId].alias;
+ }
+ } else {
+ datasource.name = types.datasourceType.function;
+ }
+ var listener = {
+ widget: widget,
+ subscriptionTimewindow: subscriptionTimewindow,
+ datasource: datasource,
+ deviceId: deviceId,
+ dataUpdated: function (data, datasourceIndex, dataKeyIndex) {
+ onDataUpdated(data, datasourceIndex, dataKeyIndex);
+ },
+ datasourceIndex: index
+ };
+
+ for (var a = 0; a < datasource.dataKeys.length; a++) {
+ data[index + a].data = [];
+ }
+
+ index += datasource.dataKeys.length;
+
+ datasourceListeners.push(listener);
+ datasourceService.subscribeToDatasource(listener);
+ }
+
+ if (subscriptionTimewindow.realtimeWindowMs) {
+ timer = $timeout(onTick, 0, false);
+ }
+ }
+ }
+
+}
+
+/* eslint-enable angular/angularelement */
\ No newline at end of file
ui/src/app/components/widget.directive.js 127(+127 -0)
diff --git a/ui/src/app/components/widget.directive.js b/ui/src/app/components/widget.directive.js
new file mode 100644
index 0000000..6ba25c3
--- /dev/null
+++ b/ui/src/app/components/widget.directive.js
@@ -0,0 +1,127 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import thingsboardTypes from '../common/types.constant';
+import thingsboardApiDatasource from '../api/datasource.service';
+
+import WidgetController from './widget.controller';
+
+export default angular.module('thingsboard.directives.widget', [thingsboardTypes, thingsboardApiDatasource])
+ .controller('WidgetController', WidgetController)
+ .directive('tbWidget', Widget)
+ .name;
+
+/*@ngInject*/
+function Widget($controller, $compile, widgetService) {
+ return {
+ scope: true,
+ link: function (scope, elem, attrs) {
+
+ var widgetController;
+ var locals = scope.$eval(attrs.locals);
+ var widget = locals.widget;
+ var gridsterItem;
+
+ scope.$on('gridster-item-initialized', function (event, item) {
+ gridsterItem = item;
+ if (widgetController) {
+ widgetController.gridsterItemInitialized(gridsterItem);
+ }
+ })
+
+ elem.html('<div flex layout="column" layout-align="center center" style="height: 100%;">' +
+ ' <md-progress-circular md-mode="indeterminate" class="md-accent md-hue-2" md-diameter="120"></md-progress-circular>' +
+ '</div>');
+ $compile(elem.contents())(scope);
+
+ widgetService.getWidgetInfo(widget.bundleAlias, widget.typeAlias, widget.isSystemType).then(
+ function(widgetInfo) {
+ loadFromWidgetInfo(widgetInfo);
+ }
+ );
+
+ function loadFromWidgetInfo(widgetInfo) {
+
+ elem.addClass("tb-widget");
+
+ var widgetNamespace = "widget-type-" + (widget.isSystemType ? 'sys-' : '')
+ + widget.bundleAlias + '-'
+ + widget.typeAlias;
+
+ elem.addClass(widgetNamespace);
+ elem.html('<div id="container">' + widgetInfo.templateHtml + '</div>');
+
+ $compile(elem.contents())(scope);
+
+ angular.extend(locals, {$scope: scope, $element: elem});
+
+ var controllerFunctionBody = 'var fns = { init: null, redraw: null, destroy: null };';
+ controllerFunctionBody += widgetInfo.controllerScript;
+ controllerFunctionBody += '' +
+ 'angular.extend(this, $controller(\'WidgetController\',' +
+ '{' +
+ '$scope: $scope,' +
+ '$timeout: $timeout,' +
+ '$window: $window,' +
+ '$element: $element,' +
+ '$log: $log,' +
+ 'types: types,' +
+ 'visibleRect: visibleRect,' +
+ 'datasourceService: datasourceService,' +
+ 'deviceService: deviceService,' +
+ 'isPreview: isPreview,' +
+ 'widget: widget,' +
+ 'deviceAliasList: deviceAliasList,' +
+ 'fns: fns' +
+ '}));' +
+ '';
+
+ var controllerFunction = new Function("$scope",
+ "$timeout",
+ "$window",
+ "$element",
+ "$log",
+ 'types',
+ "visibleRect",
+ "datasourceService",
+ "deviceService",
+ "$controller",
+ "isPreview",
+ "widget",
+ "deviceAliasList",
+ controllerFunctionBody);
+
+ controllerFunction.$inject = ["$scope",
+ "$timeout",
+ "$window",
+ "$element",
+ "$log",
+ 'types',
+ "visibleRect",
+ "datasourceService",
+ "deviceService",
+ "$controller",
+ "isPreview",
+ "widget",
+ "deviceAliasList"];
+
+ widgetController = $controller(controllerFunction, locals);
+ if (gridsterItem) {
+ widgetController.gridsterItemInitialized(gridsterItem);
+ }
+ }
+ }
+ };
+}
ui/src/app/components/widget-config.directive.js 340(+340 -0)
diff --git a/ui/src/app/components/widget-config.directive.js b/ui/src/app/components/widget-config.directive.js
new file mode 100644
index 0000000..ee6675d
--- /dev/null
+++ b/ui/src/app/components/widget-config.directive.js
@@ -0,0 +1,340 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import jsonSchemaDefaults from 'json-schema-defaults';
+import thingsboardTypes from '../common/types.constant';
+import thingsboardUtils from '../common/utils.service';
+import thingsboardDeviceAliasSelect from './device-alias-select.directive';
+import thingsboardDatasource from './datasource.directive';
+import thingsboardTimewindow from './timewindow.directive';
+import thingsboardJsonForm from "./json-form.directive";
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import widgetConfigTemplate from './widget-config.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/* eslint-disable angular/angularelement */
+
+export default angular.module('thingsboard.directives.widgetConfig', [thingsboardTypes,
+ thingsboardUtils,
+ thingsboardJsonForm,
+ thingsboardDeviceAliasSelect,
+ thingsboardDatasource,
+ thingsboardTimewindow])
+ .directive('tbWidgetConfig', WidgetConfig)
+ .name;
+
+/*@ngInject*/
+function WidgetConfig($compile, $templateCache, $rootScope, types, utils) {
+
+ var linker = function (scope, element, attrs, ngModelCtrl) {
+
+ var template = $templateCache.get(widgetConfigTemplate);
+
+ element.html(template);
+
+ scope.types = types;
+ scope.widgetEditMode = $rootScope.widgetEditMode;
+
+ scope.emptySettingsSchema = {
+ type: "object",
+ properties: {}
+ };
+ scope.defaultSettingsForm = [
+ '*'
+ ];
+
+ if (angular.isUndefined(scope.forceExpandDatasources)) {
+ scope.forceExpandDatasources = false;
+ }
+
+ scope.currentSettingsSchema = {};
+ scope.currentSettings = angular.copy(scope.emptySettingsSchema);
+
+ scope.targetDeviceAlias = {
+ value: null
+ }
+
+ ngModelCtrl.$render = function () {
+ if (ngModelCtrl.$viewValue) {
+ scope.selectedTab = 0;
+ scope.title = ngModelCtrl.$viewValue.title;
+ scope.showTitle = ngModelCtrl.$viewValue.showTitle;
+ scope.backgroundColor = ngModelCtrl.$viewValue.backgroundColor;
+ scope.color = ngModelCtrl.$viewValue.color;
+ scope.padding = ngModelCtrl.$viewValue.padding;
+ scope.timewindow = ngModelCtrl.$viewValue.timewindow;
+ if (scope.widgetType !== types.widgetType.rpc.value) {
+ if (scope.datasources) {
+ scope.datasources.splice(0, scope.datasources.length);
+ } else {
+ scope.datasources = [];
+ }
+ if (ngModelCtrl.$viewValue.datasources) {
+ for (var i in ngModelCtrl.$viewValue.datasources) {
+ scope.datasources.push({value: ngModelCtrl.$viewValue.datasources[i]});
+ }
+ }
+ } else {
+ if (ngModelCtrl.$viewValue.targetDeviceAliasIds && ngModelCtrl.$viewValue.targetDeviceAliasIds.length > 0) {
+ var aliasId = ngModelCtrl.$viewValue.targetDeviceAliasIds[0];
+ if (scope.deviceAliases[aliasId]) {
+ scope.targetDeviceAlias.value = {id: aliasId, alias: scope.deviceAliases[aliasId].alias,
+ deviceId: scope.deviceAliases[aliasId].deviceId};
+ } else {
+ scope.targetDeviceAlias.value = null;
+ }
+ } else {
+ scope.targetDeviceAlias.value = null;
+ }
+ }
+
+ scope.settings = ngModelCtrl.$viewValue.settings;
+
+ scope.updateSchemaForm();
+
+ scope.updateDatasourcesAccordionState();
+ }
+ };
+
+ scope.displayAdvanced = function() {
+ return scope.widgetSettingsSchema && scope.widgetSettingsSchema.schema;
+ }
+
+ scope.updateSchemaForm = function() {
+ if (scope.widgetSettingsSchema && scope.widgetSettingsSchema.schema) {
+ scope.currentSettingsSchema = scope.widgetSettingsSchema.schema;
+ scope.currentSettingsForm = scope.widgetSettingsSchema.form || angular.copy(scope.defaultSettingsForm);
+ scope.currentSettings = scope.settings;
+ } else {
+ scope.currentSettingsForm = angular.copy(scope.defaultSettingsForm);
+ scope.currentSettingsSchema = angular.copy(scope.emptySettingsSchema);
+ scope.currentSettings = {};
+ }
+ }
+
+ scope.$on('datasources-accordion:onReady', function () {
+ if (scope.updateDatasourcesAccordionStatePending) {
+ scope.updateDatasourcesAccordionState();
+ }
+ });
+
+ scope.updateValidity = function () {
+ if (ngModelCtrl.$viewValue) {
+ var value = ngModelCtrl.$viewValue;
+ var valid;
+ if (scope.widgetType === types.widgetType.rpc.value) {
+ valid = value && value.targetDeviceAliasIds && value.targetDeviceAliasIds.length > 0;
+ ngModelCtrl.$setValidity('targetDeviceAliasIds', valid);
+ } else {
+ valid = value && value.datasources && value.datasources.length > 0;
+ ngModelCtrl.$setValidity('datasources', valid);
+ }
+ }
+ };
+
+ scope.$watch('title + showTitle + backgroundColor + color + padding + intervalSec', function () {
+ if (ngModelCtrl.$viewValue) {
+ var value = ngModelCtrl.$viewValue;
+ value.title = scope.title;
+ value.showTitle = scope.showTitle;
+ value.backgroundColor = scope.backgroundColor;
+ value.color = scope.color;
+ value.padding = scope.padding;
+ value.intervalSec = scope.intervalSec;
+ ngModelCtrl.$setViewValue(value);
+ }
+ });
+
+ scope.$watch('currentSettings', function () {
+ if (ngModelCtrl.$viewValue) {
+ var value = ngModelCtrl.$viewValue;
+ value.settings = scope.currentSettings;
+ ngModelCtrl.$setViewValue(value);
+ }
+ }, true);
+
+ scope.$watch('timewindow', function () {
+ if (ngModelCtrl.$viewValue) {
+ var value = ngModelCtrl.$viewValue;
+ value.timewindow = scope.timewindow;
+ ngModelCtrl.$setViewValue(value);
+ }
+ }, true);
+
+ scope.$watch('datasources', function () {
+ if (ngModelCtrl.$viewValue && scope.widgetType !== types.widgetType.rpc.value) {
+ var value = ngModelCtrl.$viewValue;
+ if (value.datasources) {
+ value.datasources.splice(0, value.datasources.length);
+ } else {
+ value.datasources = [];
+ }
+ if (scope.datasources) {
+ for (var i in scope.datasources) {
+ value.datasources.push(scope.datasources[i].value);
+ }
+ }
+ ngModelCtrl.$setViewValue(value);
+ scope.updateValidity();
+ }
+ }, true);
+
+ scope.$watch('targetDeviceAlias.value', function () {
+ if (ngModelCtrl.$viewValue && scope.widgetType === types.widgetType.rpc.value) {
+ var value = ngModelCtrl.$viewValue;
+ if (scope.targetDeviceAlias.value) {
+ value.targetDeviceAliasIds = [scope.targetDeviceAlias.value.id];
+ } else {
+ value.targetDeviceAliasIds = [];
+ }
+ ngModelCtrl.$setViewValue(value);
+ scope.updateValidity();
+ }
+ });
+
+ scope.addDatasource = function () {
+ var newDatasource;
+ if (scope.functionsOnly) {
+ newDatasource = angular.copy(utils.getDefaultDatasource(scope.datakeySettingsSchema.schema));
+ newDatasource.dataKeys = [scope.generateDataKey('Sin', types.dataKeyType.function)];
+ } else {
+ newDatasource = { type: types.datasourceType.device,
+ dataKeys: []
+ };
+ }
+ var datasource = {value: newDatasource};
+ scope.datasources.push(datasource);
+ if (scope.theForm) {
+ scope.theForm.$setDirty();
+ }
+ }
+
+ scope.removeDatasource = function ($event, datasource) {
+ var index = scope.datasources.indexOf(datasource);
+ if (index > -1) {
+ scope.datasources.splice(index, 1);
+ if (scope.theForm) {
+ scope.theForm.$setDirty();
+ }
+ }
+ };
+
+ scope.updateDatasourcesAccordionState = function () {
+ if (scope.widgetType !== types.widgetType.rpc.value) {
+ if (scope.datasourcesAccordion) {
+ scope.updateDatasourcesAccordionStatePending = false;
+ var expand = scope.datasources && scope.datasources.length < 4;
+ if (expand) {
+ scope.datasourcesAccordion.expand('datasources-pane');
+ } else {
+ scope.datasourcesAccordion.collapse('datasources-pane');
+ }
+ } else {
+ scope.updateDatasourcesAccordionStatePending = true;
+ }
+ }
+ }
+
+ scope.generateDataKey = function (chip, type) {
+
+ if (angular.isObject(chip)) {
+ chip._hash = Math.random();
+ return chip;
+ }
+
+ var result = {
+ name: chip,
+ type: type,
+ label: scope.genNextLabel(chip),
+ color: scope.genNextColor(),
+ settings: {},
+ _hash: Math.random()
+ };
+
+ if (type === types.dataKeyType.function) {
+ result.name = 'f(x)';
+ result.funcBody = utils.getPredefinedFunctionBody(chip);
+ if (!result.funcBody) {
+ result.funcBody = "return prevValue + 1;";
+ }
+ }
+
+ if (angular.isDefined(scope.datakeySettingsSchema.schema)) {
+ result.settings = jsonSchemaDefaults(scope.datakeySettingsSchema.schema);
+ }
+
+ return result;
+ };
+
+ scope.genNextLabel = function (name) {
+ var label = name;
+ var value = ngModelCtrl.$viewValue;
+ var i = 1;
+ var matches = false;
+ do {
+ matches = false;
+ if (value.datasources) {
+ for (var d in value.datasources) {
+ var datasource = value.datasources[d];
+ for (var k in datasource.dataKeys) {
+ var dataKey = datasource.dataKeys[k];
+ if (dataKey.label === label) {
+ i++;
+ label = name + ' ' + i;
+ matches = true;
+ }
+ }
+ }
+ }
+ } while (matches);
+ return label;
+ }
+
+ scope.genNextColor = function () {
+ var i = 0;
+ var value = ngModelCtrl.$viewValue;
+ if (value.datasources) {
+ for (var d in value.datasources) {
+ var datasource = value.datasources[d];
+ i += datasource.dataKeys.length;
+ }
+ }
+ return utils.getMaterialColor(i);
+ }
+
+ $compile(element.contents())(scope);
+ }
+ return {
+ restrict: "E",
+ require: "^ngModel",
+ scope: {
+ forceExpandDatasources: '=?',
+ widgetType: '=',
+ widgetSettingsSchema: '=',
+ datakeySettingsSchema: '=',
+ deviceAliases: '=',
+ functionsOnly: '=',
+ fetchDeviceKeys: '&',
+ onCreateDeviceAlias: '&',
+ theForm: '='
+ },
+ link: linker
+ };
+}
+
+/* eslint-enable angular/angularelement */
ui/src/app/components/widget-config.tpl.html 163(+163 -0)
diff --git a/ui/src/app/components/widget-config.tpl.html b/ui/src/app/components/widget-config.tpl.html
new file mode 100644
index 0000000..896e1e8
--- /dev/null
+++ b/ui/src/app/components/widget-config.tpl.html
@@ -0,0 +1,163 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-tabs ng-class="{'tb-headless': !displayAdvanced()}" id="tabs" md-border-bottom flex class="tb-absolute-fill"
+ md-selected="selectedTab">
+ <md-tab label="{{ 'widget-config.settings' | translate }}">
+ <div id="settings-tab">
+ <md-content class="md-padding" layout="column">
+ <md-input-container class="md-block">
+ <label translate>widget-config.title</label>
+ <input name="title" ng-model="title">
+ </md-input-container>
+ <span translate>widget-config.general-settings</span>
+ <div layout="row" layout-align="start center">
+ <div layout="row" layout-padding>
+ <md-checkbox flex aria-label="{{ 'widget-config.display-title' | translate }}"
+ ng-model="showTitle">{{ 'widget-config.display-title' | translate }}
+ </md-checkbox>
+ </div>
+ <div flex
+ md-color-picker
+ ng-model="backgroundColor"
+ label="{{ 'widget-config.background-color' | translate }}"
+ icon="format_color_fill"
+ default="#fff"
+ md-color-clear-button="false"
+ open-on-input="true"
+ md-color-generic-palette="false"
+ md-color-history="false"
+ ></div>
+ <div flex
+ md-color-picker
+ ng-model="color"
+ label="{{ 'widget-config.text-color' | translate }}"
+ icon="format_color_fill"
+ default="rgba(0, 0, 0, 0.87)"
+ md-color-clear-button="false"
+ open-on-input="true"
+ md-color-generic-palette="false"
+ md-color-history="false"
+ ></div>
+ <md-input-container flex>
+ <label translate>widget-config.padding</label>
+ <input ng-model="padding">
+ </md-input-container>
+ </div>
+ <div ng-show="widgetType === types.widgetType.timeseries.value" layout="row"
+ layout-align="center center">
+ <span translate style="padding-right: 8px;">widget-config.timewindow</span>
+ <tb-timewindow as-button="true" flex ng-model="timewindow"></tb-timewindow>
+ </div>
+ <v-accordion id="datasources-accordion" control="datasourcesAccordion" class="vAccordion--default"
+ ng-show="widgetType !== types.widgetType.rpc.value">
+ <v-pane id="datasources-pane" expanded="forceExpandDatasources">
+ <v-pane-header>
+ {{ 'widget-config.datasources' | translate }}
+ </v-pane-header>
+ <v-pane-content>
+ <div ng-if="datasources.length === 0">
+ <span translate layout-align="center center"
+ class="tb-prompt">datasource.add-datasource-prompt</span>
+ </div>
+ <div ng-if="datasources.length > 0">
+ <div flex layout="row" layout-align="start center">
+ <span flex="5"></span>
+ <div flex layout="row" layout-align="start center"
+ style="padding: 0 0 0 10px; margin: 5px;">
+ <span translate style="min-width: 110px;">widget-config.datasource-type</span>
+ <span translate flex
+ style="padding-left: 10px;">widget-config.datasource-parameters</span>
+ <span style="min-width: 40px;"></span>
+ </div>
+ </div>
+ <div style="max-height: 300px; overflow: auto; padding-bottom: 15px;">
+ <div flex layout="row" layout-align="start center"
+ ng-repeat="datasource in datasources">
+ <span flex="5">{{$index + 1}}.</span>
+ <div class="md-whiteframe-4dp" flex layout="row" layout-align="start center"
+ style="padding: 0 0 0 10px; margin: 5px;">
+ <tb-datasource flex ng-model="datasource.value"
+ widget-type="widgetType"
+ device-aliases="deviceAliases"
+ functions-only="functionsOnly"
+ datakey-settings-schema="datakeySettingsSchema"
+ generate-data-key="generateDataKey(chip,type)"
+ fetch-device-keys="fetchDeviceKeys({deviceAliasId: deviceAliasId, query: query, type: type})"
+ on-create-device-alias="onCreateDeviceAlias({event: event, alias: alias})"></tb-datasource>
+ <md-button ng-disabled="loading" class="md-icon-button md-primary"
+ style="min-width: 40px;"
+ ng-click="removeDatasource($event, datasource)"
+ aria-label="{{ 'action.remove' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'widget-config.remove-datasource' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.delete' | translate }}"
+ class="material-icons">
+ close
+ </md-icon>
+ </md-button>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div flex layout="row" layout-align="start center">
+ <md-button ng-disabled="loading" class="md-primary md-raised"
+ ng-click="addDatasource($event)" aria-label="{{ 'action.add' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'widget-config.add-datasource' | translate }}
+ </md-tooltip>
+ <md-icon class="material-icons">add</md-icon>
+ <span translate>action.add</span>
+ </md-button>
+ </div>
+ </v-pane-content>
+ </v-pane>
+ </v-accordion>
+ <v-accordion id="target-devices-accordion" control="targetDevicesAccordion" class="vAccordion--default"
+ ng-show="widgetType === types.widgetType.rpc.value">
+ <v-pane id="target-devices-pane" expanded="true">
+ <v-pane-header>
+ {{ 'widget-config.target-device' | translate }}
+ </v-pane-header>
+ <v-pane-content style="padding: 0 5px;">
+ <tb-device-alias-select flex
+ tb-required="widgetType === types.widgetType.rpc.value && !widgetEditMode"
+ device-aliases="deviceAliases"
+ ng-model="targetDeviceAlias.value"
+ on-create-device-alias="onCreateDeviceAlias({event: event, alias: alias})">
+ </tb-device-alias-select>
+ </v-pane-content>
+ </v-pane>
+ </v-accordion>
+ </md-content>
+ </div>
+ </md-tab>
+ <md-tab ng-if="displayAdvanced()" label="{{ 'widget-config.advanced' | translate }}">
+ <md-content class="md-padding" layout="column">
+ <ng-form name="ngform"
+ layout="column"
+ layout-padding>
+ <tb-json-form schema="currentSettingsSchema"
+ form="currentSettingsForm"
+ model="currentSettings"
+ form-control="ngform">
+ </tb-json-form>
+ </ng-form>
+ </md-content>
+ </md-tab>
+</md-tabs>
diff --git a/ui/src/app/components/widgets-bundle-select.directive.js b/ui/src/app/components/widgets-bundle-select.directive.js
new file mode 100644
index 0000000..fcf5def
--- /dev/null
+++ b/ui/src/app/components/widgets-bundle-select.directive.js
@@ -0,0 +1,96 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './widgets-bundle-select.scss';
+
+import thingsboardApiWidget from '../api/widget.service';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import widgetsBundleSelectTemplate from './widgets-bundle-select.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+
+export default angular.module('thingsboard.directives.widgetsBundleSelect', [thingsboardApiWidget])
+ .directive('tbWidgetsBundleSelect', WidgetsBundleSelect)
+ .name;
+
+/*@ngInject*/
+function WidgetsBundleSelect($compile, $templateCache, widgetService, types) {
+
+ var linker = function (scope, element, attrs, ngModelCtrl) {
+ var template = $templateCache.get(widgetsBundleSelectTemplate);
+ element.html(template);
+
+ scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false;
+ scope.widgetsBundle = null;
+ scope.widgetsBundles = [];
+
+ var widgetsBundleFetchFunction = widgetService.getAllWidgetsBundles;
+ if (angular.isDefined(scope.bundlesScope)) {
+ if (scope.bundlesScope === 'system') {
+ widgetsBundleFetchFunction = widgetService.getSystemWidgetsBundles;
+ } else if (scope.bundlesScope === 'tenant') {
+ widgetsBundleFetchFunction = widgetService.getTenantWidgetsBundles;
+ }
+ }
+
+ widgetsBundleFetchFunction().then(
+ function success(widgetsBundles) {
+ scope.widgetsBundles = widgetsBundles;
+ if (scope.selectFirstBundle) {
+ if (widgetsBundles.length > 0) {
+ scope.widgetsBundle = widgetsBundles[0];
+ }
+ }
+ },
+ function fail() {
+ }
+ );
+
+ scope.isSystem = function(item) {
+ return item && item.tenantId.id === types.id.nullUid;
+ }
+
+ scope.updateView = function () {
+ ngModelCtrl.$setViewValue(scope.widgetsBundle);
+ }
+
+ ngModelCtrl.$render = function () {
+ if (ngModelCtrl.$viewValue) {
+ scope.widgetsBundle = ngModelCtrl.$viewValue;
+ }
+ }
+
+ scope.$watch('widgetsBundle', function () {
+ scope.updateView();
+ });
+
+ $compile(element.contents())(scope);
+ }
+
+ return {
+ restrict: "E",
+ require: "^ngModel",
+ link: linker,
+ scope: {
+ bundlesScope: '@',
+ theForm: '=?',
+ tbRequired: '=?',
+ selectFirstBundle: '='
+ }
+ };
+}
\ No newline at end of file
diff --git a/ui/src/app/components/widgets-bundle-select.scss b/ui/src/app/components/widgets-bundle-select.scss
new file mode 100644
index 0000000..7b573f2
--- /dev/null
+++ b/ui/src/app/components/widgets-bundle-select.scss
@@ -0,0 +1,80 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import "../../scss/mixins";
+
+tb-widgets-bundle-select {
+ md-select {
+ margin: 0;
+ padding: 5px 20px;
+ }
+ .tb-bundle-item {
+ height: 24px;
+ line-height: 24px;
+ }
+}
+
+.tb-widgets-bundle-select {
+ .tb-bundle-item {
+ height: 48px;
+ line-height: 48px;
+ }
+}
+
+tb-widgets-bundle-select, .tb-widgets-bundle-select {
+ .md-text {
+ width: 100%;
+ }
+ .tb-bundle-item {
+ display: block;
+ span {
+ display: inline-block;
+ vertical-align: middle;
+ }
+ .tb-bundle-system {
+ font-size: 0.8rem;
+ opacity: 0.8;
+ float: right;
+ }
+ }
+ md-option {
+ height: auto !important;
+ white-space: normal !important;
+ }
+}
+
+md-toolbar {
+ tb-widgets-bundle-select {
+ md-select {
+ background: rgba(255,255,255,0.2);
+ .md-select-value {
+ color: #fff;
+ font-size: 1.2rem;
+ span:first-child:after {
+ color: #fff;
+ }
+ }
+ .md-select-value.md-select-placeholder {
+ color: #fff;
+ opacity: 0.8;
+ }
+ }
+ md-select.ng-invalid.ng-touched {
+ .md-select-value {
+ color: #fff !important;
+ }
+ }
+ }
+}
diff --git a/ui/src/app/components/widgets-bundle-select.tpl.html b/ui/src/app/components/widgets-bundle-select.tpl.html
new file mode 100644
index 0000000..7ba7a95
--- /dev/null
+++ b/ui/src/app/components/widgets-bundle-select.tpl.html
@@ -0,0 +1,29 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-select ng-required="tbRequired"
+ ng-model="widgetsBundle"
+ md-container-class="tb-widgets-bundle-select"
+ placeholder="{{ 'widget.select-widgets-bundle' | translate }}"
+ class="md-no-underline">
+ <md-option ng-repeat="item in widgetsBundles" ng-value="item">
+ <div class="tb-bundle-item">
+ <span>{{item.title}}</span>
+ <span translate class="tb-bundle-system" ng-if="isSystem(item)">widgets-bundle.system</span>
+ </div>
+ </md-option>
+</md-select>
ui/src/app/customer/add-customer.tpl.html 46(+46 -0)
diff --git a/ui/src/app/customer/add-customer.tpl.html b/ui/src/app/customer/add-customer.tpl.html
new file mode 100644
index 0000000..026342d
--- /dev/null
+++ b/ui/src/app/customer/add-customer.tpl.html
@@ -0,0 +1,46 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'customer.add' | translate }}" tb-help="'customers'" help-container-id="help-container">
+ <form name="theForm" ng-submit="vm.add()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>customer.add</h2>
+ <span flex></span>
+ <div id="help-container"></div>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <tb-customer customer="vm.item" is-edit="true" the-form="theForm"></tb-customer>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit" class="md-raised md-primary">
+ {{ 'action.add' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
+
ui/src/app/customer/customer.controller.js 164(+164 -0)
diff --git a/ui/src/app/customer/customer.controller.js b/ui/src/app/customer/customer.controller.js
new file mode 100644
index 0000000..68a9e0f
--- /dev/null
+++ b/ui/src/app/customer/customer.controller.js
@@ -0,0 +1,164 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import addCustomerTemplate from './add-customer.tpl.html';
+import customerCard from './customer-card.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function CustomerController(customerService, $state, $stateParams, $translate) {
+
+ var customerActionsList = [
+ {
+ onAction: function ($event, item) {
+ openCustomerUsers($event, item);
+ },
+ name: function() { return $translate.instant('user.users') },
+ details: function() { return $translate.instant('customer.manage-customer-users') },
+ icon: "account_circle"
+ },
+ {
+ onAction: function ($event, item) {
+ openCustomerDevices($event, item);
+ },
+ name: function() { return $translate.instant('device.devices') },
+ details: function() { return $translate.instant('customer.manage-customer-devices') },
+ icon: "devices_other"
+ },
+ {
+ onAction: function ($event, item) {
+ openCustomerDashboards($event, item);
+ },
+ name: function() { return $translate.instant('dashboard.dashboards') },
+ details: function() { return $translate.instant('customer.manage-customer-dashboards') },
+ icon: "dashboard"
+ },
+ {
+ onAction: function ($event, item) {
+ vm.grid.deleteItem($event, item);
+ },
+ name: function() { return $translate.instant('action.delete') },
+ details: function() { return $translate.instant('customer.delete') },
+ icon: "delete"
+ }
+ ];
+
+ var vm = this;
+
+ vm.customerGridConfig = {
+
+ refreshParamsFunc: null,
+
+ deleteItemTitleFunc: deleteCustomerTitle,
+ deleteItemContentFunc: deleteCustomerText,
+ deleteItemsTitleFunc: deleteCustomersTitle,
+ deleteItemsActionTitleFunc: deleteCustomersActionTitle,
+ deleteItemsContentFunc: deleteCustomersText,
+
+ fetchItemsFunc: fetchCustomers,
+ saveItemFunc: saveCustomer,
+ deleteItemFunc: deleteCustomer,
+
+ getItemTitleFunc: getCustomerTitle,
+
+ itemCardTemplateUrl: customerCard,
+
+ actionsList: customerActionsList,
+
+ onGridInited: gridInited,
+
+ addItemTemplateUrl: addCustomerTemplate,
+
+ addItemText: function() { return $translate.instant('customer.add-customer-text') },
+ noItemsText: function() { return $translate.instant('customer.no-customers-text') },
+ itemDetailsText: function() { return $translate.instant('customer.customer-details') }
+ };
+
+ if (angular.isDefined($stateParams.items) && $stateParams.items !== null) {
+ vm.customerGridConfig.items = $stateParams.items;
+ }
+
+ if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) {
+ vm.customerGridConfig.topIndex = $stateParams.topIndex;
+ }
+
+ vm.openCustomerUsers = openCustomerUsers;
+ vm.openCustomerDevices = openCustomerDevices;
+ vm.openCustomerDashboards = openCustomerDashboards;
+
+ function deleteCustomerTitle(customer) {
+ return $translate.instant('customer.delete-customer-title', {customerTitle: customer.title});
+ }
+
+ function deleteCustomerText() {
+ return $translate.instant('customer.delete-customer-text');
+ }
+
+ function deleteCustomersTitle(selectedCount) {
+ return $translate.instant('customer.delete-customers-title', {count: selectedCount}, 'messageformat');
+ }
+
+ function deleteCustomersActionTitle(selectedCount) {
+ return $translate.instant('customer.delete-customers-action-title', {count: selectedCount}, 'messageformat');
+ }
+
+ function deleteCustomersText() {
+ return $translate.instant('customer.delete-customers-text');
+ }
+
+ function gridInited(grid) {
+ vm.grid = grid;
+ }
+
+ function fetchCustomers(pageLink) {
+ return customerService.getCustomers(pageLink);
+ }
+
+ function saveCustomer(customer) {
+ return customerService.saveCustomer(customer);
+ }
+
+ function deleteCustomer(customerId) {
+ return customerService.deleteCustomer(customerId);
+ }
+
+ function getCustomerTitle(customer) {
+ return customer ? customer.title : '';
+ }
+
+ function openCustomerUsers($event, customer) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ $state.go('home.customers.users', {customerId: customer.id.id});
+ }
+
+ function openCustomerDevices($event, customer) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ $state.go('home.customers.devices', {customerId: customer.id.id});
+ }
+
+ function openCustomerDashboards($event, customer) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ $state.go('home.customers.dashboards', {customerId: customer.id.id});
+ }
+}
ui/src/app/customer/customer.directive.js 42(+42 -0)
diff --git a/ui/src/app/customer/customer.directive.js b/ui/src/app/customer/customer.directive.js
new file mode 100644
index 0000000..21ca44c
--- /dev/null
+++ b/ui/src/app/customer/customer.directive.js
@@ -0,0 +1,42 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import customerFieldsetTemplate from './customer-fieldset.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function CustomerDirective($compile, $templateCache) {
+ var linker = function (scope, element) {
+ var template = $templateCache.get(customerFieldsetTemplate);
+ element.html(template);
+ $compile(element.contents())(scope);
+ }
+ return {
+ restrict: "E",
+ link: linker,
+ scope: {
+ customer: '=',
+ isEdit: '=',
+ theForm: '=',
+ onManageUsers: '&',
+ onManageDevices: '&',
+ onManageDashboards: '&',
+ onDeleteCustomer: '&'
+ }
+ };
+}
ui/src/app/customer/customer.routes.js 47(+47 -0)
diff --git a/ui/src/app/customer/customer.routes.js b/ui/src/app/customer/customer.routes.js
new file mode 100644
index 0000000..bf0f27d
--- /dev/null
+++ b/ui/src/app/customer/customer.routes.js
@@ -0,0 +1,47 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import customersTemplate from './customers.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function CustomerRoutes($stateProvider) {
+
+ $stateProvider
+ .state('home.customers', {
+ url: '/customers',
+ params: {'topIndex': 0},
+ module: 'private',
+ auth: ['TENANT_ADMIN'],
+ views: {
+ "content@home": {
+ templateUrl: customersTemplate,
+ controllerAs: 'vm',
+ controller: 'CustomerController'
+ }
+ },
+ data: {
+ searchEnabled: true,
+ pageTitle: 'customer.customers'
+ },
+ ncyBreadcrumb: {
+ label: '{"icon": "supervisor_account", "label": "customer.customers"}'
+ }
+ });
+
+}
ui/src/app/customer/customer-card.tpl.html 18(+18 -0)
diff --git a/ui/src/app/customer/customer-card.tpl.html b/ui/src/app/customer/customer-card.tpl.html
new file mode 100644
index 0000000..99071bd
--- /dev/null
+++ b/ui/src/app/customer/customer-card.tpl.html
@@ -0,0 +1,18 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div class="tb-uppercase">{{ item | contactShort }}</div>
\ No newline at end of file
diff --git a/ui/src/app/customer/customer-fieldset.tpl.html b/ui/src/app/customer/customer-fieldset.tpl.html
new file mode 100644
index 0000000..dcecc2b
--- /dev/null
+++ b/ui/src/app/customer/customer-fieldset.tpl.html
@@ -0,0 +1,38 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-button ng-click="onManageUsers({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'customer.manage-users' | translate }}</md-button>
+<md-button ng-click="onManageDevices({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'customer.manage-devices' | translate }}</md-button>
+<md-button ng-click="onManageDashboards({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'customer.manage-dashboards' | translate }}</md-button>
+<md-button ng-click="onDeleteCustomer({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'customer.delete' | translate }}</md-button>
+
+<md-content class="md-padding" layout="column">
+ <fieldset ng-disabled="loading || !isEdit">
+ <md-input-container class="md-block">
+ <label translate>customer.title</label>
+ <input required name="title" ng-model="customer.title">
+ <div ng-messages="theForm.title.$error">
+ <div translate ng-message="required">customer.title-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container class="md-block">
+ <label translate>customer.description</label>
+ <textarea ng-model="customer.additionalInfo.description" rows="2"></textarea>
+ </md-input-container>
+ <tb-contact contact="customer" the-form="theForm" is-edit="isEdit"></tb-contact>
+ </fieldset>
+</md-content>
ui/src/app/customer/customers.tpl.html 29(+29 -0)
diff --git a/ui/src/app/customer/customers.tpl.html b/ui/src/app/customer/customers.tpl.html
new file mode 100644
index 0000000..a8b8aac
--- /dev/null
+++ b/ui/src/app/customer/customers.tpl.html
@@ -0,0 +1,29 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<tb-grid grid-configuration="vm.customerGridConfig">
+ <details-buttons tb-help="'customers'" help-container-id="help-container">
+ <div id="help-container"></div>
+ </details-buttons>
+ <tb-customer customer="vm.grid.operatingItem()"
+ is-edit="vm.grid.detailsConfig.isDetailsEditMode"
+ the-form="vm.grid.detailsForm"
+ on-manage-users="vm.openCustomerUsers(event, vm.grid.detailsConfig.currentItem)"
+ on-manage-devices="vm.openCustomerDevices(event, vm.grid.detailsConfig.currentItem)"
+ on-manage-dashboards="vm.openCustomerDashboards(event, vm.grid.detailsConfig.currentItem)"
+ on-delete-customer="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-customer>
+</tb-grid>
ui/src/app/customer/index.js 36(+36 -0)
diff --git a/ui/src/app/customer/index.js b/ui/src/app/customer/index.js
new file mode 100644
index 0000000..08a1c19
--- /dev/null
+++ b/ui/src/app/customer/index.js
@@ -0,0 +1,36 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import uiRouter from 'angular-ui-router';
+import thingsboardApiCustomer from '../api/customer.service';
+import thingsboardGrid from '../components/grid.directive';
+import thingsboardContact from '../components/contact.directive';
+import thingsboardContactShort from '../components/contact-short.filter';
+
+import CustomerRoutes from './customer.routes';
+import CustomerController from './customer.controller';
+import CustomerDirective from './customer.directive';
+
+export default angular.module('thingsboard.customer', [
+ uiRouter,
+ thingsboardApiCustomer,
+ thingsboardGrid,
+ thingsboardContact,
+ thingsboardContactShort
+])
+ .config(CustomerRoutes)
+ .controller('CustomerController', CustomerController)
+ .directive('tbCustomer', CustomerDirective)
+ .name;
ui/src/app/dashboard/add-dashboard.tpl.html 45(+45 -0)
diff --git a/ui/src/app/dashboard/add-dashboard.tpl.html b/ui/src/app/dashboard/add-dashboard.tpl.html
new file mode 100644
index 0000000..516f396
--- /dev/null
+++ b/ui/src/app/dashboard/add-dashboard.tpl.html
@@ -0,0 +1,45 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'dashboard.add' | translate }}" tb-help="'dashboards'" help-container-id="help-container">
+ <form name="theForm" ng-submit="vm.add()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>dashboard.add</h2>
+ <span flex></span>
+ <div id="help-container"></div>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <tb-dashboard-details dashboard="vm.item" is-edit="true" the-form="theForm"></tb-dashboard-details>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit" class="md-raised md-primary">
+ {{ 'action.add' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
\ No newline at end of file
diff --git a/ui/src/app/dashboard/add-dashboards-to-customer.controller.js b/ui/src/app/dashboard/add-dashboards-to-customer.controller.js
new file mode 100644
index 0000000..4a360b7
--- /dev/null
+++ b/ui/src/app/dashboard/add-dashboards-to-customer.controller.js
@@ -0,0 +1,122 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*@ngInject*/
+export default function AddDashboardsToCustomerController(dashboardService, $mdDialog, $q, customerId, dashboards) {
+
+ var vm = this;
+
+ vm.dashboards = dashboards;
+ vm.searchText = '';
+
+ vm.assign = assign;
+ vm.cancel = cancel;
+ vm.hasData = hasData;
+ vm.noData = noData;
+ vm.searchDashboardTextUpdated = searchDashboardTextUpdated;
+ vm.toggleDashboardSelection = toggleDashboardSelection;
+
+ vm.theDashboards = {
+ getItemAtIndex: function (index) {
+ if (index > vm.dashboards.data.length) {
+ vm.theDashboards.fetchMoreItems_(index);
+ return null;
+ }
+ var item = vm.dashboards.data[index];
+ if (item) {
+ item.indexNumber = index + 1;
+ }
+ return item;
+ },
+
+ getLength: function () {
+ if (vm.dashboards.hasNext) {
+ return vm.dashboards.data.length + vm.dashboards.nextPageLink.limit;
+ } else {
+ return vm.dashboards.data.length;
+ }
+ },
+
+ fetchMoreItems_: function () {
+ if (vm.dashboards.hasNext && !vm.dashboards.pending) {
+ vm.dashboards.pending = true;
+ dashboardService.getTenantDashboards(vm.dashboards.nextPageLink).then(
+ function success(dashboards) {
+ vm.dashboards.data = vm.dashboards.data.concat(dashboards.data);
+ vm.dashboards.nextPageLink = dashboards.nextPageLink;
+ vm.dashboards.hasNext = dashboards.hasNext;
+ if (vm.dashboards.hasNext) {
+ vm.dashboards.nextPageLink.limit = vm.dashboards.pageSize;
+ }
+ vm.dashboards.pending = false;
+ },
+ function fail() {
+ vm.dashboards.hasNext = false;
+ vm.dashboards.pending = false;
+ });
+ }
+ }
+ }
+
+ function cancel () {
+ $mdDialog.cancel();
+ }
+
+ function assign () {
+ var tasks = [];
+ for (var dashboardId in vm.dashboards.selections) {
+ tasks.push(dashboardService.assignDashboardToCustomer(customerId, dashboardId));
+ }
+ $q.all(tasks).then(function () {
+ $mdDialog.hide();
+ });
+ }
+
+ function noData () {
+ return vm.dashboards.data.length == 0 && !vm.dashboards.hasNext;
+ }
+
+ function hasData () {
+ return vm.dashboards.data.length > 0;
+ }
+
+ function toggleDashboardSelection ($event, dashboard) {
+ $event.stopPropagation();
+ var selected = angular.isDefined(dashboard.selected) && dashboard.selected;
+ dashboard.selected = !selected;
+ if (dashboard.selected) {
+ vm.dashboards.selections[dashboard.id.id] = true;
+ vm.dashboards.selectedCount++;
+ } else {
+ delete vm.dashboards.selections[dashboard.id.id];
+ vm.dashboards.selectedCount--;
+ }
+ }
+
+ function searchDashboardTextUpdated () {
+ vm.dashboards = {
+ pageSize: vm.dashboards.pageSize,
+ data: [],
+ nextPageLink: {
+ limit: vm.dashboards.pageSize,
+ textSearch: vm.searchText
+ },
+ selections: {},
+ selectedCount: 0,
+ hasNext: true,
+ pending: false
+ };
+ }
+}
\ No newline at end of file
diff --git a/ui/src/app/dashboard/add-dashboards-to-customer.tpl.html b/ui/src/app/dashboard/add-dashboards-to-customer.tpl.html
new file mode 100644
index 0000000..f258a9b
--- /dev/null
+++ b/ui/src/app/dashboard/add-dashboards-to-customer.tpl.html
@@ -0,0 +1,77 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'dashboard.assign-dashboard-to-customer' | translate }}">
+ <form name="theForm" ng-submit="vm.assign()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>dashboard.assign-dashboard-to-customer</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <fieldset>
+ <span translate>dashboard.assign-dashboard-to-customer-text</span>
+ <md-input-container class="md-block" style='margin-bottom: 0px;'>
+ <label> </label>
+ <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">
+ search
+ </md-icon>
+ <input id="dashboard-search" autofocus ng-model="vm.searchText"
+ ng-change="vm.searchDashboardTextUpdated()"
+ placeholder="{{ 'common.enter-search' | translate }}"/>
+ </md-input-container>
+ <div style='min-height: 150px;'>
+ <span translate layout-align="center center"
+ style="text-transform: uppercase; display: flex; height: 150px;"
+ class="md-subhead"
+ ng-show="vm.noData()">dashboard.no-dashboards-text</span>
+ <md-virtual-repeat-container ng-show="vm.hasData()"
+ tb-scope-element="repeatContainer" md-top-index="vm.topIndex" flex
+ style='min-height: 150px; width: 100%;'>
+ <md-list>
+ <md-list-item md-virtual-repeat="dashboard in vm.theDashboards" md-on-demand
+ class="repeated-item" flex>
+ <md-checkbox ng-click="vm.toggleDashboardSelection($event, dashboard)"
+ aria-label="{{ 'item.selected' | translate }}"
+ ng-checked="dashboard.selected"></md-checkbox>
+ <span> {{ dashboard.title }} </span>
+ </md-list-item>
+ </md-list>
+ </md-virtual-repeat-container>
+ </div>
+ </fieldset>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || vm.dashboards.selectedCount == 0" type="submit"
+ class="md-raised md-primary">
+ {{ 'action.assign' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+ translate }}
+ </md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
\ No newline at end of file
ui/src/app/dashboard/add-widget.controller.js 124(+124 -0)
diff --git a/ui/src/app/dashboard/add-widget.controller.js b/ui/src/app/dashboard/add-widget.controller.js
new file mode 100644
index 0000000..ec77404
--- /dev/null
+++ b/ui/src/app/dashboard/add-widget.controller.js
@@ -0,0 +1,124 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import deviceAliasesTemplate from './device-aliases.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function AddWidgetController($scope, widgetService, deviceService, $mdDialog, $q, $document, types, dashboard, widget, widgetInfo) {
+
+ var vm = this;
+
+ vm.dashboard = dashboard;
+ vm.widget = widget;
+ vm.widgetInfo = widgetInfo;
+
+ vm.functionsOnly = false;
+
+ vm.helpLinkIdForWidgetType = helpLinkIdForWidgetType;
+ vm.add = add;
+ vm.cancel = cancel;
+ vm.fetchDeviceKeys = fetchDeviceKeys;
+ vm.createDeviceAlias = createDeviceAlias;
+
+ vm.widgetConfig = vm.widget.config;
+ var settingsSchema = vm.widgetInfo.settingsSchema;
+ var dataKeySettingsSchema = vm.widgetInfo.dataKeySettingsSchema;
+ if (!settingsSchema || settingsSchema === '') {
+ vm.settingsSchema = {};
+ } else {
+ vm.settingsSchema = angular.fromJson(settingsSchema);
+ }
+ if (!dataKeySettingsSchema || dataKeySettingsSchema === '') {
+ vm.dataKeySettingsSchema = {};
+ } else {
+ vm.dataKeySettingsSchema = angular.fromJson(dataKeySettingsSchema);
+ }
+
+ function helpLinkIdForWidgetType() {
+ var link = 'widgetsConfig';
+ if (vm.widget && vm.widget.type) {
+ switch (vm.widget.type) {
+ case types.widgetType.timeseries.value: {
+ link = 'widgetsConfigTimeseries';
+ break;
+ }
+ case types.widgetType.latest.value: {
+ link = 'widgetsConfigLatest';
+ break;
+ }
+ case types.widgetType.rpc.value: {
+ link = 'widgetsConfigRpc';
+ break;
+ }
+ }
+ }
+ return link;
+ }
+
+ function cancel () {
+ $mdDialog.cancel();
+ }
+
+ function add () {
+ if ($scope.theForm.$valid) {
+ $scope.theForm.$setPristine();
+ vm.widget.config = vm.widgetConfig;
+ $mdDialog.hide(vm.widget);
+ }
+ }
+
+ function fetchDeviceKeys (deviceAliasId, query, type) {
+ var deviceAlias = vm.dashboard.configuration.deviceAliases[deviceAliasId];
+ if (deviceAlias && deviceAlias.deviceId) {
+ return deviceService.getDeviceKeys(deviceAlias.deviceId, query, type);
+ } else {
+ return $q.when([]);
+ }
+ }
+
+ function createDeviceAlias (event, alias) {
+
+ var deferred = $q.defer();
+ var singleDeviceAlias = {id: null, alias: alias, deviceId: null};
+
+ $mdDialog.show({
+ controller: 'DeviceAliasesController',
+ controllerAs: 'vm',
+ templateUrl: deviceAliasesTemplate,
+ locals: {
+ deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases),
+ aliasToWidgetsMap: null,
+ isSingleDevice: true,
+ singleDeviceAlias: singleDeviceAlias
+ },
+ parent: angular.element($document[0].body),
+ fullscreen: true,
+ skipHide: true,
+ targetEvent: event
+ }).then(function (singleDeviceAlias) {
+ vm.dashboard.configuration.deviceAliases[singleDeviceAlias.id] =
+ { alias: singleDeviceAlias.alias, deviceId: singleDeviceAlias.deviceId };
+ deferred.resolve(singleDeviceAlias);
+ }, function () {
+ deferred.reject();
+ });
+
+ return deferred.promise;
+ }
+}
ui/src/app/dashboard/add-widget.tpl.html 59(+59 -0)
diff --git a/ui/src/app/dashboard/add-widget.tpl.html b/ui/src/app/dashboard/add-widget.tpl.html
new file mode 100644
index 0000000..aefcdaf
--- /dev/null
+++ b/ui/src/app/dashboard/add-widget.tpl.html
@@ -0,0 +1,59 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'widget.add' | translate }}" style="width: 900px;" tb-help="vm.helpLinkIdForWidgetType()" help-container-id="help-container">
+ <form name="theForm" ng-submit="vm.add()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>widget.add</h2>
+ <span flex></span>
+ <div id="help-container"></div>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content" style="padding-top: 0px;">
+ <fieldset ng-disabled="loading" style="position: relative; height: 600px;">
+ <tb-widget-config widget-type="vm.widget.type"
+ force-expand-datasources="true"
+ ng-model="vm.widgetConfig"
+ widget-settings-schema="vm.settingsSchema"
+ datakey-settings-schema="vm.dataKeySettingsSchema"
+ device-aliases="vm.dashboard.configuration.deviceAliases"
+ functions-only="vm.functionsOnly"
+ fetch-device-keys="vm.fetchDeviceKeys(deviceAliasId, query, type)"
+ on-create-device-alias="vm.createDeviceAlias(event, alias)"
+ the-form="theForm"></tb-widget-config>
+ </fieldset>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || theForm.$invalid" type="submit"
+ class="md-raised md-primary">
+ {{ 'action.add' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+ translate }}
+ </md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
diff --git a/ui/src/app/dashboard/assign-to-customer.controller.js b/ui/src/app/dashboard/assign-to-customer.controller.js
new file mode 100644
index 0000000..a5ca3cb
--- /dev/null
+++ b/ui/src/app/dashboard/assign-to-customer.controller.js
@@ -0,0 +1,123 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*@ngInject*/
+export default function AssignDashboardToCustomerController(customerService, dashboardService, $mdDialog, $q, dashboardIds, customers) {
+
+ var vm = this;
+
+ vm.customers = customers;
+ vm.searchText = '';
+
+ vm.assign = assign;
+ vm.cancel = cancel;
+ vm.isCustomerSelected = isCustomerSelected;
+ vm.hasData = hasData;
+ vm.noData = noData;
+ vm.searchCustomerTextUpdated = searchCustomerTextUpdated;
+ vm.toggleCustomerSelection = toggleCustomerSelection;
+
+ vm.theCustomers = {
+ getItemAtIndex: function (index) {
+ if (index > vm.customers.data.length) {
+ vm.theCustomers.fetchMoreItems_(index);
+ return null;
+ }
+ var item = vm.customers.data[index];
+ if (item) {
+ item.indexNumber = index + 1;
+ }
+ return item;
+ },
+
+ getLength: function () {
+ if (vm.customers.hasNext) {
+ return vm.customers.data.length + vm.customers.nextPageLink.limit;
+ } else {
+ return vm.customers.data.length;
+ }
+ },
+
+ fetchMoreItems_: function () {
+ if (vm.customers.hasNext && !vm.customers.pending) {
+ vm.customers.pending = true;
+ customerService.getCustomers(vm.customers.nextPageLink).then(
+ function success(customers) {
+ vm.customers.data = vm.customers.data.concat(customers.data);
+ vm.customers.nextPageLink = customers.nextPageLink;
+ vm.customers.hasNext = customers.hasNext;
+ if (vm.customers.hasNext) {
+ vm.customers.nextPageLink.limit = vm.customers.pageSize;
+ }
+ vm.customers.pending = false;
+ },
+ function fail() {
+ vm.customers.hasNext = false;
+ vm.customers.pending = false;
+ });
+ }
+ }
+ };
+
+ function cancel () {
+ $mdDialog.cancel();
+ }
+
+ function assign () {
+ var tasks = [];
+ for (var dashboardId in dashboardIds) {
+ tasks.push(dashboardService.assignDashboardToCustomer(vm.customers.selection.id.id, dashboardIds[dashboardId]));
+ }
+ $q.all(tasks).then(function () {
+ $mdDialog.hide();
+ });
+ }
+
+ function noData () {
+ return vm.customers.data.length == 0 && !vm.customers.hasNext;
+ }
+
+ function hasData () {
+ return vm.customers.data.length > 0;
+ }
+
+ function toggleCustomerSelection ($event, customer) {
+ $event.stopPropagation();
+ if (vm.isCustomerSelected(customer)) {
+ vm.customers.selection = null;
+ } else {
+ vm.customers.selection = customer;
+ }
+ }
+
+ function isCustomerSelected (customer) {
+ return vm.customers.selection != null && customer &&
+ customer.id.id === vm.customers.selection.id.id;
+ }
+
+ function searchCustomerTextUpdated () {
+ vm.customers = {
+ pageSize: vm.customers.pageSize,
+ data: [],
+ nextPageLink: {
+ limit: vm.customers.pageSize,
+ textSearch: vm.searchText
+ },
+ selection: null,
+ hasNext: true,
+ pending: false
+ };
+ }
+}
diff --git a/ui/src/app/dashboard/assign-to-customer.tpl.html b/ui/src/app/dashboard/assign-to-customer.tpl.html
new file mode 100644
index 0000000..dd539fa
--- /dev/null
+++ b/ui/src/app/dashboard/assign-to-customer.tpl.html
@@ -0,0 +1,76 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'dashboard.assign-dashboard-to-customer' | translate }}">
+ <form name="theForm" ng-submit="vm.assign()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>dashboard.assign-dashboard-to-customer</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <fieldset>
+ <span translate>dashboard.assign-to-customer-text</span>
+ <md-input-container class="md-block" style='margin-bottom: 0px;'>
+ <label> </label>
+ <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">
+ search
+ </md-icon>
+ <input id="customer-search" autofocus ng-model="vm.searchText"
+ ng-change="vm.searchCustomerTextUpdated()"
+ placeholder="{{ 'common.enter-search' | translate }}"/>
+ </md-input-container>
+ <div style='min-height: 150px;'>
+ <span translate layout-align="center center"
+ style="text-transform: uppercase; display: flex; height: 150px;"
+ class="md-subhead"
+ ng-show="vm.noData()">customer.no-customers-text</span>
+ <md-virtual-repeat-container ng-show="vm.hasData()"
+ tb-scope-element="repeatContainer" md-top-index="vm.topIndex" flex
+ style='min-height: 150px; width: 100%;'>
+ <md-list>
+ <md-list-item md-virtual-repeat="customer in vm.theCustomers" md-on-demand
+ class="repeated-item" flex>
+ <md-checkbox ng-click="vm.toggleCustomerSelection($event, customer)"
+ aria-label="{{ 'item.selected' | translate }}"
+ ng-checked="vm.isCustomerSelected(customer)"></md-checkbox>
+ <span> {{ customer.title }} </span>
+ </md-list-item>
+ </md-list>
+ </md-virtual-repeat-container>
+ </div>
+ </fieldset>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || vm.customers.selection==null" type="submit" class="md-raised md-primary">
+ {{ 'action.assign' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+ translate }}
+ </md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
\ No newline at end of file
ui/src/app/dashboard/dashboard.controller.js 424(+424 -0)
diff --git a/ui/src/app/dashboard/dashboard.controller.js b/ui/src/app/dashboard/dashboard.controller.js
new file mode 100644
index 0000000..18c2b5f
--- /dev/null
+++ b/ui/src/app/dashboard/dashboard.controller.js
@@ -0,0 +1,424 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import deviceAliasesTemplate from './device-aliases.tpl.html';
+import addWidgetTemplate from './add-widget.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function DashboardController(types, widgetService, userService,
+ dashboardService, $window, $rootScope,
+ $scope, $state, $stateParams, $mdDialog, $timeout, $document, $q, $translate, $filter) {
+
+ var user = userService.getCurrentUser();
+
+ var vm = this;
+
+ vm.dashboard = null;
+ vm.editingWidget = null;
+ vm.editingWidgetIndex = null;
+ vm.editingWidgetSubtitle = null;
+ vm.forceDashboardMobileMode = false;
+ vm.isAddingWidget = false;
+ vm.isEdit = false;
+ vm.isEditingWidget = false;
+ vm.latestWidgetTypes = [];
+ vm.timeseriesWidgetTypes = [];
+ vm.rpcWidgetTypes = [];
+ vm.widgetEditMode = $state.$current.data.widgetEditMode;
+ vm.widgets = [];
+
+ vm.addWidget = addWidget;
+ vm.addWidgetFromType = addWidgetFromType;
+ vm.dashboardInited = dashboardInited;
+ vm.dashboardInitFailed = dashboardInitFailed;
+ vm.widgetClicked = widgetClicked;
+ vm.editWidget = editWidget;
+ vm.isTenantAdmin = isTenantAdmin;
+ vm.loadDashboard = loadDashboard;
+ vm.noData = noData;
+ vm.onAddWidgetClosed = onAddWidgetClosed;
+ vm.onEditWidgetClosed = onEditWidgetClosed;
+ vm.openDeviceAliases = openDeviceAliases;
+ vm.removeWidget = removeWidget;
+ vm.saveDashboard = saveDashboard;
+ vm.saveWidget = saveWidget;
+ vm.toggleDashboardEditMode = toggleDashboardEditMode;
+ vm.onRevertWidgetEdit = onRevertWidgetEdit;
+ vm.helpLinkIdForWidgetType = helpLinkIdForWidgetType;
+
+ vm.widgetsBundle;
+
+ $scope.$watch('vm.widgetsBundle', function (newVal, prevVal) {
+ if (newVal !== prevVal && !vm.widgetEditMode) {
+ loadWidgetLibrary();
+ }
+ });
+
+ function loadWidgetLibrary() {
+ vm.latestWidgetTypes = [];
+ vm.timeseriesWidgetTypes = [];
+ vm.rpcWidgetTypes = [];
+ if (vm.widgetsBundle) {
+ var bundleAlias = vm.widgetsBundle.alias;
+ var isSystem = vm.widgetsBundle.tenantId.id === types.id.nullUid;
+
+ widgetService.getBundleWidgetTypes(bundleAlias, isSystem).then(
+ function (widgetTypes) {
+
+ widgetTypes = $filter('orderBy')(widgetTypes, ['-name']);
+
+ var top = 0;
+ var sizeY = 0;
+
+ if (widgetTypes.length > 0) {
+ loadNext(0);
+ }
+
+ function loadNextOrComplete(i) {
+ i++;
+ if (i < widgetTypes.length) {
+ loadNext(i);
+ }
+ }
+
+ function loadNext(i) {
+ var widgetType = widgetTypes[i];
+ var widgetTypeInfo = widgetService.toWidgetInfo(widgetType);
+ var widget = {
+ isSystemType: isSystem,
+ bundleAlias: bundleAlias,
+ typeAlias: widgetTypeInfo.alias,
+ type: widgetTypeInfo.type,
+ title: widgetTypeInfo.widgetName,
+ sizeX: widgetTypeInfo.sizeX,
+ sizeY: widgetTypeInfo.sizeY,
+ row: top,
+ col: 0,
+ config: angular.fromJson(widgetTypeInfo.defaultConfig)
+ };
+ widget.config.title = widgetTypeInfo.widgetName;
+ if (widgetTypeInfo.type === types.widgetType.timeseries.value) {
+ vm.timeseriesWidgetTypes.push(widget);
+ } else if (widgetTypeInfo.type === types.widgetType.latest.value) {
+ vm.latestWidgetTypes.push(widget);
+ } else if (widgetTypeInfo.type === types.widgetType.rpc.value) {
+ vm.rpcWidgetTypes.push(widget);
+ }
+ top += sizeY;
+ loadNextOrComplete(i);
+
+ }
+ }
+ );
+ }
+ }
+
+ function loadDashboard() {
+
+ var deferred = $q.defer();
+
+ if (vm.widgetEditMode) {
+ $timeout(function () {
+ vm.widgets = [{
+ isSystemType: true,
+ bundleAlias: 'customWidgetBundle',
+ typeAlias: 'customWidget',
+ type: $rootScope.editWidgetInfo.type,
+ title: 'My widget',
+ sizeX: $rootScope.editWidgetInfo.sizeX * 2,
+ sizeY: $rootScope.editWidgetInfo.sizeY * 2,
+ row: 2,
+ col: 4,
+ config: angular.fromJson($rootScope.editWidgetInfo.defaultConfig)
+ }];
+ vm.widgets[0].config.title = vm.widgets[0].config.title || $rootScope.editWidgetInfo.widgetName;
+ deferred.resolve();
+ var parentScope = $window.parent.angular.element($window.frameElement).scope();
+ parentScope.$root.$broadcast('widgetEditModeInited');
+ parentScope.$root.$apply();
+
+ $scope.$watch('vm.widgets', function () {
+ var widget = vm.widgets[0];
+ parentScope.$root.$broadcast('widgetEditUpdated', widget);
+ parentScope.$root.$apply();
+ }, true);
+
+ });
+ } else {
+
+ dashboardService.getDashboard($stateParams.dashboardId)
+ .then(function success(dashboard) {
+ vm.dashboard = dashboard;
+ if (vm.dashboard.configuration == null) {
+ vm.dashboard.configuration = {widgets: [], deviceAliases: {}};
+ }
+ if (angular.isUndefined(vm.dashboard.configuration.widgets)) {
+ vm.dashboard.configuration.widgets = [];
+ }
+ if (angular.isUndefined(vm.dashboard.configuration.deviceAliases)) {
+ vm.dashboard.configuration.deviceAliases = {};
+ }
+ vm.widgets = vm.dashboard.configuration.widgets;
+ deferred.resolve();
+ }, function fail(e) {
+ deferred.reject(e);
+ });
+
+ }
+ return deferred.promise;
+ }
+
+ function dashboardInitFailed() {
+ var parentScope = $window.parent.angular.element($window.frameElement).scope();
+ parentScope.$emit('widgetEditModeInited');
+ parentScope.$apply();
+ }
+
+ function dashboardInited(dashboard) {
+ vm.dashboardContainer = dashboard;
+ }
+
+ function isTenantAdmin() {
+ return user.authority === 'TENANT_ADMIN';
+ }
+
+ function noData() {
+ return vm.widgets.length == 0;
+ }
+
+ function openDeviceAliases($event) {
+ var aliasToWidgetsMap = {};
+ var widgetsTitleList;
+ for (var w in vm.widgets) {
+ var widget = vm.widgets[w];
+ if (widget.type === types.widgetType.rpc.value) {
+ if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length > 0) {
+ var targetDeviceAliasId = widget.config.targetDeviceAliasIds[0];
+ widgetsTitleList = aliasToWidgetsMap[targetDeviceAliasId];
+ if (!widgetsTitleList) {
+ widgetsTitleList = [];
+ aliasToWidgetsMap[targetDeviceAliasId] = widgetsTitleList;
+ }
+ widgetsTitleList.push(widget.config.title);
+ }
+ } else {
+ for (var i in widget.config.datasources) {
+ var datasource = widget.config.datasources[i];
+ if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) {
+ widgetsTitleList = aliasToWidgetsMap[datasource.deviceAliasId];
+ if (!widgetsTitleList) {
+ widgetsTitleList = [];
+ aliasToWidgetsMap[datasource.deviceAliasId] = widgetsTitleList;
+ }
+ widgetsTitleList.push(widget.config.title);
+ }
+ }
+ }
+ }
+
+ $mdDialog.show({
+ controller: 'DeviceAliasesController',
+ controllerAs: 'vm',
+ templateUrl: deviceAliasesTemplate,
+ locals: {
+ deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases),
+ aliasToWidgetsMap: aliasToWidgetsMap,
+ isSingleDevice: false,
+ singleDeviceAlias: null
+ },
+ parent: angular.element($document[0].body),
+ skipHide: true,
+ fullscreen: true,
+ targetEvent: $event
+ }).then(function (deviceAliases) {
+ vm.dashboard.configuration.deviceAliases = deviceAliases;
+ }, function () {
+ });
+ }
+
+ function editWidget($event, widget) {
+ $event.stopPropagation();
+ var newEditingIndex = vm.widgets.indexOf(widget);
+ if (vm.editingWidgetIndex === newEditingIndex) {
+ $timeout(onEditWidgetClosed());
+ } else {
+ var transition = !vm.forceDashboardMobileMode;
+ vm.editingWidgetIndex = vm.widgets.indexOf(widget);
+ vm.editingWidget = angular.copy(widget);
+ vm.editingWidgetSubtitle = widgetService.getInstantWidgetInfo(vm.editingWidget).widgetName;
+ vm.forceDashboardMobileMode = true;
+ vm.isEditingWidget = true;
+
+ if (vm.dashboardContainer) {
+ var delayOffset = transition ? 350 : 0;
+ var delay = transition ? 400 : 300;
+ $timeout(function () {
+ vm.dashboardContainer.highlightWidget(vm.editingWidgetIndex, delay);
+ }, delayOffset, false);
+ }
+ }
+ }
+
+ function widgetClicked($event, widget) {
+ if (vm.isEditingWidget) {
+ editWidget($event, widget);
+ }
+ }
+
+ function helpLinkIdForWidgetType() {
+ var link = 'widgetsConfig';
+ if (vm.editingWidget && vm.editingWidget.type) {
+ switch (vm.editingWidget.type) {
+ case types.widgetType.timeseries.value: {
+ link = 'widgetsConfigTimeseries';
+ break;
+ }
+ case types.widgetType.latest.value: {
+ link = 'widgetsConfigLatest';
+ break;
+ }
+ case types.widgetType.rpc.value: {
+ link = 'widgetsConfigRpc';
+ break;
+ }
+ }
+ }
+ return link;
+ }
+
+ function onRevertWidgetEdit(widgetForm) {
+ if (widgetForm.$dirty) {
+ widgetForm.$setPristine();
+ vm.editingWidget = angular.copy(vm.widgets[vm.editingWidgetIndex]);
+ }
+ }
+
+ function saveWidget(widgetForm) {
+ widgetForm.$setPristine();
+ vm.widgets[vm.editingWidgetIndex] = angular.copy(vm.editingWidget);
+ }
+
+ function onEditWidgetClosed() {
+ vm.editingWidgetIndex = null;
+ vm.editingWidget = null;
+ vm.editingWidgetSubtitle = null;
+ vm.isEditingWidget = false;
+ if (vm.dashboardContainer) {
+ vm.dashboardContainer.resetHighlight();
+ }
+ vm.forceDashboardMobileMode = false;
+ }
+
+ function addWidget() {
+ loadWidgetLibrary();
+ vm.isAddingWidget = true;
+ }
+
+ function onAddWidgetClosed() {
+ vm.timeseriesWidgetTypes = [];
+ vm.latestWidgetTypes = [];
+ vm.rpcWidgetTypes = [];
+ }
+
+ function addWidgetFromType(event, widget) {
+ vm.onAddWidgetClosed();
+ vm.isAddingWidget = false;
+ widgetService.getWidgetInfo(widget.bundleAlias, widget.typeAlias, widget.isSystemType).then(
+ function (widgetTypeInfo) {
+ var config = angular.fromJson(widgetTypeInfo.defaultConfig);
+ config.title = 'New ' + widgetTypeInfo.widgetName;
+ config.datasources = [];
+ var newWidget = {
+ isSystemType: widget.isSystemType,
+ bundleAlias: widget.bundleAlias,
+ typeAlias: widgetTypeInfo.alias,
+ type: widgetTypeInfo.type,
+ title: 'New widget',
+ sizeX: widgetTypeInfo.sizeX,
+ sizeY: widgetTypeInfo.sizeY,
+ config: config
+ };
+ $mdDialog.show({
+ controller: 'AddWidgetController',
+ controllerAs: 'vm',
+ templateUrl: addWidgetTemplate,
+ locals: {dashboard: vm.dashboard, widget: newWidget, widgetInfo: widgetTypeInfo},
+ parent: angular.element($document[0].body),
+ fullscreen: true,
+ skipHide: true,
+ targetEvent: event,
+ onComplete: function () {
+ var w = angular.element($window);
+ w.triggerHandler('resize');
+ }
+ }).then(function (widget) {
+ vm.widgets.push(widget);
+ }, function () {
+ });
+ }
+ );
+ }
+
+ function removeWidget(event, widget) {
+ var title = widget.config.title;
+ if (!title || title.length === 0) {
+ title = widgetService.getInstantWidgetInfo(widget).widgetName;
+ }
+ var confirm = $mdDialog.confirm()
+ .targetEvent(event)
+ .title($translate.instant('widget.remove-widget-title', {widgetTitle: title}))
+ .htmlContent($translate.instant('widget.remove-widget-text'))
+ .ariaLabel($translate.instant('widget.remove'))
+ .cancel($translate.instant('action.no'))
+ .ok($translate.instant('action.yes'));
+ $mdDialog.show(confirm).then(function () {
+ vm.widgets.splice(vm.widgets.indexOf(widget), 1);
+ });
+ }
+
+ function toggleDashboardEditMode() {
+ vm.isEdit = !vm.isEdit;
+ if (vm.isEdit) {
+ if (vm.widgetEditMode) {
+ vm.prevWidgets = angular.copy(vm.widgets);
+ } else {
+ vm.prevDashboard = angular.copy(vm.dashboard);
+ }
+ } else {
+ if (vm.widgetEditMode) {
+ vm.widgets = vm.prevWidgets;
+ } else {
+ vm.dashboard = vm.prevDashboard;
+ vm.widgets = vm.dashboard.configuration.widgets;
+ }
+ }
+ }
+
+ function saveDashboard() {
+ vm.isEdit = false;
+ notifyDashboardUpdated();
+ }
+
+ function notifyDashboardUpdated() {
+ if (!vm.widgetEditMode) {
+ dashboardService.saveDashboard(vm.dashboard);
+ }
+ }
+
+}
ui/src/app/dashboard/dashboard.directive.js 42(+42 -0)
diff --git a/ui/src/app/dashboard/dashboard.directive.js b/ui/src/app/dashboard/dashboard.directive.js
new file mode 100644
index 0000000..cd9243f
--- /dev/null
+++ b/ui/src/app/dashboard/dashboard.directive.js
@@ -0,0 +1,42 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import dashboardFieldsetTemplate from './dashboard-fieldset.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function DashboardDirective($compile, $templateCache) {
+ var linker = function (scope, element) {
+ var template = $templateCache.get(dashboardFieldsetTemplate);
+ element.html(template);
+ $compile(element.contents())(scope);
+ }
+ return {
+ restrict: "E",
+ link: linker,
+ scope: {
+ dashboard: '=',
+ isEdit: '=',
+ dashboardScope: '=',
+ theForm: '=',
+ onAssignToCustomer: '&',
+ onUnassignFromCustomer: '&',
+ onDeleteDashboard: '&'
+ }
+ };
+}
ui/src/app/dashboard/dashboard.routes.js 107(+107 -0)
diff --git a/ui/src/app/dashboard/dashboard.routes.js b/ui/src/app/dashboard/dashboard.routes.js
new file mode 100644
index 0000000..6c3012c
--- /dev/null
+++ b/ui/src/app/dashboard/dashboard.routes.js
@@ -0,0 +1,107 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import dashboardsTemplate from './dashboards.tpl.html';
+import dashboardTemplate from './dashboard.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function DashboardRoutes($stateProvider) {
+ $stateProvider
+ .state('home.dashboards', {
+ url: '/dashboards',
+ params: {'topIndex': 0},
+ module: 'private',
+ auth: ['TENANT_ADMIN', 'CUSTOMER_USER'],
+ views: {
+ "content@home": {
+ templateUrl: dashboardsTemplate,
+ controller: 'DashboardsController',
+ controllerAs: 'vm'
+ }
+ },
+ data: {
+ dashboardsType: 'tenant',
+ searchEnabled: true,
+ pageTitle: 'dashboard.dashboards'
+ },
+ ncyBreadcrumb: {
+ label: '{"icon": "dashboard", "label": "dashboard.dashboards"}'
+ }
+ })
+ .state('home.customers.dashboards', {
+ url: '/:customerId/dashboards',
+ params: {'topIndex': 0},
+ module: 'private',
+ auth: ['TENANT_ADMIN'],
+ views: {
+ "content@home": {
+ templateUrl: dashboardsTemplate,
+ controllerAs: 'vm',
+ controller: 'DashboardsController'
+ }
+ },
+ data: {
+ dashboardsType: 'customer',
+ searchEnabled: true,
+ pageTitle: 'customer.dashboards'
+ },
+ ncyBreadcrumb: {
+ label: '{"icon": "dashboard", "label": "customer.dashboards"}'
+ }
+ })
+ .state('home.dashboards.dashboard', {
+ url: '/:dashboardId',
+ module: 'private',
+ auth: ['TENANT_ADMIN', 'CUSTOMER_USER'],
+ views: {
+ "content@home": {
+ templateUrl: dashboardTemplate,
+ controller: 'DashboardController',
+ controllerAs: 'vm'
+ }
+ },
+ data: {
+ widgetEditMode: false,
+ searchEnabled: false,
+ pageTitle: 'dashboard.dashboard'
+ },
+ ncyBreadcrumb: {
+ label: '{"icon": "dashboard", "label": "{{ vm.dashboard.title }}", "translate": "false"}'
+ }
+ })
+ .state('home.customers.dashboards.dashboard', {
+ url: '/:dashboardId',
+ module: 'private',
+ auth: ['TENANT_ADMIN', 'CUSTOMER_USER'],
+ views: {
+ "content@home": {
+ templateUrl: dashboardTemplate,
+ controller: 'DashboardController',
+ controllerAs: 'vm'
+ }
+ },
+ data: {
+ searchEnabled: false,
+ pageTitle: 'customer.dashboard'
+ },
+ ncyBreadcrumb: {
+ label: '{"icon": "dashboard", "label": "customer.dashboard"}'
+ }
+ })
+}
ui/src/app/dashboard/dashboard.scss 55(+55 -0)
diff --git a/ui/src/app/dashboard/dashboard.scss b/ui/src/app/dashboard/dashboard.scss
new file mode 100644
index 0000000..11efbec
--- /dev/null
+++ b/ui/src/app/dashboard/dashboard.scss
@@ -0,0 +1,55 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import '../../scss/constants';
+
+section.tb-dashboard-title {
+ position: absolute;
+ top: 0;
+ left: 20px;
+}
+
+input.tb-dashboard-title {
+ font-size: 2.000rem;
+ font-weight: 500;
+ letter-spacing: 0.005em;
+ height: 38px;
+}
+
+div.tb-padded {
+ top: 60px;
+}
+
+section.tb-padded {
+ top: 60px;
+}
+
+div.tb-shrinked {
+ width: 40%;
+}
+
+tb-details-sidenav.tb-widget-details-sidenav {
+ md-sidenav.tb-sidenav-details {
+ @media (min-width: $layout-breakpoint-gt-sm) {
+ width: 85% !important;
+ }
+ @media (min-width: $layout-breakpoint-gt-md) {
+ width: 75% !important;
+ }
+ @media (min-width: $layout-breakpoint-lg) {
+ width: 60% !important;
+ }
+ }
+}
ui/src/app/dashboard/dashboard.tpl.html 177(+177 -0)
diff --git a/ui/src/app/dashboard/dashboard.tpl.html b/ui/src/app/dashboard/dashboard.tpl.html
new file mode 100644
index 0000000..b18c392
--- /dev/null
+++ b/ui/src/app/dashboard/dashboard.tpl.html
@@ -0,0 +1,177 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-content flex tb-expand-fullscreen="vm.widgetEditMode" hide-expand-button="vm.widgetEditMode">
+ <section ng-show="!vm.isAddingWidget && !loading && !vm.widgetEditMode" layout="row" layout-wrap
+ class="tb-header-buttons tb-top-header-buttons md-fab" ng-style="{'right': '50px'}">
+ <md-button ng-if="vm.isTenantAdmin()" ng-show="vm.isEdit" ng-disabled="loading"
+ class="tb-btn-header md-accent md-hue-2 md-fab md-fab-bottom-right"
+ aria-label="{{ 'action.apply' | translate }}"
+ ng-click="vm.saveDashboard()">
+ <md-tooltip md-direction="top">
+ {{ 'action.apply-changes' | translate }}
+ </md-tooltip>
+ <ng-md-icon icon="done"></ng-md-icon>
+ </md-button>
+ <md-button ng-if="vm.isTenantAdmin()" ng-disabled="loading"
+ class="tb-btn-header md-accent md-hue-2 md-fab md-fab-bottom-right"
+ aria-label="{{ 'action.edit-mode' | translate }}"
+ ng-click="vm.toggleDashboardEditMode()">
+ <md-tooltip md-direction="top">
+ {{ (vm.isEdit ? 'action.decline-changes' : 'action.enter-edit-mode') | translate }}
+ </md-tooltip>
+ <ng-md-icon icon="{{vm.isEdit ? 'close' : 'edit'}}"
+ options='{"easing": "circ-in-out", "duration": 375, "rotation": "none"}'></ng-md-icon>
+ </md-button>
+ </section>
+ <section ng-show="!loading && vm.noData()" layout-align="center center"
+ ng-class="{'tb-padded' : !vm.widgetEditMode}"
+ style="text-transform: uppercase; display: flex; z-index: 1;"
+ class="md-headline tb-absolute-fill">
+ <span translate ng-if="!vm.isEdit">
+ dashboard.no-widgets
+ </span>
+ <md-button ng-if="vm.isEdit && !vm.widgetEditMode" class="tb-add-new-widget" ng-click="vm.addWidget($event)">
+ <md-icon aria-label="{{ 'action.add' | translate }}" class="material-icons tb-md-96">add</md-icon>
+ {{ 'dashboard.add-widget' | translate }}
+ </md-button>
+ </section>
+ <section ng-if="!vm.widgetEditMode" class="tb-dashboard-title" layout="row" layout-align="center center">
+ <h3 ng-show="!vm.isEdit">{{ vm.dashboard.title }}</h3>
+ <md-input-container ng-show="vm.isEdit" class="md-block" style="height: 30px;">
+ <label translate>dashboard.title</label>
+ <input class="tb-dashboard-title" required name="title" ng-model="vm.dashboard.title">
+ </md-input-container>
+ <md-button class="md-raised" flex="none" ng-show="vm.isEdit" ng-click="vm.openDeviceAliases($event)">
+ {{ 'device.aliases' | translate }}
+ </md-button>
+ </section>
+ <div class="tb-absolute-fill" ng-class="{ 'tb-padded' : !vm.widgetEditMode, 'tb-shrinked' : vm.isEditingWidget }">
+ <tb-dashboard
+ widgets="vm.widgets"
+ device-alias-list="vm.dashboard.configuration.deviceAliases"
+ is-edit="vm.isEdit || vm.widgetEditMode"
+ is-mobile="vm.forceDashboardMobileMode"
+ is-mobile-disabled="vm.widgetEditMode"
+ is-edit-action-enabled="vm.isEdit || vm.widgetEditMode"
+ is-remove-action-enabled="vm.isEdit && !vm.widgetEditMode"
+ on-edit-widget="vm.editWidget(event, widget)"
+ on-widget-clicked="vm.widgetClicked(event, widget)"
+ on-remove-widget="vm.removeWidget(event, widget)"
+ load-widgets="vm.loadDashboard()"
+ on-init="vm.dashboardInited(dashboard)"
+ on-init-failed="vm.dashboardInitFailed(e)">
+ </tb-dashboard>
+ </div>
+ <tb-details-sidenav class="tb-widget-details-sidenav"
+ header-title="vm.editingWidget.config.title"
+ header-subtitle="{{vm.editingWidgetSubtitle}}"
+ is-read-only="false"
+ is-open="vm.isEditingWidget"
+ is-always-edit="true"
+ on-close-details="vm.onEditWidgetClosed()"
+ on-toggle-details-edit-mode="vm.onRevertWidgetEdit(vm.widgetForm)"
+ on-apply-details="vm.saveWidget(vm.widgetForm)"
+ the-form="vm.widgetForm">
+ <details-buttons tb-help="vm.helpLinkIdForWidgetType()" help-container-id="help-container">
+ <div id="help-container"></div>
+ </details-buttons>
+ <form name="vm.widgetForm">
+ <tb-edit-widget
+ dashboard="vm.dashboard"
+ widget="vm.editingWidget"
+ the-form="vm.widgetForm">
+ </tb-edit-widget>
+ </form>
+ </tb-details-sidenav>
+ <tb-details-sidenav ng-if="!vm.widgetEditMode" class="tb-select-widget-sidenav"
+ header-title="'dashboard.select-widget-title' | translate"
+ header-height-px="120"
+ is-read-only="true"
+ is-open="vm.isAddingWidget"
+ is-edit="false"
+ on-close-details="vm.onAddWidgetClosed()">
+ <header-pane>
+ <div layout="row">
+ <span class="tb-details-subtitle">{{ 'widgets-bundle.current' | translate }}</span>
+ <tb-widgets-bundle-select flex-offset="5"
+ flex
+ ng-model="vm.widgetsBundle"
+ tb-required="true"
+ select-first-bundle="false">
+ </tb-widgets-bundle-select>
+ </div>
+ </header-pane>
+ <div>
+ <md-tabs ng-if="vm.timeseriesWidgetTypes.length > 0 || vm.latestWidgetTypes.length > 0 ||
+ vm.rpcWidgetTypes.length > 0"
+ flex
+ class="tb-absolute-fill" md-border-bottom>
+ <md-tab ng-if="vm.timeseriesWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.timeseries' | translate }}">
+ <tb-dashboard
+ widgets="vm.timeseriesWidgetTypes"
+ is-edit="false"
+ is-mobile="true"
+ is-edit-action-enabled="false"
+ is-remove-action-enabled="false"
+ on-widget-clicked="vm.addWidgetFromType(event, widget)">
+ </tb-dashboard>
+ </md-tab>
+ <md-tab ng-if="vm.latestWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.latest-values' | translate }}">
+ <tb-dashboard
+ widgets="vm.latestWidgetTypes"
+ is-edit="false"
+ is-mobile="true"
+ is-edit-action-enabled="false"
+ is-remove-action-enabled="false"
+ on-widget-clicked="vm.addWidgetFromType(event, widget)">
+ </tb-dashboard>
+ </md-tab>
+ <md-tab ng-if="vm.rpcWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.rpc' | translate }}">
+ <tb-dashboard
+ widgets="vm.rpcWidgetTypes"
+ is-edit="false"
+ is-mobile="true"
+ is-edit-action-enabled="false"
+ is-remove-action-enabled="false"
+ on-widget-clicked="vm.addWidgetFromType(event, widget)">
+ </tb-dashboard>
+ </md-tab>
+ </md-tabs>
+ <span translate ng-if="vm.timeseriesWidgetTypes.length === 0 && vm.latestWidgetTypes.length === 0 &&
+ vm.rpcWidgetTypes.length === 0 && vm.widgetsBundle"
+ layout-align="center center"
+ style="text-transform: uppercase; display: flex;"
+ class="md-headline tb-absolute-fill">widgets-bundle.empty</span>
+ <span translate ng-if="!vm.widgetsBundle"
+ layout-align="center center"
+ style="text-transform: uppercase; display: flex;"
+ class="md-headline tb-absolute-fill">widget.select-widgets-bundle</span>
+ </div>
+ </tb-details-sidenav>
+ <!-- </section> -->
+ <section layout="row" layout-wrap class="tb-footer-buttons md-fab ">
+ <md-button ng-disabled="loading" ng-if="!vm.isAddingWidget && vm.isEdit && !vm.widgetEditMode"
+ class="tb-btn-footer md-accent md-hue-2 md-fab" ng-click="vm.addWidget($event)"
+ aria-label="{{ 'dashboard.add-widget' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'dashboard.add-widget' | translate }}
+ </md-tooltip>
+ <ng-md-icon icon="add"></ng-md-icon>
+ </md-button>
+ </section>
+</md-content>
ui/src/app/dashboard/dashboard-card.tpl.html 18(+18 -0)
diff --git a/ui/src/app/dashboard/dashboard-card.tpl.html b/ui/src/app/dashboard/dashboard-card.tpl.html
new file mode 100644
index 0000000..8151b2f
--- /dev/null
+++ b/ui/src/app/dashboard/dashboard-card.tpl.html
@@ -0,0 +1,18 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div></div>
\ No newline at end of file
diff --git a/ui/src/app/dashboard/dashboard-fieldset.tpl.html b/ui/src/app/dashboard/dashboard-fieldset.tpl.html
new file mode 100644
index 0000000..46e15b4
--- /dev/null
+++ b/ui/src/app/dashboard/dashboard-fieldset.tpl.html
@@ -0,0 +1,36 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-button ng-click="onAssignToCustomer({event: $event})" ng-show="!isEdit && dashboardScope === 'tenant'" class="md-raised md-primary">{{ 'dashboard.assign-to-customer' | translate }}</md-button>
+<md-button ng-click="onUnassignFromCustomer({event: $event})" ng-show="!isEdit && dashboardScope === 'customer'" class="md-raised md-primary">{{ 'dashboard.unassign-from-customer' | translate }}</md-button>
+<md-button ng-click="onDeleteDashboard({event: $event})" ng-show="!isEdit && dashboardScope === 'tenant'" class="md-raised md-primary">{{ 'dashboard.delete' | translate }}</md-button>
+
+<md-content class="md-padding" layout="column">
+ <fieldset ng-disabled="loading || !isEdit">
+ <md-input-container class="md-block">
+ <label translate>dashboard.title</label>
+ <input required name="title" ng-model="dashboard.title">
+ <div ng-messages="theForm.title.$error">
+ <div translate ng-message="required">dashboard.title-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container class="md-block">
+ <label translate>dashboard.description</label>
+ <textarea ng-model="dashboard.configuration.description" rows="2"></textarea>
+ </md-input-container>
+ </fieldset>
+</md-content>
ui/src/app/dashboard/dashboards.controller.js 379(+379 -0)
diff --git a/ui/src/app/dashboard/dashboards.controller.js b/ui/src/app/dashboard/dashboards.controller.js
new file mode 100644
index 0000000..5a93ad3
--- /dev/null
+++ b/ui/src/app/dashboard/dashboards.controller.js
@@ -0,0 +1,379 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import addDashboardTemplate from './add-dashboard.tpl.html';
+import dashboardCard from './dashboard-card.tpl.html';
+import assignToCustomerTemplate from './assign-to-customer.tpl.html';
+import addDashboardsToCustomerTemplate from './add-dashboards-to-customer.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function DashboardsController(userService, dashboardService, customerService, $scope, $controller, $state, $stateParams, $mdDialog, $document, $q, $translate) {
+
+ var customerId = $stateParams.customerId;
+
+ var dashboardActionsList = [
+ {
+ onAction: function ($event, item) {
+ vm.grid.openItem($event, item);
+ },
+ name: function() { return $translate.instant('dashboard.details') },
+ details: function() { return $translate.instant('dashboard.dashboard-details') },
+ icon: "edit"
+ }
+ ];
+
+ var dashboardGroupActionsList = [];
+
+ var vm = this;
+
+ vm.dashboardGridConfig = {
+ deleteItemTitleFunc: deleteDashboardTitle,
+ deleteItemContentFunc: deleteDashboardText,
+ deleteItemsTitleFunc: deleteDashboardsTitle,
+ deleteItemsActionTitleFunc: deleteDashboardsActionTitle,
+ deleteItemsContentFunc: deleteDashboardsText,
+
+ saveItemFunc: saveDashboard,
+
+ clickItemFunc: openDashboard,
+
+ getItemTitleFunc: getDashboardTitle,
+ itemCardTemplateUrl: dashboardCard,
+
+ actionsList: dashboardActionsList,
+ groupActionsList: dashboardGroupActionsList,
+
+ onGridInited: gridInited,
+
+ addItemTemplateUrl: addDashboardTemplate,
+
+ addItemText: function() { return $translate.instant('dashboard.add-dashboard-text') },
+ noItemsText: function() { return $translate.instant('dashboard.no-dashboards-text') },
+ itemDetailsText: function() { return $translate.instant('dashboard.dashboard-details') },
+ isDetailsReadOnly: function () {
+ return vm.dashboardsScope === 'customer_user';
+ },
+ isSelectionEnabled: function () {
+ return !(vm.dashboardsScope === 'customer_user');
+ }
+ };
+
+ if (angular.isDefined($stateParams.items) && $stateParams.items !== null) {
+ vm.dashboardGridConfig.items = $stateParams.items;
+ }
+
+ if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) {
+ vm.dashboardGridConfig.topIndex = $stateParams.topIndex;
+ }
+
+ vm.dashboardsScope = $state.$current.data.dashboardsType;
+
+ vm.assignToCustomer = assignToCustomer;
+ vm.unassignFromCustomer = unassignFromCustomer;
+
+ initController();
+
+ function initController() {
+ var fetchDashboardsFunction = null;
+ var deleteDashboardFunction = null;
+ var refreshDashboardsParamsFunction = null;
+
+ var user = userService.getCurrentUser();
+
+ if (user.authority === 'CUSTOMER_USER') {
+ vm.dashboardsScope = 'customer_user';
+ customerId = user.customerId;
+ }
+
+ if (vm.dashboardsScope === 'tenant') {
+ fetchDashboardsFunction = function (pageLink) {
+ return dashboardService.getTenantDashboards(pageLink);
+ };
+ deleteDashboardFunction = function (dashboardId) {
+ return dashboardService.deleteDashboard(dashboardId);
+ };
+ refreshDashboardsParamsFunction = function () {
+ return {"topIndex": vm.topIndex};
+ };
+
+ dashboardActionsList.push(
+ {
+ onAction: function ($event, item) {
+ assignToCustomer($event, [ item.id.id ]);
+ },
+ name: function() { return $translate.instant('action.assign') },
+ details: function() { return $translate.instant('dashboard.assign-to-customer') },
+ icon: "assignment_ind"
+ }
+ );
+
+ dashboardActionsList.push(
+ {
+ onAction: function ($event, item) {
+ vm.grid.deleteItem($event, item);
+ },
+ name: function() { return $translate.instant('action.delete') },
+ details: function() { return $translate.instant('dashboard.delete') },
+ icon: "delete"
+ }
+ );
+
+ dashboardGroupActionsList.push(
+ {
+ onAction: function ($event, items) {
+ assignDashboardsToCustomer($event, items);
+ },
+ name: function() { return $translate.instant('dashboard.assign-dashboards') },
+ details: function(selectedCount) {
+ return $translate.instant('dashboard.assign-dashboards-text', {count: selectedCount}, "messageformat");
+ },
+ icon: "assignment_ind"
+ }
+ );
+
+ dashboardGroupActionsList.push(
+ {
+ onAction: function ($event) {
+ vm.grid.deleteItems($event);
+ },
+ name: function() { return $translate.instant('dashboard.delete-dashboards') },
+ details: deleteDashboardsActionTitle,
+ icon: "delete"
+ }
+ );
+
+
+ } else if (vm.dashboardsScope === 'customer' || vm.dashboardsScope === 'customer_user') {
+ fetchDashboardsFunction = function (pageLink) {
+ return dashboardService.getCustomerDashboards(customerId, pageLink);
+ };
+ deleteDashboardFunction = function (dashboardId) {
+ return dashboardService.unassignDashboardFromCustomer(dashboardId);
+ };
+ refreshDashboardsParamsFunction = function () {
+ return {"customerId": customerId, "topIndex": vm.topIndex};
+ };
+
+ if (vm.dashboardsScope === 'customer') {
+ dashboardActionsList.push(
+ {
+ onAction: function ($event, item) {
+ unassignFromCustomer($event, item);
+ },
+ name: function() { return $translate.instant('action.unassign') },
+ details: function() { return $translate.instant('dashboard.unassign-from-customer') },
+ icon: "assignment_return"
+ }
+ );
+
+ dashboardGroupActionsList.push(
+ {
+ onAction: function ($event, items) {
+ unassignDashboardsFromCustomer($event, items);
+ },
+ name: function() { return $translate.instant('dashboard.unassign-dashboards') },
+ details: function(selectedCount) {
+ return $translate.instant('dashboard.unassign-dashboards-action-title', {count: selectedCount}, "messageformat");
+ },
+ icon: "assignment_return"
+ }
+ );
+
+
+ vm.dashboardGridConfig.addItemAction = {
+ onAction: function ($event) {
+ addDashboardsToCustomer($event);
+ },
+ name: function() { return $translate.instant('dashboard.assign-dashboards') },
+ details: function() { return $translate.instant('dashboard.assign-new-dashboard') },
+ icon: "add"
+ };
+ } else if (vm.dashboardsScope === 'customer_user') {
+ vm.dashboardGridConfig.addItemAction = {};
+ }
+ }
+
+ vm.dashboardGridConfig.refreshParamsFunc = refreshDashboardsParamsFunction;
+ vm.dashboardGridConfig.fetchItemsFunc = fetchDashboardsFunction;
+ vm.dashboardGridConfig.deleteItemFunc = deleteDashboardFunction;
+
+ }
+
+ function deleteDashboardTitle (dashboard) {
+ return $translate.instant('dashboard.delete-dashboard-title', {dashboardTitle: dashboard.title});
+ }
+
+ function deleteDashboardText () {
+ return $translate.instant('dashboard.delete-dashboard-text');
+ }
+
+ function deleteDashboardsTitle (selectedCount) {
+ return $translate.instant('dashboard.delete-dashboards-title', {count: selectedCount}, 'messageformat');
+ }
+
+ function deleteDashboardsActionTitle(selectedCount) {
+ return $translate.instant('dashboard.delete-dashboards-action-title', {count: selectedCount}, 'messageformat');
+ }
+
+ function deleteDashboardsText () {
+ return $translate.instant('dashboard.delete-dashboards-text');
+ }
+
+ function gridInited(grid) {
+ vm.grid = grid;
+ }
+
+ function getDashboardTitle(dashboard) {
+ return dashboard ? dashboard.title : '';
+ }
+
+ function saveDashboard(dashboard) {
+ return dashboardService.saveDashboard(dashboard);
+ }
+
+ function assignToCustomer($event, dashboardIds) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ var pageSize = 10;
+ customerService.getCustomers({limit: pageSize, textSearch: ''}).then(
+ function success(_customers) {
+ var customers = {
+ pageSize: pageSize,
+ data: _customers.data,
+ nextPageLink: _customers.nextPageLink,
+ selection: null,
+ hasNext: _customers.hasNext,
+ pending: false
+ };
+ if (customers.hasNext) {
+ customers.nextPageLink.limit = pageSize;
+ }
+ $mdDialog.show({
+ controller: 'AssignDashboardToCustomerController',
+ controllerAs: 'vm',
+ templateUrl: assignToCustomerTemplate,
+ locals: {dashboardIds: dashboardIds, customers: customers},
+ parent: angular.element($document[0].body),
+ fullscreen: true,
+ targetEvent: $event
+ }).then(function () {
+ vm.grid.refreshList();
+ }, function () {
+ });
+ },
+ function fail() {
+ });
+ }
+
+ function addDashboardsToCustomer($event) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ var pageSize = 10;
+ dashboardService.getTenantDashboards({limit: pageSize, textSearch: ''}).then(
+ function success(_dashboards) {
+ var dashboards = {
+ pageSize: pageSize,
+ data: _dashboards.data,
+ nextPageLink: _dashboards.nextPageLink,
+ selections: {},
+ selectedCount: 0,
+ hasNext: _dashboards.hasNext,
+ pending: false
+ };
+ if (dashboards.hasNext) {
+ dashboards.nextPageLink.limit = pageSize;
+ }
+ $mdDialog.show({
+ controller: 'AddDashboardsToCustomerController',
+ controllerAs: 'vm',
+ templateUrl: addDashboardsToCustomerTemplate,
+ locals: {customerId: customerId, dashboards: dashboards},
+ parent: angular.element($document[0].body),
+ fullscreen: true,
+ targetEvent: $event
+ }).then(function () {
+ vm.grid.refreshList();
+ }, function () {
+ });
+ },
+ function fail() {
+ });
+ }
+
+ function assignDashboardsToCustomer($event, items) {
+ var dashboardIds = [];
+ for (var id in items.selections) {
+ dashboardIds.push(id);
+ }
+ assignToCustomer($event, dashboardIds);
+ }
+
+ function unassignFromCustomer($event, dashboard) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ var confirm = $mdDialog.confirm()
+ .targetEvent($event)
+ .title($translate.instant('dashboard.unassign-dashboard-title', {dashboardTitle: dashboard.title}))
+ .htmlContent($translate.instant('dashboard.unassign-dashboard-text'))
+ .ariaLabel($translate.instant('dashboard.unassign-dashboard'))
+ .cancel($translate.instant('action.no'))
+ .ok($translate.instant('action.yes'));
+ $mdDialog.show(confirm).then(function () {
+ dashboardService.unassignDashboardFromCustomer(dashboard.id.id).then(function success() {
+ vm.grid.refreshList();
+ });
+ });
+ }
+
+ function unassignDashboardsFromCustomer($event, items) {
+ var confirm = $mdDialog.confirm()
+ .targetEvent($event)
+ .title($translate.instant('dashboard.unassign-dashboards-title', {count: items.selectedCount}, 'messageformat'))
+ .htmlContent($translate.instant('dashboard.unassign-dashboards-text'))
+ .ariaLabel($translate.instant('dashboard.unassign-dashboards'))
+ .cancel($translate.instant('action.no'))
+ .ok($translate.instant('action.yes'));
+ $mdDialog.show(confirm).then(function () {
+ var tasks = [];
+ for (var id in items.selections) {
+ tasks.push(dashboardService.unassignDashboardFromCustomer(id));
+ }
+ $q.all(tasks).then(function () {
+ vm.grid.refreshList();
+ });
+ });
+ }
+
+ function openDashboard($event, dashboard) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ if (vm.dashboardsScope === 'customer') {
+ $state.go('home.customers.dashboards.dashboard', {
+ customerId: customerId,
+ dashboardId: dashboard.id.id
+ });
+ } else {
+ $state.go('home.dashboards.dashboard', {dashboardId: dashboard.id.id});
+ }
+ }
+}
ui/src/app/dashboard/dashboards.tpl.html 29(+29 -0)
diff --git a/ui/src/app/dashboard/dashboards.tpl.html b/ui/src/app/dashboard/dashboards.tpl.html
new file mode 100644
index 0000000..ac8ccb2
--- /dev/null
+++ b/ui/src/app/dashboard/dashboards.tpl.html
@@ -0,0 +1,29 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<tb-grid grid-configuration="vm.dashboardGridConfig">
+ <details-buttons tb-help="'dashboards'" help-container-id="help-container">
+ <div id="help-container"></div>
+ </details-buttons>
+ <tb-dashboard-details dashboard="vm.grid.operatingItem()"
+ is-edit="vm.grid.detailsConfig.isDetailsEditMode"
+ dashboard-scope="vm.dashboardsScope"
+ the-form="vm.grid.detailsForm"
+ on-assign-to-customer="vm.assignToCustomer(event, [ vm.grid.detailsConfig.currentItem.id.id ])"
+ on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem)"
+ on-delete-dashboard="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-dashboard-details>
+</tb-grid>
ui/src/app/dashboard/device-aliases.controller.js 183(+183 -0)
diff --git a/ui/src/app/dashboard/device-aliases.controller.js b/ui/src/app/dashboard/device-aliases.controller.js
new file mode 100644
index 0000000..ab2b6f4
--- /dev/null
+++ b/ui/src/app/dashboard/device-aliases.controller.js
@@ -0,0 +1,183 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './device-aliases.scss';
+
+/*@ngInject*/
+export default function DeviceAliasesController(deviceService, toast, $scope, $mdDialog, $document, $q, $translate,
+ deviceAliases, aliasToWidgetsMap, isSingleDevice, singleDeviceAlias) {
+
+ var vm = this;
+
+ vm.isSingleDevice = isSingleDevice;
+ vm.singleDeviceAlias = singleDeviceAlias;
+ vm.deviceAliases = [];
+ vm.aliasToWidgetsMap = aliasToWidgetsMap;
+ vm.singleDevice = null;
+ vm.singleDeviceSearchText = '';
+
+ vm.addAlias = addAlias;
+ vm.cancel = cancel;
+ vm.deviceSearchTextChanged = deviceSearchTextChanged;
+ vm.deviceChanged = deviceChanged;
+ vm.fetchDevices = fetchDevices;
+ vm.removeAlias = removeAlias;
+ vm.save = save;
+
+ initController();
+
+ function initController() {
+ for (var aliasId in deviceAliases) {
+ var alias = deviceAliases[aliasId].alias;
+ var deviceId = deviceAliases[aliasId].deviceId;
+ var deviceAlias = {id: aliasId, alias: alias, device: null, changed: false, searchText: ''};
+ if (deviceId) {
+ fetchAliasDevice(deviceAlias, deviceId);
+ }
+ vm.deviceAliases.push(deviceAlias);
+ }
+ }
+
+ function fetchDevices(searchText) {
+ var pageLink = {limit: 10, textSearch: searchText};
+
+ var deferred = $q.defer();
+
+ deviceService.getTenantDevices(pageLink).then(function success(result) {
+ deferred.resolve(result.data);
+ }, function fail() {
+ deferred.reject();
+ });
+
+ return deferred.promise;
+ }
+
+ function deviceSearchTextChanged() {
+ }
+
+ function deviceChanged(deviceAlias) {
+ if (deviceAlias && deviceAlias.device) {
+ if (angular.isDefined(deviceAlias.changed) && !deviceAlias.changed) {
+ deviceAlias.changed = true;
+ } else {
+ deviceAlias.alias = deviceAlias.device.name;
+ }
+ }
+ }
+
+ function addAlias() {
+ var aliasId = 0;
+ for (var a in vm.deviceAliases) {
+ aliasId = Math.max(vm.deviceAliases[a].id, aliasId);
+ }
+ aliasId++;
+ var deviceAlias = {id: aliasId, alias: '', device: null, searchText: ''};
+ vm.deviceAliases.push(deviceAlias);
+ }
+
+ function removeAlias($event, deviceAlias) {
+ var index = vm.deviceAliases.indexOf(deviceAlias);
+ if (index > -1) {
+ var widgetsTitleList = vm.aliasToWidgetsMap[deviceAlias.id];
+ if (widgetsTitleList) {
+ var widgetsListHtml = '';
+ for (var t in widgetsTitleList) {
+ widgetsListHtml += '<br/>\'' + widgetsTitleList[t] + '\'';
+ }
+ var alert = $mdDialog.alert()
+ .parent(angular.element($document[0].body))
+ .clickOutsideToClose(true)
+ .title($translate.instant('device.unable-delete-device-alias-title'))
+ .htmlContent($translate.instant('device.unable-delete-device-alias-text', {deviceAlias: deviceAlias.alias, widgetsList: widgetsListHtml}))
+ .ariaLabel($translate.instant('device.unable-delete-device-alias-title'))
+ .ok($translate.instant('action.close'))
+ .targetEvent($event);
+ alert._options.skipHide = true;
+ alert._options.fullscreen = true;
+
+ $mdDialog.show(alert);
+ } else {
+ for (var i = index + 1; i < vm.deviceAliases.length; i++) {
+ vm.deviceAliases[i].changed = false;
+ }
+ vm.deviceAliases.splice(index, 1);
+ if ($scope.theForm) {
+ $scope.theForm.$setDirty();
+ }
+ }
+ }
+ }
+
+ function cancel() {
+ $mdDialog.cancel();
+ }
+
+ function save() {
+
+ var deviceAliases = {};
+ var uniqueAliasList = {};
+
+ var valid = true;
+ var aliasId, maxAliasId;
+ var alias;
+ var i;
+
+ if (vm.isSingleDevice) {
+ maxAliasId = 0;
+ vm.singleDeviceAlias.deviceId = vm.singleDevice.id.id;
+ for (i in vm.deviceAliases) {
+ aliasId = vm.deviceAliases[i].id;
+ alias = vm.deviceAliases[i].alias;
+ if (alias === vm.singleDeviceAlias.alias) {
+ valid = false;
+ break;
+ }
+ maxAliasId = Math.max(aliasId, maxAliasId);
+ }
+ maxAliasId++;
+ vm.singleDeviceAlias.id = maxAliasId;
+ } else {
+ for (i in vm.deviceAliases) {
+ aliasId = vm.deviceAliases[i].id;
+ alias = vm.deviceAliases[i].alias;
+ if (!uniqueAliasList[alias]) {
+ uniqueAliasList[alias] = alias;
+ deviceAliases[aliasId] = {alias: alias, deviceId: vm.deviceAliases[i].device.id.id};
+ } else {
+ valid = false;
+ break;
+ }
+ }
+ }
+ if (valid) {
+ $scope.theForm.$setPristine();
+ if (vm.isSingleDevice) {
+ $mdDialog.hide(vm.singleDeviceAlias);
+ } else {
+ $mdDialog.hide(deviceAliases);
+ }
+ } else {
+ toast.showError($translate.instant('device.duplicate-alias-error', {alias: alias}));
+ }
+ }
+
+ function fetchAliasDevice(deviceAlias, deviceId) {
+ deviceService.getDevice(deviceId).then(function (device) {
+ deviceAlias.device = device;
+ deviceAlias.searchText = device.name;
+ });
+ }
+
+}
ui/src/app/dashboard/device-aliases.scss 32(+32 -0)
diff --git a/ui/src/app/dashboard/device-aliases.scss b/ui/src/app/dashboard/device-aliases.scss
new file mode 100644
index 0000000..d0bd4fb
--- /dev/null
+++ b/ui/src/app/dashboard/device-aliases.scss
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+.tb-alias {
+ padding: 10px 0 0 10px;
+ margin: 5px;
+
+ md-input-container {
+ margin: 0;
+ }
+ md-autocomplete {
+ height: 30px;
+ md-autocomplete-wrap {
+ height: 30px;
+ }
+ input, input:not(.md-input) {
+ height: 30px;
+ }
+ }
+}
ui/src/app/dashboard/device-aliases.tpl.html 131(+131 -0)
diff --git a/ui/src/app/dashboard/device-aliases.tpl.html b/ui/src/app/dashboard/device-aliases.tpl.html
new file mode 100644
index 0000000..bf5bb61
--- /dev/null
+++ b/ui/src/app/dashboard/device-aliases.tpl.html
@@ -0,0 +1,131 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog style="width: 700px;" aria-label="{{ 'device.aliases' | translate }}">
+ <form name="theForm" ng-submit="vm.save()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2>{{ vm.isSingleDevice ? ('device.select-device-for-alias' | translate:vm.singleDeviceAlias ) : ('device.aliases' | translate) }}</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <fieldset ng-disabled="loading">
+ <div ng-show="vm.isSingleDevice">
+ <md-autocomplete
+ ng-required="vm.isSingleDevice"
+ md-input-name="device_id"
+ ng-model="vm.singleDevice"
+ md-selected-item="vm.singleDevice"
+ md-search-text="vm.singleDeviceSearchText"
+ md-search-text-change="vm.deviceSearchTextChanged(vm.singleDevice)"
+ md-items="item in vm.fetchDevices(vm.singleDeviceSearchText)"
+ md-item-text="item.name"
+ md-min-length="0"
+ placeholder="{{ 'device.device' | translate }}">
+ <md-item-template>
+ <span md-highlight-text="vm.singleDeviceSearchText" md-highlight-flags="^i">{{item.name}}</span>
+ </md-item-template>
+ <md-not-found>
+ <span translate translate-values='{ device: vm.singleDeviceSearchText }'>device.no-devices-matching</span>
+ </md-not-found>
+ <div ng-messages="theForm.device_id.$error">
+ <div translate ng-message="required">device.device-required</div>
+ </div>
+ </md-autocomplete>
+ </div>
+ <div ng-show="!vm.isSingleDevice" flex layout="row" layout-align="start center">
+ <span flex="5"></span>
+ <div flex layout="row" layout-align="start center"
+ style="padding: 0 0 0 10px; margin: 5px;">
+ <span translate flex="40" style="min-width: 100px;">device.alias</span>
+ <span translate flex="60" style="min-width: 190px; padding-left: 10px;">device.device</span>
+ <span style="min-width: 40px;"></span>
+ </div>
+ </div>
+ <div ng-show="!vm.isSingleDevice" style="max-height: 300px; overflow: auto; padding-bottom: 20px;">
+ <div ng-form name="aliasForm" flex layout="row" layout-align="start center" ng-repeat="deviceAlias in vm.deviceAliases track by $index">
+ <span flex="5">{{$index + 1}}.</span>
+ <div class="md-whiteframe-4dp tb-alias" flex layout="row" layout-align="start center">
+ <md-input-container flex="40" style="min-width: 100px;" md-no-float class="md-block">
+ <input required name="alias" placeholder="{{ 'device.alias' | translate }}" ng-model="deviceAlias.alias">
+ <div ng-messages="aliasForm.alias.$error">
+ <div translate ng-message="required">device.alias-required</div>
+ </div>
+ </md-input-container>
+ <section flex="60" layout="column">
+ <md-autocomplete flex
+ ng-required="!vm.isSingleDevice"
+ md-input-name="device_id"
+ ng-model="deviceAlias.device"
+ md-selected-item="deviceAlias.device"
+ md-search-text="deviceAlias.searchText"
+ md-search-text-change="vm.deviceSearchTextChanged(deviceAlias)"
+ md-selected-item-change="vm.deviceChanged(deviceAlias)"
+ md-items="item in vm.fetchDevices(deviceAlias.searchText)"
+ md-item-text="item.name"
+ md-min-length="0"
+ placeholder="{{ 'device.device' | translate }}">
+ <md-item-template>
+ <span md-highlight-text="deviceAlias.searchText" md-highlight-flags="^i">{{item.name}}</span>
+ </md-item-template>
+ <md-not-found>
+ <span translate translate-values='{ device: deviceAlias.searchText }'>device.no-devices-matching</span>
+ </md-not-found>
+ </md-autocomplete>
+ <div class="tb-error-messages" ng-messages="aliasForm.device_id.$error">
+ <div translate ng-message="required" class="tb-error-message">device.device-required</div>
+ </div>
+ </section>
+ <md-button ng-disabled="loading" class="md-icon-button md-primary" style="min-width: 40px;"
+ ng-click="vm.removeAlias($event, deviceAlias)" aria-label="{{ 'action.remove' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'device.remove-alias' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.delete' | translate }}" class="material-icons">
+ close
+ </md-icon>
+ </md-button>
+ </div>
+ </div>
+ </div>
+ <div ng-show="!vm.isSingleDevice" style="padding-bottom: 10px;">
+ <md-button ng-disabled="loading" class="md-primary md-raised" ng-click="vm.addAlias($event)" aria-label="{{ 'action.add' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'device.add-alias' | translate }}
+ </md-tooltip>
+ <span translate>action.add</span>
+ </md-button>
+ </div>
+ </fieldset>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit" class="md-raised md-primary">
+ {{ 'action.save' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
\ No newline at end of file
ui/src/app/dashboard/edit-widget.directive.js 111(+111 -0)
diff --git a/ui/src/app/dashboard/edit-widget.directive.js b/ui/src/app/dashboard/edit-widget.directive.js
new file mode 100644
index 0000000..34b11f7
--- /dev/null
+++ b/ui/src/app/dashboard/edit-widget.directive.js
@@ -0,0 +1,111 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import deviceAliasesTemplate from './device-aliases.tpl.html';
+import editWidgetTemplate from './edit-widget.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function EditWidgetDirective($compile, $templateCache, widgetService, deviceService, $q, $document, $mdDialog) {
+
+ var linker = function (scope, element) {
+ var template = $templateCache.get(editWidgetTemplate);
+ element.html(template);
+
+ scope.$watch('widget', function () {
+ if (scope.widget) {
+ widgetService.getWidgetInfo(scope.widget.bundleAlias,
+ scope.widget.typeAlias,
+ scope.widget.isSystemType).then(
+ function(widgetInfo) {
+ scope.$applyAsync(function(scope) {
+ scope.widgetConfig = scope.widget.config;
+ var settingsSchema = widgetInfo.settingsSchema;
+ var dataKeySettingsSchema = widgetInfo.dataKeySettingsSchema;
+ if (!settingsSchema || settingsSchema === '') {
+ scope.settingsSchema = {};
+ } else {
+ scope.settingsSchema = angular.fromJson(settingsSchema);
+ }
+ if (!dataKeySettingsSchema || dataKeySettingsSchema === '') {
+ scope.dataKeySettingsSchema = {};
+ } else {
+ scope.dataKeySettingsSchema = angular.fromJson(dataKeySettingsSchema);
+ }
+
+ scope.functionsOnly = scope.dashboard ? false : true;
+
+ scope.theForm.$setPristine();
+ });
+ }
+ );
+ }
+ });
+
+ scope.fetchDeviceKeys = function (deviceAliasId, query, type) {
+ var deviceAlias = scope.dashboard.configuration.deviceAliases[deviceAliasId];
+ if (deviceAlias && deviceAlias.deviceId) {
+ return deviceService.getDeviceKeys(deviceAlias.deviceId, query, type);
+ } else {
+ return $q.when([]);
+ }
+ };
+
+ scope.createDeviceAlias = function (event, alias) {
+
+ var deferred = $q.defer();
+ var singleDeviceAlias = {id: null, alias: alias, deviceId: null};
+
+ $mdDialog.show({
+ controller: 'DeviceAliasesController',
+ controllerAs: 'vm',
+ templateUrl: deviceAliasesTemplate,
+ locals: {
+ deviceAliases: angular.copy(scope.dashboard.configuration.deviceAliases),
+ aliasToWidgetsMap: null,
+ isSingleDevice: true,
+ singleDeviceAlias: singleDeviceAlias
+ },
+ parent: angular.element($document[0].body),
+ fullscreen: true,
+ skipHide: true,
+ targetEvent: event
+ }).then(function (singleDeviceAlias) {
+ scope.dashboard.configuration.deviceAliases[singleDeviceAlias.id] =
+ { alias: singleDeviceAlias.alias, deviceId: singleDeviceAlias.deviceId };
+ deferred.resolve(singleDeviceAlias);
+ }, function () {
+ deferred.reject();
+ });
+
+ return deferred.promise;
+ };
+
+ $compile(element.contents())(scope);
+ }
+
+ return {
+ restrict: "E",
+ link: linker,
+ scope: {
+ dashboard: '=',
+ widget: '=',
+ theForm: '='
+ }
+ };
+}
ui/src/app/dashboard/edit-widget.tpl.html 28(+28 -0)
diff --git a/ui/src/app/dashboard/edit-widget.tpl.html b/ui/src/app/dashboard/edit-widget.tpl.html
new file mode 100644
index 0000000..5f03daf
--- /dev/null
+++ b/ui/src/app/dashboard/edit-widget.tpl.html
@@ -0,0 +1,28 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<fieldset ng-disabled="loading">
+ <tb-widget-config widget-type="widget.type"
+ ng-model="widgetConfig"
+ widget-settings-schema="settingsSchema"
+ datakey-settings-schema="dataKeySettingsSchema"
+ device-aliases="dashboard.configuration.deviceAliases"
+ functions-only="functionsOnly"
+ fetch-device-keys="fetchDeviceKeys(deviceAliasId, query, type)"
+ on-create-device-alias="createDeviceAlias(event, alias)"
+ the-form="theForm"></tb-widget-config>
+</fieldset>
ui/src/app/dashboard/index.js 67(+67 -0)
diff --git a/ui/src/app/dashboard/index.js b/ui/src/app/dashboard/index.js
new file mode 100644
index 0000000..d740b10
--- /dev/null
+++ b/ui/src/app/dashboard/index.js
@@ -0,0 +1,67 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './dashboard.scss';
+
+import uiRouter from 'angular-ui-router';
+import gridster from 'angular-gridster';
+
+import thingsboardGrid from '../components/grid.directive';
+import thingsboardApiWidget from '../api/widget.service';
+import thingsboardApiUser from '../api/user.service';
+import thingsboardApiDashboard from '../api/dashboard.service';
+import thingsboardApiCustomer from '../api/customer.service';
+import thingsboardDetailsSidenav from '../components/details-sidenav.directive';
+import thingsboardWidgetConfig from '../components/widget-config.directive';
+import thingsboardDashboard from '../components/dashboard.directive';
+import thingsboardExpandFullscreen from '../components/expand-fullscreen.directive';
+import thingsboardWidgetsBundleSelect from '../components/widgets-bundle-select.directive';
+import thingsboardTypes from '../common/types.constant';
+
+import DashboardRoutes from './dashboard.routes';
+import DashboardsController from './dashboards.controller';
+import DashboardController from './dashboard.controller';
+import DeviceAliasesController from './device-aliases.controller';
+import AssignDashboardToCustomerController from './assign-to-customer.controller';
+import AddDashboardsToCustomerController from './add-dashboards-to-customer.controller';
+import AddWidgetController from './add-widget.controller';
+import DashboardDirective from './dashboard.directive';
+import EditWidgetDirective from './edit-widget.directive';
+
+export default angular.module('thingsboard.dashboard', [
+ uiRouter,
+ gridster.name,
+ thingsboardTypes,
+ thingsboardGrid,
+ thingsboardApiWidget,
+ thingsboardApiUser,
+ thingsboardApiDashboard,
+ thingsboardApiCustomer,
+ thingsboardDetailsSidenav,
+ thingsboardWidgetConfig,
+ thingsboardDashboard,
+ thingsboardExpandFullscreen,
+ thingsboardWidgetsBundleSelect
+])
+ .config(DashboardRoutes)
+ .controller('DashboardsController', DashboardsController)
+ .controller('DashboardController', DashboardController)
+ .controller('DeviceAliasesController', DeviceAliasesController)
+ .controller('AssignDashboardToCustomerController', AssignDashboardToCustomerController)
+ .controller('AddDashboardsToCustomerController', AddDashboardsToCustomerController)
+ .controller('AddWidgetController', AddWidgetController)
+ .directive('tbDashboardDetails', DashboardDirective)
+ .directive('tbEditWidget', EditWidgetDirective)
+ .name;
ui/src/app/device/add-device.tpl.html 45(+45 -0)
diff --git a/ui/src/app/device/add-device.tpl.html b/ui/src/app/device/add-device.tpl.html
new file mode 100644
index 0000000..171d631
--- /dev/null
+++ b/ui/src/app/device/add-device.tpl.html
@@ -0,0 +1,45 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'device.add' | translate }}" tb-help="'devices'" help-container-id="help-container">
+ <form name="theForm" ng-submit="vm.add()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>device.add</h2>
+ <span flex></span>
+ <div id="help-container"></div>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <tb-device device="vm.item" is-edit="true" the-form="theForm"></tb-device>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit" class="md-raised md-primary">
+ {{ 'action.add' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
\ No newline at end of file
diff --git a/ui/src/app/device/add-devices-to-customer.controller.js b/ui/src/app/device/add-devices-to-customer.controller.js
new file mode 100644
index 0000000..ba4c6e1
--- /dev/null
+++ b/ui/src/app/device/add-devices-to-customer.controller.js
@@ -0,0 +1,123 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*@ngInject*/
+export default function AddDevicesToCustomerController(deviceService, $mdDialog, $q, customerId, devices) {
+
+ var vm = this;
+
+ vm.devices = devices;
+ vm.searchText = '';
+
+ vm.assign = assign;
+ vm.cancel = cancel;
+ vm.hasData = hasData;
+ vm.noData = noData;
+ vm.searchDeviceTextUpdated = searchDeviceTextUpdated;
+ vm.toggleDeviceSelection = toggleDeviceSelection;
+
+ vm.theDevices = {
+ getItemAtIndex: function (index) {
+ if (index > vm.devices.data.length) {
+ vm.theDevices.fetchMoreItems_(index);
+ return null;
+ }
+ var item = vm.devices.data[index];
+ if (item) {
+ item.indexNumber = index + 1;
+ }
+ return item;
+ },
+
+ getLength: function () {
+ if (vm.devices.hasNext) {
+ return vm.devices.data.length + vm.devices.nextPageLink.limit;
+ } else {
+ return vm.devices.data.length;
+ }
+ },
+
+ fetchMoreItems_: function () {
+ if (vm.devices.hasNext && !vm.devices.pending) {
+ vm.devices.pending = true;
+ deviceService.getTenantDevices(vm.devices.nextPageLink).then(
+ function success(devices) {
+ vm.devices.data = vm.devices.data.concat(devices.data);
+ vm.devices.nextPageLink = devices.nextPageLink;
+ vm.devices.hasNext = devices.hasNext;
+ if (vm.devices.hasNext) {
+ vm.devices.nextPageLink.limit = vm.devices.pageSize;
+ }
+ vm.devices.pending = false;
+ },
+ function fail() {
+ vm.devices.hasNext = false;
+ vm.devices.pending = false;
+ });
+ }
+ }
+ };
+
+ function cancel () {
+ $mdDialog.cancel();
+ }
+
+ function assign() {
+ var tasks = [];
+ for (var deviceId in vm.devices.selections) {
+ tasks.push(deviceService.assignDeviceToCustomer(customerId, deviceId));
+ }
+ $q.all(tasks).then(function () {
+ $mdDialog.hide();
+ });
+ }
+
+ function noData() {
+ return vm.devices.data.length == 0 && !vm.devices.hasNext;
+ }
+
+ function hasData() {
+ return vm.devices.data.length > 0;
+ }
+
+ function toggleDeviceSelection($event, device) {
+ $event.stopPropagation();
+ var selected = angular.isDefined(device.selected) && device.selected;
+ device.selected = !selected;
+ if (device.selected) {
+ vm.devices.selections[device.id.id] = true;
+ vm.devices.selectedCount++;
+ } else {
+ delete vm.devices.selections[device.id.id];
+ vm.devices.selectedCount--;
+ }
+ }
+
+ function searchDeviceTextUpdated() {
+ vm.devices = {
+ pageSize: vm.devices.pageSize,
+ data: [],
+ nextPageLink: {
+ limit: vm.devices.pageSize,
+ textSearch: vm.searchText
+ },
+ selections: {},
+ selectedCount: 0,
+ hasNext: true,
+ pending: false
+ };
+ }
+
+}
diff --git a/ui/src/app/device/add-devices-to-customer.tpl.html b/ui/src/app/device/add-devices-to-customer.tpl.html
new file mode 100644
index 0000000..ac855f7
--- /dev/null
+++ b/ui/src/app/device/add-devices-to-customer.tpl.html
@@ -0,0 +1,77 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'device.assign-to-customer' | translate }}">
+ <form name="theForm" ng-submit="vm.assign()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>device.assign-device-to-customer</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <fieldset>
+ <span translate>device.assign-device-to-customer-text</span>
+ <md-input-container class="md-block" style='margin-bottom: 0px;'>
+ <label> </label>
+ <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">
+ search
+ </md-icon>
+ <input id="device-search" autofocus ng-model="vm.searchText"
+ ng-change="vm.searchDeviceTextUpdated()"
+ placeholder="{{ 'common.enter-search' | translate }}"/>
+ </md-input-container>
+ <div style='min-height: 150px;'>
+ <span translate layout-align="center center"
+ style="text-transform: uppercase; display: flex; height: 150px;"
+ class="md-subhead"
+ ng-show="vm.noData()">device.no-devices-text</span>
+ <md-virtual-repeat-container ng-show="vm.hasData()"
+ tb-scope-element="repeatContainer" md-top-index="vm.topIndex" flex
+ style='min-height: 150px; width: 100%;'>
+ <md-list>
+ <md-list-item md-virtual-repeat="device in vm.theDevices" md-on-demand
+ class="repeated-item" flex>
+ <md-checkbox ng-click="vm.toggleDeviceSelection($event, device)"
+ aria-label="{{ 'item.selected' | translate }}"
+ ng-checked="device.selected"></md-checkbox>
+ <span> {{ device.name }} </span>
+ </md-list-item>
+ </md-list>
+ </md-virtual-repeat-container>
+ </div>
+ </fieldset>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || vm.devices.selectedCount == 0" type="submit"
+ class="md-raised md-primary">
+ {{ 'action.assign' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+ translate }}
+ </md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
\ No newline at end of file
ui/src/app/device/assign-to-customer.controller.js 123(+123 -0)
diff --git a/ui/src/app/device/assign-to-customer.controller.js b/ui/src/app/device/assign-to-customer.controller.js
new file mode 100644
index 0000000..5c3083e
--- /dev/null
+++ b/ui/src/app/device/assign-to-customer.controller.js
@@ -0,0 +1,123 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*@ngInject*/
+export default function AssignDeviceToCustomerController(customerService, deviceService, $mdDialog, $q, deviceIds, customers) {
+
+ var vm = this;
+
+ vm.customers = customers;
+ vm.searchText = '';
+
+ vm.assign = assign;
+ vm.cancel = cancel;
+ vm.isCustomerSelected = isCustomerSelected;
+ vm.hasData = hasData;
+ vm.noData = noData;
+ vm.searchCustomerTextUpdated = searchCustomerTextUpdated;
+ vm.toggleCustomerSelection = toggleCustomerSelection;
+
+ vm.theCustomers = {
+ getItemAtIndex: function (index) {
+ if (index > vm.customers.data.length) {
+ vm.theCustomers.fetchMoreItems_(index);
+ return null;
+ }
+ var item = vm.customers.data[index];
+ if (item) {
+ item.indexNumber = index + 1;
+ }
+ return item;
+ },
+
+ getLength: function () {
+ if (vm.customers.hasNext) {
+ return vm.customers.data.length + vm.customers.nextPageLink.limit;
+ } else {
+ return vm.customers.data.length;
+ }
+ },
+
+ fetchMoreItems_: function () {
+ if (vm.customers.hasNext && !vm.customers.pending) {
+ vm.customers.pending = true;
+ customerService.getCustomers(vm.customers.nextPageLink).then(
+ function success(customers) {
+ vm.customers.data = vm.customers.data.concat(customers.data);
+ vm.customers.nextPageLink = customers.nextPageLink;
+ vm.customers.hasNext = customers.hasNext;
+ if (vm.customers.hasNext) {
+ vm.customers.nextPageLink.limit = vm.customers.pageSize;
+ }
+ vm.customers.pending = false;
+ },
+ function fail() {
+ vm.customers.hasNext = false;
+ vm.customers.pending = false;
+ });
+ }
+ }
+ };
+
+ function cancel() {
+ $mdDialog.cancel();
+ }
+
+ function assign() {
+ var tasks = [];
+ for (var deviceId in deviceIds) {
+ tasks.push(deviceService.assignDeviceToCustomer(vm.customers.selection.id.id, deviceIds[deviceId]));
+ }
+ $q.all(tasks).then(function () {
+ $mdDialog.hide();
+ });
+ }
+
+ function noData() {
+ return vm.customers.data.length == 0 && !vm.customers.hasNext;
+ }
+
+ function hasData() {
+ return vm.customers.data.length > 0;
+ }
+
+ function toggleCustomerSelection($event, customer) {
+ $event.stopPropagation();
+ if (vm.isCustomerSelected(customer)) {
+ vm.customers.selection = null;
+ } else {
+ vm.customers.selection = customer;
+ }
+ }
+
+ function isCustomerSelected(customer) {
+ return vm.customers.selection != null && customer &&
+ customer.id.id === vm.customers.selection.id.id;
+ }
+
+ function searchCustomerTextUpdated() {
+ vm.customers = {
+ pageSize: vm.customers.pageSize,
+ data: [],
+ nextPageLink: {
+ limit: vm.customers.pageSize,
+ textSearch: vm.searchText
+ },
+ selection: null,
+ hasNext: true,
+ pending: false
+ };
+ }
+}
diff --git a/ui/src/app/device/assign-to-customer.tpl.html b/ui/src/app/device/assign-to-customer.tpl.html
new file mode 100644
index 0000000..ed4c692
--- /dev/null
+++ b/ui/src/app/device/assign-to-customer.tpl.html
@@ -0,0 +1,76 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'device.assign-device-to-customer' | translate }}">
+ <form name="theForm" ng-submit="vm.assign()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>device.assign-device-to-customer</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <fieldset>
+ <span translate>device.assign-to-customer-text</span>
+ <md-input-container class="md-block" style='margin-bottom: 0px;'>
+ <label> </label>
+ <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">
+ search
+ </md-icon>
+ <input id="customer-search" autofocus ng-model="vm.searchText"
+ ng-change="vm.searchCustomerTextUpdated()"
+ placeholder="{{ 'common.enter-search' | translate }}"/>
+ </md-input-container>
+ <div style='min-height: 150px;'>
+ <span translate layout-align="center center"
+ style="text-transform: uppercase; display: flex; height: 150px;"
+ class="md-subhead"
+ ng-show="vm.noData()">customer.no-customers-text</span>
+ <md-virtual-repeat-container ng-show="vm.hasData()"
+ tb-scope-element="repeatContainer" md-top-index="vm.topIndex" flex
+ style='min-height: 150px; width: 100%;'>
+ <md-list>
+ <md-list-item md-virtual-repeat="customer in vm.theCustomers" md-on-demand
+ class="repeated-item" flex>
+ <md-checkbox ng-click="vm.toggleCustomerSelection($event, customer)"
+ aria-label="{{ 'item.selected' | translate }}"
+ ng-checked="vm.isCustomerSelected(customer)"></md-checkbox>
+ <span> {{ customer.title }} </span>
+ </md-list-item>
+ </md-list>
+ </md-virtual-repeat-container>
+ </div>
+ </fieldset>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || vm.customers.selection==null" type="submit" class="md-raised md-primary">
+ {{ 'action.assign' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+ translate }}
+ </md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
\ No newline at end of file
diff --git a/ui/src/app/device/attribute/add-attribute-dialog.controller.js b/ui/src/app/device/attribute/add-attribute-dialog.controller.js
new file mode 100644
index 0000000..c17742d
--- /dev/null
+++ b/ui/src/app/device/attribute/add-attribute-dialog.controller.js
@@ -0,0 +1,50 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*@ngInject*/
+export default function AddAttributeDialogController($scope, $mdDialog, types, deviceService, deviceId, attributeScope) {
+
+ var vm = this;
+
+ vm.attribute = {};
+
+ vm.valueTypes = types.valueType;
+
+ vm.valueType = types.valueType.string;
+
+ vm.add = add;
+ vm.cancel = cancel;
+
+ function cancel() {
+ $mdDialog.cancel();
+ }
+
+ function add() {
+ $scope.theForm.$setPristine();
+ deviceService.saveDeviceAttributes(deviceId, attributeScope, [vm.attribute]).then(
+ function success() {
+ $mdDialog.hide();
+ }
+ );
+ }
+
+ $scope.$watch('vm.valueType', function() {
+ if (vm.valueType === types.valueType.boolean) {
+ vm.attribute.value = false;
+ } else {
+ vm.attribute.value = null;
+ }
+ });
+}
diff --git a/ui/src/app/device/attribute/add-attribute-dialog.tpl.html b/ui/src/app/device/attribute/add-attribute-dialog.tpl.html
new file mode 100644
index 0000000..fc2bf57
--- /dev/null
+++ b/ui/src/app/device/attribute/add-attribute-dialog.tpl.html
@@ -0,0 +1,95 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'attribute.add' | translate }}" style="min-width: 400px;">
+ <form name="theForm" ng-submit="vm.add()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>attribute.add</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <md-content class="md-padding" layout="column">
+ <fieldset ng-disabled="loading">
+ <md-input-container class="md-block">
+ <label translate>attribute.key</label>
+ <input required name="key" ng-model="vm.attribute.key">
+ <div ng-messages="theForm.key.$error">
+ <div translate ng-message="required">attribute.key-required</div>
+ </div>
+ </md-input-container>
+ <section layout="row">
+ <md-input-container flex="40" class="md-block" style="width: 200px;">
+ <label translate>value.type</label>
+ <md-select ng-model="vm.valueType" ng-disabled="loading()">
+ <md-option ng-repeat="type in vm.valueTypes" ng-value="type">
+ <md-icon md-svg-icon="{{ type.icon }}"></md-icon>
+ <span>{{type.name | translate}}</span>
+ </md-option>
+ </md-select>
+ </md-input-container>
+ <md-input-container ng-if="vm.valueType===vm.valueTypes.string" flex="60" class="md-block">
+ <label translate>value.string-value</label>
+ <input required name="value" ng-model="vm.attribute.value">
+ <div ng-messages="theForm.value.$error">
+ <div translate ng-message="required">attribute.value-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container ng-if="vm.valueType===vm.valueTypes.integer" flex="60" class="md-block">
+ <label translate>value.integer-value</label>
+ <input required name="value" type="number" step="1" ng-pattern="/^-?[0-9]+$/" ng-model="vm.attribute.value">
+ <div ng-messages="theForm.value.$error">
+ <div translate ng-message="required">attribute.value-required</div>
+ <div translate ng-message="pattern">value.invalid-integer-value</div>
+ </div>
+ </md-input-container>
+ <md-input-container ng-if="vm.valueType===vm.valueTypes.double" flex="60" class="md-block">
+ <label translate>value.double-value</label>
+ <input required name="value" type="number" step="any" ng-model="vm.attribute.value">
+ <div ng-messages="theForm.value.$error">
+ <div translate ng-message="required">attribute.value-required</div>
+ </div>
+ </md-input-container>
+ <div layout="column" layout-align="center" flex="60" ng-if="vm.valueType===vm.valueTypes.boolean">
+ <md-checkbox ng-model="vm.attribute.value" style="margin-bottom: 0px;">
+ {{ (vm.attribute.value ? 'value.true' : 'value.false') | translate }}
+ </md-checkbox>
+ </div>
+ </section>
+ </fieldset>
+ </md-content>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit"
+ class="md-raised md-primary">
+ {{ 'action.add' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+ translate }}
+ </md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
diff --git a/ui/src/app/device/attribute/add-widget-to-dashboard-dialog.controller.js b/ui/src/app/device/attribute/add-widget-to-dashboard-dialog.controller.js
new file mode 100644
index 0000000..9149cf1
--- /dev/null
+++ b/ui/src/app/device/attribute/add-widget-to-dashboard-dialog.controller.js
@@ -0,0 +1,122 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*@ngInject*/
+export default function AddWidgetToDashboardDialogController($scope, $mdDialog, $state, dashboardService, deviceId, deviceName, widget) {
+
+ var vm = this;
+
+ vm.widget = widget;
+ vm.dashboard = null;
+ vm.addToDashboardType = 0;
+ vm.newDashboard = {};
+ vm.openDashboard = false;
+
+ vm.add = add;
+ vm.cancel = cancel;
+
+ function cancel() {
+ $mdDialog.cancel();
+ }
+
+ function add() {
+ $scope.theForm.$setPristine();
+ var theDashboard;
+ var deviceAliases;
+ widget.col = 0;
+ widget.sizeX /= 2;
+ widget.sizeY /= 2;
+ if (vm.addToDashboardType === 0) {
+ theDashboard = vm.dashboard;
+ if (!theDashboard.configuration) {
+ theDashboard.configuration = {};
+ }
+ deviceAliases = theDashboard.configuration.deviceAliases;
+ if (!deviceAliases) {
+ deviceAliases = {};
+ theDashboard.configuration.deviceAliases = deviceAliases;
+ }
+ var newAliasId;
+ for (var aliasId in deviceAliases) {
+ if (deviceAliases[aliasId].deviceId === deviceId) {
+ newAliasId = aliasId;
+ break;
+ }
+ }
+ if (!newAliasId) {
+ var newAliasName = createDeviceAliasName(deviceAliases, deviceName);
+ newAliasId = 0;
+ for (aliasId in deviceAliases) {
+ newAliasId = Math.max(newAliasId, aliasId);
+ }
+ newAliasId++;
+ deviceAliases[newAliasId] = {alias: newAliasName, deviceId: deviceId};
+ }
+ widget.config.datasources[0].deviceAliasId = newAliasId;
+
+ if (!theDashboard.configuration.widgets) {
+ theDashboard.configuration.widgets = [];
+ }
+
+ var row = 0;
+ for (var w in theDashboard.configuration.widgets) {
+ var existingWidget = theDashboard.configuration.widgets[w];
+ var wRow = existingWidget.row ? existingWidget.row : 0;
+ var wSizeY = existingWidget.sizeY ? existingWidget.sizeY : 1;
+ var bottom = wRow + wSizeY;
+ row = Math.max(row, bottom);
+ }
+ widget.row = row;
+ theDashboard.configuration.widgets.push(widget);
+ } else {
+ theDashboard = vm.newDashboard;
+ deviceAliases = {};
+ deviceAliases['1'] = {alias: deviceName, deviceId: deviceId};
+ theDashboard.configuration = {};
+ theDashboard.configuration.widgets = [];
+ widget.row = 0;
+ theDashboard.configuration.widgets.push(widget);
+ theDashboard.configuration.deviceAliases = deviceAliases;
+ }
+ dashboardService.saveDashboard(theDashboard).then(
+ function success(dashboard) {
+ $mdDialog.hide();
+ if (vm.openDashboard) {
+ $state.go('home.dashboards.dashboard', {dashboardId: dashboard.id.id});
+ }
+ }
+ );
+
+ }
+
+ function createDeviceAliasName(deviceAliases, alias) {
+ var c = 0;
+ var newAlias = angular.copy(alias);
+ var unique = false;
+ while (!unique) {
+ unique = true;
+ for (var devAliasId in deviceAliases) {
+ var devAlias = deviceAliases[devAliasId];
+ if (newAlias === devAlias.alias) {
+ c++;
+ newAlias = alias + c;
+ unique = false;
+ }
+ }
+ }
+ return newAlias;
+ }
+
+}
diff --git a/ui/src/app/device/attribute/add-widget-to-dashboard-dialog.tpl.html b/ui/src/app/device/attribute/add-widget-to-dashboard-dialog.tpl.html
new file mode 100644
index 0000000..dffc229
--- /dev/null
+++ b/ui/src/app/device/attribute/add-widget-to-dashboard-dialog.tpl.html
@@ -0,0 +1,80 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'attribute.add-widget-to-dashboard' | translate }}" style="min-width: 400px;">
+ <form name="theForm" ng-submit="vm.add()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>attribute.add-widget-to-dashboard</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <md-content class="md-padding" layout="column">
+ <fieldset ng-disabled="loading">
+ <md-radio-group ng-model="vm.addToDashboardType" class="md-primary">
+ <md-radio-button flex ng-value=0 class="md-primary md-align-top-left md-radio-interactive">
+ <section flex layout="column" style="width: 300px;">
+ <span translate style="padding-bottom: 10px;">dashboard.select-existing</span>
+ <tb-dashboard-select the-form="theForm"
+ tb-required="vm.addToDashboardType === 0"
+ ng-model="vm.dashboard"
+ select-first-dashboard="false">
+ </tb-dashboard-select>
+ </section>
+ </md-radio-button>
+ <md-radio-button flex ng-value=1 class="md-primary md-align-top-left md-radio-interactive">
+ <section flex layout="column" style="width: 300px;">
+ <span translate>dashboard.create-new</span>
+ <md-input-container class="md-block">
+ <label translate>dashboard.new-dashboard-title</label>
+ <input ng-required="vm.addToDashboardType === 1" name="title" ng-model="vm.newDashboard.title">
+ <div ng-messages="theForm.title.$error">
+ <div translate ng-message="required">dashboard.title-required</div>
+ </div>
+ </md-input-container>
+ </section>
+ </md-radio-button>
+ </md-radio-group>
+ </fieldset>
+ </md-content>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-checkbox
+ ng-model="vm.openDashboard"
+ aria-label="{{ 'dashboard.open-dashboard' | translate }}"
+ style="margin-bottom: 0px; padding-right: 20px;">
+ {{ 'dashboard.open-dashboard' | translate }}
+ </md-checkbox>
+ <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit"
+ class="md-raised md-primary">
+ {{ 'action.add' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+ translate }}
+ </md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
diff --git a/ui/src/app/device/attribute/attribute-table.directive.js b/ui/src/app/device/attribute/attribute-table.directive.js
new file mode 100644
index 0000000..0a5e0cd
--- /dev/null
+++ b/ui/src/app/device/attribute/attribute-table.directive.js
@@ -0,0 +1,385 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import 'angular-material-data-table/dist/md-data-table.min.css';
+import './attribute-table.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import attributeTableTemplate from './attribute-table.tpl.html';
+import addAttributeDialogTemplate from './add-attribute-dialog.tpl.html';
+import addWidgetToDashboardDialogTemplate from './add-widget-to-dashboard-dialog.tpl.html';
+import editAttributeValueTemplate from './edit-attribute-value.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+import EditAttributeValueController from './edit-attribute-value.controller';
+
+/*@ngInject*/
+export default function AttributeTableDirective($compile, $templateCache, $rootScope, $q, $mdEditDialog, $mdDialog,
+ $document, $translate, utils, types, deviceService, widgetService) {
+
+ var linker = function (scope, element, attrs) {
+
+ var template = $templateCache.get(attributeTableTemplate);
+
+ element.html(template);
+
+ scope.types = types;
+ scope.attributeScopes = types.deviceAttributesScope;
+
+ var getAttributeScopeByValue = function(attributeScopeValue) {
+ if (scope.types.latestTelemetry.value === attributeScopeValue) {
+ return scope.types.latestTelemetry;
+ }
+ for (var attrScope in scope.attributeScopes) {
+ if (scope.attributeScopes[attrScope].value === attributeScopeValue) {
+ return scope.attributeScopes[attrScope];
+ }
+ }
+ }
+
+ scope.attributeScope = getAttributeScopeByValue(attrs.defaultAttributeScope);
+
+ scope.attributes = {
+ count: 0,
+ data: []
+ };
+
+ scope.selectedAttributes = [];
+ scope.mode = 'default'; // 'widget'
+ scope.subscriptionId = null;
+
+ scope.query = {
+ order: 'key',
+ limit: 5,
+ page: 1,
+ search: null
+ };
+
+ scope.$watch("deviceId", function(newVal, prevVal) {
+ if (newVal && !angular.equals(newVal, prevVal)) {
+ scope.resetFilter();
+ scope.getDeviceAttributes();
+ }
+ });
+
+ scope.$watch("attributeScope", function(newVal, prevVal) {
+ if (newVal && !angular.equals(newVal, prevVal)) {
+ scope.mode = 'default';
+ scope.query.search = null;
+ scope.selectedAttributes = [];
+ scope.getDeviceAttributes();
+ }
+ });
+
+ scope.resetFilter = function() {
+ scope.mode = 'default';
+ scope.query.search = null;
+ scope.selectedAttributes = [];
+ scope.attributeScope = getAttributeScopeByValue(attrs.defaultAttributeScope);
+ }
+
+ scope.enterFilterMode = function() {
+ scope.query.search = '';
+ }
+
+ scope.exitFilterMode = function() {
+ scope.query.search = null;
+ scope.getDeviceAttributes();
+ }
+
+ scope.$watch("query.search", function(newVal, prevVal) {
+ if (!angular.equals(newVal, prevVal) && scope.query.search != null) {
+ scope.getDeviceAttributes();
+ }
+ });
+
+ function success(attributes, update) {
+ scope.attributes = attributes;
+ if (!update) {
+ scope.selectedAttributes = [];
+ }
+ }
+
+ scope.getDeviceAttributes = function(forceUpdate) {
+ if (scope.attributesDeferred) {
+ scope.attributesDeferred.resolve();
+ }
+ if (scope.deviceId && scope.attributeScope) {
+ scope.checkSubscription();
+ scope.attributesDeferred = deviceService.getDeviceAttributes(scope.deviceId, scope.attributeScope.value,
+ scope.query, function(attributes, update) {
+ success(attributes, update || forceUpdate);
+ }
+ );
+ } else {
+ var deferred = $q.defer();
+ scope.attributesDeferred = deferred;
+ success({
+ count: 0,
+ data: []
+ });
+ deferred.resolve();
+ }
+ }
+
+ scope.checkSubscription = function() {
+ var newSubscriptionId = null;
+ if (scope.deviceId && scope.attributeScope.clientSide && scope.mode != 'widget') {
+ newSubscriptionId = deviceService.subscribeForDeviceAttributes(scope.deviceId, scope.attributeScope.value);
+ }
+ if (scope.subscriptionId && scope.subscriptionId != newSubscriptionId) {
+ deviceService.unsubscribeForDeviceAttributes(scope.subscriptionId);
+ }
+ scope.subscriptionId = newSubscriptionId;
+ }
+
+ scope.editAttribute = function($event, attribute) {
+ if (!scope.attributeScope.clientSide) {
+ $event.stopPropagation();
+ $mdEditDialog.show({
+ controller: EditAttributeValueController,
+ templateUrl: editAttributeValueTemplate,
+ locals: {attributeValue: attribute.value,
+ save: function (model) {
+ var updatedAttribute = angular.copy(attribute);
+ updatedAttribute.value = model.value;
+ deviceService.saveDeviceAttributes(scope.deviceId, scope.attributeScope.value, [updatedAttribute]).then(
+ function success() {
+ scope.getDeviceAttributes();
+ }
+ );
+ }},
+ targetEvent: $event
+ });
+ }
+ }
+
+ scope.addAttribute = function($event) {
+ if (!scope.attributeScope.clientSide) {
+ $event.stopPropagation();
+ $mdDialog.show({
+ controller: 'AddAttributeDialogController',
+ controllerAs: 'vm',
+ templateUrl: addAttributeDialogTemplate,
+ parent: angular.element($document[0].body),
+ locals: {deviceId: scope.deviceId, attributeScope: scope.attributeScope.value},
+ fullscreen: true,
+ targetEvent: $event
+ }).then(function () {
+ scope.getDeviceAttributes();
+ });
+ }
+ }
+
+ scope.deleteAttributes = function($event) {
+ if (!scope.attributeScope.clientSide) {
+ $event.stopPropagation();
+ var confirm = $mdDialog.confirm()
+ .targetEvent($event)
+ .title($translate.instant('attribute.delete-attributes-title', {count: scope.selectedAttributes.length}, 'messageformat'))
+ .htmlContent($translate.instant('attribute.delete-attributes-text'))
+ .ariaLabel($translate.instant('attribute.delete-attributes'))
+ .cancel($translate.instant('action.no'))
+ .ok($translate.instant('action.yes'));
+ $mdDialog.show(confirm).then(function () {
+ deviceService.deleteDeviceAttributes(scope.deviceId, scope.attributeScope.value, scope.selectedAttributes).then(
+ function success() {
+ scope.selectedAttributes = [];
+ scope.getDeviceAttributes();
+ }
+ )
+ });
+ }
+ }
+
+ scope.nextWidget = function() {
+ if (scope.widgetsCarousel.index < scope.widgetsList.length-1) {
+ scope.widgetsCarousel.index++;
+ }
+ }
+
+ scope.prevWidget = function() {
+ if (scope.widgetsCarousel.index > 0) {
+ scope.widgetsCarousel.index--;
+ }
+ }
+
+ scope.enterWidgetMode = function() {
+
+ if (scope.widgetsIndexWatch) {
+ scope.widgetsIndexWatch();
+ scope.widgetsIndexWatch = null;
+ }
+
+ if (scope.widgetsBundleWatch) {
+ scope.widgetsBundleWatch();
+ scope.widgetsBundleWatch = null;
+ }
+
+ scope.mode = 'widget';
+ scope.checkSubscription();
+ scope.widgetsList = [];
+ scope.widgetsListCache = [];
+ scope.widgetsLoaded = false;
+ scope.widgetsCarousel = {
+ index: 0
+ }
+ scope.widgetsBundle = null;
+
+ scope.deviceAliases = {};
+ scope.deviceAliases['1'] = {alias: scope.deviceName, deviceId: scope.deviceId};
+
+ var dataKeyType = scope.attributeScope === types.latestTelemetry ?
+ types.dataKeyType.timeseries : types.dataKeyType.attribute;
+
+ var datasource = {
+ type: types.datasourceType.device,
+ deviceAliasId: '1',
+ dataKeys: []
+ }
+ var i = 0;
+ for (var attr in scope.selectedAttributes) {
+ var attribute = scope.selectedAttributes[attr];
+ var dataKey = {
+ name: attribute.key,
+ label: attribute.key,
+ type: dataKeyType,
+ color: utils.getMaterialColor(i),
+ settings: {},
+ _hash: Math.random()
+ }
+ datasource.dataKeys.push(dataKey);
+ i++;
+ }
+
+ scope.widgetsIndexWatch = scope.$watch('widgetsCarousel.index', function(newVal, prevVal) {
+ if (scope.mode === 'widget' && (newVal != prevVal)) {
+ var index = scope.widgetsCarousel.index;
+ for (var i = 0; i < scope.widgetsList.length; i++) {
+ scope.widgetsList[i].splice(0, scope.widgetsList[i].length);
+ if (i === index) {
+ scope.widgetsList[i].push(scope.widgetsListCache[i][0]);
+ }
+ }
+ }
+ });
+
+ scope.widgetsBundleWatch = scope.$watch('widgetsBundle', function(newVal, prevVal) {
+ if (scope.mode === 'widget' && (scope.firstBundle === true || newVal != prevVal)) {
+ scope.widgetsList = [];
+ scope.widgetsListCache = [];
+ scope.widgetsCarousel.index = 0;
+ scope.firstBundle = false;
+ if (scope.widgetsBundle) {
+ scope.widgetsLoaded = false;
+ var bundleAlias = scope.widgetsBundle.alias;
+ var isSystem = scope.widgetsBundle.tenantId.id === types.id.nullUid;
+ widgetService.getBundleWidgetTypes(scope.widgetsBundle.alias, isSystem).then(
+ function success(widgetTypes) {
+ for (var i = 0; i < widgetTypes.length; i++) {
+ var widgetType = widgetTypes[i];
+ var widgetInfo = widgetService.toWidgetInfo(widgetType);
+ var sizeX = widgetInfo.sizeX*2;
+ var sizeY = widgetInfo.sizeY*2;
+ var col = Math.floor(Math.max(0, (20 - sizeX)/2));
+ var widget = {
+ isSystemType: isSystem,
+ bundleAlias: bundleAlias,
+ typeAlias: widgetInfo.alias,
+ type: widgetInfo.type,
+ title: widgetInfo.widgetName,
+ sizeX: sizeX,
+ sizeY: sizeY,
+ row: 0,
+ col: col,
+ config: angular.fromJson(widgetInfo.defaultConfig)
+ };
+
+ widget.config.title = widgetInfo.widgetName;
+ widget.config.datasources = [datasource];
+ var length;
+ if (scope.attributeScope === types.latestTelemetry && widgetInfo.type !== types.widgetType.rpc.value) {
+ length = scope.widgetsListCache.push([widget]);
+ scope.widgetsList.push(length === 1 ? [widget] : []);
+ } else if (widgetInfo.type === types.widgetType.latest.value) {
+ length = scope.widgetsListCache.push([widget]);
+ scope.widgetsList.push(length === 1 ? [widget] : []);
+ }
+ }
+ scope.widgetsLoaded = true;
+ }
+ );
+ }
+ }
+ });
+
+ widgetService.getWidgetsBundleByAlias(types.systemBundleAlias.cards).then(
+ function success(widgetsBundle) {
+ scope.firstBundle = true;
+ scope.widgetsBundle = widgetsBundle;
+ }
+ );
+ }
+
+ scope.exitWidgetMode = function() {
+ if (scope.widgetsBundleWatch) {
+ scope.widgetsBundleWatch();
+ scope.widgetsBundleWatch = null;
+ }
+ if (scope.widgetsIndexWatch) {
+ scope.widgetsIndexWatch();
+ scope.widgetsIndexWatch = null;
+ }
+ scope.mode = 'default';
+ scope.getDeviceAttributes(true);
+ }
+
+ scope.addWidgetToDashboard = function($event) {
+ if (scope.mode === 'widget' && scope.widgetsListCache.length > 0) {
+ var widget = scope.widgetsListCache[scope.widgetsCarousel.index][0];
+ $event.stopPropagation();
+ $mdDialog.show({
+ controller: 'AddWidgetToDashboardDialogController',
+ controllerAs: 'vm',
+ templateUrl: addWidgetToDashboardDialogTemplate,
+ parent: angular.element($document[0].body),
+ locals: {deviceId: scope.deviceId, deviceName: scope.deviceName, widget: angular.copy(widget)},
+ fullscreen: true,
+ targetEvent: $event
+ }).then(function () {
+
+ });
+ }
+ }
+
+ scope.loading = function() {
+ return $rootScope.loading;
+ }
+
+ $compile(element.contents())(scope);
+ }
+
+ return {
+ restrict: "E",
+ link: linker,
+ scope: {
+ deviceId: '=',
+ deviceName: '=',
+ disableAttributeScopeSelection: '@?'
+ }
+ };
+}
diff --git a/ui/src/app/device/attribute/attribute-table.scss b/ui/src/app/device/attribute/attribute-table.scss
new file mode 100644
index 0000000..bce0864
--- /dev/null
+++ b/ui/src/app/device/attribute/attribute-table.scss
@@ -0,0 +1,51 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import '../../../scss/constants';
+
+$md-light: rgba(255, 255, 255, 100%);
+$md-edit-icon-fill: #757575;
+
+md-toolbar.md-table-toolbar.alternate {
+ .md-toolbar-tools {
+ md-icon {
+ color: $md-light;
+ }
+ }
+}
+
+.md-table {
+ .md-cell {
+ ng-md-icon {
+ fill: $md-edit-icon-fill;
+ float: right;
+ height: 16px;
+ }
+ }
+}
+
+.widgets-carousel {
+ position: relative;
+ margin: 0px;
+
+ min-height: 150px !important;
+
+ tb-dashboard {
+ #gridster-parent {
+ padding: 0 7px;
+ }
+ }
+}
\ No newline at end of file
ui/src/app/device/attribute/attribute-table.tpl.html 208(+208 -0)
diff --git a/ui/src/app/device/attribute/attribute-table.tpl.html b/ui/src/app/device/attribute/attribute-table.tpl.html
new file mode 100644
index 0000000..880d880
--- /dev/null
+++ b/ui/src/app/device/attribute/attribute-table.tpl.html
@@ -0,0 +1,208 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-content flex class="md-padding tb-absolute-fill" layout="column">
+ <section layout="row" ng-show="!disableAttributeScopeSelection">
+ <md-input-container class="md-block" style="width: 200px;">
+ <label translate>attribute.attributes-scope</label>
+ <md-select ng-model="attributeScope" ng-disabled="loading()">
+ <md-option ng-repeat="scope in attributeScopes" ng-value="scope">
+ {{scope.name | translate}}
+ </md-option>
+ </md-select>
+ </md-input-container>
+ </section>
+ <div layout="column" class="md-whiteframe-z1" ng-class="{flex: mode==='widget'}">
+ <md-toolbar class="md-table-toolbar md-default" ng-show="mode==='default'
+ && !selectedAttributes.length
+ && query.search === null">
+ <div class="md-toolbar-tools">
+ <span translate>{{ attributeScope.name }}</span>
+ <span flex></span>
+ <md-button ng-show="!attributeScope.clientSide" class="md-icon-button" ng-click="addAttribute($event)">
+ <md-icon>add</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.add' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-button class="md-icon-button" ng-click="enterFilterMode()">
+ <md-icon>search</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.search' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-button ng-show="!attributeScope.clientSide" class="md-icon-button" ng-click="getDeviceAttributes()">
+ <md-icon>refresh</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.refresh' | translate }}
+ </md-tooltip>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-toolbar class="md-table-toolbar md-default" ng-show="mode==='default'
+ && !selectedAttributes.length
+ && query.search != null">
+ <div class="md-toolbar-tools">
+ <md-button class="md-icon-button" aria-label="{{ 'action.search' | translate }}">
+ <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.search' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-input-container md-theme="tb-search-input" flex>
+ <label> </label>
+ <input ng-model="query.search" placeholder="{{ 'common.enter-search' | translate }}"/>
+ </md-input-container>
+ <md-button class="md-icon-button" aria-label="{{ 'action.back' | translate }}" ng-click="exitFilterMode()">
+ <md-icon aria-label="{{ 'action.close' | translate }}" class="material-icons">close</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.close' | translate }}
+ </md-tooltip>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-toolbar class="md-table-toolbar alternate" ng-show="mode==='default' && selectedAttributes.length">
+ <div class="md-toolbar-tools">
+ <span translate="{{attributeScope === types.latestTelemetry
+ ? 'attribute.selected-telemetry'
+ : 'attribute.selected-attributes'}}"
+ translate-values="{count: selectedAttributes.length}"
+ translate-interpolation="messageformat"></span>
+ <span flex></span>
+ <md-button ng-show="!attributeScope.clientSide" class="md-icon-button" ng-click="deleteAttributes($event)">
+ <md-icon>delete</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.delete' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-button ng-show="attributeScope.clientSide" class="md-accent md-hue-2 md-raised" ng-click="enterWidgetMode()">
+ <md-tooltip md-direction="top">
+ {{ 'attribute.show-on-widget' | translate }}
+ </md-tooltip>
+ <md-icon>now_widgets</md-icon>
+ <span translate>attribute.show-on-widget</span>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-toolbar class="md-table-toolbar alternate" ng-show="mode==='widget'">
+ <div class="md-toolbar-tools">
+ <div flex layout="row" layout-align="start">
+ <span class="tb-details-subtitle">{{ 'widgets-bundle.current' | translate }}</span>
+ <tb-widgets-bundle-select flex-offset="5"
+ flex
+ ng-model="widgetsBundle"
+ select-first-bundle="false">
+ </tb-widgets-bundle-select>
+ </div>
+ <md-button ng-show="widgetsList.length > 0" class="md-accent md-hue-2 md-raised" ng-click="addWidgetToDashboard($event)">
+ <md-tooltip md-direction="top">
+ {{ 'attribute.add-to-dashboard' | translate }}
+ </md-tooltip>
+ <md-icon>dashboard</md-icon>
+ <span translate>attribute.add-to-dashboard</span>
+ </md-button>
+ <md-button class="md-icon-button" aria-label="{{ 'action.back' | translate }}" ng-click="exitWidgetMode()">
+ <md-icon aria-label="{{ 'action.close' | translate }}" class="material-icons">close</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.close' | translate }}
+ </md-tooltip>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-table-container ng-show="mode!='widget'">
+ <table md-table md-row-select multiple="" ng-model="selectedAttributes" md-progress="attributesDeferred.promise">
+ <thead md-head md-order="query.order" md-on-reorder="getDeviceAttributes">
+ <tr md-row>
+ <th md-column md-order-by="lastUpdateTs"><span>Last update time</span></th>
+ <th md-column md-order-by="key"><span>Key</span></th>
+ <th md-column>Value</th>
+ </tr>
+ </thead>
+ <tbody md-body>
+ <tr md-row md-select="attribute" md-select-id="key" md-auto-select ng-repeat="attribute in attributes.data">
+ <td md-cell>{{attribute.lastUpdateTs | date : 'yyyy-MM-dd HH:mm:ss'}}</td>
+ <td md-cell>{{attribute.key}}</td>
+ <td md-cell ng-click="editAttribute($event, attribute)">
+ <span>{{attribute.value}}</span>
+ <span ng-show="!attributeScope.clientSide"><ng-md-icon size="16" icon="edit"></ng-md-icon></span>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </md-table-container>
+ <md-table-pagination ng-show="mode!='widget'" md-limit="query.limit" md-limit-options="[5, 10, 15]"
+ md-page="query.page" md-total="{{attributes.count}}"
+ md-on-paginate="getDeviceAttributes" md-page-select>
+ </md-table-pagination>
+ <ul flex rn-carousel ng-if="mode==='widget'" class="widgets-carousel"
+ rn-carousel-index="widgetsCarousel.index"
+ rn-carousel-buffered
+ rn-carousel-transition="fadeAndSlide"
+ rn-swipe-disabled="true">
+ <li ng-repeat="widgets in widgetsList">
+ <tb-dashboard
+ device-alias-list="deviceAliases"
+ widgets="widgets"
+ columns="20"
+ is-edit="true"
+ is-mobile-disabled="true"
+ is-edit-action-enabled="false"
+ is-remove-action-enabled="false">
+ </tb-dashboard>
+ </li>
+ <span translate ng-if="widgetsLoaded &&
+ widgetsList.length === 0 &&
+ widgetsBundle"
+ layout-align="center center"
+ style="text-transform: uppercase; display: flex;"
+ class="md-headline tb-absolute-fill">widgets-bundle.empty</span>
+ <span translate ng-if="!widgetsBundle"
+ layout-align="center center"
+ style="text-transform: uppercase; display: flex;"
+ class="md-headline tb-absolute-fill">widget.select-widgets-bundle</span>
+ <div ng-show="widgetsList.length > 1"
+ style="position: absolute; left: 0; height: 100%;" layout="column" layout-align="center">
+ <md-button ng-show="widgetsCarousel.index > 0"
+ class="md-icon-button"
+ ng-click="prevWidget()">
+ <md-icon>navigate_before</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'attribute.prev-widget' | translate }}
+ </md-tooltip>
+ </md-button>
+ </div>
+ <div ng-show="widgetsList.length > 1"
+ style="position: absolute; right: 0; height: 100%;" layout="column" layout-align="center">
+ <md-button ng-show="widgetsCarousel.index < widgetsList.length-1"
+ class="md-icon-button"
+ ng-click="nextWidget()">
+ <md-icon>navigate_next</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'attribute.next-widget' | translate }}
+ </md-tooltip>
+ </md-button>
+ </div>
+ <div style="position: absolute; bottom: 0; width: 100%; font-size: 24px;" layout="row" layout-align="center">
+ <div rn-carousel-indicators
+ ng-if="widgetsList.length > 1"
+ slides="widgetsList"
+ rn-carousel-index="widgetsCarousel.index">
+ </div>
+ </div>
+ </ul>
+ </div>
+</md-content>
diff --git a/ui/src/app/device/attribute/edit-attribute-value.controller.js b/ui/src/app/device/attribute/edit-attribute-value.controller.js
new file mode 100644
index 0000000..62022fb
--- /dev/null
+++ b/ui/src/app/device/attribute/edit-attribute-value.controller.js
@@ -0,0 +1,71 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*@ngInject*/
+export default function EditAttributeValueController($scope, $q, $element, types, attributeValue, save) {
+
+ $scope.valueTypes = types.valueType;
+
+ $scope.model = {};
+
+ $scope.model.value = attributeValue;
+
+ if ($scope.model.value === true || $scope.model.value === false) {
+ $scope.valueType = types.valueType.boolean;
+ } else if (angular.isNumber($scope.model.value)) {
+ if ($scope.model.value.toString().indexOf('.') == -1) {
+ $scope.valueType = types.valueType.integer;
+ } else {
+ $scope.valueType = types.valueType.double;
+ }
+ } else {
+ $scope.valueType = types.valueType.string;
+ }
+
+ $scope.submit = submit;
+ $scope.dismiss = dismiss;
+
+ function dismiss() {
+ $element.remove();
+ }
+
+ function update() {
+ if($scope.editDialog.$invalid) {
+ return $q.reject();
+ }
+
+ if(angular.isFunction(save)) {
+ return $q.when(save($scope.model));
+ }
+
+ return $q.resolve();
+ }
+
+ function submit() {
+ update().then(function () {
+ $scope.dismiss();
+ });
+ }
+
+ $scope.$watch('valueType', function(newVal, prevVal) {
+ if (newVal != prevVal) {
+ if ($scope.valueType === types.valueType.boolean) {
+ $scope.model.value = false;
+ } else {
+ $scope.model.value = null;
+ }
+ }
+ });
+}
diff --git a/ui/src/app/device/attribute/edit-attribute-value.tpl.html b/ui/src/app/device/attribute/edit-attribute-value.tpl.html
new file mode 100644
index 0000000..bf1c6e1
--- /dev/null
+++ b/ui/src/app/device/attribute/edit-attribute-value.tpl.html
@@ -0,0 +1,72 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-edit-dialog>
+ <form name="editDialog" ng-submit="submit()">
+ <div layout="column" class="md-content" style="width: 400px;">
+ <fieldset>
+ <section layout="row">
+ <md-input-container flex="40" class="md-block">
+ <label translate>value.type</label>
+ <md-select ng-model="valueType">
+ <md-option ng-repeat="type in valueTypes" ng-value="type">
+ <md-icon md-svg-icon="{{ type.icon }}"></md-icon>
+ <span>{{type.name | translate}}</span>
+ </md-option>
+ </md-select>
+ </md-input-container>
+ <md-input-container ng-if="valueType===valueTypes.string" flex="60" class="md-block">
+ <label translate>value.string-value</label>
+ <input required name="value" ng-model="model.value">
+ <div ng-messages="editDialog.value.$error">
+ <div translate ng-message="required">attribute.value-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container ng-if="valueType===valueTypes.integer" flex="60" class="md-block">
+ <label translate>value.integer-value</label>
+ <input required name="value" type="number" step="1" ng-pattern="/^-?[0-9]+$/" ng-model="model.value">
+ <div ng-messages="editDialog.value.$error">
+ <div translate ng-message="required">attribute.value-required</div>
+ <div translate ng-message="pattern">value.invalid-integer-value</div>
+ </div>
+ </md-input-container>
+ <md-input-container ng-if="valueType===valueTypes.double" flex="60" class="md-block">
+ <label translate>value.double-value</label>
+ <input required name="value" type="number" step="any" ng-model="model.value">
+ <div ng-messages="editDialog.value.$error">
+ <div translate ng-message="required">attribute.value-required</div>
+ </div>
+ </md-input-container>
+ <div layout="column" layout-align="center" flex="60" ng-if="valueType===valueTypes.boolean">
+ <md-checkbox ng-model="model.value" style="margin-bottom: 0px;">
+ {{ (model.value ? 'value.true' : 'value.false') | translate }}
+ </md-checkbox>
+ </div>
+ </section>
+ </fieldset>
+ </div>
+ <div layout="row" layout-align="end" class="md-actions">
+ <md-button ng-click="dismiss()">{{ 'action.cancel' |
+ translate }}
+ </md-button>
+ <md-button ng-disabled="editDialog.$invalid || !editDialog.$dirty" type="submit"
+ class="md-raised md-primary">
+ {{ 'action.update' | translate }}
+ </md-button>
+ </div>
+ </form>
+</md-edit-dialog>
\ No newline at end of file
ui/src/app/device/device.controller.js 429(+429 -0)
diff --git a/ui/src/app/device/device.controller.js b/ui/src/app/device/device.controller.js
new file mode 100644
index 0000000..e031aa0
--- /dev/null
+++ b/ui/src/app/device/device.controller.js
@@ -0,0 +1,429 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import addDeviceTemplate from './add-device.tpl.html';
+import deviceCard from './device-card.tpl.html';
+import assignToCustomerTemplate from './assign-to-customer.tpl.html';
+import addDevicesToCustomerTemplate from './add-devices-to-customer.tpl.html';
+import deviceCredentialsTemplate from './device-credentials.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function DeviceController(userService, deviceService, customerService, $scope, $controller, $state, $stateParams, $document, $mdDialog, $q, $translate, types) {
+
+ var customerId = $stateParams.customerId;
+
+ var deviceActionsList = [];
+
+ var deviceGroupActionsList = [];
+
+ var vm = this;
+
+ vm.types = types;
+
+ vm.deviceGridConfig = {
+ deleteItemTitleFunc: deleteDeviceTitle,
+ deleteItemContentFunc: deleteDeviceText,
+ deleteItemsTitleFunc: deleteDevicesTitle,
+ deleteItemsActionTitleFunc: deleteDevicesActionTitle,
+ deleteItemsContentFunc: deleteDevicesText,
+
+ saveItemFunc: saveDevice,
+
+ getItemTitleFunc: getDeviceTitle,
+
+ itemCardTemplateUrl: deviceCard,
+
+ actionsList: deviceActionsList,
+ groupActionsList: deviceGroupActionsList,
+
+ onGridInited: gridInited,
+
+ addItemTemplateUrl: addDeviceTemplate,
+
+ addItemText: function() { return $translate.instant('device.add-device-text') },
+ noItemsText: function() { return $translate.instant('device.no-devices-text') },
+ itemDetailsText: function() { return $translate.instant('device.device-details') },
+ isDetailsReadOnly: isCustomerUser,
+ isSelectionEnabled: function () {
+ return !isCustomerUser();
+ }
+ };
+
+ if (angular.isDefined($stateParams.items) && $stateParams.items !== null) {
+ vm.deviceGridConfig.items = $stateParams.items;
+ }
+
+ if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) {
+ vm.deviceGridConfig.topIndex = $stateParams.topIndex;
+ }
+
+ vm.devicesScope = $state.$current.data.devicesType;
+
+ vm.assignToCustomer = assignToCustomer;
+ vm.unassignFromCustomer = unassignFromCustomer;
+ vm.manageCredentials = manageCredentials;
+
+ initController();
+
+ function initController() {
+ var fetchDevicesFunction = null;
+ var deleteDeviceFunction = null;
+ var refreshDevicesParamsFunction = null;
+
+ var user = userService.getCurrentUser();
+
+ if (user.authority === 'CUSTOMER_USER') {
+ vm.devicesScope = 'customer_user';
+ customerId = user.customerId;
+ }
+
+ if (vm.devicesScope === 'tenant') {
+ fetchDevicesFunction = function (pageLink) {
+ return deviceService.getTenantDevices(pageLink);
+ };
+ deleteDeviceFunction = function (deviceId) {
+ return deviceService.deleteDevice(deviceId);
+ };
+ refreshDevicesParamsFunction = function() {
+ return {"topIndex": vm.topIndex};
+ };
+
+ deviceActionsList.push(
+ {
+ onAction: function ($event, item) {
+ assignToCustomer($event, [ item.id.id ]);
+ },
+ name: function() { return $translate.instant('action.assign') },
+ details: function() { return $translate.instant('device.assign-to-customer') },
+ icon: "assignment_ind",
+ isEnabled: function(device) {
+ return device && (!device.customerId || device.customerId.id === types.id.nullUid);
+ }
+ }
+ );
+
+ deviceActionsList.push(
+ {
+ onAction: function ($event, item) {
+ unassignFromCustomer($event, item);
+ },
+ name: function() { return $translate.instant('action.unassign') },
+ details: function() { return $translate.instant('device.unassign-from-customer') },
+ icon: "assignment_return",
+ isEnabled: function(device) {
+ return device && device.customerId && device.customerId.id !== types.id.nullUid;
+ }
+ }
+ );
+
+ deviceActionsList.push(
+ {
+ onAction: function ($event, item) {
+ manageCredentials($event, item);
+ },
+ name: function() { return $translate.instant('device.credentials') },
+ details: function() { return $translate.instant('device.manage-credentials') },
+ icon: "security"
+ }
+ );
+
+ deviceActionsList.push(
+ {
+ onAction: function ($event, item) {
+ vm.grid.deleteItem($event, item);
+ },
+ name: function() { return $translate.instant('action.delete') },
+ details: function() { return $translate.instant('device.delete') },
+ icon: "delete"
+ }
+ );
+
+ deviceGroupActionsList.push(
+ {
+ onAction: function ($event, items) {
+ assignDevicesToCustomer($event, items);
+ },
+ name: function() { return $translate.instant('device.assign-devices') },
+ details: function(selectedCount) {
+ return $translate.instant('device.assign-devices-text', {count: selectedCount}, "messageformat");
+ },
+ icon: "assignment_ind"
+ }
+ );
+
+ deviceGroupActionsList.push(
+ {
+ onAction: function ($event) {
+ vm.grid.deleteItems($event);
+ },
+ name: function() { return $translate.instant('device.delete-devices') },
+ details: deleteDevicesActionTitle,
+ icon: "delete"
+ }
+ );
+
+
+
+ } else if (vm.devicesScope === 'customer' || vm.devicesScope === 'customer_user') {
+ fetchDevicesFunction = function (pageLink) {
+ return deviceService.getCustomerDevices(customerId, pageLink);
+ };
+ deleteDeviceFunction = function (deviceId) {
+ return deviceService.unassignDeviceFromCustomer(deviceId);
+ };
+ refreshDevicesParamsFunction = function () {
+ return {"customerId": customerId, "topIndex": vm.topIndex};
+ };
+
+ if (vm.devicesScope === 'customer') {
+ deviceActionsList.push(
+ {
+ onAction: function ($event, item) {
+ unassignFromCustomer($event, item);
+ },
+ name: function() { return $translate.instant('action.unassign') },
+ details: function() { return $translate.instant('device.unassign-from-customer') },
+ icon: "assignment_return"
+ }
+ );
+ deviceActionsList.push(
+ {
+ onAction: function ($event, item) {
+ manageCredentials($event, item);
+ },
+ name: function() { return $translate.instant('device.credentials') },
+ details: function() { return $translate.instant('device.manage-credentials') },
+ icon: "security"
+ }
+ );
+
+ deviceGroupActionsList.push(
+ {
+ onAction: function ($event, items) {
+ unassignDevicesFromCustomer($event, items);
+ },
+ name: function() { return $translate.instant('device.unassign-devices') },
+ details: function(selectedCount) {
+ return $translate.instant('device.unassign-devices-action-title', {count: selectedCount}, "messageformat");
+ },
+ icon: "assignment_return"
+ }
+ );
+
+ vm.deviceGridConfig.addItemAction = {
+ onAction: function ($event) {
+ addDevicesToCustomer($event);
+ },
+ name: function() { return $translate.instant('device.assign-devices') },
+ details: function() { return $translate.instant('device.assign-new-device') },
+ icon: "add"
+ };
+
+
+ } else if (vm.devicesScope === 'customer_user') {
+ deviceActionsList.push(
+ {
+ onAction: function ($event, item) {
+ manageCredentials($event, item);
+ },
+ name: function() { return $translate.instant('device.credentials') },
+ details: function() { return $translate.instant('device.view-credentials') },
+ icon: "security"
+ }
+ );
+
+ vm.deviceGridConfig.addItemAction = {};
+ }
+ }
+
+ vm.deviceGridConfig.refreshParamsFunc = refreshDevicesParamsFunction;
+ vm.deviceGridConfig.fetchItemsFunc = fetchDevicesFunction;
+ vm.deviceGridConfig.deleteItemFunc = deleteDeviceFunction;
+
+ }
+
+ function deleteDeviceTitle(device) {
+ return $translate.instant('device.delete-device-title', {deviceName: device.name});
+ }
+
+ function deleteDeviceText() {
+ return $translate.instant('device.delete-device-text');
+ }
+
+ function deleteDevicesTitle(selectedCount) {
+ return $translate.instant('device.delete-devices-title', {count: selectedCount}, 'messageformat');
+ }
+
+ function deleteDevicesActionTitle(selectedCount) {
+ return $translate.instant('device.delete-devices-action-title', {count: selectedCount}, 'messageformat');
+ }
+
+ function deleteDevicesText () {
+ return $translate.instant('device.delete-devices-text');
+ }
+
+ function gridInited(grid) {
+ vm.grid = grid;
+ }
+
+ function getDeviceTitle(device) {
+ return device ? device.name : '';
+ }
+
+ function saveDevice (device) {
+ return deviceService.saveDevice(device);
+ }
+
+ function isCustomerUser() {
+ return vm.devicesScope === 'customer_user';
+ }
+
+ function assignToCustomer($event, deviceIds) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ var pageSize = 10;
+ customerService.getCustomers({limit: pageSize, textSearch: ''}).then(
+ function success(_customers) {
+ var customers = {
+ pageSize: pageSize,
+ data: _customers.data,
+ nextPageLink: _customers.nextPageLink,
+ selection: null,
+ hasNext: _customers.hasNext,
+ pending: false
+ };
+ if (customers.hasNext) {
+ customers.nextPageLink.limit = pageSize;
+ }
+ $mdDialog.show({
+ controller: 'AssignDeviceToCustomerController',
+ controllerAs: 'vm',
+ templateUrl: assignToCustomerTemplate,
+ locals: {deviceIds: deviceIds, customers: customers},
+ parent: angular.element($document[0].body),
+ fullscreen: true,
+ targetEvent: $event
+ }).then(function () {
+ vm.grid.refreshList();
+ }, function () {
+ });
+ },
+ function fail() {
+ });
+ }
+
+ function addDevicesToCustomer($event) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ var pageSize = 10;
+ deviceService.getTenantDevices({limit: pageSize, textSearch: ''}).then(
+ function success(_devices) {
+ var devices = {
+ pageSize: pageSize,
+ data: _devices.data,
+ nextPageLink: _devices.nextPageLink,
+ selections: {},
+ selectedCount: 0,
+ hasNext: _devices.hasNext,
+ pending: false
+ };
+ if (devices.hasNext) {
+ devices.nextPageLink.limit = pageSize;
+ }
+ $mdDialog.show({
+ controller: 'AddDevicesToCustomerController',
+ controllerAs: 'vm',
+ templateUrl: addDevicesToCustomerTemplate,
+ locals: {customerId: customerId, devices: devices},
+ parent: angular.element($document[0].body),
+ fullscreen: true,
+ targetEvent: $event
+ }).then(function () {
+ vm.grid.refreshList();
+ }, function () {
+ });
+ },
+ function fail() {
+ });
+ }
+
+ function assignDevicesToCustomer($event, items) {
+ var deviceIds = [];
+ for (var id in items.selections) {
+ deviceIds.push(id);
+ }
+ assignToCustomer($event, deviceIds);
+ }
+
+ function unassignFromCustomer($event, device) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ var confirm = $mdDialog.confirm()
+ .targetEvent($event)
+ .title($translate.instant('device.unassign-device-title', {deviceName: device.name}))
+ .htmlContent($translate.instant('device.unassign-device-text'))
+ .ariaLabel($translate.instant('device.unassign-device'))
+ .cancel($translate.instant('action.no'))
+ .ok($translate.instant('action.yes'));
+ $mdDialog.show(confirm).then(function () {
+ deviceService.unassignDeviceFromCustomer(device.id.id).then(function success() {
+ vm.grid.refreshList();
+ });
+ });
+ }
+
+ function unassignDevicesFromCustomer($event, items) {
+ var confirm = $mdDialog.confirm()
+ .targetEvent($event)
+ .title($translate.instant('device.unassign-devices-title', {count: items.selectedCount}, 'messageformat'))
+ .htmlContent($translate.instant('device.unassign-devices-text'))
+ .ariaLabel($translate.instant('device.unassign-device'))
+ .cancel($translate.instant('action.no'))
+ .ok($translate.instant('action.yes'));
+ $mdDialog.show(confirm).then(function () {
+ var tasks = [];
+ for (var id in items.selections) {
+ tasks.push(deviceService.unassignDeviceFromCustomer(id));
+ }
+ $q.all(tasks).then(function () {
+ vm.grid.refreshList();
+ });
+ });
+ }
+
+ function manageCredentials($event, device) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ $mdDialog.show({
+ controller: 'ManageDeviceCredentialsController',
+ controllerAs: 'vm',
+ templateUrl: deviceCredentialsTemplate,
+ locals: {deviceId: device.id.id, isReadOnly: isCustomerUser()},
+ parent: angular.element($document[0].body),
+ fullscreen: true,
+ targetEvent: $event
+ }).then(function () {
+ }, function () {
+ });
+ }
+}
ui/src/app/device/device.directive.js 69(+69 -0)
diff --git a/ui/src/app/device/device.directive.js b/ui/src/app/device/device.directive.js
new file mode 100644
index 0000000..918040b
--- /dev/null
+++ b/ui/src/app/device/device.directive.js
@@ -0,0 +1,69 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import deviceFieldsetTemplate from './device-fieldset.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function DeviceDirective($compile, $templateCache, toast, $translate, types, customerService) {
+ var linker = function (scope, element) {
+ var template = $templateCache.get(deviceFieldsetTemplate);
+ element.html(template);
+
+ scope.isAssignedToCustomer = false;
+
+ scope.assignedCustomer = null;
+
+
+ scope.$watch('device', function(newVal) {
+ if (newVal) {
+ if (scope.device.customerId && scope.device.customerId.id !== types.id.nullUid) {
+ scope.isAssignedToCustomer = true;
+ customerService.getCustomer(scope.device.customerId.id).then(
+ function success(customer) {
+ scope.assignedCustomer = customer;
+ }
+ );
+ } else {
+ scope.isAssignedToCustomer = false;
+ scope.assignedCustomer = null;
+ }
+ }
+ });
+
+ scope.onDeviceIdCopied = function() {
+ toast.showSuccess($translate.instant('device.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
+ };
+
+ $compile(element.contents())(scope);
+ }
+ return {
+ restrict: "E",
+ link: linker,
+ scope: {
+ device: '=',
+ isEdit: '=',
+ deviceScope: '=',
+ theForm: '=',
+ onAssignToCustomer: '&',
+ onUnassignFromCustomer: '&',
+ onManageCredentials: '&',
+ onDeleteDevice: '&'
+ }
+ };
+}
ui/src/app/device/device.routes.js 68(+68 -0)
diff --git a/ui/src/app/device/device.routes.js b/ui/src/app/device/device.routes.js
new file mode 100644
index 0000000..6a82d9d
--- /dev/null
+++ b/ui/src/app/device/device.routes.js
@@ -0,0 +1,68 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import devicesTemplate from './devices.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function DeviceRoutes($stateProvider) {
+ $stateProvider
+ .state('home.devices', {
+ url: '/devices',
+ params: {'topIndex': 0},
+ module: 'private',
+ auth: ['TENANT_ADMIN', 'CUSTOMER_USER'],
+ views: {
+ "content@home": {
+ templateUrl: devicesTemplate,
+ controller: 'DeviceController',
+ controllerAs: 'vm'
+ }
+ },
+ data: {
+ devicesType: 'tenant',
+ searchEnabled: true,
+ pageTitle: 'device.devices'
+ },
+ ncyBreadcrumb: {
+ label: '{"icon": "devices_other", "label": "device.devices"}'
+ }
+ })
+ .state('home.customers.devices', {
+ url: '/:customerId/devices',
+ params: {'topIndex': 0},
+ module: 'private',
+ auth: ['TENANT_ADMIN'],
+ views: {
+ "content@home": {
+ templateUrl: devicesTemplate,
+ controllerAs: 'vm',
+ controller: 'DeviceController'
+ }
+ },
+ data: {
+ devicesType: 'customer',
+ searchEnabled: true,
+ pageTitle: 'customer.devices'
+ },
+ ncyBreadcrumb: {
+ label: '{"icon": "devices_other", "label": "customer.devices"}'
+ }
+ });
+
+}
ui/src/app/device/device-card.tpl.html 18(+18 -0)
diff --git a/ui/src/app/device/device-card.tpl.html b/ui/src/app/device/device-card.tpl.html
new file mode 100644
index 0000000..8151b2f
--- /dev/null
+++ b/ui/src/app/device/device-card.tpl.html
@@ -0,0 +1,18 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div></div>
\ No newline at end of file
diff --git a/ui/src/app/device/device-credentials.controller.js b/ui/src/app/device/device-credentials.controller.js
new file mode 100644
index 0000000..42b6696
--- /dev/null
+++ b/ui/src/app/device/device-credentials.controller.js
@@ -0,0 +1,64 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*@ngInject*/
+export default function ManageDeviceCredentialsController(deviceService, $scope, $mdDialog, deviceId, isReadOnly) {
+
+ var vm = this;
+
+ vm.credentialsTypes = [
+ {
+ name: 'Access token',
+ value: 'ACCESS_TOKEN'
+ },
+ {
+ name: 'X.509 Certificate (Coming soon)',
+ value: 'X509_CERTIFICATE'
+ }
+ ];
+
+ vm.deviceCredentials = {};
+ vm.isReadOnly = isReadOnly;
+
+ vm.valid = valid;
+ vm.cancel = cancel;
+ vm.save = save;
+
+ loadDeviceCredentials();
+
+ function loadDeviceCredentials() {
+ deviceService.getDeviceCredentials(deviceId).then(function success(deviceCredentials) {
+ vm.deviceCredentials = deviceCredentials;
+ });
+ }
+
+ function cancel() {
+ $mdDialog.cancel();
+ }
+
+ function valid() {
+ return vm.deviceCredentials &&
+ vm.deviceCredentials.credentialsType === 'ACCESS_TOKEN' &&
+ vm.deviceCredentials.credentialsId && vm.deviceCredentials.credentialsId.length > 0;
+ }
+
+ function save() {
+ deviceService.saveDeviceCredentials(vm.deviceCredentials).then(function success(deviceCredentials) {
+ vm.deviceCredentials = deviceCredentials;
+ $scope.theForm.$setPristine();
+ $mdDialog.hide();
+ });
+ }
+}
diff --git a/ui/src/app/device/device-credentials.tpl.html b/ui/src/app/device/device-credentials.tpl.html
new file mode 100644
index 0000000..5bfc2c0
--- /dev/null
+++ b/ui/src/app/device/device-credentials.tpl.html
@@ -0,0 +1,62 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'device.device-credentials' | translate }}">
+ <form name="theForm" ng-submit="vm.save()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>device.device-credentials</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <fieldset ng-disabled="loading || vm.isReadOnly">
+ <md-input-container class="md-block">
+ <label translate>device.credentials-type</label>
+ <md-select ng-disabled="loading || vm.isReadOnly" ng-model="vm.deviceCredentials.credentialsType">
+ <md-option ng-repeat="credentialsType in vm.credentialsTypes" value="{{credentialsType.value}}">
+ {{credentialsType.name}}
+ </md-option>
+ </md-select>
+ </md-input-container>
+ <md-input-container class="md-block" ng-if="vm.deviceCredentials.credentialsType === 'ACCESS_TOKEN'">
+ <label translate>device.access-token</label>
+ <input required name="accessToken" ng-model="vm.deviceCredentials.credentialsId"
+ md-maxlength="20" ng-pattern="/^.{1,20}$/">
+ <div ng-messages="theForm.accessToken.$error">
+ <div translate ng-message="required">device.access-token-required</div>
+ <div translate ng-message="pattern">device.access-token-invalid</div>
+ </div>
+ </md-input-container>
+ </fieldset>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-if="!vm.isReadOnly" ng-disabled="loading || theForm.$invalid || !theForm.$dirty || !vm.valid()" type="submit" class="md-raised md-primary">
+ {{ 'action.save' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ (vm.isReadOnly ? 'action.close' : 'action.cancel') | translate }}</md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
\ No newline at end of file
ui/src/app/device/device-fieldset.tpl.html 60(+60 -0)
diff --git a/ui/src/app/device/device-fieldset.tpl.html b/ui/src/app/device/device-fieldset.tpl.html
new file mode 100644
index 0000000..6d9892e
--- /dev/null
+++ b/ui/src/app/device/device-fieldset.tpl.html
@@ -0,0 +1,60 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-button ng-click="onAssignToCustomer({event: $event})"
+ ng-show="!isEdit && deviceScope === 'tenant' && !isAssignedToCustomer"
+ class="md-raised md-primary">{{ 'device.assign-to-customer' | translate }}</md-button>
+<md-button ng-click="onUnassignFromCustomer({event: $event})"
+ ng-show="!isEdit && (deviceScope === 'customer' || deviceScope === 'tenant') && isAssignedToCustomer"
+ class="md-raised md-primary">{{ 'device.unassign-from-customer' | translate }}</md-button>
+<md-button ng-click="onManageCredentials({event: $event})"
+ ng-show="!isEdit"
+ class="md-raised md-primary">{{ (deviceScope === 'customer_user' ? 'device.view-credentials' : 'device.manage-credentials') | translate }}</md-button>
+<md-button ng-click="onDeleteDevice({event: $event})"
+ ng-show="!isEdit && deviceScope === 'tenant'"
+ class="md-raised md-primary">{{ 'device.delete' | translate }}</md-button>
+
+<div layout="row">
+ <md-button ngclipboard data-clipboard-action="copy"
+ ngclipboard-success="onDeviceIdCopied(e)"
+ data-clipboard-text="{{device.id.id}}" ng-show="!isEdit"
+ class="md-raised">
+ <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
+ <span translate>device.copyId</span>
+ </md-button>
+</div>
+
+<md-content class="md-padding" layout="column">
+ <md-input-container class="md-block"
+ ng-show="isAssignedToCustomer && deviceScope === 'tenant'">
+ <label translate>device.assignedToCustomer</label>
+ <input ng-model="assignedCustomer.title" disabled>
+ </md-input-container>
+ <fieldset ng-disabled="loading || !isEdit">
+ <md-input-container class="md-block">
+ <label translate>device.name</label>
+ <input required name="name" ng-model="device.name">
+ <div ng-messages="theForm.name.$error">
+ <div translate ng-message="required">device.name-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container class="md-block">
+ <label translate>device.description</label>
+ <textarea ng-model="device.additionalInfo.description" rows="2"></textarea>
+ </md-input-container>
+ </fieldset>
+</md-content>
ui/src/app/device/devices.tpl.html 55(+55 -0)
diff --git a/ui/src/app/device/devices.tpl.html b/ui/src/app/device/devices.tpl.html
new file mode 100644
index 0000000..8aed648
--- /dev/null
+++ b/ui/src/app/device/devices.tpl.html
@@ -0,0 +1,55 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<tb-grid grid-configuration="vm.deviceGridConfig">
+ <details-buttons tb-help="'devices'" help-container-id="help-container">
+ <div id="help-container"></div>
+ </details-buttons>
+ <md-tabs ng-class="{'tb-headless': vm.grid.detailsConfig.isDetailsEditMode}"
+ id="tabs" md-border-bottom flex class="tb-absolute-fill">
+ <md-tab label="{{ 'device.details' | translate }}">
+ <tb-device device="vm.grid.operatingItem()"
+ is-edit="vm.grid.detailsConfig.isDetailsEditMode"
+ device-scope="vm.devicesScope"
+ the-form="vm.grid.detailsForm"
+ on-assign-to-customer="vm.assignToCustomer(event, [ vm.grid.detailsConfig.currentItem.id.id ])"
+ on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem)"
+ on-manage-credentials="vm.manageCredentials(event, vm.grid.detailsConfig.currentItem)"
+ on-delete-device="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-device>
+ </md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.attributes' | translate }}">
+ <tb-attribute-table flex
+ device-id="vm.grid.operatingItem().id.id"
+ device-name="vm.grid.operatingItem().name"
+ default-attribute-scope="{{vm.types.deviceAttributesScope.client.value}}">
+ </tb-attribute-table>
+ </md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.latest-telemetry' | translate }}">
+ <tb-attribute-table flex
+ device-id="vm.grid.operatingItem().id.id"
+ default-attribute-scope="{{vm.types.latestTelemetry.value}}"
+ disable-attribute-scope-selection="true">
+ </tb-attribute-table>
+ </md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'device.events' | translate }}">
+ <tb-event-table flex entity-type="vm.types.entityType.device"
+ entity-id="vm.grid.operatingItem().id.id"
+ tenant-id="vm.grid.operatingItem().tenantId.id"
+ default-event-type="{{vm.types.eventType.alarm.value}}">
+ </tb-event-table>
+ </md-tab>
+</tb-grid>
ui/src/app/device/index.js 52(+52 -0)
diff --git a/ui/src/app/device/index.js b/ui/src/app/device/index.js
new file mode 100644
index 0000000..43f290e
--- /dev/null
+++ b/ui/src/app/device/index.js
@@ -0,0 +1,52 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import uiRouter from 'angular-ui-router';
+import thingsboardGrid from '../components/grid.directive';
+import thingsboardEvent from '../event';
+import thingsboardDashboardSelect from '../components/dashboard-select.directive';
+import thingsboardApiUser from '../api/user.service';
+import thingsboardApiDevice from '../api/device.service';
+import thingsboardApiCustomer from '../api/customer.service';
+
+import DeviceRoutes from './device.routes';
+import DeviceController from './device.controller';
+import AssignDeviceToCustomerController from './assign-to-customer.controller';
+import AddDevicesToCustomerController from './add-devices-to-customer.controller';
+import ManageDeviceCredentialsController from './device-credentials.controller';
+import AddAttributeDialogController from './attribute/add-attribute-dialog.controller';
+import AddWidgetToDashboardDialogController from './attribute/add-widget-to-dashboard-dialog.controller';
+import DeviceDirective from './device.directive';
+import AttributeTableDirective from './attribute/attribute-table.directive';
+
+export default angular.module('thingsboard.device', [
+ uiRouter,
+ thingsboardGrid,
+ thingsboardEvent,
+ thingsboardDashboardSelect,
+ thingsboardApiUser,
+ thingsboardApiDevice,
+ thingsboardApiCustomer
+])
+ .config(DeviceRoutes)
+ .controller('DeviceController', DeviceController)
+ .controller('AssignDeviceToCustomerController', AssignDeviceToCustomerController)
+ .controller('AddDevicesToCustomerController', AddDevicesToCustomerController)
+ .controller('ManageDeviceCredentialsController', ManageDeviceCredentialsController)
+ .controller('AddAttributeDialogController', AddAttributeDialogController)
+ .controller('AddWidgetToDashboardDialogController', AddWidgetToDashboardDialogController)
+ .directive('tbDevice', DeviceDirective)
+ .directive('tbAttributeTable', AttributeTableDirective)
+ .name;
ui/src/app/event/event.scss 72(+72 -0)
diff --git a/ui/src/app/event/event.scss b/ui/src/app/event/event.scss
new file mode 100644
index 0000000..a43ea3e
--- /dev/null
+++ b/ui/src/app/event/event.scss
@@ -0,0 +1,72 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+md-list.tb-table {
+ padding: 0px;
+
+ md-list-item {
+ padding: 0px;
+ }
+
+ .tb-row {
+ height: 48px;
+ padding: 0px;
+ overflow: hidden;
+ }
+
+ .tb-row:hover {
+ background-color: #EEEEEE;
+ }
+
+ .tb-header:hover {
+ background: none;
+ }
+
+ .tb-header {
+ .tb-cell {
+ color: rgba(0,0,0,.54);
+ font-size: 12px;
+ font-weight: 700;
+ white-space: nowrap;
+ background: none;
+ }
+ }
+
+ .tb-cell {
+ padding: 0 24px;
+ margin: auto 0;
+ color: rgba(0,0,0,.87);
+ font-size: 13px;
+ vertical-align: middle;
+ text-align: left;
+ overflow: hidden;
+ .md-button {
+ padding: 0;
+ margin: 0;
+ }
+ }
+
+ .tb-cell.tb-number {
+ text-align: right;
+ }
+
+}
+
+#tb-content {
+ min-width: 400px;
+ min-height: 50px;
+ width: 100%;
+ height: 100%;
+}
diff --git a/ui/src/app/event/event-content-dialog.controller.js b/ui/src/app/event/event-content-dialog.controller.js
new file mode 100644
index 0000000..80ea9c2
--- /dev/null
+++ b/ui/src/app/event/event-content-dialog.controller.js
@@ -0,0 +1,78 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import $ from 'jquery';
+import 'brace/ext/language_tools';
+import 'brace/mode/java';
+import 'brace/theme/github';
+
+/* eslint-disable angular/angularelement */
+
+/*@ngInject*/
+export default function EventContentDialogController($mdDialog, content, title, showingCallback) {
+
+ var vm = this;
+
+ showingCallback.onShowing = function(scope, element) {
+ updateEditorSize(element);
+ }
+
+ vm.content = content;
+ vm.title = title;
+
+ vm.contentOptions = {
+ useWrapMode: false,
+ mode: 'java',
+ showGutter: false,
+ showPrintMargin: false,
+ theme: 'github',
+ advanced: {
+ enableSnippets: false,
+ enableBasicAutocompletion: false,
+ enableLiveAutocompletion: false
+ },
+ onLoad: function (_ace) {
+ vm.editor = _ace;
+ }
+ };
+
+ function updateEditorSize(element) {
+ var newHeight = 400;
+ var newWidth = 600;
+ if (vm.content && vm.content.length > 0) {
+ var lines = vm.content.split('\n');
+ newHeight = 16 * lines.length + 16;
+ var maxLineLength = 0;
+ for (var i in lines) {
+ var line = lines[i].replace(/\t/g, ' ').replace(/\n/g, '');
+ var lineLength = line.length;
+ maxLineLength = Math.max(maxLineLength, lineLength);
+ }
+ newWidth = 8 * maxLineLength + 16;
+ }
+ $('#tb-content', element).height(newHeight.toString() + "px")
+ .width(newWidth.toString() + "px");
+ vm.editor.resize();
+ }
+
+ vm.close = close;
+
+ function close () {
+ $mdDialog.hide();
+ }
+
+}
+
+/* eslint-enable angular/angularelement */
diff --git a/ui/src/app/event/event-content-dialog.tpl.html b/ui/src/app/event/event-content-dialog.tpl.html
new file mode 100644
index 0000000..52d66a6
--- /dev/null
+++ b/ui/src/app/event/event-content-dialog.tpl.html
@@ -0,0 +1,42 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog aria-label="{{ vm.title | translate }}">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>{{ vm.title }}</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.close()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <div flex id="tb-content" readonly
+ ui-ace="vm.contentOptions"
+ ng-model="vm.content">
+ </div>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading" ng-click="vm.close()" style="margin-right:20px;">{{ 'action.close' |
+ translate }}
+ </md-button>
+ </md-dialog-actions>
+</md-dialog>
ui/src/app/event/event-header.directive.js 66(+66 -0)
diff --git a/ui/src/app/event/event-header.directive.js b/ui/src/app/event/event-header.directive.js
new file mode 100644
index 0000000..e776980
--- /dev/null
+++ b/ui/src/app/event/event-header.directive.js
@@ -0,0 +1,66 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import eventHeaderLcEventTemplate from './event-header-lc-event.tpl.html';
+import eventHeaderStatsTemplate from './event-header-stats.tpl.html';
+import eventHeaderErrorTemplate from './event-header-error.tpl.html';
+import eventHeaderAlarmTemplate from './event-header-alarm.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function EventHeaderDirective($compile, $templateCache, types) {
+
+ var linker = function (scope, element, attrs) {
+
+ var getTemplate = function(eventType) {
+ var template = '';
+ switch(eventType) {
+ case types.eventType.lcEvent.value:
+ template = eventHeaderLcEventTemplate;
+ break;
+ case types.eventType.stats.value:
+ template = eventHeaderStatsTemplate;
+ break;
+ case types.eventType.error.value:
+ template = eventHeaderErrorTemplate;
+ break;
+ case types.eventType.alarm.value:
+ template = eventHeaderAlarmTemplate;
+ break;
+ }
+ return $templateCache.get(template);
+ }
+
+ scope.loadTemplate = function() {
+ element.html(getTemplate(attrs.eventType));
+ $compile(element.contents())(scope);
+ }
+
+ attrs.$observe('eventType', function() {
+ scope.loadTemplate();
+ });
+
+ }
+
+ return {
+ restrict: "A",
+ replace: false,
+ link: linker,
+ scope: false
+ };
+}
ui/src/app/event/event-header-alarm.tpl.html 20(+20 -0)
diff --git a/ui/src/app/event/event-header-alarm.tpl.html b/ui/src/app/event/event-header-alarm.tpl.html
new file mode 100644
index 0000000..761c741
--- /dev/null
+++ b/ui/src/app/event/event-header-alarm.tpl.html
@@ -0,0 +1,20 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div translate class="tb-cell" flex="30">event.event-time</div>
+<div translate class="tb-cell" flex="20">event.server</div>
+<div translate class="tb-cell" flex="20">event.alarm</div>
ui/src/app/event/event-header-error.tpl.html 21(+21 -0)
diff --git a/ui/src/app/event/event-header-error.tpl.html b/ui/src/app/event/event-header-error.tpl.html
new file mode 100644
index 0000000..38c832d
--- /dev/null
+++ b/ui/src/app/event/event-header-error.tpl.html
@@ -0,0 +1,21 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div translate class="tb-cell" flex="30">event.event-time</div>
+<div translate class="tb-cell" flex="20">event.server</div>
+<div translate class="tb-cell" flex="20">event.method</div>
+<div translate class="tb-cell" flex="20">event.error</div>
diff --git a/ui/src/app/event/event-header-lc-event.tpl.html b/ui/src/app/event/event-header-lc-event.tpl.html
new file mode 100644
index 0000000..a67bcfe
--- /dev/null
+++ b/ui/src/app/event/event-header-lc-event.tpl.html
@@ -0,0 +1,22 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div translate class="tb-cell" flex="30">event.event-time</div>
+<div translate class="tb-cell" flex="20">event.server</div>
+<div translate class="tb-cell" flex="20">event.event</div>
+<div translate class="tb-cell" flex="20">event.status</div>
+<div translate class="tb-cell" flex="20">event.error</div>
ui/src/app/event/event-header-stats.tpl.html 21(+21 -0)
diff --git a/ui/src/app/event/event-header-stats.tpl.html b/ui/src/app/event/event-header-stats.tpl.html
new file mode 100644
index 0000000..cac3c78
--- /dev/null
+++ b/ui/src/app/event/event-header-stats.tpl.html
@@ -0,0 +1,21 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div translate class="tb-cell" flex>event.event-time</div>
+<div translate class="tb-cell" flex>event.server</div>
+<div translate class="tb-cell tb-number" flex>event.messages-processed</div>
+<div translate class="tb-cell tb-number" flex>event.errors-occurred</div>
ui/src/app/event/event-row.directive.js 90(+90 -0)
diff --git a/ui/src/app/event/event-row.directive.js b/ui/src/app/event/event-row.directive.js
new file mode 100644
index 0000000..b995905
--- /dev/null
+++ b/ui/src/app/event/event-row.directive.js
@@ -0,0 +1,90 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import eventErrorDialogTemplate from './event-content-dialog.tpl.html';
+
+import eventRowLcEventTemplate from './event-row-lc-event.tpl.html';
+import eventRowStatsTemplate from './event-row-stats.tpl.html';
+import eventRowErrorTemplate from './event-row-error.tpl.html';
+import eventRowAlarmTemplate from './event-row-alarm.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function EventRowDirective($compile, $templateCache, $mdDialog, $document, types) {
+
+ var linker = function (scope, element, attrs) {
+
+ var getTemplate = function(eventType) {
+ var template = '';
+ switch(eventType) {
+ case types.eventType.lcEvent.value:
+ template = eventRowLcEventTemplate;
+ break;
+ case types.eventType.stats.value:
+ template = eventRowStatsTemplate;
+ break;
+ case types.eventType.error.value:
+ template = eventRowErrorTemplate;
+ break;
+ case types.eventType.alarm.value:
+ template = eventRowAlarmTemplate;
+ break;
+ }
+ return $templateCache.get(template);
+ }
+
+ scope.loadTemplate = function() {
+ element.html(getTemplate(attrs.eventType));
+ $compile(element.contents())(scope);
+ }
+
+ attrs.$observe('eventType', function() {
+ scope.loadTemplate();
+ });
+
+ scope.event = attrs.event;
+
+ scope.showContent = function($event, content, title) {
+ var onShowingCallback = {
+ onShowing: function(){}
+ }
+ $mdDialog.show({
+ controller: 'EventContentDialogController',
+ controllerAs: 'vm',
+ templateUrl: eventErrorDialogTemplate,
+ locals: {content: content, title: title, showingCallback: onShowingCallback},
+ parent: angular.element($document[0].body),
+ fullscreen: true,
+ targetEvent: $event,
+ skipHide: true,
+ onShowing: function(scope, element) {
+ onShowingCallback.onShowing(scope, element);
+ }
+ });
+ }
+
+ $compile(element.contents())(scope);
+ }
+
+ return {
+ restrict: "A",
+ replace: false,
+ link: linker,
+ scope: false
+ };
+}
ui/src/app/event/event-row-alarm.tpl.html 32(+32 -0)
diff --git a/ui/src/app/event/event-row-alarm.tpl.html b/ui/src/app/event/event-row-alarm.tpl.html
new file mode 100644
index 0000000..c01f74a
--- /dev/null
+++ b/ui/src/app/event/event-row-alarm.tpl.html
@@ -0,0 +1,32 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div class="tb-cell" flex="30">{{event.createdTime | date : 'yyyy-MM-dd HH:mm:ss'}}</div>
+<div class="tb-cell" flex="20">{{event.body.server}}</div>
+<div class="tb-cell" flex="20">
+ <md-button ng-if="event.body.body" class="md-icon-button md-primary"
+ ng-click="showContent($event, event.body.body, 'event.alarm')"
+ aria-label="{{ 'action.view' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'action.view' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.view' | translate }}"
+ class="material-icons">
+ more_horiz
+ </md-icon>
+ </md-button>
+</div>
ui/src/app/event/event-row-error.tpl.html 33(+33 -0)
diff --git a/ui/src/app/event/event-row-error.tpl.html b/ui/src/app/event/event-row-error.tpl.html
new file mode 100644
index 0000000..8a35f4a
--- /dev/null
+++ b/ui/src/app/event/event-row-error.tpl.html
@@ -0,0 +1,33 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div class="tb-cell" flex="30">{{event.createdTime | date : 'yyyy-MM-dd HH:mm:ss'}}</div>
+<div class="tb-cell" flex="20">{{event.body.server}}</div>
+<div class="tb-cell" flex="20">{{event.body.method}}</div>
+<div class="tb-cell" flex="20">
+ <md-button ng-if="event.body.error" class="md-icon-button md-primary"
+ ng-click="showContent($event, event.body.error, 'event.error')"
+ aria-label="{{ 'action.view' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'action.view' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.view' | translate }}"
+ class="material-icons">
+ more_horiz
+ </md-icon>
+ </md-button>
+</div>
ui/src/app/event/event-row-lc-event.tpl.html 34(+34 -0)
diff --git a/ui/src/app/event/event-row-lc-event.tpl.html b/ui/src/app/event/event-row-lc-event.tpl.html
new file mode 100644
index 0000000..2f21462
--- /dev/null
+++ b/ui/src/app/event/event-row-lc-event.tpl.html
@@ -0,0 +1,34 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div class="tb-cell" flex="30">{{event.createdTime | date : 'yyyy-MM-dd HH:mm:ss'}}</div>
+<div class="tb-cell" flex="20">{{event.body.server}}</div>
+<div class="tb-cell" flex="20">{{event.body.event}}</div>
+<div translate class="tb-cell" flex="20">{{event.body.success ? 'event.success' : 'event.failed'}}</div>
+<div class="tb-cell" flex="20">
+ <md-button ng-if="event.body.error" class="md-icon-button md-primary"
+ ng-click="showContent($event, event.body.error, 'event.error')"
+ aria-label="{{ 'action.view' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'action.view' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.view' | translate }}"
+ class="material-icons">
+ more_horiz
+ </md-icon>
+ </md-button>
+</div>
ui/src/app/event/event-row-stats.tpl.html 21(+21 -0)
diff --git a/ui/src/app/event/event-row-stats.tpl.html b/ui/src/app/event/event-row-stats.tpl.html
new file mode 100644
index 0000000..a90196b
--- /dev/null
+++ b/ui/src/app/event/event-row-stats.tpl.html
@@ -0,0 +1,21 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div class="tb-cell" flex>{{event.createdTime | date : 'yyyy-MM-dd HH:mm:ss'}}</div>
+<div class="tb-cell" flex>{{event.body.server}}</div>
+<div class="tb-cell tb-number" flex>{{event.body.messagesProcessed}}</div>
+<div class="tb-cell tb-number" flex>{{event.body.errorsOccurred}}</div>
ui/src/app/event/event-table.directive.js 212(+212 -0)
diff --git a/ui/src/app/event/event-table.directive.js b/ui/src/app/event/event-table.directive.js
new file mode 100644
index 0000000..e351244
--- /dev/null
+++ b/ui/src/app/event/event-table.directive.js
@@ -0,0 +1,212 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './event.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import eventTableTemplate from './event-table.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function EventTableDirective($compile, $templateCache, $rootScope, types, eventService) {
+
+ var linker = function (scope, element, attrs) {
+
+ var template = $templateCache.get(eventTableTemplate);
+
+ element.html(template);
+
+ if (attrs.disabledEventTypes) {
+ var disabledEventTypes = attrs.disabledEventTypes.split(',');
+ scope.eventTypes = {};
+ for (var type in types.eventType) {
+ var eventType = types.eventType[type];
+ var enabled = true;
+ for (var disabledType in disabledEventTypes) {
+ if (eventType.value === disabledEventTypes[disabledType]) {
+ enabled = false;
+ break;
+ }
+ }
+ if (enabled) {
+ scope.eventTypes[type] = eventType;
+ }
+ }
+ } else {
+ scope.eventTypes = types.eventType;
+ }
+
+ scope.eventType = attrs.defaultEventType;
+
+ var pageSize = 20;
+ var startTime = 0;
+ var endTime = 0;
+
+ scope.timewindow = {
+ history: {
+ timewindowMs: 24 * 60 * 60 * 1000 // 1 day
+ }
+ }
+
+ scope.topIndex = 0;
+
+ scope.theEvents = {
+ getItemAtIndex: function (index) {
+ if (index > scope.events.data.length) {
+ scope.theEvents.fetchMoreItems_(index);
+ return null;
+ }
+ var item = scope.events.data[index];
+ if (item) {
+ item.indexNumber = index + 1;
+ }
+ return item;
+ },
+
+ getLength: function () {
+ if (scope.events.hasNext) {
+ return scope.events.data.length + scope.events.nextPageLink.limit;
+ } else {
+ return scope.events.data.length;
+ }
+ },
+
+ fetchMoreItems_: function () {
+ if (scope.events.hasNext && !scope.events.pending) {
+ if (scope.entityType && scope.entityId && scope.eventType && scope.tenantId) {
+ var promise = eventService.getEvents(scope.entityType, scope.entityId,
+ scope.eventType, scope.tenantId, scope.events.nextPageLink);
+ if (promise) {
+ scope.events.pending = true;
+ promise.then(
+ function success(events) {
+ scope.events.data = scope.events.data.concat(events.data);
+ scope.events.nextPageLink = events.nextPageLink;
+ scope.events.hasNext = events.hasNext;
+ if (scope.events.hasNext) {
+ scope.events.nextPageLink.limit = pageSize;
+ }
+ scope.events.pending = false;
+ },
+ function fail() {
+ scope.events.hasNext = false;
+ scope.events.pending = false;
+ });
+ } else {
+ scope.events.hasNext = false;
+ }
+ } else {
+ scope.events.hasNext = false;
+ }
+ }
+ }
+ };
+
+ scope.$watch("entityId", function(newVal, prevVal) {
+ if (newVal && !angular.equals(newVal, prevVal)) {
+ scope.resetFilter();
+ scope.reload();
+ }
+ });
+
+ scope.$watch("eventType", function(newVal, prevVal) {
+ if (newVal && !angular.equals(newVal, prevVal)) {
+ scope.reload();
+ }
+ });
+
+ scope.$watch("timewindow", function(newVal, prevVal) {
+ if (newVal && !angular.equals(newVal, prevVal)) {
+ scope.reload();
+ }
+ }, true);
+
+ scope.resetFilter = function() {
+ scope.timewindow = {
+ history: {
+ timewindowMs: 24 * 60 * 60 * 1000 // 1 day
+ }
+ };
+ scope.eventType = attrs.defaultEventType;
+ }
+
+ scope.updateTimeWindowRange = function() {
+ if (scope.timewindow.history.timewindowMs) {
+ var currentTime = (new Date).getTime();
+ startTime = currentTime - scope.timewindow.history.timewindowMs;
+ endTime = currentTime;
+ } else {
+ startTime = scope.timewindow.history.fixedTimewindow.startTimeMs;
+ endTime = scope.timewindow.history.fixedTimewindow.endTimeMs;
+ }
+ }
+
+ scope.reload = function() {
+ scope.topIndex = 0;
+ scope.selected = [];
+ scope.updateTimeWindowRange();
+ scope.events = {
+ data: [],
+ nextPageLink: {
+ limit: pageSize,
+ startTime: startTime,
+ endTime: endTime
+ },
+ hasNext: true,
+ pending: false
+ };
+ scope.theEvents.getItemAtIndex(pageSize);
+ }
+
+ scope.noData = function() {
+ return scope.events.data.length == 0 && !scope.events.hasNext;
+ }
+
+ scope.hasData = function() {
+ return scope.events.data.length > 0;
+ }
+
+ scope.loading = function() {
+ return $rootScope.loading;
+ }
+
+ scope.hasScroll = function() {
+ var repeatContainer = scope.repeatContainer[0];
+ if (repeatContainer) {
+ var scrollElement = repeatContainer.children[0];
+ if (scrollElement) {
+ return scrollElement.scrollHeight > scrollElement.clientHeight;
+ }
+ }
+ return false;
+ }
+
+ scope.reload();
+
+ $compile(element.contents())(scope);
+ }
+
+ return {
+ restrict: "E",
+ link: linker,
+ scope: {
+ entityType: '=',
+ entityId: '=',
+ tenantId: '='
+ }
+ };
+}
ui/src/app/event/event-table.tpl.html 47(+47 -0)
diff --git a/ui/src/app/event/event-table.tpl.html b/ui/src/app/event/event-table.tpl.html
new file mode 100644
index 0000000..7681f42
--- /dev/null
+++ b/ui/src/app/event/event-table.tpl.html
@@ -0,0 +1,47 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-content flex class="md-padding tb-absolute-fill" layout="column">
+ <section layout="row">
+ <md-input-container class="md-block" style="width: 200px;">
+ <label translate>event.event-type</label>
+ <md-select ng-model="eventType" ng-disabled="loading()">
+ <md-option ng-repeat="type in eventTypes" ng-value="type.value">
+ {{type.name | translate}}
+ </md-option>
+ </md-select>
+ </md-input-container>
+ <tb-timewindow flex ng-model="timewindow" history-only as-button="true"></tb-timewindow>
+ </section>
+ <md-list flex layout="column" class="md-whiteframe-z1 tb-table">
+ <md-list class="tb-row tb-header" layout="row" tb-event-header event-type="{{eventType}}">
+ </md-list>
+ <md-progress-linear style="max-height: 0px;" md-mode="indeterminate"
+ ng-show="loading()"></md-progress-linear>
+ <md-divider></md-divider>
+ <span translate layout-align="center center"
+ style="margin-top: 25px;"
+ class="tb-prompt" ng-show="noData()">event.no-events-prompt</span>
+ <md-virtual-repeat-container ng-show="hasData()" flex md-top-index="topIndex" tb-scope-element="repeatContainer">
+ <md-list-item md-virtual-repeat="event in theEvents" md-on-demand flex ng-style="hasScroll() ? {'margin-right':'-15px'} : {}">
+ <md-list class="tb-row" flex layout="row" tb-event-row event-type="{{eventType}}" event="{{event}}">
+ </md-list>
+ <md-divider flex></md-divider>
+ </md-list-item>
+ </md-virtual-repeat-container>
+ </md-list>
+</md-content>
ui/src/app/event/index.js 30(+30 -0)
diff --git a/ui/src/app/event/index.js b/ui/src/app/event/index.js
new file mode 100644
index 0000000..f443382
--- /dev/null
+++ b/ui/src/app/event/index.js
@@ -0,0 +1,30 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import thingsboardApiEvent from '../api/event.service';
+
+import EventContentDialogController from './event-content-dialog.controller';
+import EventHeaderDirective from './event-header.directive';
+import EventRowDirective from './event-row.directive';
+import EventTableDirective from './event-table.directive';
+
+export default angular.module('thingsboard.event', [
+ thingsboardApiEvent
+])
+ .controller('EventContentDialogController', EventContentDialogController)
+ .directive('tbEventHeader', EventHeaderDirective)
+ .directive('tbEventRow', EventRowDirective)
+ .directive('tbEventTable', EventTableDirective)
+ .name;
ui/src/app/global-interceptor.service.js 182(+182 -0)
diff --git a/ui/src/app/global-interceptor.service.js b/ui/src/app/global-interceptor.service.js
new file mode 100644
index 0000000..7061152
--- /dev/null
+++ b/ui/src/app/global-interceptor.service.js
@@ -0,0 +1,182 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*@ngInject*/
+export default function GlobalInterceptor($rootScope, $q, $injector) {
+
+ var toast;
+ var translate;
+ var userService;
+ var types;
+ var http;
+
+ var internalUrlPrefixes = [
+ '/api/auth/token',
+ '/api/plugins/rpc'
+ ];
+
+ var service = {
+ request: request,
+ requestError: requestError,
+ response: response,
+ responseError: responseError
+ }
+
+ return service;
+
+ function getToast() {
+ if (!toast) {
+ toast = $injector.get("toast");
+ }
+ return toast;
+ }
+
+ function getTranslate() {
+ if (!translate) {
+ translate = $injector.get("$translate");
+ }
+ return translate;
+ }
+
+ function getUserService() {
+ if (!userService) {
+ userService = $injector.get("userService");
+ }
+ return userService;
+ }
+
+ function getTypes() {
+ if (!types) {
+ types = $injector.get("types");
+ }
+ return types;
+ }
+
+ function getHttp() {
+ if (!http) {
+ http = $injector.get("$http");
+ }
+ return http;
+ }
+
+ function rejectionErrorCode(rejection) {
+ if (rejection && rejection.data && rejection.data.errorCode) {
+ return rejection.data.errorCode;
+ } else {
+ return undefined;
+ }
+ }
+
+ function isTokenBasedAuthEntryPoint(url) {
+ return url.startsWith('/api/') &&
+ !url.startsWith(getTypes().entryPoints.login) &&
+ !url.startsWith(getTypes().entryPoints.tokenRefresh) &&
+ !url.startsWith(getTypes().entryPoints.nonTokenBased);
+ }
+
+ function refreshTokenAndRetry(request) {
+ return getUserService().refreshJwtToken().then(function success() {
+ getUserService().updateAuthorizationHeader(request.config.headers);
+ return getHttp()(request.config);
+ }, function fail(message) {
+ $rootScope.$broadcast('unauthenticated');
+ request.status = 401;
+ request.data = {};
+ request.data.message = message || getTranslate().instant('access.unauthorized');
+ return $q.reject(request);
+ });
+ }
+
+ function isInternalUrlPrefix(url) {
+ for (var index in internalUrlPrefixes) {
+ if (url.startsWith(internalUrlPrefixes[index])) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ function request(config) {
+ var rejected = false;
+ if (config.url.startsWith('/api/')) {
+ $rootScope.loading = !isInternalUrlPrefix(config.url);
+ if (isTokenBasedAuthEntryPoint(config.url)) {
+ if (!getUserService().updateAuthorizationHeader(config.headers) &&
+ !getUserService().refreshTokenPending()) {
+ $rootScope.loading = false;
+ rejected = true;
+ getUserService().clearJwtToken(false);
+ return $q.reject({ data: {message: getTranslate().instant('access.unauthorized')}, status: 401, config: config});
+ } else if (!getUserService().isJwtTokenValid()) {
+ return $q.reject({ refreshTokenPending: true, config: config });
+ }
+ }
+ }
+ if (!rejected) {
+ return config;
+ }
+ }
+
+ function requestError(rejection) {
+ if (rejection.config.url.startsWith('/api/')) {
+ $rootScope.loading = false;
+ }
+ return $q.reject(rejection);
+ }
+
+ function response(response) {
+ if (response.config.url.startsWith('/api/')) {
+ $rootScope.loading = false;
+ }
+ return response;
+ }
+
+ function responseError(rejection) {
+ if (rejection.config.url.startsWith('/api/')) {
+ $rootScope.loading = false;
+ }
+ var unhandled = false;
+ if (rejection.refreshTokenPending || rejection.status === 401) {
+ var errorCode = rejectionErrorCode(rejection);
+ if (rejection.refreshTokenPending || (errorCode && errorCode === getTypes().serverErrorCode.jwtTokenExpired)) {
+ return refreshTokenAndRetry(rejection);
+ } else {
+ unhandled = true;
+ }
+ } else if (rejection.status === 403) {
+ $rootScope.$broadcast('forbidden');
+ } else if (rejection.status === 0 || rejection.status === -1) {
+ getToast().showError(getTranslate().instant('error.unable-to-connect'));
+ } else if (!rejection.config.url.startsWith('/api/plugins/rpc')) {
+ if (rejection.status === 404) {
+ getToast().showError(rejection.config.method + ": " + rejection.config.url + "<br/>" +
+ rejection.status + ": " + rejection.statusText);
+ } else {
+ unhandled = true;
+ }
+ }
+
+ if (unhandled) {
+ if (rejection.data && !rejection.data.message) {
+ getToast().showError(rejection.data);
+ } else if (rejection.data && rejection.data.message) {
+ getToast().showError(rejection.data.message);
+ } else {
+ getToast().showError(getTranslate().instant('error.unhandled-error-code', {errorCode: rejection.status}));
+ }
+ }
+ return $q.reject(rejection);
+ }
+}
ui/src/app/help/help.directive.js 75(+75 -0)
diff --git a/ui/src/app/help/help.directive.js b/ui/src/app/help/help.directive.js
new file mode 100644
index 0000000..5d74b1b
--- /dev/null
+++ b/ui/src/app/help/help.directive.js
@@ -0,0 +1,75 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './help.scss';
+
+import thingsboardHelpLinks from './help-links.constant';
+
+import $ from 'jquery';
+
+export default angular.module('thingsboard.directives.help', [thingsboardHelpLinks])
+ .directive('tbHelp', Help)
+ .name;
+
+/* eslint-disable angular/angularelement */
+
+/*@ngInject*/
+function Help($compile, $window, helpLinks) {
+
+ var linker = function (scope, element, attrs) {
+
+ scope.gotoHelpPage = function ($event) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ var helpUrl = helpLinks.linksMap[scope.helpLinkId];
+ if (helpUrl) {
+ $window.open(helpUrl, '_blank');
+ }
+ }
+
+ var html = '<md-tooltip md-direction="top">' +
+ '{{\'help.goto-help-page\' | translate}}' +
+ '</md-tooltip>' +
+ '<md-icon class="material-icons">' +
+ 'help' +
+ '</md-icon>';
+
+ var helpButton = angular.element('<md-button class="tb-help-button-style tb-help-button-pos md-icon-button" ' +
+ 'ng-click="gotoHelpPage($event)">' +
+ html +
+ '</md-button>');
+
+ if (attrs.helpContainerId) {
+ var helpContainer = $('#' + attrs.helpContainerId, element)[0];
+ helpContainer = angular.element(helpContainer);
+ helpContainer.append(helpButton);
+ $compile(helpContainer.contents())(scope);
+ } else {
+ $compile(helpButton)(scope);
+ element.append(helpButton);
+ }
+ }
+
+ return {
+ restrict: "A",
+ link: linker,
+ scope: {
+ helpLinkId: "=tbHelp"
+ }
+ };
+}
+
+/* eslint-enable angular/angularelement */
ui/src/app/help/help.scss 22(+22 -0)
diff --git a/ui/src/app/help/help.scss b/ui/src/app/help/help.scss
new file mode 100644
index 0000000..44280cf
--- /dev/null
+++ b/ui/src/app/help/help.scss
@@ -0,0 +1,22 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import "../../scss/constants";
+
+.md-button.tb-help-button-style, .tb-help-button-style {
+}
+
+.md-button.tb-help-button-pos, .tb-help-button-pos {
+}
ui/src/app/help/help-links.constant.js 129(+129 -0)
diff --git a/ui/src/app/help/help-links.constant.js b/ui/src/app/help/help-links.constant.js
new file mode 100644
index 0000000..c0e2e89
--- /dev/null
+++ b/ui/src/app/help/help-links.constant.js
@@ -0,0 +1,129 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var pluginClazzHelpLinkMap = {
+ 'org.thingsboard.server.extensions.core.plugin.messaging.DeviceMessagingPlugin': 'pluginDeviceMessaging',
+ 'org.thingsboard.server.extensions.core.plugin.telemetry.TelemetryStoragePlugin': 'pluginTelemetryStorage',
+ 'org.thingsboard.server.extensions.core.plugin.rpc.RpcPlugin': 'pluginRpcPlugin',
+ 'org.thingsboard.server.extensions.core.plugin.mail.MailPlugin': 'pluginMailPlugin',
+ 'org.thingsboard.server.extensions.rest.plugin.RestApiCallPlugin': 'pluginRestApiCallPlugin',
+ 'org.thingsboard.server.extensions.core.plugin.time.TimePlugin': 'pluginTimePlugin',
+ 'org.thingsboard.server.extensions.kafka.plugin.KafkaPlugin': 'pluginKafkaPlugin',
+ 'org.thingsboard.server.extensions.rabbitmq.plugin.RabbitMqPlugin': 'pluginRabbitMqPlugin'
+
+};
+
+var filterClazzHelpLinkMap = {
+ 'org.thingsboard.server.extensions.core.filter.MsgTypeFilter': 'filterMsgType',
+ 'org.thingsboard.server.extensions.core.filter.DeviceTelemetryFilter': 'filterDeviceTelemetry',
+ 'org.thingsboard.server.extensions.core.filter.MethodNameFilter': 'filterMethodName',
+ 'org.thingsboard.server.extensions.core.filter.DeviceAttributesFilter': 'filterDeviceAttributes'
+};
+
+var processorClazzHelpLinkMap = {
+ 'org.thingsboard.server.extensions.core.processor.AlarmDeduplicationProcessor': 'processorAlarmDeduplication'
+};
+
+var pluginActionsClazzHelpLinkMap = {
+ 'org.thingsboard.server.extensions.core.action.rpc.RpcPluginAction': 'pluginActionRpc',
+ 'org.thingsboard.server.extensions.core.action.mail.SendMailAction': 'pluginActionSendMail',
+ 'org.thingsboard.server.extensions.core.action.telemetry.TelemetryPluginAction': 'pluginActionTelemetry',
+ 'org.thingsboard.server.extensions.kafka.action.KafkaPluginAction': 'pluginActionKafka',
+ 'org.thingsboard.server.extensions.rabbitmq.action.RabbitMqPluginAction': 'pluginActionRabbitMq',
+ 'org.thingsboard.server.extensions.rest.action.RestApiCallPluginAction': 'pluginActionRestApiCall'
+};
+
+//var helpBaseUrl = "http://thingsboard.io";
+var helpBaseUrl = "http://localhost:4000";
+
+export default angular.module('thingsboard.help', [])
+ .constant('helpLinks',
+ {
+ linksMap: {
+ outgoingMailSettings: helpBaseUrl + "/docs/user-guide/ui/mail-settings",
+ plugins: helpBaseUrl + "/docs/user-guide/rule-engine/#plugins",
+ pluginDeviceMessaging: helpBaseUrl + "/docs/reference/plugins/messaging/",
+ pluginTelemetryStorage: helpBaseUrl + "/docs/reference/plugins/telemetry/",
+ pluginRpcPlugin: helpBaseUrl + "/docs/reference/plugins/rpc/",
+ pluginMailPlugin: helpBaseUrl + "/docs/reference/plugins/mail/",
+ pluginRestApiCallPlugin: helpBaseUrl + "/docs/reference/plugins/rest/",
+ pluginTimePlugin: helpBaseUrl + "/docs/reference/plugins/time/",
+ pluginKafkaPlugin: helpBaseUrl + "/docs/reference/plugins/kafka/",
+ pluginRabbitMqPlugin: helpBaseUrl + "/docs/reference/plugins/rabbitmq/",
+ rules: helpBaseUrl + "/docs/user-guide/rule-engine/#rules",
+ filters: helpBaseUrl + "/docs/user-guide/rule-engine/#filters",
+ filterMsgType: helpBaseUrl + "/docs/reference/filters/message-type-filter",
+ filterDeviceTelemetry: helpBaseUrl + "/docs/reference/filters/device-telemetry-filter",
+ filterMethodName: helpBaseUrl + "/docs/reference/filters/method-name-filter/",
+ filterDeviceAttributes: helpBaseUrl + "/docs/reference/filters/device-attributes-filter",
+ processors: helpBaseUrl + "/docs/user-guide/rule-engine/#processors",
+ processorAlarmDeduplication: "http://thingsboard.io/docs/#q=processorAlarmDeduplication",
+ pluginActions: helpBaseUrl + "/docs/user-guide/rule-engine/#actions",
+ pluginActionRpc: helpBaseUrl + "/docs/reference/actions/rpc-plugin-action",
+ pluginActionSendMail: helpBaseUrl + "/docs/reference/actions/send-mail-action",
+ pluginActionTelemetry: helpBaseUrl + "/docs/reference/actions/telemetry-plugin-action/",
+ pluginActionKafka: helpBaseUrl + "/docs/reference/actions/kafka-plugin-action",
+ pluginActionRabbitMq: helpBaseUrl + "/docs/reference/actions/rabbitmq-plugin-action",
+ pluginActionRestApiCall: helpBaseUrl + "/docs/reference/actions/rest-api-call-plugin-action",
+ tenants: helpBaseUrl + "/docs/user-guide/ui/tenants",
+ customers: helpBaseUrl + "/docs/user-guide/ui/customers",
+ devices: helpBaseUrl + "/docs/user-guide/ui/devices",
+ dashboards: helpBaseUrl + "/docs/user-guide/ui/dashboards",
+ users: helpBaseUrl + "/docs/user-guide/ui/users",
+ widgetsBundles: helpBaseUrl + "/docs/user-guide/ui/widget-library#bundles",
+ widgetsConfig: helpBaseUrl + "/docs/user-guide/ui/dashboards#widget-configuration",
+ widgetsConfigTimeseries: helpBaseUrl + "/docs/user-guide/ui/dashboards#timeseries",
+ widgetsConfigLatest: helpBaseUrl + "/docs/user-guide/ui/dashboards#latest",
+ widgetsConfigRpc: helpBaseUrl + "/docs/user-guide/ui/dashboards#rpc",
+ },
+ getPluginLink: function(plugin) {
+ var link = 'plugins';
+ if (plugin && plugin.clazz) {
+ if (pluginClazzHelpLinkMap[plugin.clazz]) {
+ link = pluginClazzHelpLinkMap[plugin.clazz];
+ }
+ }
+ return link;
+ },
+ getFilterLink: function(filter) {
+ var link = 'filters';
+ if (filter && filter.clazz) {
+ if (filterClazzHelpLinkMap[filter.clazz]) {
+ link = filterClazzHelpLinkMap[filter.clazz];
+ }
+ }
+ return link;
+ },
+ getProcessorLink: function(processor) {
+ var link = 'processors';
+ if (processor && processor.clazz) {
+ if (processorClazzHelpLinkMap[processor.clazz]) {
+ link = processorClazzHelpLinkMap[processor.clazz];
+ }
+ }
+ return link;
+ },
+ getPluginActionLink: function(pluginAction) {
+ var link = 'pluginActions';
+ if (pluginAction && pluginAction.clazz) {
+ if (pluginActionsClazzHelpLinkMap[pluginAction.clazz]) {
+ link = pluginActionsClazzHelpLinkMap[pluginAction.clazz];
+ }
+ }
+ return link;
+ }
+ }
+ ).name;
ui/src/app/home/home-links.controller.js 20(+20 -0)
diff --git a/ui/src/app/home/home-links.controller.js b/ui/src/app/home/home-links.controller.js
new file mode 100644
index 0000000..c4fae6b
--- /dev/null
+++ b/ui/src/app/home/home-links.controller.js
@@ -0,0 +1,20 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*@ngInject*/
+export default function HomeLinksController($scope, menu) {
+ var vm = this;
+ vm.model = menu.getHomeSections();
+}
ui/src/app/home/home-links.routes.js 45(+45 -0)
diff --git a/ui/src/app/home/home-links.routes.js b/ui/src/app/home/home-links.routes.js
new file mode 100644
index 0000000..ab805a3
--- /dev/null
+++ b/ui/src/app/home/home-links.routes.js
@@ -0,0 +1,45 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import homeLinksTemplate from './home-links.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function HomeLinksRoutes($stateProvider) {
+
+ $stateProvider
+ .state('home.links', {
+ url: '/home',
+ module: 'private',
+ auth: ['SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER'],
+ views: {
+ "content@home": {
+ templateUrl: homeLinksTemplate,
+ controllerAs: 'vm',
+ controller: 'HomeLinksController'
+ }
+ },
+ data: {
+ pageTitle: 'home.home'
+ },
+ ncyBreadcrumb: {
+ label: '{"icon": "home", "label": "home.home"}',
+ icon: 'home'
+ }
+ });
+}
ui/src/app/home/home-links.tpl.html 38(+38 -0)
diff --git a/ui/src/app/home/home-links.tpl.html b/ui/src/app/home/home-links.tpl.html
new file mode 100644
index 0000000..89cb5b2
--- /dev/null
+++ b/ui/src/app/home/home-links.tpl.html
@@ -0,0 +1,38 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-grid-list md-cols="2" md-cols-gt-xs="3" md-cols-gt-sm="4" md-row-height="280px">
+ <md-grid-tile md-colspan="{{section.places.length}}" ng-repeat="section in vm.model">
+ <md-card style='width: 100%;'>
+ <md-card-title>
+ <md-card-title-text>
+ <span translate class="md-headline">{{ section.name }}</span>
+ </md-card-title-text>
+ </md-card-title>
+ <md-card-content>
+ <md-grid-list md-row-height="170px" md-cols="{{section.places.length}}" md-cols-gt-md="{{section.places.length}}">
+ <md-grid-tile class="card-tile" ng-repeat="place in section.places">
+ <md-button class="tb-card-button md-raised md-primary" layout="column" ui-sref="{{place.state}}">
+ <md-icon class="material-icons tb-md-96" aria-label="{{place.icon}}">{{place.icon}}</md-icon>
+ <span translate>{{place.name}}</span>
+ </md-button>
+ </md-grid-tile>
+ </md-grid-list>
+ </md-card-content>
+ </md-card>
+ </md-grid-tile>
+</md-grid-list>
\ No newline at end of file
ui/src/app/home/index.js 26(+26 -0)
diff --git a/ui/src/app/home/index.js b/ui/src/app/home/index.js
new file mode 100644
index 0000000..13e4046
--- /dev/null
+++ b/ui/src/app/home/index.js
@@ -0,0 +1,26 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import uiRouter from 'angular-ui-router';
+
+import HomeLinksRoutes from './home-links.routes';
+import HomeLinksController from './home-links.controller';
+
+export default angular.module('thingsboard.homeLinks', [
+ uiRouter
+])
+ .config(HomeLinksRoutes)
+ .controller('HomeLinksController', HomeLinksController)
+ .name;
ui/src/app/jsonform/index.js 32(+32 -0)
diff --git a/ui/src/app/jsonform/index.js b/ui/src/app/jsonform/index.js
new file mode 100644
index 0000000..9666961
--- /dev/null
+++ b/ui/src/app/jsonform/index.js
@@ -0,0 +1,32 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import uiRouter from 'angular-ui-router';
+import ngMaterial from 'angular-material';
+import ngMessages from 'angular-messages';
+import thingsboardJsonForm from "../components/json-form.directive";
+
+import JsonFormRoutes from './jsonform.routes';
+import JsonFormController from './jsonform.controller';
+
+export default angular.module('thingsboard.jsonform', [
+ uiRouter,
+ ngMaterial,
+ ngMessages,
+ thingsboardJsonForm
+])
+ .config(JsonFormRoutes)
+ .controller('JsonFormController', JsonFormController)
+ .name;
ui/src/app/jsonform/jsonform.controller.js 117(+117 -0)
diff --git a/ui/src/app/jsonform/jsonform.controller.js b/ui/src/app/jsonform/jsonform.controller.js
new file mode 100644
index 0000000..5861b64
--- /dev/null
+++ b/ui/src/app/jsonform/jsonform.controller.js
@@ -0,0 +1,117 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './jsonform.scss';
+
+/*@ngInject*/
+export default function JsonFormController($scope/*, $rootScope, $log*/) {
+
+ var vm = this;
+
+ vm.pretty = pretty;
+ vm.resetModel = resetModel;
+ vm.itParses = true;
+ vm.itParsesForm = true;
+
+ vm.formJson = "[ \n" +
+ " {\n" +
+ " \"key\": \"name\",\n" +
+ "\t\"type\": \"text\" \n" +
+ " },\n" +
+ " {\n" +
+ "\t\"key\": \"name2\",\n" +
+ "\t\"type\": \"color\"\n" +
+ " },\n" +
+ " {\n" +
+ "\t\"key\": \"name3\",\n" +
+ "\t\"type\": \"javascript\"\n" +
+ " }, \n" +
+ "\t\"name4\"\n" +
+ "]";
+ vm.schemaJson = "{\n" +
+ " \"type\": \"object\",\n" +
+ " \"title\": \"Comment\",\n" +
+ " \"properties\": {\n" +
+ " \"name\": {\n" +
+ " \"title\": \"Name 1\",\n" +
+ " \"type\": \"string\"\n" +
+ " },\n" +
+ " \"name2\": {\n" +
+ " \"title\": \"Name 2\",\n" +
+ " \"type\": \"string\"\n" +
+ " },\n" +
+ " \"name3\": {\n" +
+ " \"title\": \"Name 3\",\n" +
+ " \"type\": \"string\"\n" +
+ " },\n" +
+ " \"name4\": {\n" +
+ " \"title\": \"Name 4\",\n" +
+ " \"type\": \"number\"\n" +
+ " }\n" +
+ " },\n" +
+ " \"required\": [\n" +
+ " \"name1\", \"name2\", \"name3\", \"name4\"\n" +
+ " ]\n" +
+ "}";
+/* '{\n'+
+ ' "type": "object",\n'+
+ ' "title": "Comment",\n'+
+ ' "properties": {\n'+
+ ' "name": {\n'+
+ ' "title": "Name",\n'+
+ ' "type": "string"\n'+
+ ' }\n'+
+ ' },\n'+
+ ' "required": [\n'+
+ ' "name"\n'+
+ ' ]\n'+
+ '}';*/
+
+ vm.schema = angular.fromJson(vm.schemaJson);
+ vm.form = angular.fromJson(vm.formJson);
+ vm.model = { name: '#ccc' };
+
+ $scope.$watch('vm.schemaJson',function(val,old){
+ if (val && val !== old) {
+ try {
+ vm.schema = angular.fromJson(vm.schemaJson);
+ vm.itParses = true;
+ } catch (e){
+ vm.itParses = false;
+ }
+ }
+ });
+
+
+ $scope.$watch('vm.formJson',function(val,old){
+ if (val && val !== old) {
+ try {
+ vm.form = angular.fromJson(vm.formJson);
+ vm.itParsesForm = true;
+ } catch (e){
+ vm.itParsesForm = false;
+ }
+ }
+ });
+
+ function pretty (){
+ return angular.isString(vm.model) ? vm.model : angular.toJson(vm.model, true);
+ }
+
+ function resetModel () {
+ $scope.ngform.$setPristine();
+ vm.model = { name: 'New hello world!' };
+ }
+}
ui/src/app/jsonform/jsonform.routes.js 44(+44 -0)
diff --git a/ui/src/app/jsonform/jsonform.routes.js b/ui/src/app/jsonform/jsonform.routes.js
new file mode 100644
index 0000000..323b4e0
--- /dev/null
+++ b/ui/src/app/jsonform/jsonform.routes.js
@@ -0,0 +1,44 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import jsonFormTemplate from './jsonform.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function JsonFormRoutes($stateProvider) {
+ $stateProvider
+ .state('home.jsonform', {
+ url: '/jsonform',
+ module: 'private',
+ auth: ['SYS_ADMIN'],
+ views: {
+ "content@home": {
+ templateUrl: jsonFormTemplate,
+ controllerAs: 'vm',
+ controller: 'JsonFormController'
+ }
+ },
+ data: {
+ key: 'general',
+ pageTitle: 'admin.general-settings'
+ },
+ ncyBreadcrumb: {
+ label: '{"icon": "settings", "label": "admin.system-settings"}'
+ }
+ });
+}
ui/src/app/jsonform/jsonform.scss 17(+17 -0)
diff --git a/ui/src/app/jsonform/jsonform.scss b/ui/src/app/jsonform/jsonform.scss
new file mode 100644
index 0000000..fd1513e
--- /dev/null
+++ b/ui/src/app/jsonform/jsonform.scss
@@ -0,0 +1,17 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+.form { height: 400px; }
+.schema { height: 800px; }
\ No newline at end of file
ui/src/app/jsonform/jsonform.tpl.html 54(+54 -0)
diff --git a/ui/src/app/jsonform/jsonform.tpl.html b/ui/src/app/jsonform/jsonform.tpl.html
new file mode 100644
index 0000000..7f28c8d
--- /dev/null
+++ b/ui/src/app/jsonform/jsonform.tpl.html
@@ -0,0 +1,54 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-content layout="row" layout-margin style="background-color: white;">
+
+ <div layout="column" flex="45">
+ <h3>The Generated Form</h3>
+ <form name="ngform"
+ layout="column"
+ layout-padding>
+ <tb-json-form schema="vm.schema"
+ form="vm.form"
+ model="vm.model"
+ readonly="vm.isFormReadonly === 'true'"
+ form-control="ngform">
+ </tb-json-form>
+ </form>
+ <md-checkbox ng-true-value="'true'" ng-false-value="'false'"
+ ng-model="vm.isFormReadonly">Readonly</md-checkbox>
+ <div flex layout="column">
+ <div ng-show="ngform.$valid"><em>Form is valid</em></div>
+ <div ng-show="ngform.$invalid"><em>Form is not valid</em></div>
+ <div ng-show="ngform.$dirty"><em>Form is dirty</em></div>
+ <h3>Model</h3>
+ <md-button ng-click="vm.resetModel()">Reset model</md-button>
+ <pre>{{vm.pretty()}}</pre>
+ </div>
+ </div>
+
+ <div layout="column" flex>
+ <h3>Form</h3>
+ <div ui-ace="{ mode:'json'}"
+ ng-class="{red: !vm.itParsesForm}" ng-model="vm.formJson" class="form-control form"></div>
+ <h3>Schema</h3>
+ <div ui-ace="{ mode:'json'}"
+ ng-class="{red: !vm.itParses}" ng-model="vm.schemaJson" class="form-control schema"></div>
+
+ </div>
+
+</md-content>
\ No newline at end of file
ui/src/app/layout/breadcrumb.tpl.html 38(+38 -0)
diff --git a/ui/src/app/layout/breadcrumb.tpl.html b/ui/src/app/layout/breadcrumb.tpl.html
new file mode 100644
index 0000000..b5ae863
--- /dev/null
+++ b/ui/src/app/layout/breadcrumb.tpl.html
@@ -0,0 +1,38 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div class="tb-breadcrumb">
+ <h1 flex hide-gt-sm>{{ steps[steps.length-1].ncyBreadcrumbLabel | breadcrumbLabel }}</h1>
+ <span hide-xs hide-sm ng-repeat="step in steps" ng-switch="$last || !!step.abstract">
+ <a ng-switch-when="false" href="{{step.ncyBreadcrumbLink}}">
+ <md-icon ng-show="step.ncyBreadcrumbLabel | breadcrumbIcon"
+ class="material-icons"
+ aria-label="{{step.ncyBreadcrumbLabel | breadcrumbIcon}}">
+ {{step.ncyBreadcrumbLabel | breadcrumbIcon}}
+ </md-icon>
+ {{step.ncyBreadcrumbLabel | breadcrumbLabel}}
+ </a>
+ <span ng-switch-when="true">
+ <md-icon ng-show="step.ncyBreadcrumbLabel | breadcrumbIcon"
+ class="material-icons"
+ aria-label="{{step.ncyBreadcrumbLabel | breadcrumbIcon}}">
+ {{step.ncyBreadcrumbLabel | breadcrumbIcon}}
+ </md-icon>
+ {{step.ncyBreadcrumbLabel | breadcrumbLabel}}</span>
+ <span class="divider" ng-hide="$last"> > </span>
+ </span>
+</div>
ui/src/app/layout/breadcrumb-icon.filter.js 24(+24 -0)
diff --git a/ui/src/app/layout/breadcrumb-icon.filter.js b/ui/src/app/layout/breadcrumb-icon.filter.js
new file mode 100644
index 0000000..ee33a70
--- /dev/null
+++ b/ui/src/app/layout/breadcrumb-icon.filter.js
@@ -0,0 +1,24 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export default function BreadcrumbIcon() {
+ return function (bLabel) {
+ var labelObj = angular.fromJson(bLabel);
+ if (angular.isDefined(labelObj.icon)) {
+ return labelObj.icon;
+ }
+ return null;
+ };
+}
ui/src/app/layout/breadcrumb-label.filter.js 45(+45 -0)
diff --git a/ui/src/app/layout/breadcrumb-label.filter.js b/ui/src/app/layout/breadcrumb-label.filter.js
new file mode 100644
index 0000000..701847c
--- /dev/null
+++ b/ui/src/app/layout/breadcrumb-label.filter.js
@@ -0,0 +1,45 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*@ngInject*/
+export default function BreadcrumbLabel($translate) {
+ var labels = {};
+
+ var breadcrumbLabel = function (bLabel) {
+
+ var labelObj;
+ labelObj = angular.fromJson(bLabel);
+ if (labelObj) {
+ if (!labels[labelObj.label]) {
+ labels[labelObj.label] = labelObj.label;
+ var translate = !(labelObj.translate && labelObj.translate === 'false');
+ if (translate) {
+ $translate([labelObj.label]).then(
+ function (translations) {
+ labels[labelObj.label] = translations[labelObj.label];
+ }
+ )
+ }
+ }
+ return labels[labelObj.label];
+ } else {
+ return '';
+ }
+ };
+
+ breadcrumbLabel.$stateful = true;
+
+ return breadcrumbLabel;
+}
ui/src/app/layout/home.controller.js 152(+152 -0)
diff --git a/ui/src/app/layout/home.controller.js b/ui/src/app/layout/home.controller.js
new file mode 100644
index 0000000..462d469
--- /dev/null
+++ b/ui/src/app/layout/home.controller.js
@@ -0,0 +1,152 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import logoSvg from '../../svg/logo_title_white.svg';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function HomeController(loginService, userService, deviceService, Fullscreen, $scope, $rootScope, $document, $state,
+ $log, $mdMedia, $translate) {
+
+ var isShowSidenav = false,
+ dashboardUser = userService.getCurrentUser();
+
+ var vm = this;
+
+ vm.Fullscreen = Fullscreen;
+ vm.logoSvg = logoSvg;
+
+ if (angular.isUndefined($rootScope.searchConfig)) {
+ $rootScope.searchConfig = {
+ searchEnabled: false,
+ showSearch: false,
+ searchText: ""
+ };
+ }
+
+ vm.authorityName = authorityName;
+ vm.displaySearchMode = displaySearchMode;
+ vm.lockSidenav = lockSidenav;
+ vm.logout = logout;
+ vm.openProfile = openProfile;
+ vm.openSidenav = openSidenav;
+ vm.showSidenav = showSidenav;
+ vm.searchTextUpdated = searchTextUpdated;
+ vm.sidenavClicked = sidenavClicked;
+ vm.toggleFullscreen = toggleFullscreen;
+ vm.userDisplayName = userDisplayName;
+
+ $scope.$on('$stateChangeSuccess', function (evt, to, toParams, from) {
+ if (angular.isDefined(to.data.searchEnabled)) {
+ $scope.searchConfig.searchEnabled = to.data.searchEnabled;
+ if ($scope.searchConfig.searchEnabled === false || to.name !== from.name) {
+ $scope.searchConfig.showSearch = false;
+ $scope.searchConfig.searchText = "";
+ }
+ } else {
+ $scope.searchConfig.searchEnabled = false;
+ $scope.searchConfig.showSearch = false;
+ $scope.searchConfig.searchText = "";
+ }
+ });
+
+ function displaySearchMode() {
+ return $scope.searchConfig.searchEnabled &&
+ $scope.searchConfig.showSearch;
+ }
+
+ function toggleFullscreen() {
+ if (Fullscreen.isEnabled()) {
+ Fullscreen.cancel();
+ } else {
+ Fullscreen.all();
+ }
+ }
+
+ function searchTextUpdated() {
+ $scope.$broadcast('searchTextUpdated');
+ }
+
+ function authorityName() {
+ var name = "user.anonymous";
+ if (dashboardUser) {
+ var authority = dashboardUser.authority;
+ if (authority === 'SYS_ADMIN') {
+ name = 'user.sys-admin';
+ } else if (authority === 'TENANT_ADMIN') {
+ name = 'user.tenant-admin';
+ } else if (authority === 'CUSTOMER_USER') {
+ name = 'user.customer';
+ }
+ }
+ return $translate.instant(name);
+ }
+
+ function userDisplayName() {
+ var name = "";
+ if (dashboardUser) {
+ if ((dashboardUser.firstName && dashboardUser.firstName.length > 0) ||
+ (dashboardUser.lastName && dashboardUser.lastName.length > 0)) {
+ if (dashboardUser.firstName) {
+ name += dashboardUser.firstName;
+ }
+ if (dashboardUser.lastName) {
+ if (name.length > 0) {
+ name += " ";
+ }
+ name += dashboardUser.lastName;
+ }
+ } else {
+ name = dashboardUser.email;
+ }
+ }
+ return name;
+ }
+
+ function openProfile() {
+ $state.go('home.profile');
+ }
+
+ function logout() {
+ userService.logout();
+ }
+
+ function openSidenav() {
+ isShowSidenav = true;
+ }
+
+ function closeSidenav() {
+ isShowSidenav = false;
+ }
+
+ function lockSidenav() {
+ return $mdMedia('gt-sm');
+ }
+
+ function sidenavClicked() {
+ if (!$mdMedia('gt-sm')) {
+ closeSidenav();
+ }
+ }
+
+ function showSidenav() {
+ return isShowSidenav || $mdMedia('gt-sm');
+ }
+
+}
\ No newline at end of file
ui/src/app/layout/home.routes.js 50(+50 -0)
diff --git a/ui/src/app/layout/home.routes.js b/ui/src/app/layout/home.routes.js
new file mode 100644
index 0000000..4e6eb2c
--- /dev/null
+++ b/ui/src/app/layout/home.routes.js
@@ -0,0 +1,50 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import breadcrumbTemplate from './breadcrumb.tpl.html';
+import homeTemplate from './home.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function HomeRoutes($stateProvider, $breadcrumbProvider) {
+
+ $breadcrumbProvider.setOptions({
+ prefixStateName: 'home',
+ templateUrl: breadcrumbTemplate
+ });
+
+ $stateProvider
+ .state('home', {
+ url: '',
+ module: 'private',
+ auth: ['SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER'],
+ views: {
+ "@": {
+ controller: 'HomeController',
+ controllerAs: 'vm',
+ templateUrl: homeTemplate
+ }
+ },
+ data: {
+ pageTitle: 'home.home'
+ },
+ ncyBreadcrumb: {
+ skip: true
+ }
+ });
+}
ui/src/app/layout/home.scss 93(+93 -0)
diff --git a/ui/src/app/layout/home.scss b/ui/src/app/layout/home.scss
new file mode 100644
index 0000000..f2e4100
--- /dev/null
+++ b/ui/src/app/layout/home.scss
@@ -0,0 +1,93 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import "~compass-sass-mixins/lib/animate";
+
+.tb-invisible {
+ display: none !important;
+}
+
+.tb-primary-toolbar {
+ h1 {
+ font-size: 24px !important;
+ font-weight: 400 !important;
+ }
+}
+
+.tb-breadcrumb {
+ font-size: 18px !important;
+ font-weight: 400 !important;
+ a {
+ border: none;
+ opacity: 0.75;
+ @include transition(opacity 0.35s);
+ }
+ a:hover, a:focus {
+ opacity: 1;
+ text-decoration: none !important;
+ border: none;
+ }
+ .divider {
+ padding: 0px 30px;
+ }
+}
+
+md-sidenav.tb-site-sidenav {
+ width: 250px;
+}
+
+md-icon.tb-mini-avatar {
+ margin: auto 8px;
+ font-size: 36px;
+ height: 36px;
+ width: 36px;
+}
+
+md-icon.tb-logo-title {
+ height: 36px;
+ width: 200px;
+}
+
+div.tb-user-info {
+ line-height: 1.5;
+ span {
+ text-transform: none;
+ text-align: left;
+ }
+ span.tb-user-display-name {
+ font-size: 0.800rem;
+ font-weight: 300;
+ letter-spacing: 0.008em;
+ }
+ span.tb-user-authority {
+ font-size: 0.800rem;
+ font-weight: 300;
+ letter-spacing: 0.005em;
+ opacity: 0.8;
+ }
+}
+
+.tb-nav-header {
+ flex-shrink: 0;
+ z-index: 2;
+ white-space: nowrap;
+}
+
+.tb-nav-header-toolbar {
+ border-bottom: 1px solid rgba(0, 0, 0, 0.12);
+ flex-shrink: 0;
+ z-index: 2;
+ white-space: nowrap;
+}
ui/src/app/layout/home.tpl.html 96(+96 -0)
diff --git a/ui/src/app/layout/home.tpl.html b/ui/src/app/layout/home.tpl.html
new file mode 100644
index 0000000..e5a4c15
--- /dev/null
+++ b/ui/src/app/layout/home.tpl.html
@@ -0,0 +1,96 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+ <md-sidenav class="tb-site-sidenav md-sidenav-left md-whiteframe-z2"
+ hide-print=""
+ md-component-id="left"
+ aria-label="Toggle Nav"
+ ng-click="vm.sidenavClicked()"
+ md-is-open="vm.showSidenav()"
+ md-is-locked-open="vm.lockSidenav()"
+ layout="column">
+ <header class="tb-nav-header">
+ <md-toolbar md-scroll-shrink class="tb-nav-header-toolbar">
+ <div flex layout="row" layout-align="start center" class="md-toolbar-tools inset">
+ <md-icon md-svg-src="{{vm.logoSvg}}" aria-label="logo" class="tb-logo-title"></md-icon>
+ </div>
+ </md-toolbar>
+ </header>
+ <md-content flex layout="column" role="navigation">
+ <md-toolbar flex>
+ <tb-side-menu></tb-side-menu>
+ </md-toolbar>
+ </md-content>
+ </md-sidenav>
+
+ <div flex layout="column" tabIndex="-1" role="main">
+ <md-toolbar class="md-whiteframe-z1 tb-primary-toolbar" ng-class="{'md-hue-1': vm.displaySearchMode()}">
+ <div flex class="md-toolbar-tools">
+ <md-button id="main" hide-gt-sm
+ class="md-icon-button" ng-click="vm.openSidenav()" aria-label="{{ 'home.menu' | translate }}" ng-class="{'tb-invisible': vm.displaySearchMode()}">
+ <md-icon aria-label="{{ 'home.menu' | translate }}" class="material-icons">menu</md-icon>
+ </md-button>
+ <md-button class="md-icon-button" aria-label="{{ 'action.back' | translate }}" ng-click="searchConfig.showSearch = !searchConfig.showSearch" ng-class="{'tb-invisible': !vm.displaySearchMode()}" >
+ <md-icon aria-label="{{ 'action.back' | translate }}" class="material-icons">arrow_back</md-icon>
+ </md-button>
+ <div flex ng-show="!vm.displaySearchMode()" tb-no-animate flex class="md-toolbar-tools">
+ <span ng-cloak ncy-breadcrumb></span>
+ </div>
+ <md-input-container ng-show="vm.displaySearchMode()" md-theme="tb-search-input" flex>
+ <label> </label>
+ <input ng-model="searchConfig.searchText" ng-change="vm.searchTextUpdated()" placeholder="{{ 'common.enter-search' | translate }}"/>
+ </md-input-container>
+ <md-button class="md-icon-button" aria-label="{{ 'action.search' | translate }}" ng-show="searchConfig.searchEnabled" ng-click="searchConfig.showSearch = !searchConfig.showSearch">
+ <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
+ </md-button>
+ <md-button ng-show="!vm.displaySearchMode()" hide-xs hide-sm class="md-icon-button" ng-click="vm.toggleFullscreen()" aria-label="{{ 'fullscreen.toggle' | translate }}">
+ <ng-md-icon icon="{{vm.Fullscreen.isEnabled() ? 'fullscreen_exit' : 'fullscreen'}}" options='{"easing": "circ-in-out", "duration": 375, "rotation": "none"}'></ng-md-icon>
+ </md-button>
+ <div hide-xs hide-sm ng-show="!vm.displaySearchMode()" class="tb-user-info" layout="row">
+ <md-icon aria-label="{{ 'home.avatar' | translate }}" class="material-icons tb-mini-avatar">account_circle</md-icon>
+ <div layout="column">
+ <span class="tb-user-display-name">{{vm.userDisplayName()}}</span>
+ <span class="tb-user-authority">{{vm.authorityName()}}</span>
+ </div>
+ </div>
+ <md-menu md-position-mode="target-right target">
+ <md-button class="md-icon-button" aria-label="{{ 'home.open-user-menu' | translate }}" ng-click="$mdOpenMenu($event)">
+ <md-icon md-menu-origin aria-label="{{ 'home.open-user-menu' | translate }}" class="material-icons">more_vert</md-icon>
+ </md-button>
+ <md-menu-content width="4">
+ <md-menu-item>
+ <md-button ng-click="vm.openProfile()">
+ <md-icon md-menu-align-target aria-label="{{ 'home.profile' | translate }}" class="material-icons">account_circle</md-icon>
+ <span translate>home.profile</span>
+ </md-button>
+ </md-menu-item>
+ <md-menu-item>
+ <md-button ng-click="vm.logout()">
+ <md-icon md-menu-align-target aria-label="{{ 'home.logout' | translate }}" class="material-icons">exit_to_app</md-icon>
+ <span translate>home.logout</span>
+ </md-button>
+ </md-menu-item>
+ </md-menu-content>
+ </md-menu>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" style="z-index: 10; max-height: 0px; width: 100%;" md-mode="indeterminate" ng-show="loading"></md-progress-linear>
+
+ <div flex layout="column" id="toast-parent" style="position: relative;">
+ <md-content ng-cloak flex layout="column" class="page-content" ui-view name="content"></md-content>
+ </div>
+ </div>
ui/src/app/layout/index.js 78(+78 -0)
diff --git a/ui/src/app/layout/index.js b/ui/src/app/layout/index.js
new file mode 100644
index 0000000..28c7258
--- /dev/null
+++ b/ui/src/app/layout/index.js
@@ -0,0 +1,78 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './home.scss';
+
+import uiRouter from 'angular-ui-router';
+import ngSanitize from 'angular-sanitize';
+import FBAngular from 'angular-fullscreen';
+import 'angular-breadcrumb';
+
+import thingsboardMenu from '../services/menu.service';
+import thingsboardApiDevice from '../api/device.service';
+import thingsboardApiLogin from '../api/login.service';
+import thingsboardApiUser from '../api/user.service';
+
+import thingsboardNoAnimate from '../components/no-animate.directive';
+import thingsboardSideMenu from '../components/side-menu.directive';
+
+import thingsboardTenant from '../tenant';
+import thingsboardCustomer from '../customer';
+import thingsboardUser from '../user';
+import thingsboardHomeLinks from '../home';
+import thingsboardAdmin from '../admin';
+import thingsboardProfile from '../profile';
+import thingsboardDevice from '../device';
+import thingsboardWidgetLibrary from '../widget';
+import thingsboardDashboard from '../dashboard';
+import thingsboardPlugin from '../plugin';
+import thingsboardRule from '../rule';
+
+import thingsboardJsonForm from '../jsonform';
+
+import HomeRoutes from './home.routes';
+import HomeController from './home.controller';
+import BreadcrumbLabel from './breadcrumb-label.filter';
+import BreadcrumbIcon from './breadcrumb-icon.filter';
+
+export default angular.module('thingsboard.home', [
+ uiRouter,
+ ngSanitize,
+ FBAngular.name,
+ 'ncy-angular-breadcrumb',
+ thingsboardMenu,
+ thingsboardHomeLinks,
+ thingsboardTenant,
+ thingsboardCustomer,
+ thingsboardUser,
+ thingsboardAdmin,
+ thingsboardProfile,
+ thingsboardDevice,
+ thingsboardWidgetLibrary,
+ thingsboardDashboard,
+ thingsboardPlugin,
+ thingsboardRule,
+ thingsboardJsonForm,
+ thingsboardApiDevice,
+ thingsboardApiLogin,
+ thingsboardApiUser,
+ thingsboardNoAnimate,
+ thingsboardSideMenu
+])
+ .config(HomeRoutes)
+ .controller('HomeController', HomeController)
+ .filter('breadcrumbLabel', BreadcrumbLabel)
+ .filter('breadcrumbIcon', BreadcrumbIcon)
+ .name;
diff --git a/ui/src/app/login/create-password.controller.js b/ui/src/app/login/create-password.controller.js
new file mode 100644
index 0000000..fceced2
--- /dev/null
+++ b/ui/src/app/login/create-password.controller.js
@@ -0,0 +1,37 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*@ngInject*/
+export default function CreatePasswordController($stateParams, $translate, toast, loginService, userService) {
+ var vm = this;
+
+ vm.password = '';
+ vm.password2 = '';
+
+ vm.createPassword = createPassword;
+
+ function createPassword() {
+ if (vm.password !== vm.password2) {
+ toast.showError($translate.instant('login.passwords-mismatch-error'));
+ } else {
+ loginService.activate($stateParams.activateToken, vm.password).then(function success(response) {
+ var token = response.data.token;
+ var refreshToken = response.data.refreshToken;
+ userService.setUserFromJwtToken(token, refreshToken, true);
+ }, function fail() {
+ });
+ }
+ }
+}
ui/src/app/login/create-password.tpl.html 56(+56 -0)
diff --git a/ui/src/app/login/create-password.tpl.html b/ui/src/app/login/create-password.tpl.html
new file mode 100644
index 0000000..ca951f3
--- /dev/null
+++ b/ui/src/app/login/create-password.tpl.html
@@ -0,0 +1,56 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-content layout="row" layout-align="center center" style="width: 100%;">
+ <md-card flex="initial" class="tb-login-card" md-theme="tb-dark">
+ <md-card-title>
+ <md-card-title-text>
+ <span translate class="md-headline">login.create-password</span>
+ </md-card-title-text>
+ </md-card-title>
+ <md-progress-linear class="md-warn" style="z-index: 1; max-height: 5px; width: inherit; position: absolute"
+ md-mode="indeterminate" ng-show="loading"></md-progress-linear>
+ <md-card-content>
+ <form class="create-password-form" ng-submit="vm.createPassword()">
+ <div layout="column" layout-padding="" id="toast-parent">
+ <span style="height: 50px;"></span>
+ <md-input-container class="md-block">
+ <label translate>common.password</label>
+ <md-icon aria-label="{{ 'common.password' | translate }}" class="material-icons">
+ lock
+ </md-icon>
+ <input id="password-input" type="password" ng-model="vm.password"/>
+ </md-input-container>
+ <md-input-container class="md-block">
+ <label translate>login.password-again</label>
+ <md-icon aria-label="{{ 'login.password-again' | translate }}" class="material-icons">
+ lock
+ </md-icon>
+ <input id="password-input2" type="password" ng-model="vm.password2"/>
+ </md-input-container>
+ <div layout="column" layout-gt-sm="row" layout-padding=""
+ layout-align="start center"
+ layout-align-gt-sm="center start">
+ <md-button class="md-raised md-accent" type="submit">{{ 'login.create-password' | translate }}
+ </md-button>
+ <md-button class="md-raised" ui-sref="login">{{ 'action.cancel' | translate }}</md-button>
+ </div>
+ </div>
+ </form>
+ </md-card-content>
+ </md-card>
+</md-content>
ui/src/app/login/index.js 40(+40 -0)
diff --git a/ui/src/app/login/index.js b/ui/src/app/login/index.js
new file mode 100644
index 0000000..9620beb
--- /dev/null
+++ b/ui/src/app/login/index.js
@@ -0,0 +1,40 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './login.scss';
+
+import uiRouter from 'angular-ui-router';
+import thingsboardApiLogin from '../api/login.service';
+import thingsboardApiUser from '../api/user.service';
+import thingsboardToast from '../services/toast';
+
+import LoginRoutes from './login.routes';
+import LoginController from './login.controller';
+import ResetPasswordRequestController from './reset-password-request.controller';
+import ResetPasswordController from './reset-password.controller';
+import CreatePasswordController from './create-password.controller';
+
+export default angular.module('thingsboard.login', [
+ uiRouter,
+ thingsboardApiLogin,
+ thingsboardApiUser,
+ thingsboardToast
+])
+ .config(LoginRoutes)
+ .controller('LoginController', LoginController)
+ .controller('ResetPasswordRequestController', ResetPasswordRequestController)
+ .controller('ResetPasswordController', ResetPasswordController)
+ .controller('CreatePasswordController', CreatePasswordController)
+ .name;
ui/src/app/login/login.controller.js 46(+46 -0)
diff --git a/ui/src/app/login/login.controller.js b/ui/src/app/login/login.controller.js
new file mode 100644
index 0000000..dcb2f44
--- /dev/null
+++ b/ui/src/app/login/login.controller.js
@@ -0,0 +1,46 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*@ngInject*/
+export default function LoginController(toast, loginService, userService/*, $rootScope, $log, $translate*/) {
+ var vm = this;
+
+ vm.user = {
+ name: '',
+ password: ''
+ };
+
+ vm.login = login;
+
+ function doLogin() {
+ loginService.login(vm.user).then(function success(response) {
+ var token = response.data.token;
+ var refreshToken = response.data.refreshToken;
+ userService.setUserFromJwtToken(token, refreshToken, true);
+ }, function fail(/*response*/) {
+ /*if (response && response.data && response.data.message) {
+ toast.showError(response.data.message);
+ } else if (response && response.statusText) {
+ toast.showError(response.statusText);
+ } else {
+ toast.showError($translate.instant('error.unknown-error'));
+ }*/
+ });
+ }
+
+ function login() {
+ doLogin();
+ }
+}
ui/src/app/login/login.routes.js 80(+80 -0)
diff --git a/ui/src/app/login/login.routes.js b/ui/src/app/login/login.routes.js
new file mode 100644
index 0000000..ba7291b
--- /dev/null
+++ b/ui/src/app/login/login.routes.js
@@ -0,0 +1,80 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import loginTemplate from './login.tpl.html';
+import resetPasswordTemplate from './reset-password.tpl.html';
+import resetPasswordRequestTemplate from './reset-password-request.tpl.html';
+import createPasswordTemplate from './create-password.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function LoginRoutes($stateProvider) {
+ $stateProvider.state('login', {
+ url: '/login',
+ module: 'public',
+ views: {
+ "@": {
+ controller: 'LoginController',
+ controllerAs: 'vm',
+ templateUrl: loginTemplate
+ }
+ },
+ data: {
+ pageTitle: 'login.login'
+ }
+ }).state('login.resetPasswordRequest', {
+ url: '/resetPasswordRequest',
+ module: 'public',
+ views: {
+ "@": {
+ controller: 'ResetPasswordRequestController',
+ controllerAs: 'vm',
+ templateUrl: resetPasswordRequestTemplate
+ }
+ },
+ data: {
+ pageTitle: 'login.request-password-reset'
+ }
+ }).state('login.resetPassword', {
+ url: '/resetPassword?resetToken',
+ module: 'public',
+ views: {
+ "@": {
+ controller: 'ResetPasswordController',
+ controllerAs: 'vm',
+ templateUrl: resetPasswordTemplate
+ }
+ },
+ data: {
+ pageTitle: 'login.reset-password'
+ }
+ }).state('login.createPassword', {
+ url: '/createPassword?activateToken',
+ module: 'public',
+ views: {
+ "@": {
+ controller: 'CreatePasswordController',
+ controllerAs: 'vm',
+ templateUrl: createPasswordTemplate
+ }
+ },
+ data: {
+ pageTitle: 'login.create-password'
+ }
+ });
+}
ui/src/app/login/login.scss 23(+23 -0)
diff --git a/ui/src/app/login/login.scss b/ui/src/app/login/login.scss
new file mode 100644
index 0000000..6a64eed
--- /dev/null
+++ b/ui/src/app/login/login.scss
@@ -0,0 +1,23 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import '../../scss/constants';
+
+md-card.tb-login-card {
+ width: 330px !important;
+ @media (min-width: $layout-breakpoint-sm) {
+ width: 450px !important;
+ }
+}
ui/src/app/login/login.tpl.html 56(+56 -0)
diff --git a/ui/src/app/login/login.tpl.html b/ui/src/app/login/login.tpl.html
new file mode 100644
index 0000000..80036bb
--- /dev/null
+++ b/ui/src/app/login/login.tpl.html
@@ -0,0 +1,56 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-content layout="row" layout-align="center center" style="width: 100%;">
+ <md-card flex="initial" class="tb-login-card" md-theme="tb-dark">
+ <md-card-title>
+ <md-card-title-text>
+ <span translate class="md-headline">login.sign-in</span>
+ </md-card-title-text>
+ </md-card-title>
+ <md-progress-linear class="md-warn" style="z-index: 1; max-height: 5px; width: inherit; position: absolute"
+ md-mode="indeterminate" ng-show="loading"></md-progress-linear>
+ <md-card-content>
+ <form class="login-form" ng-submit="vm.login()">
+ <div layout="column" layout-padding="" id="toast-parent">
+ <span style="height: 50px;"></span>
+ <md-input-container class="md-block">
+ <label translate>login.username</label>
+ <md-icon aria-label="{{ 'login.username' | translate }}" class="material-icons">
+ email
+ </md-icon>
+ <input id="username-input" type="email" autofocus ng-model="vm.user.name"/>
+ </md-input-container>
+ <md-input-container class="md-block">
+ <label translate>common.password</label>
+ <md-icon aria-label="{{ 'common.password' | translate }}" class="material-icons">
+ lock
+ </md-icon>
+ <input id="password-input" type="password" ng-model="vm.user.password"/>
+ </md-input-container>
+ <div layout-gt-sm="column" layout-align="space-between stretch">
+ <div layout-gt-sm="column" layout-align="space-between end">
+ <md-button ui-sref="login.resetPasswordRequest">{{ 'login.forgot-password' | translate }}
+ </md-button>
+ </div>
+ </div>
+ <md-button class="md-raised" type="submit">{{ 'login.login' | translate }}</md-button>
+ </div>
+ </form>
+ </md-card-content>
+ </md-card>
+</md-content>
diff --git a/ui/src/app/login/reset-password.controller.js b/ui/src/app/login/reset-password.controller.js
new file mode 100644
index 0000000..4855d6b
--- /dev/null
+++ b/ui/src/app/login/reset-password.controller.js
@@ -0,0 +1,37 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*@ngInject*/
+export default function ResetPasswordController($stateParams, $translate, toast, loginService, userService) {
+ var vm = this;
+
+ vm.newPassword = '';
+ vm.newPassword2 = '';
+
+ vm.resetPassword = resetPassword;
+
+ function resetPassword() {
+ if (vm.newPassword !== vm.newPassword2) {
+ toast.showError($translate.instant('login.passwords-mismatch-error'));
+ } else {
+ loginService.resetPassword($stateParams.resetToken, vm.newPassword).then(function success(response) {
+ var token = response.data.token;
+ var refreshToken = response.data.refreshToken;
+ userService.setUserFromJwtToken(token, refreshToken, true);
+ }, function fail() {
+ });
+ }
+ }
+}
ui/src/app/login/reset-password.tpl.html 56(+56 -0)
diff --git a/ui/src/app/login/reset-password.tpl.html b/ui/src/app/login/reset-password.tpl.html
new file mode 100644
index 0000000..4148c10
--- /dev/null
+++ b/ui/src/app/login/reset-password.tpl.html
@@ -0,0 +1,56 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-content layout="row" layout-align="center center" style="width: 100%;">
+ <md-card flex="initial" class="tb-login-card" md-theme="tb-dark">
+ <md-card-title>
+ <md-card-title-text>
+ <span translate class="md-headline">login.password-reset</span>
+ </md-card-title-text>
+ </md-card-title>
+ <md-progress-linear class="md-warn" style="z-index: 1; max-height: 5px; width: inherit; position: absolute"
+ md-mode="indeterminate" ng-show="loading"></md-progress-linear>
+ <md-card-content>
+ <form class="password-reset-form" ng-submit="vm.resetPassword()">
+ <div layout="column" layout-padding="" id="toast-parent">
+ <span style="height: 50px;"></span>
+ <md-input-container class="md-block">
+ <label translate>login.new-password</label>
+ <md-icon aria-label="{{ 'login.new-password' | translate }}" class="material-icons">
+ lock
+ </md-icon>
+ <input id="password-input" type="password" ng-model="vm.newPassword"/>
+ </md-input-container>
+ <md-input-container class="md-block">
+ <label translate>login.new-password-again</label>
+ <md-icon aria-label="{{ 'login.new-password-again' | translate }}" class="material-icons">
+ lock
+ </md-icon>
+ <input id="password-input2" type="password" ng-model="vm.newPassword2"/>
+ </md-input-container>
+ <div layout="column" layout-gt-sm="row" layout-padding=""
+ layout-align="start center"
+ layout-align-gt-sm="center start">
+ <md-button class="md-raised md-accent" type="submit">{{ 'login.reset-password' | translate }}
+ </md-button>
+ <md-button class="md-raised" ui-sref="login">{{ 'action.cancel' | translate }}</md-button>
+ </div>
+ </div>
+ </form>
+ </md-card-content>
+ </md-card>
+</md-content>
diff --git a/ui/src/app/login/reset-password-request.controller.js b/ui/src/app/login/reset-password-request.controller.js
new file mode 100644
index 0000000..c4cb6c6
--- /dev/null
+++ b/ui/src/app/login/reset-password-request.controller.js
@@ -0,0 +1,30 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*@ngInject*/
+export default function ResetPasswordRequestController($translate, toast, loginService) {
+ var vm = this;
+
+ vm.email = '';
+
+ vm.sendResetPasswordLink = sendResetPasswordLink;
+
+ function sendResetPasswordLink() {
+ loginService.sendResetPasswordLink(vm.email).then(function success() {
+ toast.showSuccess($translate.instant('login.password-link-sent-message'));
+ }, function fail() {
+ });
+ }
+}
diff --git a/ui/src/app/login/reset-password-request.tpl.html b/ui/src/app/login/reset-password-request.tpl.html
new file mode 100644
index 0000000..c541c59
--- /dev/null
+++ b/ui/src/app/login/reset-password-request.tpl.html
@@ -0,0 +1,50 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-content layout="row" layout-align="center center" style="width: 100%;">
+ <md-card flex="initial" class="tb-login-card" md-theme="tb-dark">
+ <md-card-title>
+ <md-card-title-text>
+ <span translate class="md-headline">login.request-password-reset</span>
+ </md-card-title-text>
+ </md-card-title>
+ <md-progress-linear class="md-warn" style="z-index: 1; max-height: 5px; width: inherit; position: absolute"
+ md-mode="indeterminate" ng-show="loading"></md-progress-linear>
+ <md-card-content>
+ <form class="request-password-reset-form" ng-submit="vm.sendResetPasswordLink()">
+ <div layout="column" layout-padding="" id="toast-parent">
+ <span style="height: 50px;"></span>
+ <md-input-container class="md-block">
+ <label translate>login.email</label>
+ <md-icon aria-label="{{ 'login.email' | translate }}" class="material-icons">
+ email
+ </md-icon>
+ <input id="email-input" type="email" autofocus ng-model="vm.email"/>
+ </md-input-container>
+ <div layout="column" layout-gt-sm="row" layout-padding=""
+ layout-align="start center"
+ layout-align-gt-sm="center start">
+ <md-button class="md-raised md-accent" type="submit">{{ 'login.request-password-reset' |
+ translate }}
+ </md-button>
+ <md-button class="md-raised" ui-sref="login">{{ 'action.cancel' | translate }}</md-button>
+ </div>
+ </div>
+ </form>
+ </md-card-content>
+ </md-card>
+</md-content>
ui/src/app/plugin/add-plugin.tpl.html 48(+48 -0)
diff --git a/ui/src/app/plugin/add-plugin.tpl.html b/ui/src/app/plugin/add-plugin.tpl.html
new file mode 100644
index 0000000..d7525ab
--- /dev/null
+++ b/ui/src/app/plugin/add-plugin.tpl.html
@@ -0,0 +1,48 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'plugin.add' | translate }}" tb-help="vm.helpLinks.getPluginLink(vm.item)" help-container-id="help-container">
+ <form name="theForm" ng-submit="vm.add()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>plugin.add</h2>
+ <span flex></span>
+ <div id="help-container"></div>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <tb-plugin plugin="vm.item" is-edit="true" the-form="theForm"></tb-plugin>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit"
+ class="md-raised md-primary">
+ {{ 'action.add' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+ translate }}
+ </md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
ui/src/app/plugin/index.js 38(+38 -0)
diff --git a/ui/src/app/plugin/index.js b/ui/src/app/plugin/index.js
new file mode 100644
index 0000000..c1262a9
--- /dev/null
+++ b/ui/src/app/plugin/index.js
@@ -0,0 +1,38 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import uiRouter from 'angular-ui-router';
+import thingsboardGrid from '../components/grid.directive';
+import thingsboardJsonForm from '../components/json-form.directive';
+import thingsboardEvent from '../event';
+import thingsboardApiPlugin from '../api/plugin.service';
+import thingsboardApiComponentDescriptor from '../api/component-descriptor.service';
+
+import PluginRoutes from './plugin.routes';
+import PluginController from './plugin.controller';
+import PluginDirective from './plugin.directive';
+
+export default angular.module('thingsboard.plugin', [
+ uiRouter,
+ thingsboardGrid,
+ thingsboardJsonForm,
+ thingsboardEvent,
+ thingsboardApiPlugin,
+ thingsboardApiComponentDescriptor
+])
+ .config(PluginRoutes)
+ .controller('PluginController', PluginController)
+ .directive('tbPlugin', PluginDirective)
+ .name;
ui/src/app/plugin/plugin.controller.js 177(+177 -0)
diff --git a/ui/src/app/plugin/plugin.controller.js b/ui/src/app/plugin/plugin.controller.js
new file mode 100644
index 0000000..9c0aac9
--- /dev/null
+++ b/ui/src/app/plugin/plugin.controller.js
@@ -0,0 +1,177 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import addPluginTemplate from './add-plugin.tpl.html';
+import pluginCard from './plugin-card.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function PluginController(pluginService, userService, $state, $stateParams, $filter, $translate, types, helpLinks) {
+
+ var pluginActionsList = [
+ {
+ onAction: function ($event, item) {
+ activatePlugin($event, item);
+ },
+ name: function() { return $translate.instant('action.activate') },
+ details: function() { return $translate.instant('plugin.activate') },
+ icon: "play_arrow",
+ isEnabled: function(plugin) {
+ return isPluginEditable(plugin) && plugin && plugin.state === 'SUSPENDED';
+ }
+ },
+ {
+ onAction: function ($event, item) {
+ suspendPlugin($event, item);
+ },
+ name: function() { return $translate.instant('action.suspend') },
+ details: function() { return $translate.instant('plugin.suspend') },
+ icon: "pause",
+ isEnabled: function(plugin) {
+ return isPluginEditable(plugin) && plugin && plugin.state === 'ACTIVE';
+ }
+ },
+ {
+ onAction: function ($event, item) {
+ vm.grid.deleteItem($event, item);
+ },
+ name: function() { return $translate.instant('action.delete') },
+ details: function() { return $translate.instant('plugin.delete') },
+ icon: "delete",
+ isEnabled: isPluginEditable
+ }
+ ];
+
+ var vm = this;
+
+ vm.types = types;
+
+ vm.helpLinkIdForPlugin = helpLinkIdForPlugin;
+
+ vm.pluginGridConfig = {
+
+ refreshParamsFunc: null,
+
+ deleteItemTitleFunc: deletePluginTitle,
+ deleteItemContentFunc: deletePluginText,
+ deleteItemsTitleFunc: deletePluginsTitle,
+ deleteItemsActionTitleFunc: deletePluginsActionTitle,
+ deleteItemsContentFunc: deletePluginsText,
+
+ fetchItemsFunc: fetchPlugins,
+ saveItemFunc: savePlugin,
+ deleteItemFunc: deletePlugin,
+
+ getItemTitleFunc: getPluginTitle,
+ itemCardTemplateUrl: pluginCard,
+ parentCtl: vm,
+
+ actionsList: pluginActionsList,
+
+ onGridInited: gridInited,
+
+ addItemTemplateUrl: addPluginTemplate,
+
+ addItemText: function() { return $translate.instant('plugin.add-plugin-text') },
+ noItemsText: function() { return $translate.instant('plugin.no-plugins-text') },
+ itemDetailsText: function() { return $translate.instant('plugin.plugin-details') },
+ isSelectionEnabled: isPluginEditable,
+ isDetailsReadOnly: function(plugin) {
+ return !isPluginEditable(plugin);
+ }
+
+ };
+
+ if (angular.isDefined($stateParams.items) && $stateParams.items !== null) {
+ vm.pluginGridConfig.items = $stateParams.items;
+ }
+
+ if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) {
+ vm.pluginGridConfig.topIndex = $stateParams.topIndex;
+ }
+
+ vm.activatePlugin = activatePlugin;
+ vm.suspendPlugin = suspendPlugin;
+
+ function helpLinkIdForPlugin() {
+ return helpLinks.getPluginLink(vm.grid.operatingItem());
+ }
+
+ function deletePluginTitle(plugin) {
+ return $translate.instant('plugin.delete-plugin-title', {pluginName: plugin.name});
+ }
+
+ function deletePluginText() {
+ return $translate.instant('plugin.delete-plugin-text');
+ }
+
+ function deletePluginsTitle(selectedCount) {
+ return $translate.instant('plugin.delete-plugins-title', {count: selectedCount}, 'messageformat');
+ }
+
+ function deletePluginsActionTitle(selectedCount) {
+ return $translate.instant('plugin.delete-plugins-action-title', {count: selectedCount}, 'messageformat');
+ }
+
+ function deletePluginsText() {
+ return $translate.instant('plugin.delete-plugins-text');
+ }
+
+ function gridInited(grid) {
+ vm.grid = grid;
+ }
+
+ function fetchPlugins(pageLink) {
+ return pluginService.getAllPlugins(pageLink);
+ }
+
+ function savePlugin(plugin) {
+ return pluginService.savePlugin(plugin);
+ }
+
+ function deletePlugin(pluginId) {
+ return pluginService.deletePlugin(pluginId);
+ }
+
+ function getPluginTitle(plugin) {
+ return plugin ? plugin.name : '';
+ }
+
+ function isPluginEditable(plugin) {
+ if (userService.getAuthority() === 'TENANT_ADMIN') {
+ return plugin && plugin.tenantId.id != types.id.nullUid;
+ } else {
+ return userService.getAuthority() === 'SYS_ADMIN';
+ }
+ }
+
+ function activatePlugin(event, plugin) {
+ pluginService.activatePlugin(plugin.id.id).then(function () {
+ vm.grid.refreshList();
+ }, function () {
+ });
+ }
+
+ function suspendPlugin(event, plugin) {
+ pluginService.suspendPlugin(plugin.id.id).then(function () {
+ vm.grid.refreshList();
+ }, function () {
+ });
+ }
+
+}
ui/src/app/plugin/plugin.directive.js 89(+89 -0)
diff --git a/ui/src/app/plugin/plugin.directive.js b/ui/src/app/plugin/plugin.directive.js
new file mode 100644
index 0000000..7799d3d
--- /dev/null
+++ b/ui/src/app/plugin/plugin.directive.js
@@ -0,0 +1,89 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './plugin.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import pluginFieldsetTemplate from './plugin-fieldset.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function PluginDirective($compile, $templateCache, types, utils, userService, componentDescriptorService) {
+ var linker = function (scope, element) {
+ var template = $templateCache.get(pluginFieldsetTemplate);
+ element.html(template);
+
+ scope.showPluginConfig = false;
+
+ scope.pluginConfiguration = {
+ data: null
+ };
+
+ if (scope.plugin && !scope.plugin.configuration) {
+ scope.plugin.configuration = {};
+ }
+
+ scope.$watch("plugin.clazz", function (newValue, prevValue) {
+ if (newValue != prevValue) {
+ scope.pluginConfiguration.data = null;
+ if (scope.plugin) {
+ componentDescriptorService.getComponentDescriptorByClazz(scope.plugin.clazz).then(
+ function success(component) {
+ scope.pluginComponent = component;
+ scope.showPluginConfig = !(userService.getAuthority() === 'TENANT_ADMIN'
+ && scope.plugin.tenantId
+ && scope.plugin.tenantId.id === types.id.nullUid)
+ && utils.isDescriptorSchemaNotEmpty(scope.pluginComponent.configurationDescriptor);
+ scope.pluginConfiguration.data = angular.copy(scope.plugin.configuration);
+ },
+ function fail() {
+ }
+ );
+ }
+ }
+ });
+
+ scope.$watch("pluginConfiguration.data", function (newValue, prevValue) {
+ if (newValue && !angular.equals(newValue, prevValue)) {
+ scope.plugin.configuration = angular.copy(scope.pluginConfiguration.data);
+ }
+ }, true);
+
+ componentDescriptorService.getComponentDescriptorsByType(types.componentType.plugin).then(
+ function success(components) {
+ scope.pluginComponents = components;
+ },
+ function fail() {
+ }
+ );
+
+ $compile(element.contents())(scope);
+ }
+ return {
+ restrict: "E",
+ link: linker,
+ scope: {
+ plugin: '=',
+ isEdit: '=',
+ isReadOnly: '=',
+ theForm: '=',
+ onActivatePlugin: '&',
+ onSuspendPlugin: '&',
+ onDeletePlugin: '&'
+ }
+ };
+}
ui/src/app/plugin/plugin.routes.js 46(+46 -0)
diff --git a/ui/src/app/plugin/plugin.routes.js b/ui/src/app/plugin/plugin.routes.js
new file mode 100644
index 0000000..9b19f92
--- /dev/null
+++ b/ui/src/app/plugin/plugin.routes.js
@@ -0,0 +1,46 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import pluginsTemplate from './plugins.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function PluginRoutes($stateProvider) {
+
+ $stateProvider
+ .state('home.plugins', {
+ url: '/plugins',
+ params: {'topIndex': 0},
+ module: 'private',
+ auth: ['SYS_ADMIN', 'TENANT_ADMIN'],
+ views: {
+ "content@home": {
+ templateUrl: pluginsTemplate,
+ controllerAs: 'vm',
+ controller: 'PluginController'
+ }
+ },
+ data: {
+ searchEnabled: true,
+ pageTitle: 'plugin.plugins'
+ },
+ ncyBreadcrumb: {
+ label: '{"icon": "extension", "label": "plugin.plugins"}'
+ }
+ });
+}
ui/src/app/plugin/plugin.scss 18(+18 -0)
diff --git a/ui/src/app/plugin/plugin.scss b/ui/src/app/plugin/plugin.scss
new file mode 100644
index 0000000..92b9cb9
--- /dev/null
+++ b/ui/src/app/plugin/plugin.scss
@@ -0,0 +1,18 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+.plugin-config {
+ min-width: 500px;
+}
\ No newline at end of file
ui/src/app/plugin/plugin-card.tpl.html 19(+19 -0)
diff --git a/ui/src/app/plugin/plugin-card.tpl.html b/ui/src/app/plugin/plugin-card.tpl.html
new file mode 100644
index 0000000..f711239
--- /dev/null
+++ b/ui/src/app/plugin/plugin-card.tpl.html
@@ -0,0 +1,19 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div class="tb-uppercase" ng-if="item && parentCtl.types.id.nullUid === item.tenantId.id" translate>plugin.system</div>
+<div class="tb-uppercase" translate>{{item && item.state === 'ACTIVE' ? 'plugin.active' : 'plugin.suspended'}}</div>
ui/src/app/plugin/plugin-fieldset.tpl.html 71(+71 -0)
diff --git a/ui/src/app/plugin/plugin-fieldset.tpl.html b/ui/src/app/plugin/plugin-fieldset.tpl.html
new file mode 100644
index 0000000..52a2578
--- /dev/null
+++ b/ui/src/app/plugin/plugin-fieldset.tpl.html
@@ -0,0 +1,71 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-button ng-click="onActivatePlugin({event: $event})" ng-show="!isEdit && !isReadOnly && plugin.state === 'SUSPENDED'" class="md-raised md-primary">{{ 'plugin.activate' | translate }}</md-button>
+<md-button ng-click="onSuspendPlugin({event: $event})" ng-show="!isEdit && !isReadOnly && plugin.state === 'ACTIVE'" class="md-raised md-primary">{{ 'plugin.suspend' | translate }}</md-button>
+<md-button ng-click="onDeletePlugin({event: $event})" ng-show="!isEdit && !isReadOnly" class="md-raised md-primary">{{ 'plugin.delete' | translate }}</md-button>
+
+<md-content class="md-padding" layout="column" style="overflow-x: hidden">
+ <fieldset ng-disabled="loading || !isEdit || isReadOnly">
+ <md-input-container class="md-block">
+ <label translate>plugin.name</label>
+ <input required name="name" ng-model="plugin.name">
+ <div ng-messages="theForm.name.$error">
+ <div translate ng-message="required">plugin.name-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container class="md-block">
+ <label translate>plugin.description</label>
+ <textarea ng-model="plugin.additionalInfo.description" rows="2"></textarea>
+ </md-input-container>
+ <section flex layout="row">
+ <md-input-container flex class="md-block">
+ <label translate>plugin.api-token</label>
+ <input required name="apiToken" ng-model="plugin.apiToken">
+ <div ng-messages="theForm.apiToken.$error">
+ <div translate ng-message="required">plugin.api-token-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container flex class="md-block">
+ <label translate>plugin.type</label>
+ <md-select required name="pluginType" ng-model="plugin.clazz" ng-disabled="loading || !isEdit">
+ <md-option ng-repeat="component in pluginComponents" ng-value="component.clazz">
+ {{component.name}}
+ </md-option>
+ </md-select>
+ <div ng-messages="theForm.pluginType.$error">
+ <div translate ng-message="required">plugin.type-required</div>
+ </div>
+ </md-input-container>
+ </section>
+ <md-card flex class="plugin-config" ng-if="showPluginConfig">
+ <md-card-title>
+ <md-card-title-text>
+ <span translate class="md-headline" ng-class="{'tb-readonly-label' : (loading || !isEdit || isReadOnly)}">plugin.configuration</span>
+ </md-card-title-text>
+ </md-card-title>
+ <md-card-content>
+ <tb-json-form schema="pluginComponent.configurationDescriptor.schema"
+ form="pluginComponent.configurationDescriptor.form"
+ model="pluginConfiguration.data"
+ readonly="loading || !isEdit || isReadOnly"
+ form-control="theForm">
+ </tb-json-form>
+ </md-card-content>
+ </md-card>
+ </fieldset>
+</md-content>
ui/src/app/plugin/plugins.tpl.html 42(+42 -0)
diff --git a/ui/src/app/plugin/plugins.tpl.html b/ui/src/app/plugin/plugins.tpl.html
new file mode 100644
index 0000000..96c41dd
--- /dev/null
+++ b/ui/src/app/plugin/plugins.tpl.html
@@ -0,0 +1,42 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<tb-grid grid-configuration="vm.pluginGridConfig">
+ <details-buttons tb-help="vm.helpLinkIdForPlugin()" help-container-id="help-container">
+ <div id="help-container"></div>
+ </details-buttons>
+ <md-tabs ng-class="{'tb-headless': vm.grid.detailsConfig.isDetailsEditMode}"
+ id="tabs" md-border-bottom flex class="tb-absolute-fill">
+ <md-tab label="{{ 'plugin.details' | translate }}">
+ <tb-plugin plugin="vm.grid.operatingItem()"
+ is-edit="vm.grid.detailsConfig.isDetailsEditMode"
+ is-read-only="vm.grid.isDetailsReadOnly(vm.grid.operatingItem())"
+ the-form="vm.grid.detailsForm"
+ on-activate-plugin="vm.activatePlugin(event, vm.grid.detailsConfig.currentItem)"
+ on-suspend-plugin="vm.suspendPlugin(event, vm.grid.detailsConfig.currentItem)"
+ on-delete-plugin="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-plugin>
+ </md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'plugin.events' | translate }}">
+ <tb-event-table flex entity-type="vm.types.entityType.plugin"
+ entity-id="vm.grid.operatingItem().id.id"
+ tenant-id="vm.grid.operatingItem().tenantId.id"
+ default-event-type="{{vm.types.eventType.lcEvent.value}}"
+ disabled-event-types="{{vm.types.eventType.alarm.value}}">
+ </tb-event-table>
+ </md-tab>
+ </md-tabs>
+</tb-grid>
diff --git a/ui/src/app/profile/change-password.controller.js b/ui/src/app/profile/change-password.controller.js
new file mode 100644
index 0000000..621683e
--- /dev/null
+++ b/ui/src/app/profile/change-password.controller.js
@@ -0,0 +1,41 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*@ngInject*/
+export default function ChangePasswordController($scope, $translate, toast, $mdDialog, loginService) {
+ var vm = this;
+
+ vm.currentPassword = '';
+ vm.newPassword = '';
+ vm.newPassword2 = '';
+
+ vm.cancel = cancel;
+ vm.changePassword = changePassword;
+
+ function cancel() {
+ $mdDialog.cancel();
+ }
+
+ function changePassword() {
+ if (vm.newPassword !== vm.newPassword2) {
+ toast.showError($translate.instant('login.passwords-mismatch-error'));
+ } else {
+ loginService.changePassword(vm.currentPassword, vm.newPassword).then(function success() {
+ $scope.theForm.$setPristine();
+ $mdDialog.hide();
+ });
+ }
+ }
+}
ui/src/app/profile/change-password.tpl.html 64(+64 -0)
diff --git a/ui/src/app/profile/change-password.tpl.html b/ui/src/app/profile/change-password.tpl.html
new file mode 100644
index 0000000..06082f7
--- /dev/null
+++ b/ui/src/app/profile/change-password.tpl.html
@@ -0,0 +1,64 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'profile.change-password' | translate }}">
+ <form name="theForm" ng-submit="vm.changePassword()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>profile.change-password</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <md-input-container class="md-block">
+ <label translate>profile.current-password</label>
+ <md-icon aria-label="{{ 'profile.current-password' | translate }}" class="material-icons">
+ lock
+ </md-icon>
+ <input id="current-password-input" type="password" ng-model="vm.currentPassword"/>
+ </md-input-container>
+ <md-input-container class="md-block">
+ <label translate>login.new-password</label>
+ <md-icon aria-label="{{ 'login.new-password' | translate }}" class="material-icons">
+ lock
+ </md-icon>
+ <input id="password-input" type="password" ng-model="vm.newPassword"/>
+ </md-input-container>
+ <md-input-container class="md-block">
+ <label translate>login.new-password-again</label>
+ <md-icon aria-label="{{ 'login.new-password-again' | translate }}" class="material-icons">
+ lock
+ </md-icon>
+ <input id="password-input2" type="password" ng-model="vm.newPassword2"/>
+ </md-input-container>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || theForm.$invalid" type="submit" class="md-raised md-primary">
+ {{ 'profile.change-password' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
ui/src/app/profile/index.js 38(+38 -0)
diff --git a/ui/src/app/profile/index.js b/ui/src/app/profile/index.js
new file mode 100644
index 0000000..255ec2c
--- /dev/null
+++ b/ui/src/app/profile/index.js
@@ -0,0 +1,38 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import uiRouter from 'angular-ui-router';
+import ngMaterial from 'angular-material';
+import ngMessages from 'angular-messages';
+import thingsboardApiUser from '../api/user.service';
+import thingsboardApiLogin from '../api/login.service';
+import thingsboardConfirmOnExit from '../components/confirm-on-exit.directive';
+
+import ProfileRoutes from './profile.routes';
+import ProfileController from './profile.controller';
+import ChangePasswordController from './change-password.controller';
+
+export default angular.module('thingsboard.profile', [
+ uiRouter,
+ ngMaterial,
+ ngMessages,
+ thingsboardApiUser,
+ thingsboardApiLogin,
+ thingsboardConfirmOnExit
+])
+ .config(ProfileRoutes)
+ .controller('ProfileController', ProfileController)
+ .controller('ChangePasswordController', ChangePasswordController)
+ .name;
ui/src/app/profile/profile.controller.js 58(+58 -0)
diff --git a/ui/src/app/profile/profile.controller.js b/ui/src/app/profile/profile.controller.js
new file mode 100644
index 0000000..0144d62
--- /dev/null
+++ b/ui/src/app/profile/profile.controller.js
@@ -0,0 +1,58 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import changePasswordTemplate from './change-password.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function ProfileController(userService, $scope, $document, $mdDialog) {
+ var vm = this;
+
+ vm.profileUser = {};
+
+ vm.save = save;
+ vm.changePassword = changePassword;
+
+ loadProfile();
+
+ function loadProfile() {
+ userService.getUser(userService.getCurrentUser().userId).then(function success(user) {
+ vm.profileUser = user;
+ });
+ }
+
+ function save() {
+ userService.saveUser(vm.profileUser).then(function success(user) {
+ vm.profileUser = user;
+ $scope.theForm.$setPristine();
+ });
+ }
+
+ function changePassword($event) {
+ $mdDialog.show({
+ controller: 'ChangePasswordController',
+ controllerAs: 'vm',
+ templateUrl: changePasswordTemplate,
+ parent: angular.element($document[0].body),
+ fullscreen: true,
+ targetEvent: $event
+ }).then(function () {
+ }, function () {
+ });
+ }
+}
ui/src/app/profile/profile.routes.js 45(+45 -0)
diff --git a/ui/src/app/profile/profile.routes.js b/ui/src/app/profile/profile.routes.js
new file mode 100644
index 0000000..ec92fec
--- /dev/null
+++ b/ui/src/app/profile/profile.routes.js
@@ -0,0 +1,45 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import profileTemplate from './profile.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function ProfileRoutes($stateProvider) {
+
+ $stateProvider
+ .state('home.profile', {
+ url: '/profile',
+ module: 'private',
+ auth: ['SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER'],
+ views: {
+ "content@home": {
+ templateUrl: profileTemplate,
+ controllerAs: 'vm',
+ controller: 'ProfileController'
+ }
+ },
+ data: {
+ pageTitle: 'profile.profile'
+ },
+ ncyBreadcrumb: {
+ label: '{"icon": "account_circle", "label": "profile.profile"}'
+ }
+ });
+
+}
ui/src/app/profile/profile.tpl.html 56(+56 -0)
diff --git a/ui/src/app/profile/profile.tpl.html b/ui/src/app/profile/profile.tpl.html
new file mode 100644
index 0000000..4f24887
--- /dev/null
+++ b/ui/src/app/profile/profile.tpl.html
@@ -0,0 +1,56 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div layout="row" width="100%" layout-wrap>
+ <md-card flex-gt-sm="60" flex="100">
+ <md-card-title>
+ <md-card-title-text>
+ <span translate class="md-headline">profile.profile</span>
+ <span style='opacity: 0.7;'>{{ vm.profileUser.email }}</span>
+ </md-card-title-text>
+ </md-card-title>
+ <md-progress-linear md-mode="indeterminate" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-card-content>
+ <form name="theForm" ng-submit="vm.save()" tb-confirm-on-exit confirm-form="theForm">
+ <fieldset ng-disabled="loading">
+ <md-input-container class="md-block">
+ <label translate>user.email</label>
+ <input name="email" type="email" ng-model="vm.profileUser.email">
+ </md-input-container>
+ <md-input-container class="md-block">
+ <label translate>user.first-name</label>
+ <input name="firstName" ng-model="vm.profileUser.firstName">
+ </md-input-container>
+ <md-input-container class="md-block">
+ <label translate>user.last-name</label>
+ <input name="lastName" ng-model="vm.profileUser.lastName">
+ </md-input-container>
+ <md-button ng-disabled="loading" ng-click="vm.changePassword($event)"
+ class="md-raised md-primary">{{ 'profile.change-password' | translate }}
+ </md-button>
+ <div layout="row" layout-align="end center" width="100%" layout-wrap>
+ <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit"
+ class="md-raised md-primary">{{ 'action.save' | translate }}
+ </md-button>
+ </div>
+ </fieldset>
+ </form>
+ </md-card-content>
+ </md-card>
+</div>
+
ui/src/app/rule/add-rule.tpl.html 48(+48 -0)
diff --git a/ui/src/app/rule/add-rule.tpl.html b/ui/src/app/rule/add-rule.tpl.html
new file mode 100644
index 0000000..44f5b85
--- /dev/null
+++ b/ui/src/app/rule/add-rule.tpl.html
@@ -0,0 +1,48 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'rule.add' | translate }}" tb-help="'rules'" help-container-id="help-container">
+ <form name="theForm" ng-submit="vm.add()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>rule.add</h2>
+ <span flex></span>
+ <div id="help-container"></div>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <tb-rule rule="vm.item" is-edit="true" the-form="theForm"></tb-rule>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit"
+ class="md-raised md-primary">
+ {{ 'action.add' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+ translate }}
+ </md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
ui/src/app/rule/index.js 42(+42 -0)
diff --git a/ui/src/app/rule/index.js b/ui/src/app/rule/index.js
new file mode 100644
index 0000000..5af6ddc
--- /dev/null
+++ b/ui/src/app/rule/index.js
@@ -0,0 +1,42 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import uiRouter from 'angular-ui-router';
+import thingsboardGrid from '../components/grid.directive';
+import thingsboardPluginSelect from '../components/plugin-select.directive';
+import thingsboardComponent from '../component';
+import thingsboardEvent from '../event';
+import thingsboardApiRule from '../api/rule.service';
+import thingsboardApiPlugin from '../api/plugin.service';
+import thingsboardApiComponentDescriptor from '../api/component-descriptor.service';
+
+import RuleRoutes from './rule.routes';
+import RuleController from './rule.controller';
+import RuleDirective from './rule.directive';
+
+export default angular.module('thingsboard.rule', [
+ uiRouter,
+ thingsboardGrid,
+ thingsboardPluginSelect,
+ thingsboardComponent,
+ thingsboardEvent,
+ thingsboardApiRule,
+ thingsboardApiPlugin,
+ thingsboardApiComponentDescriptor
+])
+ .config(RuleRoutes)
+ .controller('RuleController', RuleController)
+ .directive('tbRule', RuleDirective)
+ .name;
ui/src/app/rule/rule.controller.js 170(+170 -0)
diff --git a/ui/src/app/rule/rule.controller.js b/ui/src/app/rule/rule.controller.js
new file mode 100644
index 0000000..00be85e
--- /dev/null
+++ b/ui/src/app/rule/rule.controller.js
@@ -0,0 +1,170 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import addRuleTemplate from './add-rule.tpl.html';
+import ruleCard from './rule-card.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function RuleController(ruleService, userService, $state, $stateParams, $filter, $translate, types) {
+
+ var ruleActionsList = [
+ {
+ onAction: function ($event, item) {
+ activateRule($event, item);
+ },
+ name: function() { return $translate.instant('action.activate') },
+ details: function() { return $translate.instant('rule.activate') },
+ icon: "play_arrow",
+ isEnabled: function(rule) {
+ return isRuleEditable(rule) && rule && rule.state === 'SUSPENDED';
+ }
+ },
+ {
+ onAction: function ($event, item) {
+ suspendRule($event, item);
+ },
+ name: function() { return $translate.instant('action.suspend') },
+ details: function() { return $translate.instant('rule.suspend') },
+ icon: "pause",
+ isEnabled: function(rule) {
+ return isRuleEditable(rule) && rule.state === 'ACTIVE';
+ }
+ },
+ {
+ onAction: function ($event, item) {
+ vm.grid.deleteItem($event, item);
+ },
+ name: function() { return $translate.instant('action.delete') },
+ details: function() { return $translate.instant('rule.delete') },
+ icon: "delete",
+ isEnabled: isRuleEditable
+ }
+ ];
+
+ var vm = this;
+
+ vm.types = types;
+
+ vm.ruleGridConfig = {
+
+ refreshParamsFunc: null,
+
+ deleteItemTitleFunc: deleteRuleTitle,
+ deleteItemContentFunc: deleteRuleText,
+ deleteItemsTitleFunc: deleteRulesTitle,
+ deleteItemsActionTitleFunc: deleteRulesActionTitle,
+ deleteItemsContentFunc: deleteRulesText,
+
+ fetchItemsFunc: fetchRules,
+ saveItemFunc: saveRule,
+ deleteItemFunc: deleteRule,
+
+ getItemTitleFunc: getRuleTitle,
+ itemCardTemplateUrl: ruleCard,
+ parentCtl: vm,
+
+ actionsList: ruleActionsList,
+
+ onGridInited: gridInited,
+
+ addItemTemplateUrl: addRuleTemplate,
+
+ addItemText: function() { return $translate.instant('rule.add-rule-text') },
+ noItemsText: function() { return $translate.instant('rule.no-rules-text') },
+ itemDetailsText: function() { return $translate.instant('rule.rule-details') },
+ isSelectionEnabled: isRuleEditable,
+ isDetailsReadOnly: function(rule) {
+ return !isRuleEditable(rule);
+ }
+ };
+
+ if (angular.isDefined($stateParams.items) && $stateParams.items !== null) {
+ vm.ruleGridConfig.items = $stateParams.items;
+ }
+
+ if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) {
+ vm.ruleGridConfig.topIndex = $stateParams.topIndex;
+ }
+
+ vm.activateRule = activateRule;
+ vm.suspendRule = suspendRule;
+
+ function deleteRuleTitle(rule) {
+ return $translate.instant('rule.delete-rule-title', {ruleName: rule.name});
+ }
+
+ function deleteRuleText() {
+ return $translate.instant('rule.delete-rule-text');
+ }
+
+ function deleteRulesTitle(selectedCount) {
+ return $translate.instant('rule.delete-rules-title', {count: selectedCount}, 'messageformat');
+ }
+
+ function deleteRulesActionTitle(selectedCount) {
+ return $translate.instant('rule.delete-rules-action-title', {count: selectedCount}, 'messageformat');
+ }
+
+ function deleteRulesText() {
+ return $translate.instant('rule.delete-rules-text');
+ }
+
+ function gridInited(grid) {
+ vm.grid = grid;
+ }
+
+ function fetchRules(pageLink) {
+ return ruleService.getAllRules(pageLink);
+ }
+
+ function saveRule(rule) {
+ return ruleService.saveRule(rule);
+ }
+
+ function deleteRule(ruleId) {
+ return ruleService.deleteRule(ruleId);
+ }
+
+ function getRuleTitle(rule) {
+ return rule ? rule.name : '';
+ }
+
+ function isRuleEditable(rule) {
+ if (userService.getAuthority() === 'TENANT_ADMIN') {
+ return rule && rule.tenantId.id != types.id.nullUid;
+ } else {
+ return userService.getAuthority() === 'SYS_ADMIN';
+ }
+ }
+
+ function activateRule(event, rule) {
+ ruleService.activateRule(rule.id.id).then(function () {
+ vm.grid.refreshList();
+ }, function () {
+ });
+ }
+
+ function suspendRule(event, rule) {
+ ruleService.suspendRule(rule.id.id).then(function () {
+ vm.grid.refreshList();
+ }, function () {
+ });
+ }
+
+}
ui/src/app/rule/rule.directive.js 183(+183 -0)
diff --git a/ui/src/app/rule/rule.directive.js b/ui/src/app/rule/rule.directive.js
new file mode 100644
index 0000000..362e1a1
--- /dev/null
+++ b/ui/src/app/rule/rule.directive.js
@@ -0,0 +1,183 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './rule.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import ruleFieldsetTemplate from './rule-fieldset.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function RuleDirective($compile, $templateCache, $mdDialog, $document, $q, pluginService, componentDialogService, componentDescriptorService, types) {
+ var linker = function (scope, element) {
+ var template = $templateCache.get(ruleFieldsetTemplate);
+ element.html(template);
+
+ scope.plugin = null;
+ scope.types = types;
+ scope.filters = [];
+
+ scope.addFilter = function($event) {
+ componentDialogService.openComponentDialog($event, true, false,
+ 'rule.filter', types.componentType.filter).then(
+ function success(filter) {
+ scope.filters.push({ value: filter });
+ },
+ function fail() {}
+ );
+ }
+
+ scope.removeFilter = function ($event, filter) {
+ var index = scope.filters.indexOf(filter);
+ if (index > -1) {
+ scope.filters.splice(index, 1);
+ }
+ };
+
+ scope.addProcessor = function($event) {
+ componentDialogService.openComponentDialog($event, true, false,
+ 'rule.processor', types.componentType.processor).then(
+ function success(processor) {
+ scope.rule.processor = processor;
+ },
+ function fail() {}
+ );
+ }
+
+ scope.removeProcessor = function() {
+ if (scope.rule.processor) {
+ scope.rule.processor = null;
+ }
+ }
+
+ scope.addAction = function($event) {
+ componentDialogService.openComponentDialog($event, true, false,
+ 'rule.plugin-action', types.componentType.action, scope.plugin.clazz).then(
+ function success(action) {
+ scope.rule.action = action;
+ },
+ function fail() {}
+ );
+ }
+
+ scope.removeAction = function() {
+ if (scope.rule.action) {
+ scope.rule.action = null;
+ }
+ }
+
+ scope.updateValidity = function () {
+ if (scope.rule) {
+ var valid = scope.rule.filters && scope.rule.filters.length > 0;
+ scope.theForm.$setValidity('filters', valid);
+ valid = angular.isDefined(scope.rule.pluginToken) && scope.rule.pluginToken != null;
+ scope.theForm.$setValidity('plugin', valid);
+ valid = angular.isDefined(scope.rule.action) && scope.rule.action != null;
+ scope.theForm.$setValidity('action', valid);
+ }
+ };
+
+ scope.$watch('rule', function(newVal, prevVal) {
+ if (newVal) {
+ if (!scope.rule.filters) {
+ scope.rule.filters = [];
+ }
+ if (!angular.equals(newVal, prevVal)) {
+ if (scope.rule.pluginToken) {
+ pluginService.getPluginByToken(scope.rule.pluginToken).then(
+ function success(plugin) {
+ scope.plugin = plugin;
+ },
+ function fail() {}
+ );
+ } else {
+ scope.plugin = null;
+ }
+ if (scope.filters) {
+ scope.filters.splice(0, scope.filters.length);
+ } else {
+ scope.filters = [];
+ }
+ if (scope.rule.filters) {
+ for (var i in scope.rule.filters) {
+ scope.filters.push({value: scope.rule.filters[i]});
+ }
+ }
+ }
+ scope.updateValidity();
+ }
+ }
+ );
+
+ scope.$watch('filters', function (newVal, prevVal) {
+ if (scope.rule && scope.isEdit && !angular.equals(newVal, prevVal)) {
+ if (scope.rule.filters) {
+ scope.rule.filters.splice(0, scope.rule.filters.length);
+ } else {
+ scope.rule.filters = [];
+ }
+ if (scope.filters) {
+ for (var i in scope.filters) {
+ scope.rule.filters.push(scope.filters[i].value);
+ }
+ }
+ scope.theForm.$setDirty();
+ scope.updateValidity();
+ }
+ }, true);
+
+ scope.$watch('plugin', function(newVal, prevVal) {
+ if (scope.rule && scope.isEdit && !angular.equals(newVal, prevVal)) {
+ if (newVal) {
+ scope.rule.pluginToken = scope.plugin.apiToken;
+ } else {
+ scope.rule.pluginToken = null;
+ }
+ scope.rule.action = null;
+ scope.updateValidity();
+ }
+ }, true);
+
+ scope.$watch('rule.processor', function(newVal, prevVal) {
+ if (scope.rule && scope.isEdit && !angular.equals(newVal, prevVal)) {
+ scope.theForm.$setDirty();
+ }
+ }, true);
+
+ scope.$watch('rule.action', function(newVal, prevVal) {
+ if (scope.rule && scope.isEdit && !angular.equals(newVal, prevVal)) {
+ scope.theForm.$setDirty();
+ scope.updateValidity();
+ }
+ }, true);
+
+ $compile(element.contents())(scope);
+ }
+ return {
+ restrict: "E",
+ link: linker,
+ scope: {
+ rule: '=',
+ isEdit: '=',
+ isReadOnly: '=',
+ theForm: '=',
+ onActivateRule: '&',
+ onSuspendRule: '&',
+ onDeleteRule: '&'
+ }
+ };
+}
ui/src/app/rule/rule.routes.js 46(+46 -0)
diff --git a/ui/src/app/rule/rule.routes.js b/ui/src/app/rule/rule.routes.js
new file mode 100644
index 0000000..6b294c4
--- /dev/null
+++ b/ui/src/app/rule/rule.routes.js
@@ -0,0 +1,46 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import rulesTemplate from './rules.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function RuleRoutes($stateProvider) {
+
+ $stateProvider
+ .state('home.rules', {
+ url: '/rules',
+ params: {'topIndex': 0},
+ module: 'private',
+ auth: ['SYS_ADMIN', 'TENANT_ADMIN'],
+ views: {
+ "content@home": {
+ templateUrl: rulesTemplate,
+ controllerAs: 'vm',
+ controller: 'RuleController'
+ }
+ },
+ data: {
+ searchEnabled: true,
+ pageTitle: 'rule.rules'
+ },
+ ncyBreadcrumb: {
+ label: '{"icon": "settings_ethernet", "label": "rule.rules"}'
+ }
+ });
+}
ui/src/app/rule/rule.scss 55(+55 -0)
diff --git a/ui/src/app/rule/rule.scss b/ui/src/app/rule/rule.scss
new file mode 100644
index 0000000..d448592
--- /dev/null
+++ b/ui/src/app/rule/rule.scss
@@ -0,0 +1,55 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+.tb-rule {
+ min-width: 600px;
+ tb-plugin-select {
+ padding: 2px;
+ margin: 18px 0;
+ }
+}
+
+.tb-filter {
+ min-width: 500px;
+ min-height: 300px;
+}
+
+.tb-filters ul[dnd-list],
+.tb-filters ul[dnd-list] > li {
+ position: relative;
+}
+
+.tb-filters ul[dnd-list] {
+ min-height: 42px;
+ padding-left: 0px;
+}
+
+.tb-filters ul[dnd-list] .dndDraggingSource {
+ display: none;
+}
+
+.tb-filters ul[dnd-list] .dndPlaceholder {
+ display: block;
+ background-color: #ddd;
+ min-height: 42px;
+}
+
+.tb-filters ul[dnd-list] li {
+ display: block;
+}
+
+.tb-filters .handle {
+ cursor: move;
+}
ui/src/app/rule/rule-card.tpl.html 19(+19 -0)
diff --git a/ui/src/app/rule/rule-card.tpl.html b/ui/src/app/rule/rule-card.tpl.html
new file mode 100644
index 0000000..4fbc5d0
--- /dev/null
+++ b/ui/src/app/rule/rule-card.tpl.html
@@ -0,0 +1,19 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div class="tb-uppercase" ng-if="item && parentCtl.types.id.nullUid === item.tenantId.id" translate>rule.system</div>
+<div class="tb-uppercase" translate>{{item && item.state === 'ACTIVE' ? 'rule.active' : 'rule.suspended'}}</div>
ui/src/app/rule/rule-fieldset.tpl.html 200(+200 -0)
diff --git a/ui/src/app/rule/rule-fieldset.tpl.html b/ui/src/app/rule/rule-fieldset.tpl.html
new file mode 100644
index 0000000..e5be2ff
--- /dev/null
+++ b/ui/src/app/rule/rule-fieldset.tpl.html
@@ -0,0 +1,200 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-button ng-click="onActivateRule({event: $event})" ng-show="!isEdit && !isReadOnly && rule.state === 'SUSPENDED'" class="md-raised md-primary">{{ 'rule.activate' | translate }}</md-button>
+<md-button ng-click="onSuspendRule({event: $event})" ng-show="!isEdit && !isReadOnly && rule.state === 'ACTIVE'" class="md-raised md-primary">{{ 'rule.suspend' | translate }}</md-button>
+<md-button ng-click="onDeleteRule({event: $event})" ng-show="!isEdit && !isReadOnly" class="md-raised md-primary">{{ 'rule.delete' | translate }}</md-button>
+
+<md-content class="md-padding tb-rule" layout="column">
+ <fieldset ng-disabled="loading || !isEdit || isReadOnly">
+ <md-input-container class="md-block">
+ <label translate>rule.name</label>
+ <input required name="name" ng-model="rule.name">
+ <div ng-messages="theForm.name.$error">
+ <div translate ng-message="required">rule.name-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container class="md-block">
+ <label translate>rule.description</label>
+ <textarea ng-model="rule.additionalInfo.description" rows="2"></textarea>
+ </md-input-container>
+ </fieldset>
+ <v-accordion id="filters-accordion" class="vAccordion--default"
+ ng-class="{'tb-readonly-label' : (!isEdit || isReadOnly)}">
+ <v-pane id="filters-pane" expanded="true">
+ <v-pane-header>
+ {{ 'rule.filters' | translate }}
+ </v-pane-header>
+ <v-pane-content>
+ <div ng-if="rule.filters.length === 0">
+ <span translate layout-align="center center"
+ class="tb-prompt">rule.add-filter-prompt</span>
+ </div>
+ <div ng-if="rule.filters.length > 0">
+ <div flex layout="row" layout-align="start center">
+ <span ng-if="isEdit && !isReadOnly" style="min-width: 40px; margin: 0 6px;"></br></span>
+ <span flex="5"></span>
+ <div flex layout="row" layout-align="start center"
+ style="padding: 0 0 0 10px; margin: 5px;">
+ <span translate flex="50">rule.filter-name</span>
+ <span translate flex="50">rule.filter-type</span>
+ <span style="min-width: 80px; margin: 0 12px;"></br></span>
+ </div>
+ </div>
+ <div class="tb-filters" style="max-height: 300px; overflow: auto; padding-bottom: 15px;">
+ <ul dnd-list="filters" dnd-disable-if="!isEdit || isReadOnly">
+ <li ng-repeat="filter in filters"
+ dnd-draggable="filter"
+ dnd-moved="filters.splice($index, 1)"
+ dnd-disable-if="!isEdit || isReadOnly"
+ dnd-effect-allowed="move">
+ <div flex layout="row" layout-align="start center">
+ <md-button ng-if="isEdit && !isReadOnly" dnd-handle class="md-icon-button md-primary handle"
+ style="min-width: 40px;"
+ aria-label="{{ 'action.drag' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'action.drag' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.drag' | translate }}"
+ class="material-icons">
+ drag_handle
+ </md-icon>
+ </md-button>
+ <dnd-nodrag flex layout="row" layout-align="start center">
+ <span flex="5">{{$index + 1}}.</span>
+ <tb-component flex
+ component="filter.value"
+ type="types.componentType.filter"
+ title="rule.filter"
+ read-only="!isEdit || isReadOnly"
+ on-remove-component="removeFilter(event, filter)">
+ </tb-component>
+ </dnd-nodrag>
+ </div>
+ </li>
+ </ul>
+ </div>
+ </div>
+ <div ng-if="isEdit && !isReadOnly" flex layout="row" layout-align="start center">
+ <md-button ng-disabled="loading" class="md-primary md-raised"
+ ng-click="addFilter($event)" aria-label="{{ 'action.add' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'rule.add-filter' | translate }}
+ </md-tooltip>
+ <md-icon class="material-icons">add</md-icon>
+ <span translate>action.add</span>
+ </md-button>
+ </div>
+ </v-pane-content>
+ </v-pane>
+ </v-accordion>
+ <v-accordion id="processor-accordion" class="vAccordion--default"
+ ng-class="{'tb-readonly-label' : (!isEdit || isReadOnly)}">
+ <v-pane id="processor-pane" expanded="true">
+ <v-pane-header>
+ {{ 'rule.processor' | translate }}
+ </v-pane-header>
+ <v-pane-content>
+ <div ng-if="rule.processor && rule.processor != null">
+ <div flex layout="row" layout-align="start center"
+ style="padding: 0 0 0 10px; margin: 5px;">
+ <span translate flex="50">rule.processor-name</span>
+ <span translate flex="50">rule.processor-type</span>
+ <span style="min-width: 80px; margin: 0 12px;"></br></span>
+ </div>
+ <div flex layout="row" layout-align="start center" style="padding-bottom: 15px;">
+ <tb-component flex
+ component="rule.processor"
+ type="types.componentType.processor"
+ title="rule.processor"
+ read-only="!isEdit || isReadOnly"
+ on-remove-component="removeProcessor(event)">
+ </tb-component>
+ </div>
+ </div>
+ <div ng-if="!rule.processor || rule.processor == null">
+ <span ng-if="!isEdit || isReadOnly" translate layout-align="center center"
+ class="tb-prompt">rule.no-processor-configured</span>
+ <div ng-if="isEdit && !isReadOnly" flex layout="row" layout-align="start center">
+ <md-button ng-disabled="loading" class="md-primary md-raised"
+ ng-click="addProcessor($event)" aria-label="{{ 'action.create' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'rule.create-processor' | translate }}
+ </md-tooltip>
+ <md-icon class="material-icons">add</md-icon>
+ <span translate>action.create</span>
+ </md-button>
+ </div>
+ </div>
+ </v-pane-content>
+ </v-pane>
+ </v-accordion>
+ <fieldset ng-disabled="loading || !isEdit || isReadOnly">
+ <md-input-container ng-if="!isEdit || isReadOnly" flex class="md-block">
+ <label translate>plugin.plugin</label>
+ <input required name="name" ng-model="plugin.name">
+ </md-input-container>
+ <tb-plugin-select ng-show="isEdit && !isReadOnly" flex
+ ng-model="plugin"
+ tb-required="true"
+ the-form="theForm"
+ plugins-scope="action">
+ </tb-plugin-select>
+ </fieldset>
+ <v-accordion ng-if="plugin != null" id="plugin-action-accordion" class="vAccordion--default"
+ ng-class="{'tb-readonly-label' : (!isEdit || isReadOnly)}">
+ <v-pane id="plugin-action-pane" expanded="true">
+ <v-pane-header>
+ {{ 'rule.plugin-action' | translate }}
+ </v-pane-header>
+ <v-pane-content>
+ <div ng-if="rule.action && rule.action != null">
+ <div flex layout="row" layout-align="start center"
+ style="padding: 0 0 0 10px; margin: 5px;">
+ <span translate flex="50">rule.action-name</span>
+ <span translate flex="50">rule.action-type</span>
+ <span style="min-width: 80px; margin: 0 12px;"></br></span>
+ </div>
+ <div flex layout="row" layout-align="start center" style="padding-bottom: 15px;">
+ <tb-component flex
+ component="rule.action"
+ type="types.componentType.action"
+ plugin-clazz="plugin.clazz"
+ title="rule.plugin-action"
+ read-only="!isEdit || isReadOnly"
+ on-remove-component="removeAction(event)">
+ </tb-component>
+ </div>
+ </div>
+ <div ng-if="!rule.action || rule.action == null">
+ <span translate layout-align="center center"
+ class="tb-prompt">rule.create-action-prompt</span>
+ <div ng-if="isEdit && !isReadOnly" flex layout="row" layout-align="start center">
+ <md-button ng-disabled="loading" class="md-primary md-raised"
+ ng-click="addAction($event)" aria-label="{{ 'action.create' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'rule.create-action' | translate }}
+ </md-tooltip>
+ <md-icon class="material-icons">add</md-icon>
+ <span translate>action.create</span>
+ </md-button>
+ </div>
+ </div>
+ </v-pane-content>
+ </v-pane>
+ </v-accordion>
+</md-content>
ui/src/app/rule/rules.tpl.html 42(+42 -0)
diff --git a/ui/src/app/rule/rules.tpl.html b/ui/src/app/rule/rules.tpl.html
new file mode 100644
index 0000000..2463540
--- /dev/null
+++ b/ui/src/app/rule/rules.tpl.html
@@ -0,0 +1,42 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<tb-grid grid-configuration="vm.ruleGridConfig">
+ <details-buttons tb-help="'rules'" help-container-id="help-container">
+ <div id="help-container"></div>
+ </details-buttons>
+ <md-tabs ng-class="{'tb-headless': vm.grid.detailsConfig.isDetailsEditMode}"
+ id="tabs" md-border-bottom flex class="tb-absolute-fill">
+ <md-tab label="{{ 'rule.details' | translate }}">
+ <tb-rule rule="vm.grid.operatingItem()"
+ is-edit="vm.grid.detailsConfig.isDetailsEditMode"
+ is-read-only="vm.grid.isDetailsReadOnly(vm.grid.operatingItem())"
+ the-form="vm.grid.detailsForm"
+ on-activate-rule="vm.activateRule(event, vm.grid.detailsConfig.currentItem)"
+ on-suspend-rule="vm.suspendRule(event, vm.grid.detailsConfig.currentItem)"
+ on-delete-rule="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-rule>
+ </md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'rule.events' | translate }}">
+ <tb-event-table flex entity-type="vm.types.entityType.rule"
+ entity-id="vm.grid.operatingItem().id.id"
+ tenant-id="vm.grid.operatingItem().tenantId.id"
+ default-event-type="{{vm.types.eventType.lcEvent.value}}"
+ disabled-event-types="{{vm.types.eventType.alarm.value}}">
+ </tb-event-table>
+ </md-tab>
+ </md-tabs>
+</tb-grid>
ui/src/app/services/error-toast.tpl.html 25(+25 -0)
diff --git a/ui/src/app/services/error-toast.tpl.html b/ui/src/app/services/error-toast.tpl.html
new file mode 100644
index 0000000..4c99df3
--- /dev/null
+++ b/ui/src/app/services/error-toast.tpl.html
@@ -0,0 +1,25 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-toast class="tb-error-toast">
+ <div class="md-toast-content">
+ <div class="md-toast-text" ng-bind-html="vm.message"></div>
+ <md-button ng-click="vm.closeToast()">
+ {{ 'action.close' | translate }}
+ </md-button>
+ </div>
+</md-toast>
ui/src/app/services/menu.service.js 321(+321 -0)
diff --git a/ui/src/app/services/menu.service.js b/ui/src/app/services/menu.service.js
new file mode 100644
index 0000000..524602a
--- /dev/null
+++ b/ui/src/app/services/menu.service.js
@@ -0,0 +1,321 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import thingsboardApiUser from '../api/user.service';
+
+export default angular.module('thingsboard.menu', [thingsboardApiUser])
+ .factory('menu', Menu)
+ .name;
+
+/*@ngInject*/
+function Menu(userService, $state, $rootScope) {
+
+ var authority = '';
+ var sections = [];
+ var homeSections = [];
+
+ if (userService.isUserLoaded() === true) {
+ buildMenu();
+ }
+
+ var authenticatedHandle = $rootScope.$on('authenticated', function () {
+ buildMenu();
+ });
+
+ var service = {
+ authenticatedHandle: authenticatedHandle,
+ getHomeSections: getHomeSections,
+ getSections: getSections,
+ sectionHeight: sectionHeight,
+ sectionActive: sectionActive
+ }
+
+ return service;
+
+ function getSections() {
+ return sections;
+ }
+
+ function getHomeSections() {
+ return homeSections;
+ }
+
+ function buildMenu() {
+ var user = userService.getCurrentUser();
+ if (user) {
+ if (authority !== user.authority) {
+ sections = [];
+ authority = user.authority;
+ if (authority === 'SYS_ADMIN') {
+ sections = [
+ {
+ name: 'home.home',
+ type: 'link',
+ state: 'home',
+ icon: 'home'
+ },
+ {
+ name: 'plugin.plugins',
+ type: 'link',
+ state: 'home.plugins',
+ icon: 'extension'
+ },
+ {
+ name: 'rule.rules',
+ type: 'link',
+ state: 'home.rules',
+ icon: 'settings_ethernet'
+ },
+ {
+ name: 'tenant.tenants',
+ type: 'link',
+ state: 'home.tenants',
+ icon: 'supervisor_account'
+ },
+ {
+ name: 'widget.widget-library',
+ type: 'link',
+ state: 'home.widgets-bundles',
+ icon: 'now_widgets'
+ },
+ {
+ name: 'admin.system-settings',
+ type: 'toggle',
+ state: 'home.settings',
+ height: '80px',
+ icon: 'settings',
+ pages: [
+ {
+ name: 'admin.general',
+ type: 'link',
+ state: 'home.settings.general',
+ icon: 'settings_applications'
+ },
+ {
+ name: 'admin.outgoing-mail',
+ type: 'link',
+ state: 'home.settings.outgoing-mail',
+ icon: 'mail'
+ }
+ ]
+ }];
+ homeSections =
+ [{
+ name: 'rule-plugin.management',
+ places: [
+ {
+ name: 'plugin.plugins',
+ icon: 'extension',
+ state: 'home.plugins'
+ },
+ {
+ name: 'rule.rules',
+ icon: 'settings_ethernet',
+ state: 'home.rules'
+ }
+ ]
+ },
+ {
+ name: 'tenant.management',
+ places: [
+ {
+ name: 'tenant.tenants',
+ icon: 'supervisor_account',
+ state: 'home.tenants'
+ }
+ ]
+ },
+ {
+ name: 'widget.management',
+ places: [
+ {
+ name: 'widget.widget-library',
+ icon: 'now_widgets',
+ state: 'home.widgets-bundles'
+ }
+ ]
+ },
+ {
+ name: 'admin.system-settings',
+ places: [
+ {
+ name: 'admin.general',
+ icon: 'settings_applications',
+ state: 'home.settings.general'
+ },
+ {
+ name: 'admin.outgoing-mail',
+ icon: 'mail',
+ state: 'home.settings.outgoing-mail'
+ }
+ ]
+ }];
+ } else if (authority === 'TENANT_ADMIN') {
+ sections = [
+ {
+ name: 'home.home',
+ type: 'link',
+ state: 'home',
+ icon: 'home'
+ },
+ {
+ name: 'plugin.plugins',
+ type: 'link',
+ state: 'home.plugins',
+ icon: 'extension'
+ },
+ {
+ name: 'rule.rules',
+ type: 'link',
+ state: 'home.rules',
+ icon: 'settings_ethernet'
+ },
+ {
+ name: 'customer.customers',
+ type: 'link',
+ state: 'home.customers',
+ icon: 'supervisor_account'
+ },
+ {
+ name: 'device.devices',
+ type: 'link',
+ state: 'home.devices',
+ icon: 'devices_other'
+ },
+ {
+ name: 'widget.widget-library',
+ type: 'link',
+ state: 'home.widgets-bundles',
+ icon: 'now_widgets'
+ },
+ {
+ name: 'dashboard.dashboards',
+ type: 'link',
+ state: 'home.dashboards',
+ icon: 'dashboards'
+ }];
+
+ homeSections =
+ [{
+ name: 'rule-plugin.management',
+ places: [
+ {
+ name: 'plugin.plugins',
+ icon: 'extension',
+ state: 'home.plugins'
+ },
+ {
+ name: 'rule.rules',
+ icon: 'settings_ethernet',
+ state: 'home.rules'
+ }
+ ]
+ },
+ {
+ name: 'customer.management',
+ places: [
+ {
+ name: 'customer.customers',
+ icon: 'supervisor_account',
+ state: 'home.customers'
+ }
+ ]
+ },
+ {
+ name: 'device.management',
+ places: [
+ {
+ name: 'device.devices',
+ icon: 'devices_other',
+ state: 'home.devices'
+ }
+ ]
+ },
+ {
+ name: 'dashboard.management',
+ places: [
+ {
+ name: 'widget.widget-library',
+ icon: 'now_widgets',
+ state: 'home.widgets-bundles'
+ },
+ {
+ name: 'dashboard.dashboards',
+ icon: 'dashboard',
+ state: 'home.dashboards'
+ }
+ ]
+ }];
+
+ } else if (authority === 'CUSTOMER_USER') {
+ sections = [
+ {
+ name: 'home.home',
+ type: 'link',
+ state: 'home',
+ icon: 'home'
+ },
+ {
+ name: 'device.devices',
+ type: 'link',
+ state: 'home.devices',
+ icon: 'devices_other'
+ },
+ {
+ name: 'dashboard.dashboards',
+ type: 'link',
+ state: 'home.dashboards',
+ icon: 'dashboard'
+ }];
+
+ homeSections =
+ [{
+ name: 'device.view-devices',
+ places: [
+ {
+ name: 'device.devices',
+ icon: 'devices_other',
+ state: 'home.devices'
+ }
+ ]
+ },
+ {
+ name: 'dashboard.view-dashboards',
+ places: [
+ {
+ name: 'dashboard.dashboards',
+ icon: 'dashboard',
+ state: 'home.dashboards'
+ }
+ ]
+ }];
+ }
+ }
+ }
+ }
+
+ function sectionHeight(section) {
+ if ($state.includes(section.state)) {
+ return section.height;
+ } else {
+ return '0px';
+ }
+ }
+
+ function sectionActive(section) {
+ return $state.includes(section.state);
+ }
+
+}
\ No newline at end of file
ui/src/app/services/success-toast.tpl.html 25(+25 -0)
diff --git a/ui/src/app/services/success-toast.tpl.html b/ui/src/app/services/success-toast.tpl.html
new file mode 100644
index 0000000..23c27a9
--- /dev/null
+++ b/ui/src/app/services/success-toast.tpl.html
@@ -0,0 +1,25 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-toast class="tb-success-toast">
+ <div class="md-toast-content">
+ <div class="md-toast-text" ng-bind-html="vm.message"></div>
+ <md-button ng-click="vm.closeToast()">
+ {{ 'action.close' | translate }}
+ </md-button>
+ </div>
+</md-toast>
ui/src/app/services/toast.controller.js 27(+27 -0)
diff --git a/ui/src/app/services/toast.controller.js b/ui/src/app/services/toast.controller.js
new file mode 100644
index 0000000..72e2c14
--- /dev/null
+++ b/ui/src/app/services/toast.controller.js
@@ -0,0 +1,27 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*@ngInject*/
+export default function ToastController($mdToast, message) {
+ var vm = this;
+ vm.message = message;
+
+ vm.closeToast = closeToast;
+
+ function closeToast() {
+ $mdToast.hide();
+ }
+
+}
ui/src/app/services/toast.js 24(+24 -0)
diff --git a/ui/src/app/services/toast.js b/ui/src/app/services/toast.js
new file mode 100644
index 0000000..4e9153b
--- /dev/null
+++ b/ui/src/app/services/toast.js
@@ -0,0 +1,24 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './toast.scss';
+
+import Toast from './toast.service';
+import ToastController from './toast.controller';
+
+export default angular.module('thingsboard.toast', [])
+ .factory('toast', Toast)
+ .controller('ToastController', ToastController)
+ .name;
ui/src/app/services/toast.scss 26(+26 -0)
diff --git a/ui/src/app/services/toast.scss b/ui/src/app/services/toast.scss
new file mode 100644
index 0000000..a4c2160
--- /dev/null
+++ b/ui/src/app/services/toast.scss
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+md-toast.tb-success-toast .md-toast-content {
+ font-size: 18px !important;
+ background-color: green;
+ height: 100%;
+}
+
+md-toast.tb-error-toast .md-toast-content {
+ font-size: 18px !important;
+ background-color: maroon;
+ height: 100%;
+}
ui/src/app/services/toast.service.js 85(+85 -0)
diff --git a/ui/src/app/services/toast.service.js b/ui/src/app/services/toast.service.js
new file mode 100644
index 0000000..7f6e4f8
--- /dev/null
+++ b/ui/src/app/services/toast.service.js
@@ -0,0 +1,85 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import successToast from './success-toast.tpl.html';
+import errorToast from './error-toast.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function Toast($mdToast, $document) {
+
+ var showing = false;
+
+ var service = {
+ showSuccess: showSuccess,
+ showError: showError,
+ hide: hide
+ }
+
+ return service;
+
+ function showSuccess(successMessage, delay, toastParent, position) {
+ if (!toastParent) {
+ toastParent = angular.element($document[0].getElementById('toast-parent'));
+ }
+ if (!position) {
+ position = 'top left';
+ }
+ $mdToast.show({
+ hideDelay: delay || 0,
+ position: position,
+ controller: 'ToastController',
+ controllerAs: 'vm',
+ templateUrl: successToast,
+ locals: {message: successMessage},
+ parent: toastParent
+ });
+ }
+
+ function showError(errorMessage, toastParent, position) {
+ if (!showing) {
+ if (!toastParent) {
+ toastParent = angular.element($document[0].getElementById('toast-parent'));
+ }
+ if (!position) {
+ position = 'top left';
+ }
+ showing = true;
+ $mdToast.show({
+ hideDelay: 0,
+ position: position,
+ controller: 'ToastController',
+ controllerAs: 'vm',
+ templateUrl: errorToast,
+ locals: {message: errorMessage},
+ parent: toastParent
+ }).then(function hide() {
+ showing = false;
+ }, function cancel() {
+ showing = false;
+ });
+ }
+ }
+
+ function hide() {
+ if (showing) {
+ $mdToast.hide();
+ }
+ }
+
+}
\ No newline at end of file
ui/src/app/tenant/add-tenant.tpl.html 48(+48 -0)
diff --git a/ui/src/app/tenant/add-tenant.tpl.html b/ui/src/app/tenant/add-tenant.tpl.html
new file mode 100644
index 0000000..814a5b7
--- /dev/null
+++ b/ui/src/app/tenant/add-tenant.tpl.html
@@ -0,0 +1,48 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'tenant.add' | translate }}" tb-help="'tenants'" help-container-id="help-container">
+ <form name="theForm" ng-submit="vm.add()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>tenant.add</h2>
+ <span flex></span>
+ <div id="help-container"></div>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <tb-tenant tenant="vm.item" is-edit="true" the-form="theForm"></tb-tenant>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit"
+ class="md-raised md-primary">
+ {{ 'action.add' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+ translate }}
+ </md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
ui/src/app/tenant/index.js 36(+36 -0)
diff --git a/ui/src/app/tenant/index.js b/ui/src/app/tenant/index.js
new file mode 100644
index 0000000..6c70c40
--- /dev/null
+++ b/ui/src/app/tenant/index.js
@@ -0,0 +1,36 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import uiRouter from 'angular-ui-router';
+import thingsboardGrid from '../components/grid.directive';
+import thingsboardApiTenant from '../api/tenant.service';
+import thingsboardContact from '../components/contact.directive';
+import thingsboardContactShort from '../components/contact-short.filter';
+
+import TenantRoutes from './tenant.routes';
+import TenantController from './tenant.controller';
+import TenantDirective from './tenant.directive';
+
+export default angular.module('thingsboard.tenant', [
+ uiRouter,
+ thingsboardGrid,
+ thingsboardApiTenant,
+ thingsboardContact,
+ thingsboardContactShort
+])
+ .config(TenantRoutes)
+ .controller('TenantController', TenantController)
+ .directive('tbTenant', TenantDirective)
+ .name;
ui/src/app/tenant/tenant.controller.js 132(+132 -0)
diff --git a/ui/src/app/tenant/tenant.controller.js b/ui/src/app/tenant/tenant.controller.js
new file mode 100644
index 0000000..07e737f
--- /dev/null
+++ b/ui/src/app/tenant/tenant.controller.js
@@ -0,0 +1,132 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import addTenantTemplate from './add-tenant.tpl.html';
+import tenantCard from './tenant-card.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function TenantController(tenantService, $state, $stateParams, $translate) {
+
+ var tenantActionsList = [
+ {
+ onAction: function ($event, item) {
+ openTenantUsers($event, item);
+ },
+ name: function() { return $translate.instant('tenant.admins') },
+ details: function() { return $translate.instant('tenant.manage-tenant-admins') },
+ icon: "account_circle"
+ },
+ {
+ onAction: function ($event, item) {
+ vm.grid.deleteItem($event, item);
+ },
+ name: function() { return $translate.instant('action.delete') },
+ details: function() { return $translate.instant('tenant.delete') },
+ icon: "delete"
+ }
+ ];
+
+ var vm = this;
+
+ vm.tenantGridConfig = {
+
+ refreshParamsFunc: null,
+
+ deleteItemTitleFunc: deleteTenantTitle,
+ deleteItemContentFunc: deleteTenantText,
+ deleteItemsTitleFunc: deleteTenantsTitle,
+ deleteItemsActionTitleFunc: deleteTenantsActionTitle,
+ deleteItemsContentFunc: deleteTenantsText,
+
+ fetchItemsFunc: fetchTenants,
+ saveItemFunc: saveTenant,
+ deleteItemFunc: deleteTenant,
+
+ getItemTitleFunc: getTenantTitle,
+
+ itemCardTemplateUrl: tenantCard,
+
+ actionsList: tenantActionsList,
+
+ onGridInited: gridInited,
+
+ addItemTemplateUrl: addTenantTemplate,
+
+ addItemText: function() { return $translate.instant('tenant.add-tenant-text') },
+ noItemsText: function() { return $translate.instant('tenant.no-tenants-text') },
+ itemDetailsText: function() { return $translate.instant('tenant.tenant-details') }
+ };
+
+ if (angular.isDefined($stateParams.items) && $stateParams.items !== null) {
+ vm.tenantGridConfig.items = $stateParams.items;
+ }
+
+ if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) {
+ vm.tenantGridConfig.topIndex = $stateParams.topIndex;
+ }
+
+ vm.openTenantUsers = openTenantUsers;
+
+ function deleteTenantTitle(tenant) {
+ return $translate.instant('tenant.delete-tenant-title', {tenantTitle: tenant.title});
+ }
+
+ function deleteTenantText() {
+ return $translate.instant('tenant.delete-tenant-text');
+ }
+
+ function deleteTenantsTitle(selectedCount) {
+ return $translate.instant('tenant.delete-tenants-title', {count: selectedCount}, 'messageformat');
+ }
+
+ function deleteTenantsActionTitle(selectedCount) {
+ return $translate.instant('tenant.delete-tenants-action-title', {count: selectedCount}, 'messageformat');
+ }
+
+ function deleteTenantsText() {
+ return $translate.instant('tenant.delete-tenants-text');
+ }
+
+ function gridInited(grid) {
+ vm.grid = grid;
+ }
+
+ function fetchTenants(pageLink) {
+ return tenantService.getTenants(pageLink);
+ }
+
+ function saveTenant(tenant) {
+ return tenantService.saveTenant(tenant);
+ }
+
+ function deleteTenant(tenantId) {
+ return tenantService.deleteTenant(tenantId);
+ }
+
+ function getTenantTitle(tenant) {
+ return tenant ? tenant.title : '';
+ }
+
+ function openTenantUsers($event, tenant) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ $state.go('home.tenants.users', {tenantId: tenant.id.id});
+ }
+}
ui/src/app/tenant/tenant.directive.js 40(+40 -0)
diff --git a/ui/src/app/tenant/tenant.directive.js b/ui/src/app/tenant/tenant.directive.js
new file mode 100644
index 0000000..ef8ce8f
--- /dev/null
+++ b/ui/src/app/tenant/tenant.directive.js
@@ -0,0 +1,40 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import tenantFieldsetTemplate from './tenant-fieldset.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function TenantDirective($compile, $templateCache) {
+ var linker = function (scope, element) {
+ var template = $templateCache.get(tenantFieldsetTemplate);
+ element.html(template);
+ $compile(element.contents())(scope);
+ }
+ return {
+ restrict: "E",
+ link: linker,
+ scope: {
+ tenant: '=',
+ isEdit: '=',
+ theForm: '=',
+ onManageUsers: '&',
+ onDeleteTenant: '&'
+ }
+ };
+}
ui/src/app/tenant/tenant.routes.js 46(+46 -0)
diff --git a/ui/src/app/tenant/tenant.routes.js b/ui/src/app/tenant/tenant.routes.js
new file mode 100644
index 0000000..4c56ac3
--- /dev/null
+++ b/ui/src/app/tenant/tenant.routes.js
@@ -0,0 +1,46 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import tenantsTemplate from './tenants.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function TenantRoutes($stateProvider) {
+
+ $stateProvider
+ .state('home.tenants', {
+ url: '/tenants',
+ params: {'topIndex': 0},
+ module: 'private',
+ auth: ['SYS_ADMIN'],
+ views: {
+ "content@home": {
+ templateUrl: tenantsTemplate,
+ controllerAs: 'vm',
+ controller: 'TenantController'
+ }
+ },
+ data: {
+ searchEnabled: true,
+ pageTitle: 'tenant.tenants'
+ },
+ ncyBreadcrumb: {
+ label: '{"icon": "supervisor_account", "label": "tenant.tenants"}'
+ }
+ });
+}
ui/src/app/tenant/tenant-card.tpl.html 18(+18 -0)
diff --git a/ui/src/app/tenant/tenant-card.tpl.html b/ui/src/app/tenant/tenant-card.tpl.html
new file mode 100644
index 0000000..99071bd
--- /dev/null
+++ b/ui/src/app/tenant/tenant-card.tpl.html
@@ -0,0 +1,18 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div class="tb-uppercase">{{ item | contactShort }}</div>
\ No newline at end of file
ui/src/app/tenant/tenant-fieldset.tpl.html 36(+36 -0)
diff --git a/ui/src/app/tenant/tenant-fieldset.tpl.html b/ui/src/app/tenant/tenant-fieldset.tpl.html
new file mode 100644
index 0000000..45e7c97
--- /dev/null
+++ b/ui/src/app/tenant/tenant-fieldset.tpl.html
@@ -0,0 +1,36 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-button ng-click="onManageUsers({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'tenant.manage-tenant-admins' | translate }}</md-button>
+<md-button ng-click="onDeleteTenant({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'tenant.delete' | translate }}</md-button>
+
+<md-content class="md-padding" layout="column">
+ <fieldset ng-disabled="loading || !isEdit">
+ <md-input-container class="md-block">
+ <label translate>tenant.title</label>
+ <input required name="title" ng-model="tenant.title">
+ <div ng-messages="theForm.title.$error">
+ <div translate ng-message="required">tenant.title-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container class="md-block">
+ <label translate>tenant.description</label>
+ <textarea ng-model="tenant.additionalInfo.description" rows="2"></textarea>
+ </md-input-container>
+ <tb-contact contact="tenant" the-form="theForm" is-edit="isEdit"></tb-contact>
+ </fieldset>
+</md-content>
ui/src/app/tenant/tenants.tpl.html 27(+27 -0)
diff --git a/ui/src/app/tenant/tenants.tpl.html b/ui/src/app/tenant/tenants.tpl.html
new file mode 100644
index 0000000..77cb80f
--- /dev/null
+++ b/ui/src/app/tenant/tenants.tpl.html
@@ -0,0 +1,27 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<tb-grid grid-configuration="vm.tenantGridConfig">
+ <details-buttons tb-help="'tenants'" help-container-id="help-container">
+ <div id="help-container"></div>
+ </details-buttons>
+ <tb-tenant tenant="vm.grid.operatingItem()"
+ is-edit="vm.grid.detailsConfig.isDetailsEditMode"
+ the-form="vm.grid.detailsForm"
+ on-manage-users="vm.openTenantUsers(event, vm.grid.detailsConfig.currentItem)"
+ on-delete-tenant="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-tenant>
+</tb-grid>
ui/src/app/user/add-user.tpl.html 45(+45 -0)
diff --git a/ui/src/app/user/add-user.tpl.html b/ui/src/app/user/add-user.tpl.html
new file mode 100644
index 0000000..28657da
--- /dev/null
+++ b/ui/src/app/user/add-user.tpl.html
@@ -0,0 +1,45 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'user.add' | translate }}" tb-help="'users'" help-container-id="help-container">
+ <form name="theForm" ng-submit="vm.add()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>user.add</h2>
+ <span flex></span>
+ <div id="help-container"></div>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <tb-user user="vm.item" is-edit="true" the-form="theForm"></tb-user>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit" class="md-raised md-primary">
+ {{ 'action.add' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
ui/src/app/user/index.js 34(+34 -0)
diff --git a/ui/src/app/user/index.js b/ui/src/app/user/index.js
new file mode 100644
index 0000000..68ff865
--- /dev/null
+++ b/ui/src/app/user/index.js
@@ -0,0 +1,34 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import uiRouter from 'angular-ui-router';
+import thingsboardGrid from '../components/grid.directive';
+import thingsboardApiUser from '../api/user.service';
+import thingsboardToast from '../services/toast';
+
+import UserRoutes from './user.routes';
+import UserController from './user.controller';
+import UserDirective from './user.directive';
+
+export default angular.module('thingsboard.user', [
+ uiRouter,
+ thingsboardGrid,
+ thingsboardApiUser,
+ thingsboardToast
+])
+ .config(UserRoutes)
+ .controller('UserController', UserController)
+ .directive('tbUser', UserDirective)
+ .name;
ui/src/app/user/user.controller.js 153(+153 -0)
diff --git a/ui/src/app/user/user.controller.js b/ui/src/app/user/user.controller.js
new file mode 100644
index 0000000..733f16d
--- /dev/null
+++ b/ui/src/app/user/user.controller.js
@@ -0,0 +1,153 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import addUserTemplate from './add-user.tpl.html';
+import userCard from './user-card.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+
+/*@ngInject*/
+export default function UserController(userService, toast, $scope, $controller, $state, $stateParams, $translate) {
+
+ var tenantId = $stateParams.tenantId;
+ var customerId = $stateParams.customerId;
+ var usersType = $state.$current.data.usersType;
+
+ var userActionsList = [
+ {
+ onAction: function ($event, item) {
+ vm.grid.deleteItem($event, item);
+ },
+ name: function() { return $translate.instant('action.delete') },
+ details: function() { return $translate.instant('user.delete') },
+ icon: "delete"
+ }
+ ];
+
+ var vm = this;
+
+ vm.userGridConfig = {
+ deleteItemTitleFunc: deleteUserTitle,
+ deleteItemContentFunc: deleteUserText,
+ deleteItemsTitleFunc: deleteUsersTitle,
+ deleteItemsActionTitleFunc: deleteUsersActionTitle,
+ deleteItemsContentFunc: deleteUsersText,
+
+ deleteItemFunc: deleteUser,
+
+ getItemTitleFunc: getUserTitle,
+ itemCardTemplateUrl: userCard,
+
+ actionsList: userActionsList,
+
+ onGridInited: gridInited,
+
+ addItemTemplateUrl: addUserTemplate,
+
+ addItemText: function() { return $translate.instant('user.add-user-text') },
+ noItemsText: function() { return $translate.instant('user.no-users-text') },
+ itemDetailsText: function() { return $translate.instant('user.user-details') }
+ };
+
+ if (angular.isDefined($stateParams.items) && $stateParams.items !== null) {
+ vm.userGridConfig.items = $stateParams.items;
+ }
+
+ if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) {
+ vm.userGridConfig.topIndex = $stateParams.topIndex;
+ }
+
+ vm.resendActivation = resendActivation;
+
+ initController();
+
+ function initController() {
+ var fetchUsersFunction = null;
+ var saveUserFunction = null;
+ var refreshUsersParamsFunction = null;
+
+ if (usersType === 'tenant') {
+ fetchUsersFunction = function (pageLink) {
+ return userService.getTenantAdmins(tenantId, pageLink);
+ };
+ saveUserFunction = function (user) {
+ user.authority = "TENANT_ADMIN";
+ user.tenantId = {id: tenantId};
+ return userService.saveUser(user);
+ };
+ refreshUsersParamsFunction = function () {
+ return {"tenantId": tenantId, "topIndex": vm.topIndex};
+ };
+
+ } else if (usersType === 'customer') {
+ fetchUsersFunction = function (pageLink) {
+ return userService.getCustomerUsers(customerId, pageLink);
+ };
+ saveUserFunction = function (user) {
+ user.authority = "CUSTOMER_USER";
+ user.customerId = {id: customerId};
+ return userService.saveUser(user);
+ };
+ refreshUsersParamsFunction = function () {
+ return {"customerId": customerId, "topIndex": vm.topIndex};
+ };
+ }
+
+ vm.userGridConfig.refreshParamsFunc = refreshUsersParamsFunction;
+ vm.userGridConfig.fetchItemsFunc = fetchUsersFunction;
+ vm.userGridConfig.saveItemFunc = saveUserFunction;
+ }
+
+ function deleteUserTitle(user) {
+ return $translate.instant('user.delete-user-title', {userEmail: user.email});
+ }
+
+ function deleteUserText() {
+ return $translate.instant('user.delete-user-text');
+ }
+
+ function deleteUsersTitle(selectedCount) {
+ return $translate.instant('user.delete-users-title', {count: selectedCount}, 'messageformat');
+ }
+
+ function deleteUsersActionTitle(selectedCount) {
+ return $translate.instant('user.delete-users-action-title', {count: selectedCount}, 'messageformat');
+ }
+
+ function deleteUsersText() {
+ return $translate.instant('user.delete-users-text');
+ }
+
+ function gridInited(grid) {
+ vm.grid = grid;
+ }
+
+ function getUserTitle(user) {
+ return user ? user.email : '';
+ }
+
+ function deleteUser(userId) {
+ return userService.deleteUser(userId);
+ }
+
+ function resendActivation(user) {
+ userService.sendActivationEmail(user.email).then(function success() {
+ toast.showSuccess($translate.instant('user.activation-email-sent-message'));
+ });
+ }
+}
ui/src/app/user/user.directive.js 40(+40 -0)
diff --git a/ui/src/app/user/user.directive.js b/ui/src/app/user/user.directive.js
new file mode 100644
index 0000000..d9d38ab
--- /dev/null
+++ b/ui/src/app/user/user.directive.js
@@ -0,0 +1,40 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import userFieldsetTemplate from './user-fieldset.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function UserDirective($compile, $templateCache) {
+ var linker = function (scope, element) {
+ var template = $templateCache.get(userFieldsetTemplate);
+ element.html(template);
+ $compile(element.contents())(scope);
+ }
+ return {
+ restrict: "E",
+ link: linker,
+ scope: {
+ user: '=',
+ isEdit: '=',
+ theForm: '=',
+ onResendActivation: '&',
+ onDeleteUser: '&'
+ }
+ };
+}
ui/src/app/user/user.routes.js 69(+69 -0)
diff --git a/ui/src/app/user/user.routes.js b/ui/src/app/user/user.routes.js
new file mode 100644
index 0000000..75688ea
--- /dev/null
+++ b/ui/src/app/user/user.routes.js
@@ -0,0 +1,69 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import usersTemplate from '../user/users.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function UserRoutes($stateProvider) {
+
+ $stateProvider
+ .state('home.tenants.users', {
+ url: '/:tenantId/users',
+ params: {'topIndex': 0},
+ module: 'private',
+ auth: ['SYS_ADMIN'],
+ views: {
+ "content@home": {
+ templateUrl: usersTemplate,
+ controllerAs: 'vm',
+ controller: 'UserController'
+ }
+ },
+ data: {
+ usersType: 'tenant',
+ searchEnabled: true,
+ pageTitle: 'user.tenant-admins'
+ },
+ ncyBreadcrumb: {
+ label: '{"icon": "account_circle", "label": "user.tenant-admins"}'
+ }
+ })
+ .state('home.customers.users', {
+ url: '/:customerId/users',
+ params: {'topIndex': 0},
+ module: 'private',
+ auth: ['TENANT_ADMIN'],
+ views: {
+ "content@home": {
+ templateUrl: usersTemplate,
+ controllerAs: 'vm',
+ controller: 'UserController'
+ }
+ },
+ data: {
+ usersType: 'customer',
+ searchEnabled: true,
+ pageTitle: 'user.customer-users'
+ },
+ ncyBreadcrumb: {
+ label: '{"icon": "account_circle", "label": "user.customer-users"}'
+ }
+ });
+
+}
ui/src/app/user/user-card.tpl.html 18(+18 -0)
diff --git a/ui/src/app/user/user-card.tpl.html b/ui/src/app/user/user-card.tpl.html
new file mode 100644
index 0000000..ee0aa33
--- /dev/null
+++ b/ui/src/app/user/user-card.tpl.html
@@ -0,0 +1,18 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div class="tb-uppercase">{{item ? ( ( item.firstName ? item.firstName : '' ) + ' ' + ( item.lastName ? item.lastName : '') ) : ''}}</div>
ui/src/app/user/user-fieldset.tpl.html 47(+47 -0)
diff --git a/ui/src/app/user/user-fieldset.tpl.html b/ui/src/app/user/user-fieldset.tpl.html
new file mode 100644
index 0000000..e80d628
--- /dev/null
+++ b/ui/src/app/user/user-fieldset.tpl.html
@@ -0,0 +1,47 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-button ng-click="onResendActivation({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{
+ 'user.resend-activation' | translate }}
+</md-button>
+<md-button ng-click="onDeleteUser({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'user.delete' |
+ translate }}
+</md-button>
+
+<md-content class="md-padding" layout="column">
+ <fieldset ng-disabled="loading || !isEdit">
+ <md-input-container class="md-block">
+ <label translate>user.email</label>
+ <input required name="email" type="email" ng-model="user.email">
+ <div ng-messages="theForm.email.$error">
+ <div translate ng-message="required">user.email-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container class="md-block">
+ <label translate>user.first-name</label>
+ <input name="firstname" ng-model="user.firstName">
+ </md-input-container>
+ <md-input-container class="md-block">
+ <label translate>user.last-name</label>
+ <input name="lastname" ng-model="user.lastName">
+ </md-input-container>
+ <md-input-container class="md-block">
+ <label translate>user.description</label>
+ <textarea ng-model="user.additionalInfo.description" rows="2"></textarea>
+ </md-input-container>
+ </fieldset>
+</md-content>
ui/src/app/user/users.tpl.html 27(+27 -0)
diff --git a/ui/src/app/user/users.tpl.html b/ui/src/app/user/users.tpl.html
new file mode 100644
index 0000000..24d7982
--- /dev/null
+++ b/ui/src/app/user/users.tpl.html
@@ -0,0 +1,27 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<tb-grid grid-configuration="vm.userGridConfig">
+ <details-buttons tb-help="'users'" help-container-id="help-container">
+ <div id="help-container"></div>
+ </details-buttons>
+ <tb-user user="vm.grid.operatingItem()"
+ is-edit="vm.grid.detailsConfig.isDetailsEditMode"
+ the-form="vm.grid.detailsForm"
+ on-resend-activation="vm.resendActivation(vm.grid.detailsConfig.currentItem)"
+ on-delete-user="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-user>
+</tb-grid>
diff --git a/ui/src/app/widget/add-widgets-bundle.tpl.html b/ui/src/app/widget/add-widgets-bundle.tpl.html
new file mode 100644
index 0000000..15e080f
--- /dev/null
+++ b/ui/src/app/widget/add-widgets-bundle.tpl.html
@@ -0,0 +1,48 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'widgets-bundle.add' | translate }}" tb-help="'widgetsBundles'" help-container-id="help-container">
+ <form name="theForm" ng-submit="vm.add()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>widgets-bundle.add</h2>
+ <span flex></span>
+ <div id="help-container"></div>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <tb-widgets-bundle widgets-bundle="vm.item" is-edit="true" the-form="theForm"></tb-widgets-bundle>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit"
+ class="md-raised md-primary">
+ {{ 'action.add' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+ translate }}
+ </md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
ui/src/app/widget/index.js 59(+59 -0)
diff --git a/ui/src/app/widget/index.js b/ui/src/app/widget/index.js
new file mode 100644
index 0000000..92c5426
--- /dev/null
+++ b/ui/src/app/widget/index.js
@@ -0,0 +1,59 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './widget-editor.scss';
+
+import 'angular-hotkeys';
+import 'angular-ui-ace';
+
+import uiRouter from 'angular-ui-router';
+import thingsboardApiUser from '../api/user.service';
+import thingsboardApiWidget from '../api/widget.service';
+import thingsboardTypes from '../common/types.constant';
+import thingsboardToast from '../services/toast';
+import thingsboardConfirmOnExit from '../components/confirm-on-exit.directive';
+import thingsboardDashboard from '../components/dashboard.directive';
+import thingsboardExpandFullscreen from '../components/expand-fullscreen.directive';
+import thingsboardCircularProgress from '../components/circular-progress.directive';
+
+import WidgetLibraryRoutes from './widget-library.routes';
+import WidgetLibraryController from './widget-library.controller';
+import SelectWidgetTypeController from './select-widget-type.controller';
+import WidgetEditorController from './widget-editor.controller';
+import WidgetsBundleController from './widgets-bundle.controller';
+import WidgetsBundleDirective from './widgets-bundle.directive';
+import SaveWidgetTypeAsController from './save-widget-type-as.controller';
+
+export default angular.module('thingsboard.widget-library', [
+ uiRouter,
+ thingsboardApiWidget,
+ thingsboardApiUser,
+ thingsboardTypes,
+ thingsboardToast,
+ thingsboardConfirmOnExit,
+ thingsboardDashboard,
+ thingsboardExpandFullscreen,
+ thingsboardCircularProgress,
+ 'cfp.hotkeys',
+ 'ui.ace'
+])
+ .config(WidgetLibraryRoutes)
+ .controller('WidgetLibraryController', WidgetLibraryController)
+ .controller('SelectWidgetTypeController', SelectWidgetTypeController)
+ .controller('WidgetEditorController', WidgetEditorController)
+ .controller('WidgetsBundleController', WidgetsBundleController)
+ .controller('SaveWidgetTypeAsController', SaveWidgetTypeAsController)
+ .directive('tbWidgetsBundle', WidgetsBundleDirective)
+ .name;
ui/src/app/widget/lib/analogue-linear-gauge.js 184(+184 -0)
diff --git a/ui/src/app/widget/lib/analogue-linear-gauge.js b/ui/src/app/widget/lib/analogue-linear-gauge.js
new file mode 100644
index 0000000..72b5d6e
--- /dev/null
+++ b/ui/src/app/widget/lib/analogue-linear-gauge.js
@@ -0,0 +1,184 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import $ from 'jquery';
+import canvasGauges from 'canvas-gauges';
+import tinycolor from 'tinycolor2';
+
+/* eslint-disable angular/angularelement */
+export default class TbAnalogueLinearGauge {
+ constructor(containerElement, settings, data, canvasId) {
+
+ canvasGauges.performance = window.performance; // eslint-disable-line no-undef, angular/window-service
+
+ var gaugeElement = $('#'+canvasId, containerElement);
+
+ var minValue = settings.minValue || 0;
+ var maxValue = settings.maxValue || 100;
+
+ var dataKey = data[0].dataKey;
+ var keyColor = settings.defaultColor || dataKey.color;
+
+ var majorTicksCount = settings.majorTicksCount || 10;
+ var total = maxValue-minValue;
+ var step = (total/majorTicksCount);
+ step = parseFloat(parseFloat(step).toPrecision(12));
+
+ var majorTicks = [];
+ var highlights = [];
+ var tick = 0;
+
+ while(tick<=total) {
+ var majorTick = tick + minValue;
+ majorTicks.push(majorTick);
+ var nextTick = tick+step;
+ nextTick = parseFloat(parseFloat(nextTick).toPrecision(12));
+ if (tick<total) {
+ var highlightColor = tinycolor(keyColor);
+ var percent = tick/total;
+ highlightColor.setAlpha(percent);
+ var highlight = {
+ from: tick,
+ to: nextTick,
+ color: highlightColor.toRgbString()
+ }
+ highlights.push(highlight);
+ }
+ tick = nextTick;
+ }
+
+ var colorNumbers = tinycolor(keyColor).darken(20).toRgbString();
+ var barStrokeColor = tinycolor(keyColor).darken().setAlpha(0.6).toRgbString();
+
+ var progressColorStart = tinycolor(keyColor).setAlpha(0.05).toRgbString();
+ var progressColorEnd = tinycolor(keyColor).darken().toRgbString();
+
+ var gaugeData = {
+
+ renderTo: gaugeElement[0],
+
+ /* Generic options */
+
+ minValue: minValue,
+ maxValue: maxValue,
+ majorTicks: majorTicks,
+ minorTicks: settings.minorTicks || 2,
+ units: settings.units,
+ title: ((settings.showUnitTitle !== false) ?
+ (settings.unitTitle && settings.unitTitle.length > 0 ?
+ settings.unitTitle : dataKey.label) : ''),
+
+ borders: settings.showBorder === true,
+ borderShadowWidth: (settings.showBorder === true) ? 3 : 0,
+ borderOuterWidth: (settings.showBorder === true) ? 3 : 0,
+ borderMiddleWidth: (settings.showBorder === true) ? 3 : 0,
+ borderInnerWidth: (settings.showBorder === true) ? 3 : 0,
+
+ // borders
+
+ // number formats
+ valueInt: settings.valueInt || 3,
+ valueDec: (angular.isDefined(settings.valueDec) && settings.valueDec !== null)
+ ? settings.valueDec : 2,
+ majorTicksInt: 1,
+ majorTicksDec: 0,
+
+ valueBox: settings.valueBox !== false,
+ valueBoxStroke: 5,
+ valueBoxWidth: 0,
+ valueText: '',
+ valueTextShadow: true,
+ valueBoxBorderRadius: 2.5,
+
+ //highlights
+ highlights: (settings.highlights && settings.highlights.length > 0) ? settings.highlights : highlights,
+ highlightsWidth: (angular.isDefined(settings.highlightsWidth) && settings.highlightsWidth !== null) ? settings.highlightsWidth : 10,
+
+ //fonts
+ fontNumbers: settings.numbersFont && settings.numbersFont.family ? settings.numbersFont.family : 'RobotoDraft',
+ fontTitle: settings.titleFont && settings.titleFont.family ? settings.titleFont.family : 'RobotoDraft',
+ fontUnits: settings.unitsFont && settings.unitsFont.family ? settings.unitsFont.family : 'RobotoDraft',
+ fontValue: settings.valueFont && settings.valueFont.family ? settings.valueFont.family : 'RobotoDraft',
+
+ fontNumbersSize: settings.numbersFont && settings.numbersFont.size ? settings.numbersFont.size : 18,
+ fontTitleSize: settings.titleFont && settings.titleFont.size ? settings.titleFont.size : 24,
+ fontUnitsSize: settings.unitsFont && settings.unitsFont.size ? settings.unitsFont.size : 22,
+ fontValueSize: settings.valueFont && settings.valueFont.size ? settings.valueFont.size : 40,
+
+ fontNumbersStyle: settings.numbersFont && settings.numbersFont.style ? settings.numbersFont.style : 'normal',
+ fontTitleStyle: settings.titleFont && settings.titleFont.style ? settings.titleFont.style : 'normal',
+ fontUnitsStyle: settings.unitsFont && settings.unitsFont.style ? settings.unitsFont.style : 'normal',
+ fontValueStyle: settings.valueFont && settings.valueFont.style ? settings.valueFont.style : 'normal',
+
+ fontNumbersWeight: settings.numbersFont && settings.numbersFont.weight ? settings.numbersFont.weight : '500',
+ fontTitleWeight: settings.titleFont && settings.titleFont.weight ? settings.titleFont.weight : '500',
+ fontUnitsWeight: settings.unitsFont && settings.unitsFont.weight ? settings.unitsFont.weight : '500',
+ fontValueWeight: settings.valueFont && settings.valueFont.weight ? settings.valueFont.weight : '500',
+
+ colorNumbers: settings.numbersFont && settings.numbersFont.color ? settings.numbersFont.color : colorNumbers,
+ colorTitle: settings.titleFont && settings.titleFont.color ? settings.titleFont.color : '#888',
+ colorUnits: settings.unitsFont && settings.unitsFont.color ? settings.unitsFont.color : '#888',
+ colorValueText: settings.valueFont && settings.valueFont.color ? settings.valueFont.color : '#444',
+ colorValueTextShadow: settings.valueFont && settings.valueFont.shadowColor ? settings.valueFont.shadowColor : 'rgba(0,0,0,0.3)',
+
+ //colors
+ colorPlate: settings.colorPlate || '#fff',
+ colorMajorTicks: settings.colorMajorTicks || '#444',
+ colorMinorTicks: settings.colorMinorTicks || '#666',
+ colorNeedle: settings.colorNeedle || keyColor,
+ colorNeedleEnd: settings.colorNeedleEnd || keyColor,
+
+ colorValueBoxRect: settings.colorValueBoxRect || '#888',
+ colorValueBoxRectEnd: settings.colorValueBoxRectEnd || '#666',
+ colorValueBoxBackground: settings.colorValueBoxBackground || '#babab2',
+ colorValueBoxShadow: settings.colorValueBoxShadow || 'rgba(0,0,0,1)',
+ colorNeedleShadowUp: settings.colorNeedleShadowUp || 'rgba(2,255,255,0.2)',
+ colorNeedleShadowDown: settings.colorNeedleShadowDown || 'rgba(188,143,143,0.45)',
+
+ // animations
+ animation: settings.animation !== false,
+ animationDuration: (angular.isDefined(settings.animationDuration) && settings.animationDuration !== null) ? settings.animationDuration : 500,
+ animationRule: settings.animationRule || 'cycle',
+
+ /* Linear gauge specific */
+
+ barStrokeWidth: (angular.isDefined(settings.barStrokeWidth) && settings.barStrokeWidth !== null) ? settings.barStrokeWidth : 2.5,
+ colorBarStroke: settings.colorBarStroke || barStrokeColor,
+ colorBar: settings.colorBar || "#fff",
+ colorBarEnd: settings.colorBarEnd || "#ddd",
+ colorBarProgress: settings.colorBarProgress || progressColorStart,
+ colorBarProgressEnd: settings.colorBarProgressEnd || progressColorEnd
+ };
+ this.gauge = new canvasGauges.LinearGauge(gaugeData).draw();
+ }
+
+ redraw(width, height, data, sizeChanged) {
+ if (sizeChanged) {
+ this.gauge.update({width: width, height: height});
+ }
+
+ if (data.length > 0) {
+ var cellData = data[0];
+ if (cellData.data.length > 0) {
+ var tvPair = cellData.data[cellData.data.length -
+ 1];
+ var value = tvPair[1];
+ this.gauge.value = value;
+ }
+ }
+ }
+}
+
+/* eslint-enable angular/angularelement */
ui/src/app/widget/lib/analogue-radial-gauge.js 193(+193 -0)
diff --git a/ui/src/app/widget/lib/analogue-radial-gauge.js b/ui/src/app/widget/lib/analogue-radial-gauge.js
new file mode 100644
index 0000000..c970b9d
--- /dev/null
+++ b/ui/src/app/widget/lib/analogue-radial-gauge.js
@@ -0,0 +1,193 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import $ from 'jquery';
+import canvasGauges from 'canvas-gauges';
+import tinycolor from 'tinycolor2';
+
+/* eslint-disable angular/angularelement */
+
+export default class TbAnalogueRadialGauge {
+ constructor(containerElement, settings, data, canvasId) {
+
+ canvasGauges.performance = window.performance; // eslint-disable-line no-undef, angular/window-service
+
+ var gaugeElement = $('#'+canvasId, containerElement);
+
+ var minValue = settings.minValue || 0;
+ var maxValue = settings.maxValue || 100;
+
+ var dataKey = data[0].dataKey;
+ var keyColor = settings.defaultColor || dataKey.color;
+
+ var majorTicksCount = settings.majorTicksCount || 10;
+ var total = maxValue-minValue;
+ var step = (total/majorTicksCount);
+ step = parseFloat(parseFloat(step).toPrecision(12));
+
+ var majorTicks = [];
+ var highlights = [];
+ var tick = minValue;
+
+ while(tick<=maxValue) {
+ majorTicks.push(tick);
+ var nextTick = tick+step;
+ nextTick = parseFloat(parseFloat(nextTick).toPrecision(12));
+ if (tick<maxValue) {
+ var highlightColor = tinycolor(keyColor);
+ var percent = (tick-minValue)/total;
+ highlightColor.setAlpha(percent);
+ var highlight = {
+ from: tick,
+ to: nextTick,
+ color: highlightColor.toRgbString()
+ }
+ highlights.push(highlight);
+ }
+ tick = nextTick;
+ }
+
+ var colorNumbers = tinycolor(keyColor).darken(20).toRgbString();
+
+ var gaugeData = {
+
+ renderTo: gaugeElement[0],
+
+ /* Generic options */
+
+ minValue: minValue,
+ maxValue: maxValue,
+ majorTicks: majorTicks,
+ minorTicks: settings.minorTicks || 2,
+ units: settings.units,
+ title: ((settings.showUnitTitle !== false) ?
+ (settings.unitTitle && settings.unitTitle.length > 0 ?
+ settings.unitTitle : dataKey.label) : ''),
+
+ borders: settings.showBorder !== false,
+ borderShadowWidth: (settings.showBorder !== false) ? 3 : 0,
+
+ // borders
+ //borderOuterWidth: (settings.showBorder !== false) ? 3 : 0,
+ //borderMiddleWidth: (settings.showBorder !== false) ? 3 : 0,
+ //borderInnerWidth: (settings.showBorder !== false) ? 3 : 0,
+ //borderShadowWidth: (settings.showBorder !== false) ? 3 : 0,
+
+ // number formats
+ valueInt: settings.valueInt || 3,
+ valueDec: (angular.isDefined(settings.valueDec) && settings.valueDec !== null)
+ ? settings.valueDec : 2,
+ majorTicksInt: 1,
+ majorTicksDec: 0,
+
+ valueBox: settings.valueBox !== false,
+ valueBoxStroke: 5,
+ valueBoxWidth: 0,
+ valueText: '',
+ valueTextShadow: true,
+ valueBoxBorderRadius: 2.5,
+
+ //highlights
+ highlights: (settings.highlights && settings.highlights.length > 0) ? settings.highlights : highlights,
+ highlightsWidth: (angular.isDefined(settings.highlightsWidth) && settings.highlightsWidth !== null) ? settings.highlightsWidth : 15,
+
+ //fonts
+ fontNumbers: settings.numbersFont && settings.numbersFont.family ? settings.numbersFont.family : 'RobotoDraft',
+ fontTitle: settings.titleFont && settings.titleFont.family ? settings.titleFont.family : 'RobotoDraft',
+ fontUnits: settings.unitsFont && settings.unitsFont.family ? settings.unitsFont.family : 'RobotoDraft',
+ fontValue: settings.valueFont && settings.valueFont.family ? settings.valueFont.family : 'RobotoDraft',
+
+ fontNumbersSize: settings.numbersFont && settings.numbersFont.size ? settings.numbersFont.size : 18,
+ fontTitleSize: settings.titleFont && settings.titleFont.size ? settings.titleFont.size : 24,
+ fontUnitsSize: settings.unitsFont && settings.unitsFont.size ? settings.unitsFont.size : 22,
+ fontValueSize: settings.valueFont && settings.valueFont.size ? settings.valueFont.size : 40,
+
+ fontNumbersStyle: settings.numbersFont && settings.numbersFont.style ? settings.numbersFont.style : 'normal',
+ fontTitleStyle: settings.titleFont && settings.titleFont.style ? settings.titleFont.style : 'normal',
+ fontUnitsStyle: settings.unitsFont && settings.unitsFont.style ? settings.unitsFont.style : 'normal',
+ fontValueStyle: settings.valueFont && settings.valueFont.style ? settings.valueFont.style : 'normal',
+
+ fontNumbersWeight: settings.numbersFont && settings.numbersFont.weight ? settings.numbersFont.weight : '500',
+ fontTitleWeight: settings.titleFont && settings.titleFont.weight ? settings.titleFont.weight : '500',
+ fontUnitsWeight: settings.unitsFont && settings.unitsFont.weight ? settings.unitsFont.weight : '500',
+ fontValueWeight: settings.valueFont && settings.valueFont.weight ? settings.valueFont.weight : '500',
+
+ colorNumbers: settings.numbersFont && settings.numbersFont.color ? settings.numbersFont.color : colorNumbers,
+ colorTitle: settings.titleFont && settings.titleFont.color ? settings.titleFont.color : '#888',
+ colorUnits: settings.unitsFont && settings.unitsFont.color ? settings.unitsFont.color : '#888',
+ colorValueText: settings.valueFont && settings.valueFont.color ? settings.valueFont.color : '#444',
+ colorValueTextShadow: settings.valueFont && settings.valueFont.shadowColor ? settings.valueFont.shadowColor : 'rgba(0,0,0,0.3)',
+
+ //colors
+ colorPlate: settings.colorPlate || '#fff',
+ colorMajorTicks: settings.colorMajorTicks || '#444',
+ colorMinorTicks: settings.colorMinorTicks || '#666',
+ colorNeedle: settings.colorNeedle || keyColor,
+ colorNeedleEnd: settings.colorNeedleEnd || keyColor,
+
+ colorValueBoxRect: settings.colorValueBoxRect || '#888',
+ colorValueBoxRectEnd: settings.colorValueBoxRectEnd || '#666',
+ colorValueBoxBackground: settings.colorValueBoxBackground || '#babab2',
+ colorValueBoxShadow: settings.colorValueBoxShadow || 'rgba(0,0,0,1)',
+ colorNeedleShadowUp: settings.colorNeedleShadowUp || 'rgba(2,255,255,0.2)',
+ colorNeedleShadowDown: settings.colorNeedleShadowDown || 'rgba(188,143,143,0.45)',
+
+ // animations
+ animation: settings.animation !== false,
+ animationDuration: (angular.isDefined(settings.animationDuration) && settings.animationDuration !== null) ? settings.animationDuration : 500,
+ animationRule: settings.animationRule || 'cycle',
+
+ /* Radial gauge specific */
+
+ ticksAngle: settings.ticksAngle || 270,
+ startAngle: settings.startAngle || 45,
+
+ // colors
+
+ colorNeedleCircleOuter: '#f0f0f0',
+ colorNeedleCircleOuterEnd: '#ccc',
+ colorNeedleCircleInner: '#e8e8e8', //tinycolor(keyColor).lighten(30).toRgbString(),//'#e8e8e8',
+ colorNeedleCircleInnerEnd: '#f5f5f5',
+
+ // needle
+ needleCircleSize: settings.needleCircleSize || 10,
+ needleCircleInner: true,
+ needleCircleOuter: true,
+
+ // custom animations
+ animationTarget: 'needle' // 'needle' or 'plate'
+
+ };
+ this.gauge = new canvasGauges.RadialGauge(gaugeData).draw();
+ }
+
+ redraw(width, height, data, sizeChanged) {
+ if (sizeChanged) {
+ this.gauge.update({width: width, height: height});
+ }
+
+ if (data.length > 0) {
+ var cellData = data[0];
+ if (cellData.data.length > 0) {
+ var tvPair = cellData.data[cellData.data.length -
+ 1];
+ var value = tvPair[1];
+ this.gauge.value = value;
+ }
+ }
+ }
+}
+
+/* eslint-enable angular/angularelement */
ui/src/app/widget/lib/digital-gauge.js 586(+586 -0)
diff --git a/ui/src/app/widget/lib/digital-gauge.js b/ui/src/app/widget/lib/digital-gauge.js
new file mode 100644
index 0000000..4e1418d
--- /dev/null
+++ b/ui/src/app/widget/lib/digital-gauge.js
@@ -0,0 +1,586 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import $ from 'jquery';
+import tinycolor from 'tinycolor2';
+import 'justgage';
+import Raphael from 'raphael';
+
+/* eslint-disable angular/angularelement */
+
+export default class TbDigitalGauge {
+ constructor(containerElement, settings, data) {
+
+ var tbGauge = this;
+
+ window.Raphael = Raphael; // eslint-disable-line no-undef, angular/window-service
+
+ var isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; // eslint-disable-line no-undef
+
+ var gaugeElement = $(containerElement);
+
+ this.localSettings = {};
+
+ this.localSettings.minValue = settings.minValue || 0;
+ this.localSettings.maxValue = settings.maxValue || 100;
+ this.localSettings.gaugeType = settings.gaugeType || 'arc';
+ this.localSettings.donutStartAngle = (angular.isDefined(settings.donutStartAngle) && settings.donutStartAngle !== null)
+ ? settings.donutStartAngle : 90;
+ this.localSettings.neonGlowBrightness = settings.neonGlowBrightness || 0;
+ this.localSettings.dashThickness = settings.dashThickness || 0;
+ this.localSettings.roundedLineCap = settings.roundedLineCap === true;
+
+ var dataKey = data[0].dataKey;
+ var keyColor = settings.defaultColor || dataKey.color;
+
+ this.localSettings.title = ((settings.showTitle === true) ?
+ (settings.title && settings.title.length > 0 ?
+ settings.title : dataKey.label) : '');
+
+ this.localSettings.unitTitle = ((settings.showUnitTitle === true) ?
+ (settings.unitTitle && settings.unitTitle.length > 0 ?
+ settings.unitTitle : dataKey.label) : '');
+
+ this.localSettings.gaugeWidthScale = settings.gaugeWidthScale || 0.75;
+ this.localSettings.gaugeColor = settings.gaugeColor || tinycolor(keyColor).setAlpha(0.2).toRgbString();
+
+ if (!settings.levelColors || settings.levelColors.length <= 0) {
+ this.localSettings.levelColors = [keyColor, keyColor];
+ } else {
+ this.localSettings.levelColors = settings.levelColors.slice();
+ }
+ if (this.localSettings.neonGlowBrightness) {
+ this.localSettings.origLevelColors = [];
+ for (var i = 0; i < this.localSettings.levelColors.length; i++) {
+ this.localSettings.origLevelColors.push(this.localSettings.levelColors[i]);
+ this.localSettings.levelColors[i] = tinycolor(this.localSettings.levelColors[i]).brighten(this.localSettings.neonGlowBrightness).toHexString();
+ }
+ var colorsCount = this.localSettings.origLevelColors.length;
+ var inc = colorsCount > 1 ? (1 / (colorsCount - 1)) : 1;
+ this.localSettings.colorsRange = [];
+ for (i = 0; i < this.localSettings.origLevelColors.length; i++) {
+ var percentage = inc * i;
+ var tColor = tinycolor(this.localSettings.origLevelColors[i]);
+ this.localSettings.colorsRange[i] = {
+ pct: percentage,
+ color: tColor.toRgb(),
+ rgbString: tColor.toRgbString
+ };
+ }
+ }
+
+
+ this.localSettings.refreshAnimationType = settings.refreshAnimationType || '>';
+ this.localSettings.refreshAnimationTime = settings.refreshAnimationTime || 700;
+ this.localSettings.startAnimationType = settings.startAnimationType || '>';
+ this.localSettings.startAnimationTime = settings.startAnimationTime || 700;
+ this.localSettings.decimals = (angular.isDefined(settings.decimals) && settings.decimals !== null)
+ ? settings.decimals : 0;
+ this.localSettings.units = settings.units || '';
+ this.localSettings.hideValue = settings.showValue !== true;
+ this.localSettings.hideMinMax = settings.showMinMax !== true;
+
+ this.localSettings.titleFont = {};
+ var settingsTitleFont = settings.titleFont;
+ if (!settingsTitleFont) {
+ settingsTitleFont = {};
+ }
+
+ this.localSettings.titleFont.family = settingsTitleFont.family || 'RobotoDraft';
+ this.localSettings.titleFont.size = settingsTitleFont.size ? settingsTitleFont.size : 12;
+ this.localSettings.titleFont.style = settingsTitleFont.style ? settingsTitleFont.style : 'normal';
+ this.localSettings.titleFont.weight = settingsTitleFont.weight ? settingsTitleFont.weight : '500';
+ this.localSettings.titleFont.color = settingsTitleFont.color ? settingsTitleFont.color : keyColor;
+
+ this.localSettings.labelFont = {};
+ var settingsLabelFont = settings.labelFont;
+ if (!settingsLabelFont) {
+ settingsLabelFont = {};
+ }
+
+ this.localSettings.labelFont.family = settingsLabelFont.family || 'RobotoDraft';
+ this.localSettings.labelFont.size = settingsLabelFont.size ? settingsLabelFont.size : 8;
+ this.localSettings.labelFont.style = settingsLabelFont.style ? settingsLabelFont.style : 'normal';
+ this.localSettings.labelFont.weight = settingsLabelFont.weight ? settingsLabelFont.weight : '500';
+ this.localSettings.labelFont.color = settingsLabelFont.color ? settingsLabelFont.color : keyColor;
+
+ this.localSettings.valueFont = {};
+ var settingsValueFont = settings.valueFont;
+ if (!settingsValueFont) {
+ settingsValueFont = {};
+ }
+
+ this.localSettings.valueFont.family = settingsValueFont.family || 'RobotoDraft';
+ this.localSettings.valueFont.size = settingsValueFont.size ? settingsValueFont.size : 18;
+ this.localSettings.valueFont.style = settingsValueFont.style ? settingsValueFont.style : 'normal';
+ this.localSettings.valueFont.weight = settingsValueFont.weight ? settingsValueFont.weight : '500';
+ this.localSettings.valueFont.color = settingsValueFont.color ? settingsValueFont.color : keyColor;
+
+ this.localSettings.minMaxFont = {};
+ var settingsMinMaxFont = settings.minMaxFont;
+ if (!settingsMinMaxFont) {
+ settingsMinMaxFont = {};
+ }
+
+ this.localSettings.minMaxFont.family = settingsMinMaxFont.family || 'RobotoDraft';
+ this.localSettings.minMaxFont.size = settingsMinMaxFont.size ? settingsMinMaxFont.size : 10;
+ this.localSettings.minMaxFont.style = settingsMinMaxFont.style ? settingsMinMaxFont.style : 'normal';
+ this.localSettings.minMaxFont.weight = settingsMinMaxFont.weight ? settingsMinMaxFont.weight : '500';
+ this.localSettings.minMaxFont.color = settingsMinMaxFont.color ? settingsMinMaxFont.color : keyColor;
+
+ if (this.localSettings.neonGlowBrightness) {
+ this.localSettings.titleFont.origColor = this.localSettings.titleFont.color;
+ this.localSettings.titleFont.color = tinycolor(this.localSettings.titleFont.color).brighten(this.localSettings.neonGlowBrightness).toHexString();
+ this.localSettings.labelFont.origColor = this.localSettings.labelFont.color;
+ this.localSettings.labelFont.color = tinycolor(this.localSettings.labelFont.color).brighten(this.localSettings.neonGlowBrightness).toHexString();
+ this.localSettings.valueFont.origColor = this.localSettings.valueFont.color;
+ this.localSettings.valueFont.color = tinycolor(this.localSettings.valueFont.color).brighten(this.localSettings.neonGlowBrightness).toHexString();
+ this.localSettings.minMaxFont.origColor = this.localSettings.minMaxFont.color;
+ this.localSettings.minMaxFont.color = tinycolor(this.localSettings.minMaxFont.color).brighten(this.localSettings.neonGlowBrightness).toHexString();
+ }
+
+ var gaugeOptions = {
+ parentNode: gaugeElement[0],
+ value: 0,
+ min: this.localSettings.minValue,
+ max: this.localSettings.maxValue,
+ title: this.localSettings.title,
+ label: this.localSettings.unitTitle,
+ humanFriendlyDecimal: 0,
+ gaugeWidthScale: this.localSettings.gaugeWidthScale,
+ relativeGaugeSize: true,
+ gaugeColor: this.localSettings.gaugeColor,
+ levelColors: this.localSettings.levelColors,
+ refreshAnimationType: this.localSettings.refreshAnimationType,
+ refreshAnimationTime: this.localSettings.refreshAnimationTime,
+ startAnimationType: this.localSettings.startAnimationType,
+ startAnimationTime: this.localSettings.startAnimationTime,
+ humanFriendly: false,
+ donut: this.localSettings.gaugeType === 'donut',
+ donutStartAngle: this.localSettings.donutStartAngle,
+ decimals: this.localSettings.decimals,
+ pointer: false,
+ symbol: this.localSettings.units,
+ hideValue: this.localSettings.hideValue,
+ hideMinMax: this.localSettings.hideMinMax,
+ titleFontColor: this.localSettings.titleFont.color,
+ labelFontColor: this.localSettings.labelFont.color,
+ valueFontColor: this.localSettings.valueFont.color,
+ valueFontFamily: this.localSettings.valueFont.family
+ };
+
+ this.gauge = new JustGage(gaugeOptions); // eslint-disable-line no-undef
+
+ var gParams = this.gauge.params;
+
+ var titleTextElement = $(this.gauge.txtTitle.node);
+ titleTextElement.css('fontFamily', this.localSettings.titleFont.family);
+ titleTextElement.css('fontSize', this.localSettings.titleFont.size + 'px');
+ titleTextElement.css('fontStyle', this.localSettings.titleFont.style);
+ titleTextElement.css('fontWeight', this.localSettings.titleFont.weight);
+ titleTextElement.css('textTransform', 'uppercase');
+
+ var labelTextElement = $(this.gauge.txtLabel.node);
+ labelTextElement.css('fontFamily', this.localSettings.labelFont.family);
+ labelTextElement.css('fontSize', this.localSettings.labelFont.size + 'px');
+ labelTextElement.css('fontStyle', this.localSettings.labelFont.style);
+ labelTextElement.css('fontWeight', this.localSettings.labelFont.weight);
+ labelTextElement.css('textTransform', 'uppercase');
+
+ var valueTextElement = $(this.gauge.txtValue.node);
+ valueTextElement.css('fontSize', this.localSettings.valueFont.size + 'px');
+ valueTextElement.css('fontStyle', this.localSettings.valueFont.style);
+ valueTextElement.css('fontWeight', this.localSettings.valueFont.weight);
+
+ var minValTextElement = $(this.gauge.txtMin.node);
+ var maxValTextElement = $(this.gauge.txtMax.node);
+ minValTextElement.css('fontFamily', this.localSettings.minMaxFont.family);
+ maxValTextElement.css('fontFamily', this.localSettings.minMaxFont.family);
+ minValTextElement.css('fontSize', this.localSettings.minMaxFont.size+'px');
+ maxValTextElement.css('fontSize', this.localSettings.minMaxFont.size+'px');
+ minValTextElement.css('fontStyle', this.localSettings.minMaxFont.style);
+ maxValTextElement.css('fontStyle', this.localSettings.minMaxFont.style);
+ minValTextElement.css('fontWeight', this.localSettings.minMaxFont.weight);
+ maxValTextElement.css('fontWeight', this.localSettings.minMaxFont.weight);
+ minValTextElement.css('fill', this.localSettings.minMaxFont.color);
+ maxValTextElement.css('fill', this.localSettings.minMaxFont.color);
+
+ var gaugeLevelElement = $(this.gauge.level.node);
+ var gaugeBackElement = $(this.gauge.gauge.node);
+
+ var w = gParams.widgetW;
+ var gws = this.localSettings.gaugeWidthScale;
+ var Ro, Ri;
+ if (this.localSettings.gaugeType === 'donut') {
+ Ro = w / 2 - w / 7;
+ } else {
+ Ro = w / 2 - w / 10;
+ }
+ Ri = Ro - w / 6.666666666666667 * gws;
+ gParams.strokeWidth = Ro - Ri;
+
+ gParams.viewport = {
+ x: 0,
+ y: 0,
+ width: gParams.canvasW,
+ height: gParams.canvasH
+ }
+ var maxW;
+ if (this.localSettings.gaugeType === 'donut') {
+ if (gaugeOptions.title && gaugeOptions.title.length > 0) {
+ gParams.viewport.height = 140;
+ } else {
+ gParams.viewport.y = 17;
+ gParams.viewport.height = 120;
+ }
+ gParams.viewport.x = 40;
+ gParams.viewport.width = 120;
+ $('tspan', labelTextElement).attr('dy', '6');
+ if (!this.localSettings.unitTitle || this.localSettings.unitTitle.length === 0) {
+ var Cy = gParams.widgetH / 1.95 + gParams.dy;
+ gParams.valueY = Cy + (this.localSettings.valueFont.size-4)/2;
+ this.gauge.txtValue.attr({"y": gParams.valueY });
+ }
+ } else if (this.localSettings.gaugeType === 'arc') {
+ if (gaugeOptions.title && gaugeOptions.title.length > 0) {
+ gParams.viewport.y = 5;
+ gParams.viewport.height = 140;
+ } else {
+ gParams.viewport.y = 40;
+ gParams.viewport.height = 100;
+ }
+ if (this.localSettings.roundedLineCap) {
+ $('tspan', minValTextElement).attr('dy', ''+(gParams.strokeWidth/2));
+ $('tspan', maxValTextElement).attr('dy', ''+(gParams.strokeWidth/2));
+ }
+ } else if (this.localSettings.gaugeType === 'horizontalBar') {
+ gParams.titleY = gParams.dy + gParams.widgetH / 3.5 + (this.localSettings.title === '' ? 0 : this.localSettings.titleFont.size);
+ this.gauge.txtTitle.attr({"y": gParams.titleY });
+ gParams.titleBottom = gParams.titleY + (this.localSettings.title === '' ? 0 : 8);
+
+ gParams.valueY = gParams.titleBottom + (this.localSettings.hideValue ? 0 : this.localSettings.valueFont.size);
+ gParams.barTop = gParams.valueY + 8;
+ gParams.barBottom = gParams.barTop + gParams.strokeWidth;
+
+ this.gauge.txtValue.attr({"y": gParams.valueY });
+
+ if (this.localSettings.hideMinMax && this.localSettings.unitTitle === '') {
+ gParams.labelY = gParams.barBottom;
+ gParams.barLeft = this.localSettings.minMaxFont.size/3;
+ gParams.barRight = gParams.viewport.width - this.localSettings.minMaxFont.size/3;
+ } else {
+ maxW = Math.max(this.gauge.txtMin.node.getComputedTextLength(), this.gauge.txtMax.node.getComputedTextLength());
+ gParams.minX = maxW/2 + this.localSettings.minMaxFont.size/3;
+ gParams.maxX = gParams.viewport.width - maxW/2 - this.localSettings.minMaxFont.size/3;
+ gParams.barLeft = gParams.minX;
+ gParams.barRight = gParams.maxX;
+ gParams.labelY = gParams.barBottom + 4 + this.localSettings.labelFont.size;
+ this.gauge.txtLabel.attr({"y": gParams.labelY });
+ this.gauge.txtMin.attr({"x": gParams.minX, "y": gParams.labelY });
+ this.gauge.txtMax.attr({"x": gParams.maxX, "y": gParams.labelY });
+ }
+ gParams.viewport.y = 40;
+ gParams.viewport.height = gParams.labelY-25;
+ } else if (this.localSettings.gaugeType === 'verticalBar') {
+ gParams.titleY = (this.localSettings.title === '' ? 0 : this.localSettings.titleFont.size) + 8;
+ this.gauge.txtTitle.attr({"y": gParams.titleY });
+ gParams.titleBottom = gParams.titleY + (this.localSettings.title === '' ? 0 : 8);
+
+ gParams.valueY = gParams.titleBottom + (this.localSettings.hideValue ? 0 : this.localSettings.valueFont.size);
+ gParams.barTop = gParams.valueY + 8;
+ this.gauge.txtValue.attr({"y": gParams.valueY });
+
+ gParams.labelY = gParams.widgetH - 16;
+ if (this.localSettings.unitTitle === '') {
+ gParams.barBottom = gParams.labelY;
+ } else {
+ gParams.barBottom = gParams.labelY - 4 - this.localSettings.labelFont.size;
+ this.gauge.txtLabel.attr({"y": gParams.labelY });
+ }
+ gParams.minX = gParams.maxX = (gParams.widgetW/2 + gParams.dx) + gParams.strokeWidth/2 + this.localSettings.minMaxFont.size/3;
+ gParams.minY = gParams.barBottom;
+ gParams.maxY = gParams.barTop;
+ this.gauge.txtMin.attr({"text-anchor": "start", "x": gParams.minX, "y": gParams.minY });
+ this.gauge.txtMax.attr({"text-anchor": "start", "x": gParams.maxX, "y": gParams.maxY });
+ maxW = Math.max(this.gauge.txtMin.node.getComputedTextLength(), this.gauge.txtMax.node.getComputedTextLength());
+ gParams.prefWidth = gParams.strokeWidth + (maxW + this.localSettings.minMaxFont.size ) * 2;
+ gParams.viewport.x = (gParams.canvasW - gParams.prefWidth)/2;
+ gParams.viewport.width = gParams.prefWidth;
+ }
+ this.gauge.canvas.setViewBox(gParams.viewport.x, gParams.viewport.y, gParams.viewport.width, gParams.viewport.height, true);
+
+ if (this.localSettings.dashThickness) {
+ var Rm = Ri + gParams.strokeWidth * 0.5;
+ var circumference = Math.PI * Rm;
+ if (this.localSettings.gaugeType === 'donut') {
+ circumference *=2;
+ }
+ var dashCount = Math.floor(circumference / (this.localSettings.dashThickness));
+ if (this.localSettings.gaugeType === 'donut') {
+ dashCount = (dashCount | 1) - 1;
+ } else {
+ dashCount = (dashCount - 1) | 1;
+ }
+ var dashLength = circumference/dashCount;
+ gaugeLevelElement.attr('stroke-dasharray', '' + dashLength + 'px');
+ gaugeBackElement.attr('stroke-dasharray', '' + dashLength + 'px');
+ }
+
+ function getColor(val, pct) {
+
+ var lower, upper, range, rangePct, pctLower, pctUpper, color;
+
+ if (tbGauge.localSettings.colorsRange.length === 1) {
+ return tbGauge.localSettings.colorsRange[0].rgbString;
+ }
+ if (pct === 0) {
+ return tbGauge.localSettings.colorsRange[0].rgbString;
+ }
+
+ for (var j = 0; j < tbGauge.localSettings.colorsRange.length; j++) {
+ if (pct <= tbGauge.localSettings.colorsRange[j].pct) {
+ lower = tbGauge.localSettings.colorsRange[j - 1];
+ upper = tbGauge.localSettings.colorsRange[j];
+ range = upper.pct - lower.pct;
+ rangePct = (pct - lower.pct) / range;
+ pctLower = 1 - rangePct;
+ pctUpper = rangePct;
+ color = tinycolor({
+ r: Math.floor(lower.color.r * pctLower + upper.color.r * pctUpper),
+ g: Math.floor(lower.color.g * pctLower + upper.color.g * pctUpper),
+ b: Math.floor(lower.color.b * pctLower + upper.color.b * pctUpper)
+ });
+ return color.toRgbString();
+ }
+ }
+
+ }
+
+ this.gauge.canvas.customAttributes.pki = function(value, min, max, w, h, dx, dy, gws, donut, reverse) { // eslint-disable-line no-unused-vars
+ var alpha, Rm, Ro, Ri, Cx, Cy, Xm, Ym, Xo, Yo, path;
+
+ if (tbGauge.localSettings.neonGlowBrightness && !isFirefox
+ && tbGauge.floodColorElement1 && tbGauge.floodColorElement2) {
+ var progress = (value - min) / (max - min);
+ var resultColor = getColor(value, progress);
+ var brightenColor1 = tinycolor(resultColor).brighten(tbGauge.localSettings.neonGlowBrightness).toRgbString();
+ var brightenColor2 = resultColor;
+ tbGauge.floodColorElement1.setAttribute('flood-color', brightenColor1);
+ tbGauge.floodColorElement2.setAttribute('flood-color', brightenColor2);
+ }
+
+ var gaugeType = tbGauge.localSettings.gaugeType;
+
+ if (gaugeType === 'donut') {
+ alpha = (1 - 2 * (value - min) / (max - min)) * Math.PI;
+ Ro = w / 2 - w / 7;
+ Ri = Ro - w / 6.666666666666667 * gws;
+ Rm = Ri + (Ro - Ri)/2;
+
+ Cx = w / 2 + dx;
+ Cy = h / 1.95 + dy;
+
+ Xm = w / 2 + dx + Rm * Math.cos(alpha);
+ Ym = h - (h - Cy) - Rm * Math.sin(alpha);
+
+ path = "M" + (Cx - Rm) + "," + Cy + " ";
+ if ((value - min) > ((max - min) / 2)) {
+ path += "A" + Rm + "," + Rm + " 0 0 1 " + (Cx + Rm) + "," + Cy + " ";
+ path += "A" + Rm + "," + Rm + " 0 0 1 " + Xm + "," + Ym + " ";
+ } else {
+ path += "A" + Rm + "," + Rm + " 0 0 1 " + Xm + "," + Ym + " ";
+ }
+ return {
+ path: path
+ };
+
+ } else if (gaugeType === 'arc') {
+ alpha = (1 - (value - min) / (max - min)) * Math.PI;
+ Ro = w / 2 - w / 10;
+ Ri = Ro - w / 6.666666666666667 * gws;
+ Rm = Ri + (Ro - Ri)/2;
+
+ Cx = w / 2 + dx;
+ Cy = h / 1.25 + dy;
+
+ Xm = w / 2 + dx + Rm * Math.cos(alpha);
+ Ym = h - (h - Cy) - Rm * Math.sin(alpha);
+
+ path = "M" + (Cx - Rm) + "," + Cy + " ";
+ path += "A" + Rm + "," + Rm + " 0 0 1 " + Xm + "," + Ym + " ";
+
+ return {
+ path: path
+ };
+ } else if (gaugeType === 'horizontalBar') {
+ Cx = tbGauge.gauge.params.barLeft;
+ Cy = tbGauge.gauge.params.barTop + tbGauge.gauge.params.strokeWidth/2;
+ Ro = (tbGauge.gauge.params.barRight - tbGauge.gauge.params.barLeft)/2;
+ alpha = (value - min) / (max - min);
+ Xo = Cx + 2 * Ro * alpha;
+ path = "M" + Cx + "," + Cy + " ";
+ path += "H" + " " + Xo;
+ return {
+ path: path
+ };
+ } else if (gaugeType === 'verticalBar') {
+ Cx = w / 2 + dx;
+ Cy = tbGauge.gauge.params.barBottom;
+ Ro = (tbGauge.gauge.params.barBottom - tbGauge.gauge.params.barTop)/2;
+ alpha = (value - min) / (max - min);
+ Yo = Cy - 2 * Ro * alpha;
+ path = "M" + Cx + "," + Cy + " ";
+ path += "V" + " " + Yo;
+ return {
+ path: path
+ };
+ }
+ };
+
+ var gaugeAttrs = {
+ "stroke": this.gauge.gauge.attrs.fill,
+ "fill": 'rgba(0,0,0,0)',
+ pki: [ this.gauge.config.max,
+ this.gauge.config.min,
+ this.gauge.config.max,
+ gParams.widgetW,
+ gParams.widgetH,
+ gParams.dx,
+ gParams.dy,
+ this.gauge.config.gaugeWidthScale,
+ this.gauge.config.donut,
+ this.gauge.config.reverse
+ ]
+ };
+ gaugeAttrs['stroke-width'] = gParams.strokeWidth;
+
+
+ var gaugeLevelAttrs = {
+ "stroke": this.gauge.level.attrs.fill,
+ "fill": 'rgba(0,0,0,0)'
+ };
+ gaugeLevelAttrs['stroke-width'] = gParams.strokeWidth;
+ if (this.localSettings.roundedLineCap) {
+ gaugeAttrs['stroke-linecap'] = 'round';
+ gaugeLevelAttrs['stroke-linecap'] = 'round';
+ }
+
+ this.gauge.gauge.attr(gaugeAttrs);
+ this.gauge.level.attr(gaugeLevelAttrs);
+
+ this.gauge.level.animate = function(attrs, refreshAnimationTime, refreshAnimationType) {
+ if (attrs.fill) {
+ attrs.stroke = attrs.fill;
+ attrs.fill = 'rgba(0,0,0,0)';
+ }
+ return Raphael.el.animate.call(tbGauge.gauge.level, attrs, refreshAnimationTime, refreshAnimationType);
+ }
+
+ function neonShadow(color) {
+ var brightenColor = tinycolor(color).brighten(tbGauge.localSettings.neonGlowBrightness);
+ return '0 0 10px '+brightenColor+','+
+ '0 0 20px '+brightenColor+','+
+ '0 0 30px '+brightenColor+','+
+ '0 0 40px '+ color +','+
+ '0 0 70px '+ color +','+
+ '0 0 80px '+ color +','+
+ '0 0 100px '+ color +','+
+ '0 0 150px '+ color;
+ }
+
+ if (this.localSettings.neonGlowBrightness) {
+ titleTextElement.css('textShadow', neonShadow(this.localSettings.titleFont.origColor));
+ valueTextElement.css('textShadow', neonShadow(this.localSettings.valueFont.origColor));
+ labelTextElement.css('textShadow', neonShadow(this.localSettings.labelFont.origColor));
+ minValTextElement.css('textShadow', neonShadow(this.localSettings.minMaxFont.origColor));
+ maxValTextElement.css('textShadow', neonShadow(this.localSettings.minMaxFont.origColor));
+ }
+
+ if (this.localSettings.neonGlowBrightness && !isFirefox) {
+ var filterX = (gParams.viewport.x / gParams.viewport.width)*100 + '%';
+ var filterY = (gParams.viewport.y / gParams.viewport.height)*100 + '%';
+ var svgBackFilterId = 'backBlurFilter' + Math.random();
+ var svgBackFilter = document.createElementNS("http://www.w3.org/2000/svg", "filter"); // eslint-disable-line no-undef, angular/document-service
+ svgBackFilter.setAttribute('id', svgBackFilterId);
+ svgBackFilter.setAttribute('filterUnits', 'userSpaceOnUse');
+ svgBackFilter.setAttribute('x', filterX);
+ svgBackFilter.setAttribute('y', filterY);
+ svgBackFilter.setAttribute('width', '100%');
+ svgBackFilter.setAttribute('height', '100%');
+ svgBackFilter.innerHTML =
+ '<feComponentTransfer>'+
+ '<feFuncR type="linear" slope="1.5"/>'+
+ '<feFuncG type="linear" slope="1.5"/>'+
+ '<feFuncB type="linear" slope="1.5"/>'+
+ '</feComponentTransfer>'+
+ '<feGaussianBlur stdDeviation="3" result="coloredBlur"></feGaussianBlur>'+
+ '<feMerge>'+
+ '<feMergeNode in="coloredBlur"/>'+
+ '<feMergeNode in="SourceGraphic"/>'+
+ '</feMerge>';
+ gaugeBackElement.attr('filter', 'url(#'+svgBackFilterId+')');
+
+ var svgFillFilterId = 'fillBlurFilter' + Math.random();
+ var svgFillFilter = document.createElementNS("http://www.w3.org/2000/svg", "filter"); // eslint-disable-line no-undef, angular/document-service
+ svgFillFilter.setAttribute('id', svgFillFilterId);
+ svgFillFilter.setAttribute('filterUnits', 'userSpaceOnUse');
+ svgFillFilter.setAttribute('x', filterX);
+ svgFillFilter.setAttribute('y', filterY);
+ svgFillFilter.setAttribute('width', '100%');
+ svgFillFilter.setAttribute('height', '100%');
+
+ var brightenColor1 = tinycolor(this.localSettings.origLevelColors[0]).brighten(this.localSettings.neonGlowBrightness).toRgbString();
+ var brightenColor2 = tinycolor(this.localSettings.origLevelColors[0]).toRgbString();
+ svgFillFilter.innerHTML =
+ '<feFlood flood-color="'+brightenColor1+'" result="flood1" />'+
+ '<feComposite in="flood1" in2="SourceGraphic" operator="in" result="floodShape" />'+
+ '<feGaussianBlur in="floodShape" stdDeviation="3" result="blur" />'+
+ '<feFlood flood-color="'+brightenColor2+'" result="flood2" />'+
+ '<feComposite in="flood2" in2="SourceGraphic" operator="in" result="floodShape2" />'+
+ '<feGaussianBlur in="floodShape2" stdDeviation="12" result="blur2" />'+
+ '<feMerge result="blurs">'+
+ ' <feMergeNode in="blur2"/>'+
+ ' <feMergeNode in="blur2"/>'+
+ ' <feMergeNode in="blur"/>'+
+ ' <feMergeNode in="blur"/>'+
+ ' <feMergeNode in="SourceGraphic"/>'+
+ '</feMerge>';
+ this.floodColorElement1 = $('feFlood:nth-of-type(1)', svgFillFilter)[0];
+ this.floodColorElement2 = $('feFlood:nth-of-type(2)', svgFillFilter)[0];
+ gaugeLevelElement.attr('filter', 'url(#'+svgFillFilterId+')');
+
+ var svgDefsElement = $('svg > defs', containerElement);
+ svgDefsElement[0].appendChild(svgBackFilter);
+ svgDefsElement[0].appendChild(svgFillFilter);
+ } else {
+ gaugeBackElement.attr('filter', '');
+ gaugeLevelElement.attr('filter', '');
+ }
+ }
+
+ redraw(data) {
+ if (data.length > 0) {
+ var cellData = data[0];
+ if (cellData.data.length > 0) {
+ var tvPair = cellData.data[cellData.data.length -
+ 1];
+ var value = tvPair[1];
+ this.gauge.refresh(value);
+ }
+ }
+ }
+}
+
+/* eslint-enable angular/angularelement */
diff --git a/ui/src/app/widget/save-widget-type-as.controller.js b/ui/src/app/widget/save-widget-type-as.controller.js
new file mode 100644
index 0000000..a514741
--- /dev/null
+++ b/ui/src/app/widget/save-widget-type-as.controller.js
@@ -0,0 +1,41 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*@ngInject*/
+export default function SaveWidgetTypeAsController($mdDialog, $q, userService) {
+
+ var vm = this;
+
+ vm.saveAs = saveAs;
+ vm.cancel = cancel;
+
+ vm.widgetName;
+ vm.widgetsBundle;
+
+ vm.bundlesScope = 'system';
+
+ if (userService.getAuthority() === 'TENANT_ADMIN') {
+ vm.bundlesScope = 'tenant';
+ }
+
+ function cancel () {
+ $mdDialog.cancel();
+ }
+
+ function saveAs () {
+ $mdDialog.hide({widgetName: vm.widgetName, bundleId: vm.widgetsBundle.id.id, bundleAlias: vm.widgetsBundle.alias});
+ }
+
+}
diff --git a/ui/src/app/widget/save-widget-type-as.tpl.html b/ui/src/app/widget/save-widget-type-as.tpl.html
new file mode 100644
index 0000000..ec84ef3
--- /dev/null
+++ b/ui/src/app/widget/save-widget-type-as.tpl.html
@@ -0,0 +1,60 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'widget.save-widget-type-as' | translate }}">
+ <form name="theForm" ng-submit="vm.saveAs()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>widget.save-widget-type-as</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <fieldset>
+ <span translate>widget.save-widget-type-as-text</span>
+ <md-input-container class="md-block">
+ <label translate>widget.title</label>
+ <input required name="title" ng-model="vm.widgetName">
+ <div ng-messages="theForm.title.$error">
+ <div translate ng-message="required">widget.title-required</div>
+ </div>
+ </md-input-container>
+ <tb-widgets-bundle-select flex
+ ng-model="vm.widgetsBundle"
+ tb-required="true"
+ bundles-scope="{{vm.bundlesScope}}">
+ </tb-widgets-bundle-select>
+ </fieldset>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || theForm.$invalid" type="submit" class="md-raised md-primary">
+ {{ 'action.saveAs' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+ translate }}
+ </md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
diff --git a/ui/src/app/widget/select-widget-type.controller.js b/ui/src/app/widget/select-widget-type.controller.js
new file mode 100644
index 0000000..cb21e96
--- /dev/null
+++ b/ui/src/app/widget/select-widget-type.controller.js
@@ -0,0 +1,34 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*@ngInject*/
+export default function SelectWidgetTypeController($mdDialog, types) {
+
+ var vm = this;
+
+ vm.types = types;
+
+ vm.cancel = cancel;
+ vm.typeSelected = typeSelected;
+
+ function cancel() {
+ $mdDialog.cancel();
+ }
+
+ function typeSelected(widgetType) {
+ $mdDialog.hide(widgetType);
+ }
+
+}
diff --git a/ui/src/app/widget/select-widget-type.tpl.html b/ui/src/app/widget/select-widget-type.tpl.html
new file mode 100644
index 0000000..a805401
--- /dev/null
+++ b/ui/src/app/widget/select-widget-type.tpl.html
@@ -0,0 +1,67 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog style="width: 800px;" aria-label="{{ 'widget.select-widget-type' | translate }}">
+ <form name="theForm">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>widget.select-widget-type</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <fieldset ng-disabled="loading">
+ <div layout="column" layout-gt-sm="row" layout-align="center center">
+ <md-button class="tb-card-button md-raised md-primary" layout="column"
+ ng-click="vm.typeSelected(vm.types.widgetType.timeseries.value)">
+ <md-icon class="material-icons tb-md-96" aria-label="{{ vm.types.widgetType.timeseries.name | translate }}">
+ timeline
+ </md-icon>
+ <span translate>{{vm.types.widgetType.timeseries.name}}</span>
+ </md-button>
+ <md-button class="tb-card-button md-raised md-primary" layout="column"
+ ng-click="vm.typeSelected(vm.types.widgetType.latest.value)">
+ <md-icon class="material-icons tb-md-96"
+ aria-label="{{ vm.types.widgetType.latest.name | translate }}">track_changes
+ </md-icon>
+ <span translate>{{vm.types.widgetType.latest.name}}</span>
+ </md-button>
+ <md-button class="tb-card-button md-raised md-primary" layout="column"
+ ng-click="vm.typeSelected(vm.types.widgetType.rpc.value)">
+ <md-icon class="material-icons tb-md-96"
+ aria-label="{{ vm.types.widgetType.rpc.name | translate }}" md-svg-icon="mdi:developer-board">
+ </md-icon>
+ <span translate>{{vm.types.widgetType.rpc.name}}</span>
+ </md-button>
+ </div>
+ </fieldset>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+ translate }}
+ </md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
ui/src/app/widget/widget-editor.controller.js 645(+645 -0)
diff --git a/ui/src/app/widget/widget-editor.controller.js b/ui/src/app/widget/widget-editor.controller.js
new file mode 100644
index 0000000..ca9008a
--- /dev/null
+++ b/ui/src/app/widget/widget-editor.controller.js
@@ -0,0 +1,645 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import $ from 'jquery';
+import ace from 'brace';
+import 'brace/ext/language_tools';
+import 'brace/mode/javascript';
+import 'brace/mode/html';
+import 'brace/mode/css';
+import 'brace/mode/json';
+import 'ace-builds/src-min-noconflict/snippets/javascript';
+import 'ace-builds/src-min-noconflict/snippets/text';
+import 'ace-builds/src-min-noconflict/snippets/html';
+import 'ace-builds/src-min-noconflict/snippets/css';
+import 'ace-builds/src-min-noconflict/snippets/json';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import saveWidgetTypeAsTemplate from './save-widget-type-as.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+import Split from 'split.js';
+import beautify from 'js-beautify';
+
+const js_beautify = beautify.js;
+const html_beautify = beautify.html;
+const css_beautify = beautify.css;
+
+/* eslint-disable angular/angularelement */
+
+/*@ngInject*/
+export default function WidgetEditorController(widgetService, userService, types, toast, hotkeys,
+ $element, $rootScope, $scope, $state, $stateParams, $timeout,
+ $window, $document, $translate, $mdDialog) {
+
+ var Range = ace.acequire("ace/range").Range;
+ var ace_editors = [];
+ var js_editor;
+ var iframe = $('iframe', $element);
+ var gotError = false;
+ var errorMarkers = [];
+ var errorAnnotationId = -1;
+ var elem = $($element);
+
+ var widgetsBundleId = $stateParams.widgetsBundleId;
+
+ var vm = this;
+
+ vm.widgetsBundle;
+ vm.isDirty = false;
+ vm.fullscreen = false;
+ vm.widgetType = null;
+ vm.widget = null;
+ vm.origWidget = null;
+ vm.widgetTypes = types.widgetType;
+ vm.iframeWidgetEditModeInited = false;
+ vm.layoutInited = false;
+ vm.htmlEditorOptions = {
+ useWrapMode: true,
+ mode: 'html',
+ advanced: {
+ enableSnippets: true,
+ enableBasicAutocompletion: true,
+ enableLiveAutocompletion: true
+ },
+ onLoad: function (_ace) {
+ ace_editors.push(_ace);
+ }
+ };
+ vm.cssEditorOptions = {
+ useWrapMode: true,
+ mode: 'css',
+ advanced: {
+ enableSnippets: true,
+ enableBasicAutocompletion: true,
+ enableLiveAutocompletion: true
+ },
+ onLoad: function (_ace) {
+ ace_editors.push(_ace);
+ }
+ };
+ vm.jsonSettingsEditorOptions = {
+ useWrapMode: true,
+ mode: 'json',
+ advanced: {
+ enableSnippets: true,
+ enableBasicAutocompletion: true,
+ enableLiveAutocompletion: true
+ },
+ onLoad: function (_ace) {
+ ace_editors.push(_ace);
+ }
+ };
+ vm.dataKeyJsonSettingsEditorOptions = {
+ useWrapMode: true,
+ mode: 'json',
+ advanced: {
+ enableSnippets: true,
+ enableBasicAutocompletion: true,
+ enableLiveAutocompletion: true
+ },
+ onLoad: function (_ace) {
+ ace_editors.push(_ace);
+ }
+ };
+ vm.jsEditorOptions = {
+ useWrapMode: true,
+ mode: 'javascript',
+ advanced: {
+ enableSnippets: true,
+ enableBasicAutocompletion: true,
+ enableLiveAutocompletion: true
+ },
+ onLoad: function (_ace) {
+ ace_editors.push(_ace);
+ js_editor = _ace;
+ js_editor.session.on("change", function () {
+ cleanupJsErrors();
+ });
+ }
+ };
+
+ vm.addResource = addResource;
+ vm.applyWidgetScript = applyWidgetScript;
+ vm.beautifyCss = beautifyCss;
+ vm.beautifyDataKeyJson = beautifyDataKeyJson;
+ vm.beautifyHtml = beautifyHtml;
+ vm.beautifyJs = beautifyJs;
+ vm.beautifyJson = beautifyJson;
+ vm.removeResource = removeResource;
+ vm.undoDisabled = undoDisabled;
+ vm.undoWidget = undoWidget;
+ vm.saveDisabled = saveDisabled;
+ vm.saveWidget = saveWidget;
+ vm.saveAsDisabled = saveAsDisabled;
+ vm.saveWidgetAs = saveWidgetAs;
+ vm.toggleFullscreen = toggleFullscreen;
+ vm.isReadOnly = isReadOnly;
+
+ initWidgetEditor();
+
+ function initWidgetEditor() {
+
+ $rootScope.loading = true;
+
+ widgetService.getWidgetsBundle(widgetsBundleId).then(
+ function success(widgetsBundle) {
+ vm.widgetsBundle = widgetsBundle;
+ if ($stateParams.widgetTypeId) {
+ widgetService.getWidgetTypeById($stateParams.widgetTypeId).then(
+ function success(widgetType) {
+ setWidgetType(widgetType)
+ widgetTypeLoaded();
+ },
+ function fail() {
+ toast.showError($translate.instant('widget.widget-type-load-failed-error'));
+ widgetTypeLoaded();
+ }
+ );
+ } else {
+ var type = $stateParams.widgetType;
+ if (!type) {
+ type = types.widgetType.timeseries.value;
+ }
+ widgetService.getWidgetTemplate(type).then(
+ function success(widgetTemplate) {
+ vm.widget = angular.copy(widgetTemplate);
+ vm.widget.widgetName = null;
+ vm.origWidget = angular.copy(vm.widget);
+ vm.isDirty = true;
+ widgetTypeLoaded();
+ },
+ function fail() {
+ toast.showError($translate.instant('widget.widget-template-load-failed-error'));
+ widgetTypeLoaded();
+ }
+ );
+ }
+ },
+ function fail() {
+ toast.showError($translate.instant('widget.widget-type-load-failed-error'));
+ widgetTypeLoaded();
+ }
+ );
+
+ }
+
+ function setWidgetType(widgetType) {
+ vm.widgetType = widgetType;
+ vm.widget = widgetService.toWidgetInfo(vm.widgetType);
+ var config = angular.fromJson(vm.widget.defaultConfig);
+ vm.widget.defaultConfig = angular.toJson(config)
+ vm.origWidget = angular.copy(vm.widget);
+ vm.isDirty = false;
+ }
+
+ function widgetTypeLoaded() {
+ initHotKeys();
+
+ initWatchers();
+
+ angular.element($document[0]).ready(function () {
+ var w = elem.width();
+ if (w > 0) {
+ initSplitLayout();
+ } else {
+ $scope.$watch(
+ function () {
+ return elem[0].offsetWidth || parseInt(elem.css('width'), 10);
+ },
+ function (newSize) {
+ if (newSize > 0) {
+ initSplitLayout();
+ }
+ }
+ );
+ }
+ });
+
+ iframe.attr('data-widget', angular.toJson(vm.widget));
+ iframe.attr('src', '/widget-editor');
+ }
+
+ function undoDisabled() {
+ return $scope.loading
+ || !vm.isDirty
+ || !vm.iframeWidgetEditModeInited
+ || vm.saveWidgetPending
+ || vm.saveWidgetAsPending;
+ }
+
+ function saveDisabled() {
+ return vm.isReadOnly()
+ || $scope.loading
+ || !vm.isDirty
+ || !vm.iframeWidgetEditModeInited
+ || vm.saveWidgetPending
+ || vm.saveWidgetAsPending;
+ }
+
+ function saveAsDisabled() {
+ return $scope.loading
+ || !vm.iframeWidgetEditModeInited
+ || vm.saveWidgetPending
+ || vm.saveWidgetAsPending;
+ }
+
+ function initHotKeys() {
+ $translate(['widget.undo', 'widget.save', 'widget.saveAs', 'widget.toggle-fullscreen', 'widget.run']).then(function (translations) {
+ hotkeys.bindTo($scope)
+ .add({
+ combo: 'ctrl+q',
+ description: translations['widget.undo'],
+ allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+ callback: function (event) {
+ if (!undoDisabled()) {
+ event.preventDefault();
+ undoWidget();
+ }
+ }
+ })
+ .add({
+ combo: 'ctrl+s',
+ description: translations['widget.save'],
+ allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+ callback: function (event) {
+ if (!saveDisabled()) {
+ event.preventDefault();
+ saveWidget();
+ }
+ }
+ })
+ .add({
+ combo: 'shift+ctrl+s',
+ description: translations['widget.saveAs'],
+ allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+ callback: function (event) {
+ if (!saveAsDisabled()) {
+ event.preventDefault();
+ saveWidgetAs();
+ }
+ }
+ })
+ .add({
+ combo: 'shift+ctrl+f',
+ description: translations['widget.toggle-fullscreen'],
+ allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+ callback: function (event) {
+ event.preventDefault();
+ toggleFullscreen();
+ }
+ })
+ .add({
+ combo: 'ctrl+enter',
+ description: translations['widget.run'],
+ allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+ callback: function (event) {
+ event.preventDefault();
+ applyWidgetScript();
+ }
+ });
+ });
+ }
+
+ function initWatchWidget() {
+ $scope.widgetWatcher = $scope.$watch('vm.widget', function (newVal, oldVal) {
+ if (!angular.equals(newVal, oldVal)) {
+ vm.isDirty = true;
+ }
+ }, true);
+ }
+
+ function initWatchers() {
+ initWatchWidget();
+
+ $scope.$watch('vm.widget.type', function (newVal, oldVal) {
+ if (!angular.equals(newVal, oldVal)) {
+ var config = angular.fromJson(vm.widget.defaultConfig);
+ if (vm.widget.type !== types.widgetType.rpc.value) {
+ if (config.targetDeviceAliases) {
+ delete config.targetDeviceAliases;
+ }
+ if (!config.datasources) {
+ config.datasources = [];
+ }
+ if (!config.timewindow) {
+ config.timewindow = {
+ realtime: {
+ timewindowMs: 60000
+ }
+ };
+ }
+ for (var i in config.datasources) {
+ var datasource = config.datasources[i];
+ datasource.type = vm.widget.type;
+ if (vm.widget.type !== types.widgetType.timeseries.value && datasource.intervalSec) {
+ delete datasource.intervalSec;
+ } else if (vm.widget.type === types.widgetType.timeseries.value && !datasource.intervalSec) {
+ datasource.intervalSec = 60;
+ }
+ }
+ } else {
+ if (config.datasources) {
+ delete config.datasources;
+ }
+ if (config.timewindow) {
+ delete config.timewindow;
+ }
+ if (!config.targetDeviceAliases) {
+ config.targetDeviceAliases = [];
+ }
+ }
+ vm.widget.defaultConfig = angular.toJson(config);
+ }
+ });
+
+ $scope.$on('widgetEditModeInited', function () {
+ vm.iframeWidgetEditModeInited = true;
+ if (vm.saveWidgetPending || vm.saveWidgetAsPending) {
+ if (!vm.saveWidgetTimeout) {
+ vm.saveWidgetTimeout = $timeout(function () {
+ if (!gotError) {
+ if (vm.saveWidgetPending) {
+ commitSaveWidget();
+ } else if (vm.saveWidgetAsPending) {
+ commitSaveWidgetAs();
+ }
+ } else {
+ toast.showError($translate.instant('widget.unable-to-save-widget-error'));
+ vm.saveWidgetPending = false;
+ vm.saveWidgetAsPending = false;
+ initWatchWidget();
+ }
+ vm.saveWidgetTimeout = undefined;
+ }, 1500);
+ }
+ }
+ });
+
+ $scope.$on('widgetEditUpdated', function (event, widget) {
+ vm.widget.sizeX = widget.sizeX / 2;
+ vm.widget.sizeY = widget.sizeY / 2;
+ vm.widget.defaultConfig = angular.toJson(widget.config);
+ iframe.attr('data-widget', angular.toJson(vm.widget));
+ });
+
+ $scope.$on('widgetException', function (event, details) {
+ if (!gotError) {
+ gotError = true;
+ var errorInfo = 'Error:';
+ if (details.name) {
+ errorInfo += ' ' + details.name + ':';
+ }
+ if (details.message) {
+ errorInfo += ' ' + details.message;
+ }
+ if (details.lineNumber) {
+ errorInfo += '<br>Line ' + details.lineNumber;
+ if (details.columnNumber) {
+ errorInfo += ' column ' + details.columnNumber;
+ }
+ errorInfo += ' of script.';
+ }
+ if (!vm.saveWidgetPending && !vm.saveWidgetAsPending) {
+ toast.showError(errorInfo, $('#javascript_panel', $element)[0]);
+ }
+ if (js_editor && details.lineNumber) {
+ var line = details.lineNumber - 1;
+ var column = 0;
+ if (details.columnNumber) {
+ column = details.columnNumber;
+ }
+
+ var errorMarkerId = js_editor.session.addMarker(new Range(line, 0, line, Infinity), "ace_active-line", "screenLine");
+ errorMarkers.push(errorMarkerId);
+ var annotations = js_editor.session.getAnnotations();
+ var errorAnnotation = {
+ row: line,
+ column: column,
+ text: details.message,
+ type: "error"
+ };
+ errorAnnotationId = annotations.push(errorAnnotation) - 1;
+ js_editor.session.setAnnotations(annotations);
+ }
+ }
+ });
+ }
+
+ function cleanupJsErrors() {
+ toast.hide();
+ for (var i = 0; i < errorMarkers.length; i++) {
+ js_editor.session.removeMarker(errorMarkers[i]);
+ }
+ errorMarkers = [];
+ if (errorAnnotationId && errorAnnotationId > -1) {
+ var annotations = js_editor.session.getAnnotations();
+ annotations.splice(errorAnnotationId, 1);
+ js_editor.session.setAnnotations(annotations);
+ errorAnnotationId = -1;
+ }
+ }
+
+ function onDividerDrag() {
+ for (var i in ace_editors) {
+ var ace = ace_editors[i];
+ ace.resize();
+ ace.renderer.updateFull();
+ }
+ }
+
+ function initSplitLayout() {
+ if (!vm.layoutInited) {
+ Split([$('#top_panel', $element)[0], $('#bottom_panel', $element)[0]], {
+ sizes: [35, 65],
+ gutterSize: 8,
+ cursor: 'row-resize',
+ direction: 'vertical',
+ onDrag: function () {
+ onDividerDrag()
+ }
+ });
+
+ Split([$('#top_left_panel', $element)[0], $('#top_right_panel', $element)[0]], {
+ sizes: [50, 50],
+ gutterSize: 8,
+ cursor: 'col-resize',
+ onDrag: function () {
+ onDividerDrag()
+ }
+ });
+
+ Split([$('#javascript_panel', $element)[0], $('#frame_panel', $element)[0]], {
+ sizes: [50, 50],
+ gutterSize: 8,
+ cursor: 'col-resize',
+ onDrag: function () {
+ onDividerDrag()
+ }
+ });
+
+ onDividerDrag();
+
+ $scope.$applyAsync(function () {
+ vm.layoutInited = true;
+ $rootScope.loading = false;
+ var w = angular.element($window);
+ $timeout(function () {
+ w.triggerHandler('resize')
+ });
+ });
+
+ }
+ }
+
+ function removeResource(index) {
+ if (index > -1) {
+ vm.widget.resources.splice(index, 1);
+ }
+ }
+
+ function addResource() {
+ vm.widget.resources.push({url: ''});
+ }
+
+ function applyWidgetScript() {
+ cleanupJsErrors();
+ gotError = false;
+ vm.iframeWidgetEditModeInited = false;
+ var config = angular.fromJson(vm.widget.defaultConfig);
+ config.title = vm.widget.widgetName;
+ vm.widget.defaultConfig = angular.toJson(config);
+ iframe.attr('data-widget', angular.toJson(vm.widget));
+ iframe[0].contentWindow.location.reload(true);
+ }
+
+ function toggleFullscreen() {
+ vm.fullscreen = !vm.fullscreen;
+ }
+
+ function isReadOnly() {
+ if (userService.getAuthority() === 'TENANT_ADMIN') {
+ return !vm.widgetsBundle || vm.widgetsBundle.tenantId.id === types.id.nullUid;
+ } else {
+ return userService.getAuthority() != 'SYS_ADMIN';
+ }
+ }
+
+ function undoWidget() {
+ if ($scope.widgetWatcher) {
+ $scope.widgetWatcher();
+ }
+ vm.widget = angular.copy(vm.origWidget);
+ vm.isDirty = false;
+ initWatchWidget();
+ applyWidgetScript();
+ }
+
+ function saveWidget() {
+ if (!vm.widget.widgetName) {
+ toast.showError($translate.instant('widget.missing-widget-title-error'));
+ } else {
+ $scope.widgetWatcher();
+ vm.saveWidgetPending = true;
+ applyWidgetScript();
+ }
+ }
+
+ function saveWidgetAs($event) {
+ $scope.widgetWatcher();
+ vm.saveWidgetAsPending = true;
+ vm.saveWidgetAsEvent = $event;
+ applyWidgetScript();
+ }
+
+ function commitSaveWidget() {
+ var id = (vm.widgetType && vm.widgetType.id) ? vm.widgetType.id : undefined;
+ widgetService.saveWidgetType(vm.widget, id, vm.widgetsBundle.alias).then(
+ function success(widgetType) {
+ setWidgetType(widgetType)
+ vm.saveWidgetPending = false;
+ initWatchWidget();
+ toast.showSuccess($translate.instant('widget.widget-saved'), 500);
+ }, function fail() {
+ vm.saveWidgetPending = false;
+ initWatchWidget();
+ }
+ );
+ }
+
+ function commitSaveWidgetAs() {
+ $mdDialog.show({
+ controller: 'SaveWidgetTypeAsController',
+ controllerAs: 'vm',
+ templateUrl: saveWidgetTypeAsTemplate,
+ parent: angular.element($document[0].body),
+ fullscreen: true,
+ targetEvent: vm.saveWidgetAsEvent
+ }).then(function (saveWidgetAsData) {
+ vm.widget.widgetName = saveWidgetAsData.widgetName;
+ vm.widget.alias = undefined;
+ var config = angular.fromJson(vm.widget.defaultConfig);
+ config.title = vm.widget.widgetName;
+ vm.widget.defaultConfig = angular.toJson(config);
+
+ vm.saveWidgetAsPending = false;
+ vm.isDirty = false;
+ initWatchWidget();
+ widgetService.saveWidgetType(vm.widget, undefined, saveWidgetAsData.bundleAlias).then(
+ function success(widgetType) {
+ $state.go('home.widgets-bundles.widget-types.widget-type',
+ {widgetsBundleId: saveWidgetAsData.bundleId, widgetTypeId: widgetType.id.id});
+ },
+ function fail() {
+ vm.saveWidgetAsPending = false;
+ initWatchWidget();
+ }
+ );
+ }, function () {
+ vm.saveWidgetAsPending = false;
+ initWatchWidget();
+ });
+ }
+
+ function beautifyJs() {
+ var res = js_beautify(vm.widget.controllerScript, {indent_size: 4, wrap_line_length: 60});
+ vm.widget.controllerScript = res;
+ }
+
+ function beautifyHtml() {
+ var res = html_beautify(vm.widget.templateHtml, {indent_size: 4, wrap_line_length: 60});
+ vm.widget.templateHtml = res;
+ }
+
+ function beautifyCss() {
+ var res = css_beautify(vm.widget.templateCss, {indent_size: 4});
+ vm.widget.templateCss = res;
+ }
+
+ function beautifyJson() {
+ var res = js_beautify(vm.widget.settingsSchema, {indent_size: 4});
+ vm.widget.settingsSchema = res;
+ }
+
+ function beautifyDataKeyJson() {
+ var res = js_beautify(vm.widget.dataKeySettingsSchema, {indent_size: 4});
+ vm.widget.dataKeySettingsSchema = res;
+ }
+
+}
+
+/* eslint-enable angular/angularelement */
\ No newline at end of file
ui/src/app/widget/widget-editor.scss 151(+151 -0)
diff --git a/ui/src/app/widget/widget-editor.scss b/ui/src/app/widget/widget-editor.scss
new file mode 100644
index 0000000..ef79477
--- /dev/null
+++ b/ui/src/app/widget/widget-editor.scss
@@ -0,0 +1,151 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import "~compass-sass-mixins/lib/compass";
+
+$edit-toolbar-height: 40px;
+
+.tb-editor {
+ .tb-split {
+ @include box-sizing(border-box);
+ overflow-y: auto;
+ overflow-x: hidden;
+ }
+
+ .ace_editor {
+ font-size: 14px !important;
+ }
+
+ .tb-content {
+ border: 1px solid #C0C0C0;
+ }
+
+ .gutter {
+ background-color: transparent;
+
+ background-repeat: no-repeat;
+ background-position: 50%;
+ }
+
+ .gutter.gutter-horizontal {
+ cursor: col-resize;
+ background-image: url('../../../node_modules/split.js/grips/vertical.png');
+ }
+
+ .gutter.gutter-vertical {
+ cursor: row-resize;
+ background-image: url('../../../node_modules/split.js/grips/horizontal.png');
+ }
+
+ .tb-split.tb-split-horizontal, .gutter.gutter-horizontal {
+ height: 100%;
+ float: left;
+ }
+
+ .tb-split.tb-split-vertical {
+ display: flex;
+ .tb-split.tb-content {
+ height: 100%;
+ }
+ }
+}
+
+.tb-split-vertical {
+ md-tabs {
+
+ md-tabs-content-wrapper {
+ height: calc(100% - 49px);
+
+ md-tab-content {
+ height: 100%;
+
+ & > div {
+ height: 100%;
+ }
+
+ }
+
+ }
+
+ }
+}
+
+div.tb-editor-area-title-panel {
+ position: absolute;
+ font-size: 0.800rem;
+ font-weight: 500;
+ top: 5px;
+ right: 20px;
+ z-index: 5;
+ label {
+ color: #00acc1;
+ background: rgba(220, 220, 220, 0.35);
+ border-radius: 5px;
+ padding: 4px;
+ text-transform: uppercase;
+ }
+ .md-button {
+ color: #7B7B7B;
+ min-width: 32px;
+ min-height: 15px;
+ line-height: 15px;
+ font-size: 0.800rem;
+ margin: 0;
+ padding: 4px;
+ background: rgba(220, 220, 220, 0.35);
+ }
+}
+
+.tb-resize-container {
+ overflow-y: auto;
+ height: 100%;
+ width: 100%;
+ position: relative;
+
+ .ace_editor {
+ height: 100%;
+ }
+}
+
+md-toolbar.tb-edit-toolbar {
+
+ min-height: $edit-toolbar-height !important;
+ max-height: $edit-toolbar-height !important;
+
+ .md-toolbar-tools {
+ min-height: $edit-toolbar-height !important;
+ max-height: $edit-toolbar-height !important;
+ .md-button {
+ min-width: 65px;
+ min-height: 30px;
+ line-height: 30px;
+ font-size: 12px;
+ md-icon {
+ font-size: 20px;
+ }
+ span {
+ padding-right: 6px;
+ }
+ }
+ md-input-container {
+ input {
+ font-size: 1.200rem;
+ font-weight: 400;
+ letter-spacing: 0.005em;
+ height: 28px;
+ }
+ }
+ }
+}
ui/src/app/widget/widget-editor.tpl.html 258(+258 -0)
diff --git a/ui/src/app/widget/widget-editor.tpl.html b/ui/src/app/widget/widget-editor.tpl.html
new file mode 100644
index 0000000..a5e661d
--- /dev/null
+++ b/ui/src/app/widget/widget-editor.tpl.html
@@ -0,0 +1,258 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div flex layout="column" tb-confirm-on-exit is-dirty="vm.isDirty">
+ <div flex layout="column" tb-expand-fullscreen="vm.fullscreen" hide-expand-button="true">
+ <md-toolbar class="md-whiteframe-z1 tb-edit-toolbar md-hue-3">
+ <div flex class="md-toolbar-tools">
+ <md-input-container>
+ <label> </label>
+ <input ng-disabled="vm.isReadOnly()" ng-model="vm.widget.widgetName" placeholder="{{ 'widget.title' | translate }}">
+ </md-input-container>
+ <md-input-container>
+ <md-select ng-disabled="vm.isReadOnly()" placeholder="{{ 'widget.type' | translate }}" required id="widgetType"
+ ng-model="vm.widget.type">
+ <md-option ng-repeat="type in vm.widgetTypes" value="{{type.value}}">
+ {{ type.name | translate }}
+ </md-option>
+ </md-select>
+ </md-input-container>
+ <span flex=""></span>
+ <md-button hide-xs hide-sm aria-label="{{ 'widget.run' | translate }}" ng-disabled="!vm.iframeWidgetEditModeInited"
+ ng-click="vm.applyWidgetScript()">
+ <md-tooltip md-direction="bottom">
+ {{ 'widget.run' | translate }} (CTRL + Return)
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.run' | translate }}">play_arrow</md-icon>
+ <span translate>action.run</span>
+ </md-button>
+ <md-button hide-xs hide-sm ng-disabled="vm.undoDisabled()" class="md-raised"
+ aria-label="{{ 'widget.undo' | translate }}" ng-click="vm.undoWidget()">
+ <md-tooltip md-direction="bottom">
+ {{ 'widget.undo' | translate }} (CTRL + Q)
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.undo' | translate }}">undo</md-icon>
+ <span translate>action.undo</span>
+ </md-button>
+ <md-button ng-if="!vm.isReadOnly()" hide-xs hide-sm ng-disabled="vm.saveDisabled()" class="md-raised"
+ aria-label="{{ 'widget.save' | translate }}" ng-click="vm.saveWidget()" tb-circular-progress="vm.saveWidgetPending">
+ <md-tooltip md-direction="bottom">
+ {{ 'widget.save' | translate }} (CTRL + S)
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.save' | translate }}">save</md-icon>
+ <span translate>action.save</span>
+ </md-button>
+ <md-button hide-xs hide-sm ng-disabled="vm.saveAsDisabled()" class="md-raised"
+ aria-label="{{ 'widget.saveAs' | translate }}" ng-click="vm.saveWidgetAs($event)" tb-circular-progress="vm.saveWidgetAsPending">
+ <md-tooltip md-direction="bottom">
+ {{ 'widget.saveAs' | translate }} (Shift + CTRL + S)
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.saveAs' | translate }}">save</md-icon>
+ <span translate>action.saveAs</span>
+ </md-button>
+ <md-button hide-xs hide-sm aria-label="{{ 'widget.toggle-fullscreen' | translate }}" ng-click="vm.toggleFullscreen()">
+ <md-tooltip md-direction="bottom">
+ {{ 'widget.toggle-fullscreen' | translate }} (Shift + CTRL + F)
+ </md-tooltip>
+ <md-icon ng-show="!vm.fullscreen" aria-label="{{ 'widget.toggle-fullscreen' | translate }}">
+ fullscreen
+ </md-icon>
+ <md-icon ng-show="vm.fullscreen" aria-label="{{ 'widget.toggle-fullscreen' | translate }}">
+ fullscreen_exit
+ </md-icon>
+ <span translate hide-xs hide-sm>widget.toggle-fullscreen</span>
+ </md-button>
+ <md-menu hide-gt-sm md-position-mode="target-right target">
+ <md-button class="md-icon-button" aria-label="{{ 'home.open-user-menu' | translate }}" ng-click="$mdOpenMenu($event)">
+ <md-icon md-menu-origin aria-label="{{ 'home.open-user-menu' | translate }}" class="material-icons">more_vert</md-icon>
+ </md-button>
+ <md-menu-content width="4">
+ <md-menu-item>
+ <md-button ng-disabled="!vm.iframeWidgetEditModeInited"
+ ng-click="vm.applyWidgetScript()">
+ <md-icon md-menu-align-target aria-label="{{ 'action.run' | translate }}" class="material-icons">play_arrow</md-icon>
+ <span translate>action.run</span>
+ </md-button>
+ </md-menu-item>
+ <md-menu-item>
+ <md-button ng-disabled="vm.undoDisabled()"
+ ng-click="vm.undoWidget()">
+ <md-icon md-menu-align-target aria-label="{{ 'action.undo' | translate }}" class="material-icons">undo</md-icon>
+ <span translate>action.undo</span>
+ </md-button>
+ </md-menu-item>
+ <md-menu-item ng-if="!vm.isReadOnly()">
+ <md-button ng-disabled="vm.saveDisabled()"
+ ng-click="vm.saveWidget()">
+ <md-icon md-menu-align-target aria-label="{{ 'action.save' | translate }}" class="material-icons">save</md-icon>
+ <span translate>action.save</span>
+ </md-button>
+ </md-menu-item>
+ <md-menu-item>
+ <md-button ng-disabled="vm.saveAsDisabled()"
+ ng-click="vm.saveWidgetAs($event)">
+ <md-icon md-menu-align-target aria-label="{{ 'action.saveAs' | translate }}" class="material-icons">save</md-icon>
+ <span translate>action.saveAs</span>
+ </md-button>
+ </md-menu-item>
+ </md-menu-content>
+ </md-menu>
+ </div>
+ </md-toolbar>
+ <div flex style="position: relative;">
+ <div class="tb-editor tb-absolute-fill" style="">
+ <div id="top_panel" class="tb-split tb-split-vertical">
+ <div id="top_left_panel" class="tb-split tb-content">
+ <md-tabs md-selected="1" md-dynamic-height md-border-bottom style="width: 100%; height: 100%;">
+ <md-tab label="{{ 'widget.resources' | translate }}" style="width: 100%; height: 100%;">
+ <div class="tb-resize-container" style="background-color: #fff;">
+ <div class="md-padding">
+ <div flex layout="row"
+ ng-repeat="resource in vm.widget.resources track by $index"
+ style="max-height: 40px;" layout-align="start center">
+ <md-input-container flex md-no-float class="md-block"
+ style="margin: 10px 0px 0px 0px; max-height: 40px;">
+ <input placeholder="{{ 'widget.resource-url' | translate }}"
+ ng-required="true" name="resource" ng-model="resource.url">
+ </md-input-container>
+ <md-button ng-disabled="loading" class="md-icon-button md-primary"
+ ng-click="vm.removeResource($index)"
+ aria-label="{{ 'action.remove' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'widget.remove-resource' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.delete' | translate }}"
+ class="material-icons">
+ close
+ </md-icon>
+ </md-button>
+ </div>
+ <div>
+ <md-button ng-disabled="loading" class="md-primary md-raised"
+ ng-click="vm.addResource()"
+ aria-label="{{ 'action.add' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'widget.add-resource' | translate }}
+ </md-tooltip>
+ <span translate>action.add</span>
+ </md-button>
+ </div>
+ </div>
+ </div>
+ </md-tab>
+ <md-tab label="{{ 'widget.html' | translate }}" style="width: 100%; height: 100%;">
+ <div class="tb-resize-container" tb-expand-fullscreen expand-button-id="expand-button">
+ <div class="tb-editor-area-title-panel">
+ <md-button aria-label="{{ 'widget.tidy' | translate }}"
+ ng-click="vm.beautifyHtml()">{{ 'widget.tidy' | translate }}
+ </md-button>
+ <md-button id="expand-button"
+ aria-label="{{ 'fullscreen.fullscreen' | translate }}"
+ class="md-icon-button tb-md-32"></md-button>
+ </div>
+ <div id="html_input" ui-ace="vm.htmlEditorOptions"
+ ng-model="vm.widget.templateHtml"></div>
+ </div>
+ </md-tab>
+ <md-tab label="{{ 'widget.css' | translate }}" style="width: 100%; height: 100%;">
+ <div class="tb-resize-container" tb-expand-fullscreen expand-button-id="expand-button">
+ <div class="tb-editor-area-title-panel">
+ <md-button aria-label="{{ 'widget.tidy' | translate }}"
+ ng-click="vm.beautifyCss()">{{ 'widget.tidy' | translate }}
+ </md-button>
+ <md-button id="expand-button"
+ aria-label="{{ 'fullscreen.fullscreen' | translate }}"
+ class="md-icon-button tb-md-32"></md-button>
+ </div>
+ <div id="css_input" ui-ace="vm.cssEditorOptions"
+ ng-model="vm.widget.templateCss"></div>
+ </div>
+ </md-tab>
+ </md-tabs>
+ </div>
+ <div id="top_right_panel" class="tb-split tb-content">
+ <md-tabs md-dynamic-height md-border-bottom style="width: 100%; height: 100%;">
+ <md-tab label="{{ 'widget.settings-schema' | translate }}"
+ style="width: 100%; height: 100%;">
+ <div class="tb-resize-container" tb-expand-fullscreen expand-button-id="expand-button">
+ <div class="tb-editor-area-title-panel">
+ <md-button aria-label="{{ 'widget.tidy' | translate }}"
+ ng-click="vm.beautifyJson()">{{ 'widget.tidy' | translate }}
+ </md-button>
+ <md-button id="expand-button"
+ aria-label="{{ 'fullscreen.fullscreen' | translate }}"
+ class="md-icon-button tb-md-32"></md-button>
+ </div>
+ <div id="settings_json_input" ui-ace="vm.jsonSettingsEditorOptions"
+ ng-model="vm.widget.settingsSchema"></div>
+ </div>
+ </md-tab>
+ <md-tab label="{{ 'widget.datakey-settings-schema' | translate }}"
+ style="width: 100%; height: 100%;">
+ <div class="tb-resize-container" tb-expand-fullscreen expand-button-id="expand-button">
+ <div class="tb-editor-area-title-panel">
+ <md-button aria-label="{{ 'widget.tidy' | translate }}"
+ ng-click="vm.beautifyDataKeyJson()">{{ 'widget.tidy' | translate }}
+ </md-button>
+ <md-button id="expand-button"
+ aria-label="{{ 'fullscreen.fullscreen' | translate }}"
+ class="md-icon-button tb-md-32"></md-button>
+ </div>
+ <div id="settings_json_input" ui-ace="vm.dataKeyJsonSettingsEditorOptions"
+ ng-model="vm.widget.dataKeySettingsSchema"></div>
+ </div>
+ </md-tab>
+ </md-tabs>
+ </div>
+ </div>
+ <div id="bottom_panel" class="tb-split tb-split-vertical">
+ <div id="javascript_panel" class="tb-split tb-content">
+ <div class="tb-resize-container" tb-expand-fullscreen expand-button-id="expand-button">
+ <div class="tb-editor-area-title-panel">
+ <label translate for="javascript_input">widget.javascript</label>
+ <md-button aria-label="{{ 'widget.tidy' | translate }}" ng-click="vm.beautifyJs()">{{
+ 'widget.tidy' | translate }}
+ </md-button>
+ <md-button id="expand-button" aria-label="{{ 'fullscreen.fullscreen' | translate }}"
+ class="md-icon-button tb-md-32"></md-button>
+ </div>
+ <div id="javascript_input" ui-ace="vm.jsEditorOptions"
+ ng-model="vm.widget.controllerScript"></div>
+ </div>
+ </div>
+ <div id="frame_panel" class="tb-split tb-content" style="overflow-y: hidden; position: relative;">
+ <md-content flex layout="column" class="tb-progress-cover" layout-align="center center"
+ ng-show="!vm.iframeWidgetEditModeInited">
+ <md-progress-circular md-mode="indeterminate" class="md-warn"
+ md-diameter="100"></md-progress-circular>
+ </md-content>
+ <div tb-expand-fullscreen expand-button-id="expand-button" style="width: 100%; height: 100%;">
+ <iframe frameborder="0" height="100%" width="100%"></iframe>
+ <md-button id="expand-button" aria-label="Fullscreen"
+ class="md-icon-button tb-fullscreen-button-style"
+ style="position: absolute; top: 10px; left: 10px; bottom: initial;"
+ ></md-button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <md-content flex layout="column" class="tb-progress-cover" layout-align="center center" ng-show="!vm.layoutInited">
+ <md-progress-circular md-mode="indeterminate" class="md-warn" md-diameter="100"></md-progress-circular>
+ </md-content>
+</div>
ui/src/app/widget/widget-library.controller.js 176(+176 -0)
diff --git a/ui/src/app/widget/widget-library.controller.js b/ui/src/app/widget/widget-library.controller.js
new file mode 100644
index 0000000..3beb0ae
--- /dev/null
+++ b/ui/src/app/widget/widget-library.controller.js
@@ -0,0 +1,176 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import selectWidgetTypeTemplate from './select-widget-type.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function WidgetLibraryController($scope, $rootScope, widgetService, userService,
+ $state, $stateParams, $document, $mdDialog, $translate, $filter, types) {
+
+ var vm = this;
+
+ var widgetsBundleId = $stateParams.widgetsBundleId;
+
+ vm.widgetsBundle;
+ vm.widgetTypes = [];
+
+ vm.noData = noData;
+ vm.addWidgetType = addWidgetType;
+ vm.openWidgetType = openWidgetType;
+ vm.removeWidgetType = removeWidgetType;
+ vm.loadWidgetLibrary = loadWidgetLibrary;
+ vm.addWidgetType = addWidgetType;
+ vm.isReadOnly = isReadOnly;
+
+ function loadWidgetLibrary() {
+ $rootScope.loading = true;
+ widgetService.getWidgetsBundle(widgetsBundleId).then(
+ function success(widgetsBundle) {
+ vm.widgetsBundle = widgetsBundle;
+ if (vm.widgetsBundle) {
+ var bundleAlias = vm.widgetsBundle.alias;
+ var isSystem = vm.widgetsBundle.tenantId.id === types.id.nullUid;
+
+ widgetService.getBundleWidgetTypes(bundleAlias, isSystem).then(
+ function (widgetTypes) {
+
+ widgetTypes = $filter('orderBy')(widgetTypes, ['-descriptor.type','name']);
+
+ var top = 0;
+ var lastTop = [0, 0, 0];
+ var col = 0;
+ var column = 0;
+
+ if (widgetTypes.length > 0) {
+ loadNext(0);
+ } else {
+ $rootScope.loading = false;
+ }
+
+ function loadNextOrComplete(i) {
+ i++;
+ if (i < widgetTypes.length) {
+ loadNext(i);
+ } else {
+ $rootScope.loading = false;
+ }
+ }
+
+ function loadNext(i) {
+ var widgetType = widgetTypes[i];
+ $scope.$applyAsync(function() {
+ var widgetTypeInfo = widgetService.toWidgetInfo(widgetType);
+ var sizeX = 8;
+ var sizeY = Math.floor(widgetTypeInfo.sizeY);
+ var widget = {
+ id: widgetType.id,
+ isSystemType: isSystem,
+ bundleAlias: bundleAlias,
+ typeAlias: widgetTypeInfo.alias,
+ type: widgetTypeInfo.type,
+ title: widgetTypeInfo.widgetName,
+ sizeX: sizeX,
+ sizeY: sizeY,
+ row: top,
+ col: col,
+ config: angular.fromJson(widgetTypeInfo.defaultConfig)
+ };
+ widget.config.title = widgetTypeInfo.widgetName;
+ vm.widgetTypes.push(widget);
+ top+=sizeY;
+ if (top > lastTop[column] + 10) {
+ lastTop[column] = top;
+ column++;
+ if (column > 2) {
+ column = 0;
+ }
+ top = lastTop[column];
+ col = column * 8;
+ }
+ loadNextOrComplete(i);
+ });
+ }
+ }
+ );
+ } else {
+ $rootScope.loading = false;
+ }
+ }, function fail() {
+ $rootScope.loading = false;
+ }
+ );
+ }
+
+ function noData() {
+ return vm.widgetTypes.length == 0;
+ }
+
+ function addWidgetType($event) {
+ vm.openWidgetType($event);
+ }
+
+ function isReadOnly() {
+ if (userService.getAuthority() === 'TENANT_ADMIN') {
+ return !vm.widgetsBundle || vm.widgetsBundle.tenantId.id === types.id.nullUid;
+ } else {
+ return userService.getAuthority() != 'SYS_ADMIN';
+ }
+ }
+
+ function openWidgetType(event, widget) {
+ if (event) {
+ event.stopPropagation();
+ }
+ if (widget) {
+ $state.go('home.widgets-bundles.widget-types.widget-type',
+ {widgetTypeId: widget.id.id});
+ } else {
+ $mdDialog.show({
+ controller: 'SelectWidgetTypeController',
+ controllerAs: 'vm',
+ templateUrl: selectWidgetTypeTemplate,
+ parent: angular.element($document[0].body),
+ fullscreen: true,
+ targetEvent: event
+ }).then(function (widgetType) {
+ $state.go('home.widgets-bundles.widget-types.widget-type',
+ {widgetType: widgetType});
+ }, function () {
+ });
+ }
+ }
+
+ function removeWidgetType(event, widget) {
+ var confirm = $mdDialog.confirm()
+ .targetEvent(event)
+ .title($translate.instant('widget.remove-widget-type-title', {widgetName: widget.config.title}))
+ .htmlContent($translate.instant('widget.remove-widget-type-text'))
+ .ariaLabel($translate.instant('widget.remove-widget-type'))
+ .cancel($translate.instant('action.no'))
+ .ok($translate.instant('action.yes'));
+ $mdDialog.show(confirm).then(function () {
+ widgetService.deleteWidgetType(widget.bundleAlias, widget.typeAlias, widget.isSystemType).then(
+ function success() {
+ vm.widgetTypes.splice(vm.widgetTypes.indexOf(widget), 1);
+ },
+ function fail() {}
+ );
+ });
+ }
+}
ui/src/app/widget/widget-library.routes.js 107(+107 -0)
diff --git a/ui/src/app/widget/widget-library.routes.js b/ui/src/app/widget/widget-library.routes.js
new file mode 100644
index 0000000..0141f84
--- /dev/null
+++ b/ui/src/app/widget/widget-library.routes.js
@@ -0,0 +1,107 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import widgetLibraryTemplate from './widget-library.tpl.html';
+import widgetEditorTemplate from './widget-editor.tpl.html';
+import dashboardTemplate from '../dashboard/dashboard.tpl.html';
+import widgetsBundlesTemplate from './widgets-bundles.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function WidgetLibraryRoutes($stateProvider) {
+ $stateProvider
+ .state('home.widgets-bundles', {
+ url: '/widgets-bundles',
+ params: {'topIndex': 0},
+ module: 'private',
+ auth: ['SYS_ADMIN', 'TENANT_ADMIN'],
+ views: {
+ "content@home": {
+ templateUrl: widgetsBundlesTemplate,
+ controller: 'WidgetsBundleController',
+ controllerAs: 'vm'
+ }
+ },
+ data: {
+ searchEnabled: true,
+ pageTitle: 'widgets-bundle.widgets-bundles'
+ },
+ ncyBreadcrumb: {
+ label: '{"icon": "now_widgets", "label": "widgets-bundle.widgets-bundles"}'
+ }
+ })
+ .state('home.widgets-bundles.widget-types', {
+ url: '/:widgetsBundleId/widgetTypes',
+ params: {'topIndex': 0},
+ module: 'private',
+ auth: ['SYS_ADMIN', 'TENANT_ADMIN'],
+ views: {
+ "content@home": {
+ templateUrl: widgetLibraryTemplate,
+ controller: 'WidgetLibraryController',
+ controllerAs: 'vm'
+ }
+ },
+ data: {
+ searchEnabled: false,
+ pageTitle: 'widget.widget-library'
+ },
+ ncyBreadcrumb: {
+ label: '{"icon": "now_widgets", "label": "{{ vm.widgetsBundle.title }}", "translate": "false"}'
+ }
+ })
+ .state('home.widgets-bundles.widget-types.widget-type', {
+ url: '/:widgetTypeId',
+ module: 'private',
+ auth: ['SYS_ADMIN', 'TENANT_ADMIN'],
+ views: {
+ "content@home": {
+ templateUrl: widgetEditorTemplate,
+ controller: 'WidgetEditorController',
+ controllerAs: 'vm'
+ }
+ },
+ params: {
+ widgetType: null
+ },
+ data: {
+ searchEnabled: false,
+ pageTitle: 'widget.editor'
+ },
+ ncyBreadcrumb: {
+ label: '{"icon": "insert_chart", "label": "{{ vm.widget.widgetName }}", "translate": "false"}'
+ }
+ })
+ .state('widgetEditor', {
+ url: '/widget-editor',
+ module: 'private',
+ auth: ['SYS_ADMIN', 'TENANT_ADMIN'],
+ views: {
+ "@": {
+ templateUrl: dashboardTemplate,
+ controller: 'DashboardController',
+ controllerAs: 'vm'
+ }
+ },
+ data: {
+ widgetEditMode: true,
+ searchEnabled: false,
+ pageTitle: 'widget.editor'
+ }
+ })
+}
ui/src/app/widget/widget-library.tpl.html 48(+48 -0)
diff --git a/ui/src/app/widget/widget-library.tpl.html b/ui/src/app/widget/widget-library.tpl.html
new file mode 100644
index 0000000..5be27b7
--- /dev/null
+++ b/ui/src/app/widget/widget-library.tpl.html
@@ -0,0 +1,48 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<section ng-show="!loading && vm.noData()" layout-align="center center"
+ style="text-transform: uppercase; display: flex; z-index: 1;"
+ class="md-headline tb-absolute-fill">
+ <md-button ng-if="!vm.isReadOnly()" class="tb-add-new-widget" ng-click="vm.addWidgetType($event)">
+ <md-icon aria-label="{{ 'action.add' | translate }}" class="material-icons tb-md-96">add</md-icon>
+ {{ 'widget.add-widget-type' | translate }}
+ </md-button>
+ <span translate ng-if="vm.isReadOnly()"
+ layout-align="center center"
+ style="text-transform: uppercase; display: flex;"
+ class="md-headline tb-absolute-fill">widgets-bundle.empty</span>
+</section>
+<tb-dashboard
+ widgets="vm.widgetTypes"
+ is-edit="false"
+ is-edit-action-enabled="true"
+ is-remove-action-enabled="!vm.isReadOnly()"
+ on-edit-widget="vm.openWidgetType(event, widget)"
+ on-remove-widget="vm.removeWidgetType(event, widget)"
+ load-widgets="vm.loadWidgetLibrary()">
+</tb-dashboard>
+<section layout="row" layout-wrap class="tb-footer-buttons md-fab ">
+ <md-button ng-if="!vm.isReadOnly()" ng-disabled="loading" class="tb-btn-footer md-accent md-hue-2 md-fab" ng-click="vm.addWidgetType($event)" aria-label="{{ 'widget.add-widget-type' | translate }}" >
+ <md-tooltip md-direction="top">
+ {{ 'widget.add-widget-type' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.add' | translate }}" class="material-icons">
+ add
+ </md-icon>
+ </md-button>
+</section>
ui/src/app/widget/widgets-bundle.controller.js 148(+148 -0)
diff --git a/ui/src/app/widget/widgets-bundle.controller.js b/ui/src/app/widget/widgets-bundle.controller.js
new file mode 100644
index 0000000..e6a2fe5
--- /dev/null
+++ b/ui/src/app/widget/widgets-bundle.controller.js
@@ -0,0 +1,148 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import addWidgetsBundleTemplate from './add-widgets-bundle.tpl.html';
+import widgetsBundleCard from './widgets-bundle-card.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function WidgetsBundleController(widgetService, userService, $state, $stateParams, $filter, $translate, types) {
+
+ var widgetsBundleActionsList = [
+ {
+ onAction: function ($event, item) {
+ vm.grid.openItem($event, item);
+ },
+ name: function() { return $translate.instant('widgets-bundle.details') },
+ details: function() { return $translate.instant('widgets-bundle.widgets-bundle-details') },
+ icon: "edit"
+ },
+ {
+ onAction: function ($event, item) {
+ vm.grid.deleteItem($event, item);
+ },
+ name: function() { return $translate.instant('action.delete') },
+ details: function() { return $translate.instant('widgets-bundle.delete') },
+ icon: "delete",
+ isEnabled: isWidgetsBundleEditable
+ }
+ ];
+
+ var vm = this;
+
+ vm.types = types;
+
+ vm.widgetsBundleGridConfig = {
+
+ refreshParamsFunc: null,
+
+ deleteItemTitleFunc: deleteWidgetsBundleTitle,
+ deleteItemContentFunc: deleteWidgetsBundleText,
+ deleteItemsTitleFunc: deleteWidgetsBundlesTitle,
+ deleteItemsActionTitleFunc: deleteWidgetsBundlesActionTitle,
+ deleteItemsContentFunc: deleteWidgetsBundlesText,
+
+ fetchItemsFunc: fetchWidgetsBundles,
+ saveItemFunc: saveWidgetsBundle,
+ clickItemFunc: openWidgetsBundle,
+ deleteItemFunc: deleteWidgetsBundle,
+
+ getItemTitleFunc: getWidgetsBundleTitle,
+ itemCardTemplateUrl: widgetsBundleCard,
+ parentCtl: vm,
+
+ actionsList: widgetsBundleActionsList,
+
+ onGridInited: gridInited,
+
+ addItemTemplateUrl: addWidgetsBundleTemplate,
+
+ addItemText: function() { return $translate.instant('widgets-bundle.add-widgets-bundle-text') },
+ noItemsText: function() { return $translate.instant('widgets-bundle.no-widgets-bundles-text') },
+ itemDetailsText: function() { return $translate.instant('widgets-bundle.widgets-bundle-details') },
+ isSelectionEnabled: isWidgetsBundleEditable,
+ isDetailsReadOnly: function(widgetsBundle) {
+ return !isWidgetsBundleEditable(widgetsBundle);
+ }
+
+ };
+
+ if (angular.isDefined($stateParams.items) && $stateParams.items !== null) {
+ vm.widgetsBundleGridConfig.items = $stateParams.items;
+ }
+
+ if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) {
+ vm.widgetsBundleGridConfig.topIndex = $stateParams.topIndex;
+ }
+
+ function deleteWidgetsBundleTitle(widgetsBundle) {
+ return $translate.instant('widgets-bundle.delete-widgets-bundle-title', {widgetsBundleTitle: widgetsBundle.title});
+ }
+
+ function deleteWidgetsBundleText() {
+ return $translate.instant('widgets-bundle.delete-widgets-bundle-text');
+ }
+
+ function deleteWidgetsBundlesTitle(selectedCount) {
+ return $translate.instant('widgets-bundle.delete-widgets-bundles-title', {count: selectedCount}, 'messageformat');
+ }
+
+ function deleteWidgetsBundlesActionTitle(selectedCount) {
+ return $translate.instant('widgets-bundle.delete-widgets-bundles-action-title', {count: selectedCount}, 'messageformat');
+ }
+
+ function deleteWidgetsBundlesText() {
+ return $translate.instant('widgets-bundle.delete-widgets-bundles-text');
+ }
+
+ function gridInited(grid) {
+ vm.grid = grid;
+ }
+
+ function fetchWidgetsBundles(pageLink) {
+ return widgetService.getAllWidgetsBundlesByPageLink(pageLink);
+ }
+
+ function saveWidgetsBundle(widgetsBundle) {
+ return widgetService.saveWidgetsBundle(widgetsBundle);
+ }
+
+ function deleteWidgetsBundle(widgetsBundleId) {
+ return widgetService.deleteWidgetsBundle(widgetsBundleId);
+ }
+
+ function getWidgetsBundleTitle(widgetsBundle) {
+ return widgetsBundle ? widgetsBundle.title : '';
+ }
+
+ function isWidgetsBundleEditable(widgetsBundle) {
+ if (userService.getAuthority() === 'TENANT_ADMIN') {
+ return widgetsBundle && widgetsBundle.tenantId.id != types.id.nullUid;
+ } else {
+ return userService.getAuthority() === 'SYS_ADMIN';
+ }
+ }
+
+ function openWidgetsBundle($event, widgetsBundle) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ $state.go('home.widgets-bundles.widget-types', {widgetsBundleId: widgetsBundle.id.id});
+ }
+
+}
diff --git a/ui/src/app/widget/widgets-bundle.directive.js b/ui/src/app/widget/widgets-bundle.directive.js
new file mode 100644
index 0000000..744e2ec
--- /dev/null
+++ b/ui/src/app/widget/widgets-bundle.directive.js
@@ -0,0 +1,40 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import widgetsBundleFieldsetTemplate from './widgets-bundle-fieldset.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function WidgetsBundleDirective($compile, $templateCache) {
+ var linker = function (scope, element) {
+ var template = $templateCache.get(widgetsBundleFieldsetTemplate);
+ element.html(template);
+ $compile(element.contents())(scope);
+ }
+ return {
+ restrict: "E",
+ link: linker,
+ scope: {
+ widgetsBundle: '=',
+ isEdit: '=',
+ isReadOnly: '=',
+ theForm: '=',
+ onDeleteWidgetsBundle: '&'
+ }
+ };
+}
diff --git a/ui/src/app/widget/widgets-bundle-card.tpl.html b/ui/src/app/widget/widgets-bundle-card.tpl.html
new file mode 100644
index 0000000..1493bc3
--- /dev/null
+++ b/ui/src/app/widget/widgets-bundle-card.tpl.html
@@ -0,0 +1,18 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div class="tb-uppercase" ng-if="item && parentCtl.types.id.nullUid === item.tenantId.id" translate>widgets-bundle.system</div>
diff --git a/ui/src/app/widget/widgets-bundle-fieldset.tpl.html b/ui/src/app/widget/widgets-bundle-fieldset.tpl.html
new file mode 100644
index 0000000..b72f3ea
--- /dev/null
+++ b/ui/src/app/widget/widgets-bundle-fieldset.tpl.html
@@ -0,0 +1,30 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-button ng-click="onDeleteWidgetsBundle({event: $event})" ng-show="!isEdit && !isReadOnly" class="md-raised md-primary">{{ 'widgets-bundle.delete' | translate }}</md-button>
+
+<md-content class="md-padding" layout="column">
+ <fieldset ng-disabled="loading || !isEdit">
+ <md-input-container class="md-block">
+ <label translate>widgets-bundle.title</label>
+ <input required name="title" ng-model="widgetsBundle.title">
+ <div ng-messages="theForm.title.$error">
+ <div translate ng-message="required">widgets-bundle.title-required</div>
+ </div>
+ </md-input-container>
+ </fieldset>
+</md-content>
ui/src/app/widget/widgets-bundles.tpl.html 24(+24 -0)
diff --git a/ui/src/app/widget/widgets-bundles.tpl.html b/ui/src/app/widget/widgets-bundles.tpl.html
new file mode 100644
index 0000000..0fdf96e
--- /dev/null
+++ b/ui/src/app/widget/widgets-bundles.tpl.html
@@ -0,0 +1,24 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<tb-grid grid-configuration="vm.widgetsBundleGridConfig">
+ <tb-widgets-bundle widgets-bundle="vm.grid.operatingItem()"
+ is-edit="vm.grid.detailsConfig.isDetailsEditMode"
+ is-read-only="vm.grid.isDetailsReadOnly(vm.grid.operatingItem())"
+ the-form="vm.grid.detailsForm"
+ on-delete-widgets-bundle="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-widgets-bundle>
+</tb-grid>
ui/src/font/Segment7Standard.otf 0(+0 -0)
diff --git a/ui/src/font/Segment7Standard.otf b/ui/src/font/Segment7Standard.otf
new file mode 100644
index 0000000..7429b0d
Binary files /dev/null and b/ui/src/font/Segment7Standard.otf differ
ui/src/index.html 37(+37 -0)
diff --git a/ui/src/index.html b/ui/src/index.html
new file mode 100644
index 0000000..ee625ac
--- /dev/null
+++ b/ui/src/index.html
@@ -0,0 +1,37 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<!DOCTYPE html>
+<html ng-app="thingsboard" ng-strict-di>
+ <head>
+ <title ng-bind="pageTitle"></title>
+ <base href="/" />
+ <meta charset="utf-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+ <meta name="description" content="" />
+ <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no" />
+ <link rel="icon"
+ type="image/x-icon"
+ href="static/thingsboard.ico" />
+ <link rel="stylesheet" href="//fonts.googleapis.com/css?family=RobotoDraft:100,100italic,300,300italic,400,500,700,900,400italic" />
+ <link rel="stylesheet" href="//fonts.googleapis.com/icon?family=Material+Icons" />
+ </head>
+ <body>
+ <ui-view layout="row" layout-fill>
+ </ui-view>
+ </body>
+</html>
ui/src/locale/en_US.json 631(+631 -0)
diff --git a/ui/src/locale/en_US.json b/ui/src/locale/en_US.json
new file mode 100644
index 0000000..4a5e48d
--- /dev/null
+++ b/ui/src/locale/en_US.json
@@ -0,0 +1,631 @@
+{
+ "access": {
+ "unauthorized": "Unauthorized",
+ "unauthorized-access": "Unauthorized Access",
+ "unauthorized-access-text": "You should sign in to have access to this resource!",
+ "access-forbidden": "Access Forbidden",
+ "access-forbidden-text": "You haven't access rights to this location!<br/>Try to sign in with different user if you still wish to gain access to this location.",
+ "refresh-token-expired": "Session has expired",
+ "refresh-token-failed": "Unable to refresh session"
+ },
+ "action": {
+ "activate": "Activate",
+ "suspend": "Suspend",
+ "save": "Save",
+ "saveAs": "Save as",
+ "cancel": "Cancel",
+ "ok": "OK",
+ "delete": "Delete",
+ "add": "Add",
+ "yes": "Yes",
+ "no": "No",
+ "update": "Update",
+ "remove": "Remove",
+ "search": "Search",
+ "assign": "Assign",
+ "unassign": "Unassign",
+ "apply": "Apply",
+ "apply-changes": "Apply changes",
+ "edit-mode": "Edit mode",
+ "enter-edit-mode": "Enter edit mode",
+ "decline-changes": "Decline changes",
+ "close": "Close",
+ "back": "Back",
+ "run": "Run",
+ "sign-in": "Sign in!",
+ "edit": "Edit",
+ "view": "View",
+ "create": "Create",
+ "drag": "Drag",
+ "refresh": "Refresh",
+ "undo": "Undo"
+ },
+ "admin": {
+ "general": "General",
+ "general-settings": "General Settings",
+ "outgoing-mail": "Outgoing Mail",
+ "outgoing-mail-settings": "Outgoing Mail Settings",
+ "system-settings": "System Settings",
+ "test-mail-sent": "Test mail was successfully sent!",
+ "base-url": "Base URL",
+ "base-url-required": "Base URL is required.",
+ "mail-from": "Mail From",
+ "mail-from-required": "Mail From is required.",
+ "smtp-protocol": "SMTP protocol",
+ "smtp-host": "SMTP host",
+ "smtp-host-required": "SMTP host is required.",
+ "smtp-port": "SMTP port",
+ "smtp-port-required": "You must supply a smtp port.",
+ "smtp-port-invalid": "That doesn't look like a valid smtp port.",
+ "timeout-msec": "Timeout (msec)",
+ "timeout-required": "Timeout is required.",
+ "timeout-invalid": "That doesn't look like a valid timeout.",
+ "enable-tls": "Enable TLS",
+ "send-test-mail": "Send test mail"
+ },
+ "attribute": {
+ "attributes": "Attributes",
+ "latest-telemetry": "Latest telemetry",
+ "attributes-scope": "Device attributes scope",
+ "scope-latest-telemetry": "Latest telemetry",
+ "scope-client": "Client attributes",
+ "scope-server": "Server attributes",
+ "scope-shared": "Shared attributes",
+ "add": "Add attribute",
+ "key": "Key",
+ "key-required": "Attribute key is required.",
+ "value": "Value",
+ "value-required": "Attribute value is required.",
+ "delete-attributes-title": "Are you sure you want to delete { count, select, 1 {1 attribute} other {# attributes} }?",
+ "delete-attributes-text": "Be careful, after the confirmation all selected attributes will be removed.",
+ "delete-attributes": "Delete attributes",
+ "enter-attribute-value": "Enter attribute value",
+ "show-on-widget": "Show on widget",
+ "widget-mode": "Widget mode",
+ "next-widget": "Next widget",
+ "prev-widget": "Previous widget",
+ "add-to-dashboard": "Add to dashboard",
+ "add-widget-to-dashboard": "Add widget to dashboard",
+ "selected-attributes": "{ count, select, 1 {1 attribute} other {# attributes} } selected",
+ "selected-telemetry": "{ count, select, 1 {1 telemetry unit} other {# telemetry units} } selected"
+ },
+ "confirm-on-exit": {
+ "message": "You have unsaved changes. Are you sure you want to leave this page?",
+ "html-message": "You have unsaved changes.<br/>Are you sure you want to leave this page?",
+ "title": "Unsaved changes"
+ },
+ "contact": {
+ "country": "Country",
+ "city": "City",
+ "state": "State",
+ "postal-code": "Postal code",
+ "postal-code-invalid": "Only digits are allowed.",
+ "address": "Address",
+ "address2": "Address 2",
+ "phone": "Phone",
+ "email": "Email",
+ "no-address": "No address"
+ },
+ "common": {
+ "username": "Username",
+ "password": "Password",
+ "enter-username": "Enter username",
+ "enter-password": "Enter password",
+ "enter-search": "Enter search"
+ },
+ "customer": {
+ "customers": "Customers",
+ "management": "Customer management",
+ "dashboard": "Customer Dashboard",
+ "dashboards": "Customer Dashboards",
+ "devices": "Customer Devices",
+ "add": "Add Customer",
+ "delete": "Delete customer",
+ "manage-customer-users": "Manage customer users",
+ "manage-customer-devices": "Manage customer devices",
+ "manage-customer-dashboards": "Manage customer dashboards",
+ "add-customer-text": "Add new customer",
+ "no-customers-text": "No customers found",
+ "customer-details": "Customer details",
+ "delete-customer-title": "Are you sure you want to delete the customer '{{customerTitle}}'?",
+ "delete-customer-text": "Be careful, after the confirmation the customer and all related data will become unrecoverable.",
+ "delete-customers-title": "Are you sure you want to delete { count, select, 1 {1 customer} other {# customers} }?",
+ "delete-customers-action-title": "Delete { count, select, 1 {1 customer} other {# customers} }",
+ "delete-customers-text": "Be careful, after the confirmation all selected customers will be removed and all related data will become unrecoverable.",
+ "manage-users": "Manage users",
+ "manage-devices": "Manage devices",
+ "manage-dashboards": "Manage dashboards",
+ "title": "Title",
+ "title-required": "Title is required.",
+ "description": "Description"
+ },
+ "datetime": {
+ "date-from": "Date from",
+ "time-from": "Time from",
+ "date-to": "Date to",
+ "time-to": "Time to"
+ },
+ "dashboard": {
+ "dashboard": "Dashboard",
+ "dashboards": "Dashboards",
+ "management": "Dashboard management",
+ "view-dashboards": "View Dashboards",
+ "add": "Add Dashboard",
+ "assign-dashboard-to-customer": "Assign Dashboard(s) To Customer",
+ "assign-dashboard-to-customer-text": "Please select the dashboards to assign to the customer",
+ "assign-to-customer-text": "Please select the customer to assign the dashboard(s)",
+ "assign-to-customer": "Assign to customer",
+ "unassign-from-customer": "Unassign from customer",
+ "no-dashboards-text": "No dashboards found",
+ "no-widgets": "No widgets configured",
+ "add-widget": "Add new widget",
+ "title": "Title",
+ "select-widget-title": "Select widget",
+ "select-widget-subtitle": "List of available widget types",
+ "delete": "Delete dashboard",
+ "title": "Title",
+ "title-required": "Title is required.",
+ "description": "Description",
+ "details": "Details",
+ "dashboard-details": "Dashboard details",
+ "add-dashboard-text": "Add new dashboard",
+ "no-dashboards-text": "No dashboards found",
+ "assign-dashboards": "Assign dashboards",
+ "assign-new-dashboard": "Assign new dashboard",
+ "assign-dashboards-text": "Assign { count, select, 1 {1 dashboard} other {# dashboards} } to customer",
+ "delete-dashboards": "Delete dashboards",
+ "unassign-dashboards": "Unassign dashboards",
+ "unassign-dashboards-action-title": "Unassign { count, select, 1 {1 dashboard} other {# dashboards} } from customer",
+ "delete-dashboard-title": "Are you sure you want to delete the dashboard '{{dashboardTitle}}'?",
+ "delete-dashboard-text": "Be careful, after the confirmation the dashboard and all related data will become unrecoverable.",
+ "delete-dashboards-title": "Are you sure you want to delete { count, select, 1 {1 dashboard} other {# dashboards} }?",
+ "delete-dashboards-action-title": "Delete { count, select, 1 {1 dashboard} other {# dashboards} }",
+ "delete-dashboards-text": "Be careful, after the confirmation all selected dashboards will be removed and all related data will become unrecoverable.",
+ "unassign-dashboard-title": "Are you sure you want to unassign the dashboard '{{dashboardTitle}}'?",
+ "unassign-dashboard-text": "After the confirmation the dashboard will be unassigned and won't be accessible by the customer.",
+ "unassign-dashboard": "Unassign dashboard",
+ "unassign-dashboards-title": "Are you sure you want to unassign { count, select, 1 {1 dashboard} other {# dashboards} }?",
+ "unassign-dashboards-text": "After the confirmation all selected dashboards will be unassigned and won't be accessible by the customer.",
+ "select-dashboard": "Select dashboard",
+ "no-dashboards-matching": "No dashboards matching '{{dashboard}}' were found.",
+ "dashboard-required": "Dashboard is required.",
+ "select-existing": "Select existing dashboard",
+ "create-new": "Create new dashboard",
+ "new-dashboard-title": "New dashboard title",
+ "open-dashboard": "Open dashboard"
+ },
+ "datakey": {
+ "settings": "Settings",
+ "advanced": "Advanced",
+ "label": "Label",
+ "color": "Color",
+ "data-generation-func": "Data generation function",
+ "use-data-post-processing-func": "Use data post-processing function",
+ "configuration": "Data key configuration",
+ "timeseries": "Timeseries",
+ "attributes": "Attributes",
+ "timeseries-required": "Device timeseries is required.",
+ "timeseries-or-attributes-required": "Device timeseries/attributes is required.",
+ "function-types": "Function types",
+ "function-types-required": "Function types is required."
+ },
+ "datasource": {
+ "type": "Datasource type",
+ "add-datasource-prompt": "Please add datasource"
+ },
+ "details": {
+ "edit-mode": "Edit mode",
+ "toggle-edit-mode": "Toggle edit mode"
+ },
+ "device": {
+ "device": "Device",
+ "device-required": "Device is required.",
+ "devices": "Devices",
+ "management": "Device management",
+ "view-devices": "View Devices",
+ "device-alias": "Device alias",
+ "aliases": "Device aliases",
+ "no-alias-matching": "'{{alias}}' not found.",
+ "no-aliases-found": "No aliases found.",
+ "no-key-matching": "'{{key}}' not found.",
+ "no-keys-found": "No keys found.",
+ "create-new-alias": "Create a new one!",
+ "create-new-key": "Create a new one!",
+ "duplicate-alias-error": "Duplicate alias found '{{alias}}'.<br>Device aliases must be unique whithin the dashboard.",
+ "select-device-for-alias": "Select device for '{{alias}}' alias",
+ "no-devices-matching": "No devices matching '{{device}}' were found.",
+ "alias": "Alias",
+ "alias-required": "Device alias is required.",
+ "remove-alias": "Remove device alias",
+ "add-alias": "Add device alias",
+ "add": "Add Device",
+ "assign-to-customer": "Assign to customer",
+ "assign-device-to-customer": "Assign Device(s) To Customer",
+ "assign-device-to-customer-text": "Please select the devices to assign to the customer",
+ "no-devices-text": "No devices found",
+ "assign-to-customer-text": "Please select the customer to assign the device(s)",
+ "device-details": "Device details",
+ "add-device-text": "Add new device",
+ "credentials": "Credentials",
+ "manage-credentials": "Manage credentials",
+ "delete": "Delete device",
+ "assign-devices": "Assign devices",
+ "assign-devices-text": "Assign { count, select, 1 {1 device} other {# devices} } to customer",
+ "delete-devices": "Delete devices",
+ "unassign-from-customer": "Unassign from customer",
+ "unassign-devices": "Unassign devices",
+ "unassign-devices-action-title": "Unassign { count, select, 1 {1 device} other {# devices} } from customer",
+ "assign-new-device": "Assign new device",
+ "view-credentials": "View credentials",
+ "delete-device-title": "Are you sure you want to delete the device '{{deviceName}}'?",
+ "delete-device-text": "Be careful, after the confirmation the device and all related data will become unrecoverable.",
+ "delete-devices-title": "Are you sure you want to delete { count, select, 1 {1 device} other {# devices} }?",
+ "delete-devices-action-title": "Delete { count, select, 1 {1 device} other {# devices} }",
+ "delete-devices-text": "Be careful, after the confirmation all selected devices will be removed and all related data will become unrecoverable.",
+ "unassign-device-title": "Are you sure you want to unassign the device '{{deviceName}}'?",
+ "unassign-device-text": "After the confirmation the device will be unassigned and won't be accessible by the customer.",
+ "unassign-device": "Unassign device",
+ "unassign-devices-title": "Are you sure you want to unassign { count, select, 1 {1 device} other {# devices} }?",
+ "unassign-devices-text": "After the confirmation all selected devices will be unassigned and won't be accessible by the customer.",
+ "device-credentials": "Device Credentials",
+ "credentials-type": "Credentials type",
+ "access-token": "Access token",
+ "access-token-required": "Access token is required.",
+ "access-token-invalid": "Access token length must be from 1 to 20 characters.",
+ "secret": "Secret",
+ "secret-required": "Secret is required.",
+ "name": "Name",
+ "name-required": "Name is required.",
+ "description": "Description",
+ "events": "Events",
+ "details": "Details",
+ "copyId": "Copy device Id",
+ "idCopiedMessage": "Device Id has been copied to clipboard",
+ "assignedToCustomer": "Assigned to customer",
+ "unable-delete-device-alias-title": "Unable to delete device alias",
+ "unable-delete-device-alias-text": "Device alias '{{deviceAlias}}' can't be deleted as it used by the following widget(s):<br/>{{widgetsList}}"
+ },
+ "dialog": {
+ "close": "Close dialog"
+ },
+ "error": {
+ "unable-to-connect": "Unable to connect to the server! Please check your internet connection.",
+ "unhandled-error-code": "Unhandled error code: {{errorCode}}",
+ "unknown-error": "Unknown error"
+ },
+ "event": {
+ "event-type": "Event type",
+ "type-alarm": "Alarm",
+ "type-error": "Error",
+ "type-lc-event": "Lifecycle event",
+ "type-stats": "Statistics",
+ "no-events-prompt": "No events found",
+ "error": "Error",
+ "alarm": "Alarm",
+ "event-time": "Event time",
+ "server": "Server",
+ "body": "Body",
+ "method": "Method",
+ "event": "Event",
+ "status": "Status",
+ "success": "Success",
+ "failed": "Failed",
+ "messages-processed": "Messages processed",
+ "errors-occurred": "Errors occurred"
+ },
+ "fullscreen": {
+ "expand": "Expand to fullscreen",
+ "exit": "Exit fullscreen",
+ "toggle": "Toggle fullscreen mode",
+ "fullscreen": "Fullscreen"
+ },
+ "function": {
+ "function": "Function"
+ },
+ "grid": {
+ "delete-item-title": "Are you sure you want to delete this item?",
+ "delete-item-text": "Be careful, after the confirmation this item and all related data will become unrecoverable.",
+ "delete-items-title": "Are you sure you want to delete { count, select, 1 {1 item} other {# items} }?",
+ "delete-items-action-title": "Delete { count, select, 1 {1 item} other {# items} }",
+ "delete-items-text": "Be careful, after the confirmation all selected items will be removed and all related data will become unrecoverable.",
+ "add-item-text": "Add new item",
+ "no-items-text": "No items found",
+ "item-details": "Item details",
+ "delete-item": "Delete Item",
+ "delete-items": "Delete Items",
+ "scroll-to-top": "Scroll to top"
+ },
+ "help": {
+ "goto-help-page": "Go to help page"
+ },
+ "home": {
+ "home": "Home",
+ "profile": "Profile",
+ "logout": "Logout",
+ "menu": "Menu",
+ "avatar": "Avatar",
+ "open-user-menu": "Open user menu"
+ },
+ "item": {
+ "selected": "Selected"
+ },
+ "js-func": {
+ "no-return-error": "Function must return value!",
+ "return-type-mismatch": "Function must return value of '{{type}}' type!"
+ },
+ "login": {
+ "login": "Login",
+ "request-password-reset": "Request Password Reset",
+ "reset-password": "Reset Password",
+ "create-password": "Create Password",
+ "passwords-mismatch-error": "Entered passwords must be same!",
+ "password-again": "Password again",
+ "sign-in": "Please sign in",
+ "username": "Username (email)",
+ "remember-me": "Remember me",
+ "forgot-password": "Forgot Password?",
+ "login": "Login",
+ "password-reset": "Password reset",
+ "new-password": "New password",
+ "new-password-again": "New password again",
+ "password-link-sent-message": "Password reset link was successfully sent!",
+ "request-password-reset": "Request password reset",
+ "email": "Email"
+ },
+ "plugin": {
+ "plugins": "Plugins",
+ "delete": "Delete plugin",
+ "activate": "Activate plugin",
+ "suspend": "Suspend plugin",
+ "active": "Active",
+ "suspended": "Suspended",
+ "name": "Name",
+ "name-required": "Name is required.",
+ "description": "Description",
+ "add": "Add Plugin",
+ "delete-plugin-title": "Are you sure you want to delete the plugin '{{pluginName}}'?",
+ "delete-plugin-text": "Be careful, after the confirmation the plugin and all related data will become unrecoverable.",
+ "delete-plugins-title": "Are you sure you want to delete { count, select, 1 {1 plugin} other {# plugins} }?",
+ "delete-plugins-action-title": "Delete { count, select, 1 {1 plugin} other {# plugins} }",
+ "delete-plugins-text": "Be careful, after the confirmation all selected plugins will be removed and all related data will become unrecoverable.",
+ "add-plugin-text": "Add new plugin",
+ "no-plugins-text": "No plugins found",
+ "plugin-details": "Plugin details",
+ "api-token": "API token",
+ "api-token-required": "API token is required.",
+ "type": "Plugin type",
+ "type-required": "Plugin type is required.",
+ "configuration": "Plugin configuration",
+ "system": "System",
+ "select-plugin": "Select plugin",
+ "plugin": "Plugin",
+ "no-plugins-matching": "No plugins matching '{{plugin}}' were found.",
+ "plugin-required": "Plugin is required.",
+ "plugin-require-match": "Please select an existing plugin.",
+ "events": "Events",
+ "details": "Details"
+ },
+ "profile": {
+ "profile": "Profile",
+ "change-password": "Change Password",
+ "current-password": "Current password"
+ },
+ "rule": {
+ "rules": "Rules",
+ "delete": "Delete rule",
+ "activate": "Activate rule",
+ "suspend": "Suspend rule",
+ "active": "Active",
+ "suspended": "Suspended",
+ "name": "Name",
+ "name-required": "Name is required.",
+ "description": "Description",
+ "add": "Add Rule",
+ "delete-rule-title": "Are you sure you want to delete the rule '{{ruleName}}'?",
+ "delete-rule-text": "Be careful, after the confirmation the rule and all related data will become unrecoverable.",
+ "delete-rules-title": "Are you sure you want to delete { count, select, 1 {1 rule} other {# rules} }?",
+ "delete-rules-action-title": "Delete { count, select, 1 {1 rule} other {# rules} }",
+ "delete-rules-text": "Be careful, after the confirmation all selected rules will be removed and all related data will become unrecoverable.",
+ "add-rule-text": "Add new rule",
+ "no-rules-text": "No rules found",
+ "rule-details": "Rule details",
+ "filters": "Filters",
+ "filter": "Filter",
+ "add-filter-prompt": "Please add filter",
+ "remove-filter": "Remove filter",
+ "add-filter": "Add filter",
+ "filter-name": "Filter name",
+ "filter-type": "Filter type",
+ "edit-filter": "Edit filter",
+ "view-filter": "View filter",
+ "component-name": "Name",
+ "component-name-required": "Name is required.",
+ "component-type": "Type",
+ "component-type-required": "Type is required.",
+ "processor": "Processor",
+ "no-processor-configured": "No processor configured",
+ "create-processor": "Create processor",
+ "processor": "Processor",
+ "processor-name": "Processor name",
+ "processor-type": "Processor type",
+ "plugin-action": "Plugin action",
+ "action-name": "Action name",
+ "action-type": "Action type",
+ "create-action-prompt": "Please create action",
+ "create-action": "Create action",
+ "details": "Details",
+ "events": "Events",
+ "system": "System"
+ },
+ "rule-plugin": {
+ "management": "Rules and plugins management"
+ },
+ "tenant": {
+ "tenants": "Tenants",
+ "management": "Tenant management",
+ "add": "Add Tenant",
+ "admins": "Admins",
+ "manage-tenant-admins": "Manage tenant admins",
+ "delete": "Delete tenant",
+ "add-tenant-text": "Add new tenant",
+ "no-tenants-text": "No tenants found",
+ "tenant-details": "Tenant details",
+ "delete-tenant-title": "Are you sure you want to delete the tenant '{{tenantTitle}}'?",
+ "delete-tenant-text": "Be careful, after the confirmation the tenant and all related data will become unrecoverable.",
+ "delete-tenants-title": "Are you sure you want to delete { count, select, 1 {1 tenant} other {# tenants} }?",
+ "delete-tenants-action-title": "Delete { count, select, 1 {1 tenant} other {# tenants} }",
+ "delete-tenants-text": "Be careful, after the confirmation all selected tenants will be removed and all related data will become unrecoverable.",
+ "title": "Title",
+ "title-required": "Title is required.",
+ "description": "Description"
+ },
+ "timeinterval": {
+ "seconds-interval": "{ seconds, select, 1 {1 second} other {# seconds} }",
+ "minutes-interval": "{ minutes, select, 1 {1 minute} other {# minutes} }",
+ "hours-interval": "{ hours, select, 1 {1 hour} other {# hours} }",
+ "days-interval": "{ days, select, 1 {1 day} other {# days} }",
+ "days": "Days",
+ "hours": "Hours",
+ "minutes": "Minutes",
+ "seconds": "Seconds"
+ },
+ "timewindow": {
+ "days": "{ days, select, 1 { day } other {# days } }",
+ "hours": "{ hours, select, 0 { hour } 1 {1 hour } other {# hours } }",
+ "minutes": "{ minutes, select, 0 { minute } 1 {1 minute } other {# minutes } }",
+ "seconds": "{ seconds, select, 0 { second } 1 {1 second } other {# seconds } }",
+ "realtime": "Realtime",
+ "history": "History",
+ "last-prefix": "last",
+ "period": "from {{ startTime }} to {{ endTime }}",
+ "edit": "Edit timewindow",
+ "date-range": "Date range",
+ "last": "Last",
+ "time-period": "Time period"
+ },
+ "user": {
+ "users": "Users",
+ "customer-users": "Customer Users",
+ "tenant-admins": "Tenant Admins",
+ "sys-admin": "System administrator",
+ "tenant-admin": "Tenant administrator",
+ "customer": "Customer",
+ "anonymous": "Anonymous",
+ "add": "Add User",
+ "delete": "Delete user",
+ "add-user-text": "Add new user",
+ "no-users-text": "No users found",
+ "user-details": "User details",
+ "delete-user-title": "Are you sure you want to delete the user '{{userEmail}}'?",
+ "delete-user-text": "Be careful, after the confirmation the user and all related data will become unrecoverable.",
+ "delete-users-title": "Are you sure you want to delete { count, select, 1 {1 user} other {# users} }?",
+ "delete-users-action-title": "Delete { count, select, 1 {1 user} other {# users} }",
+ "delete-users-text": "Be careful, after the confirmation all selected users will be removed and all related data will become unrecoverable.",
+ "activation-email-sent-message": "Activation email was successfully sent!",
+ "resend-activation": "Resend activation",
+ "email": "Email",
+ "email-required": "Email is required.",
+ "first-name": "First Name",
+ "last-name": "Last Name",
+ "description": "Description"
+ },
+ "value": {
+ "type": "Value type",
+ "string": "String",
+ "string-value": "String value",
+ "integer": "Integer",
+ "integer-value": "Integer value",
+ "invalid-integer-value": "Invalid integer value",
+ "double": "Double",
+ "double-value": "Double value",
+ "boolean": "Boolean",
+ "boolean-value": "Boolean value",
+ "false": "False",
+ "true": "True"
+ },
+ "widget": {
+ "widget-library": "Widgets Library",
+ "widget-bundle": "Widgets Bundle",
+ "select-widgets-bundle": "Select widgets bundle",
+ "management": "Widget management",
+ "editor": "Widget Editor",
+ "widget-type-not-found": "Problem loading widget configuration.<br>Probably associated\n widget type was removed.",
+ "widget-type-load-error": "Widget wasn't loaded due to the following errors:",
+ "remove": "Remove widget",
+ "edit": "Edit widget",
+ "remove-widget-title": "Are you sure you want to remove the widget '{{widgetTitle}}'?",
+ "remove-widget-text": "After the confirmation the widget and all related data will become unrecoverable.",
+ "timeseries": "Time series",
+ "latest-values": "Latest values",
+ "rpc": "Control widget",
+ "select-widget-type": "Select widget type",
+ "missing-widget-title-error": "Widget title must be specified!",
+ "widget-saved": "Widget saved",
+ "unable-to-save-widget-error": "Unable to save widget! Widget has errors!",
+ "save": "Save widget",
+ "saveAs": "Save widget as",
+ "save-widget-type-as": "Save widget type as",
+ "save-widget-type-as-text": "Please enter new widget title and/or select target widgets bundle",
+ "toggle-fullscreen": "Toggle fullscreen",
+ "run": "Run widget",
+ "title": "Widget title",
+ "title-required": "Widget title is required.",
+ "type": "Widget type",
+ "resources": "Resources",
+ "resource-url": "JavaScript/CSS URI",
+ "remove-resource": "Remove resource",
+ "add-resource": "Add resource",
+ "html": "HTML",
+ "tidy": "Tidy",
+ "css": "CSS",
+ "settings-schema": "Settings schema",
+ "datakey-settings-schema": "Data key settings schema",
+ "javascript": "Javascript",
+ "remove-widget-type-title": "Are you sure you want to remove the widget type '{{widgetName}}'?",
+ "remove-widget-type-text": "After the confirmation the widget type and all related data will become unrecoverable.",
+ "remove-widget-type": "Remove widget type",
+ "add-widget-type": "Add new widget type",
+ "widget-type-load-failed-error": "Failed to load widget type!",
+ "widget-template-load-failed-error": "Failed to load widget template!",
+ "add": "Add Widget",
+ "undo": "Undo widget changes"
+ },
+ "widgets-bundle": {
+ "current": "Current bundle",
+ "widgets-bundles": "Widgets Bundles",
+ "add": "Add Widgets Bundle",
+ "delete": "Delete widgets bundle",
+ "title": "Title",
+ "title-required": "Title is required.",
+ "add-widgets-bundle-text": "Add new widgets bundle",
+ "no-widgets-bundles-text": "No widgets bundles found",
+ "empty": "Widgets bundle is empty",
+ "details": "Details",
+ "widgets-bundle-details": "Widgets bundle details",
+ "delete-widgets-bundle-title": "Are you sure you want to delete the widgets bundle '{{widgetsBundleTitle}}'?",
+ "delete-widgets-bundle-text": "Be careful, after the confirmation the widgets bundle and all related data will become unrecoverable.",
+ "delete-widgets-bundles-title": "Are you sure you want to delete { count, select, 1 {1 widgets bundle} other {# widgets bundles} }?",
+ "delete-widgets-bundles-action-title": "Delete { count, select, 1 {1 widgets bundle} other {# widgets bundles} }",
+ "delete-widgets-bundles-text": "Be careful, after the confirmation all selected widgets bundles will be removed and all related data will become unrecoverable.",
+ "no-widgets-bundles-matching": "No widgets bundles matching '{{widgetsBundle}}' were found.",
+ "widgets-bundle-required": "Widgets bundle is required.",
+ "system": "System"
+ },
+ "widget-config": {
+ "settings": "Settings",
+ "advanced": "Advanced",
+ "title": "Title",
+ "general-settings": "General settings",
+ "display-title": "Display title",
+ "background-color": "Background color",
+ "text-color": "Text color",
+ "padding": "Padding",
+ "timewindow": "Timewindow",
+ "datasources": "Datasources",
+ "datasource-type": "Type",
+ "datasource-parameters": "Parameters",
+ "remove-datasource": "Remove datasource",
+ "add-datasource": "Add datasource",
+ "target-device": "Target device"
+ }
+}
ui/src/scss/animations.scss 44(+44 -0)
diff --git a/ui/src/scss/animations.scss b/ui/src/scss/animations.scss
new file mode 100644
index 0000000..58f1f5e
--- /dev/null
+++ b/ui/src/scss/animations.scss
@@ -0,0 +1,44 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import "~compass-sass-mixins/lib/animate";
+
+@include keyframes(tbMoveFromTopFade) {
+ from {
+ opacity: 0;
+ @include transform(translate(0, -100%));
+ }
+}
+
+@include keyframes(tbMoveToTopFade) {
+ to {
+ opacity: 0;
+ @include transform(translate(0, -100%));
+ }
+}
+
+@include keyframes(tbMoveFromBottomFade) {
+ from {
+ opacity: 0;
+ @include transform(translate(0, 100%));
+ }
+}
+
+@include keyframes(tbMoveToBottomFade) {
+ to {
+ opacity: 0;
+ @include transform(translate(0, 100%));
+ }
+}
\ No newline at end of file
ui/src/scss/constants.scss 43(+43 -0)
diff --git a/ui/src/scss/constants.scss b/ui/src/scss/constants.scss
new file mode 100644
index 0000000..74f06e6
--- /dev/null
+++ b/ui/src/scss/constants.scss
@@ -0,0 +1,43 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import "~sass-material-colors/sass/sass-material-colors";
+
+// Colors
+
+$gray: #eee;
+
+$primary-palette-color: 'indigo';
+$hue-1: '300';
+$hue-2: '800';
+$hue-3: 'a100';
+
+$primary-hue-1: material-color($primary-palette-color, $hue-1);
+$primary-hue-2: material-color($primary-palette-color, $hue-2);
+$primary-hue-3: rgb(207, 216, 220);
+
+// Layout
+
+$layout-breakpoint-xs: 600px !default;
+$layout-breakpoint-sm: 960px !default;
+$layout-breakpoint-md: 1280px !default;
+$layout-breakpoint-xmd: 1600px !default;
+$layout-breakpoint-lg: 1920px !default;
+
+$layout-breakpoint-gt-xs: 601px !default;
+$layout-breakpoint-gt-sm: 961px !default;
+$layout-breakpoint-gt-md: 1281px !default;
+$layout-breakpoint-gt-xmd: 1601px !default;
+$layout-breakpoint-gt-lg: 1921px !default;
\ No newline at end of file
ui/src/scss/main.scss 402(+402 -0)
diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss
new file mode 100644
index 0000000..e8f8283
--- /dev/null
+++ b/ui/src/scss/main.scss
@@ -0,0 +1,402 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import "~compass-sass-mixins/lib/compass";
+@import "constants";
+@import "animations";
+
+@font-face {
+ font-family: 'Segment7Standard';
+ src: url('../font/Segment7Standard.otf') format('opentype');
+ font-weight: normal;
+ font-style: italic;
+}
+
+/***************
+ * TYPE DEFAULTS
+ ***************/
+
+button, html, input, select, textarea {
+ font-family: RobotoDraft, Roboto, 'Helvetica Neue', sans-serif;
+}
+
+.mdi-set {
+ line-height: 1;
+ letter-spacing: normal;
+ text-transform: none;
+ white-space: nowrap;
+ word-wrap: normal;
+ direction: ltr;
+ -webkit-font-feature-settings: 'liga';
+}
+
+a {
+ color: #106CC8;
+ text-decoration: none;
+ font-weight: 400;
+ border-bottom: 1px solid rgba(64, 84, 178, 0.25);
+ @include transition(border-bottom 0.35s);
+}
+
+a:hover, a:focus {
+ border-bottom: 1px solid #4054B2;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ margin-bottom: 1rem;
+ margin-top: 1rem;
+}
+
+h1 {
+ font-size: 3.400rem;
+ font-weight: 400;
+ line-height: 4rem;
+}
+
+h2 {
+ font-size: 2.400rem;
+ font-weight: 400;
+ line-height: 3.2rem;
+}
+
+h3 {
+ font-size: 2.000rem;
+ font-weight: 500;
+ letter-spacing: 0.005em;
+}
+
+h4 {
+ font-size: 1.600rem;
+ font-weight: 400;
+ letter-spacing: 0.010em;
+ line-height: 2.4rem;
+}
+
+p {
+ font-size: 1.6rem;
+ font-weight: 400;
+ letter-spacing: 0.010em;
+ line-height: 1.6em;
+ margin: 0.8em 0 1.6em;
+}
+
+strong {
+ font-weight: 500;
+}
+
+blockquote {
+ border-left: 3px solid rgba(0, 0, 0, 0.12);
+ font-style: italic;
+ margin-left: 0;
+ padding-left: 16px;
+}
+
+fieldset {
+ border: none;
+ padding: 0;
+ margin: 0;
+}
+
+/*********************************
+ * MATERIAL DESIGN CUSTOMIZATIONS
+ ********************************/
+
+a.md-button {
+ border-bottom: none;
+}
+
+form {
+ md-content {
+ background-color: #fff;
+ }
+}
+
+md-bottom-sheet .md-subheader {
+ font-family: RobotoDraft, Roboto, 'Helvetica Neue', sans-serif;
+}
+
+.md-chips {
+ font-family: RobotoDraft, Roboto, 'Helvetica Neue', sans-serif;
+}
+
+md-content.md-default-theme, md-content {
+ background-color: $gray;
+}
+
+md-card {
+ background-color: #fff;
+ h2:first-of-type {
+ margin-top: 0;
+ }
+}
+
+.md-button:not([disabled]).md-icon-button:hover {
+ background-color: rgba(158, 158, 158, 0.2);
+}
+
+md-toolbar:not(.md-hue-1),
+.md-fab {
+ fill: #fff;
+}
+
+md-toolbar md-input-container .md-errors-spacer {
+ min-height: 0px;
+}
+
+md-toolbar {
+ md-select.md-default-theme:not([disabled]):focus .md-select-value, md-select:not([disabled]):focus .md-select-value {
+ color: #fff;
+ }
+}
+
+md-menu-item {
+ overflow: hidden;
+ fill: #737373;
+}
+
+md-menu-item {
+ .md-button {
+ display: block;
+ }
+}
+
+div.md-toast-text {
+ width: 100%;
+ max-width: 500px;
+ max-height: inherit;
+ word-wrap: break-word;
+}
+
+md-toast .md-button {
+ min-width: 88px;
+}
+
+md-sidenav {
+ overflow: hidden;
+ fill: #737373;
+}
+
+.md-panel-outer-wrapper {
+ overflow-y: auto;
+}
+
+.md-radio-interactive input, button {
+ pointer-events: all;
+}
+
+/***********************
+ * THINGSBOARD SPECIFIC
+ ***********************/
+
+.tb-readonly-label {
+ color: rgba(0,0,0,0.54);
+}
+
+/***********************
+ * Prompt
+ ***********************/
+
+.tb-prompt {
+ color: rgba(0,0,0,0.38);
+ text-transform: uppercase;
+ display: flex;
+ font-size: 18px;
+ font-weight: 400;
+ line-height: 18px;
+}
+
+/***********************
+ * Errors
+ ***********************/
+
+.tb-error-messages {
+ height: 24px; //30px
+}
+
+.tb-error-message {
+ font-size: 12px;
+ line-height: 14px;
+ overflow: hidden;
+ padding: 10px 0px 0px 10px;
+ color: rgb(221,44,0);
+ margin-top: -6px;
+}
+
+.tb-error-message.ng-animate {
+ @include transition(all .3s cubic-bezier(.55,0,.55,.2));
+}
+
+.tb-error-message.ng-enter-prepare, .tb-error-message.ng-enter {
+ opacity:0;
+ margin-top: -24px;
+}
+
+.tb-error-message.ng-enter.ng-enter-active {
+ opacity:1;
+ margin-top: -6px;
+}
+
+.tb-error-message.ng-leave {
+ opacity:1;
+ margin-top: -6px;
+}
+.tb-error-message.ng-leave.ng-leave-active {
+ opacity:0;
+ margin-top: -24px;
+}
+
+/***********************
+ * Tabs
+ ***********************/
+
+md-tabs.tb-headless {
+ margin-top: -50px;
+}
+
+/***********************
+ * Buttons
+ ***********************/
+
+.md-button.tb-card-button {
+ width: 100%;
+ height: 100%;
+ max-width: 240px;
+ span {
+ padding: 10px 10px 20px 10px;
+ font-size: 18px;
+ font-weight: 400;
+ white-space: normal;
+ line-height: 18px;
+ }
+}
+
+.md-button.tb-add-new-widget {
+ border-style: dashed;
+ border-width: 2px;
+ font-size: 24px;
+ padding-right: 12px;
+}
+
+/***********************
+ * Header buttons
+ ***********************/
+
+section.tb-header-buttons {
+ position: absolute;
+ right: 0px;
+ top: 86px;
+ z-index: 3;
+ @media (min-width: $layout-breakpoint-sm) {
+ top: 86px;
+ }
+}
+
+section.tb-top-header-buttons {
+ top: 23px;
+}
+
+.tb-header-buttons .tb-btn-header {
+ @include animation(tbMoveFromTopFade .3s ease both);
+ position: relative !important;
+ display: inline-block !important;
+}
+
+.tb-header-buttons .tb-btn-header.ng-hide {
+ @include animation(tbMoveToTopFade .3s ease both);
+}
+
+/***********************
+ * Footer buttons
+ ***********************/
+
+section.tb-footer-buttons {
+ position: fixed;
+ right: 20px;
+ bottom: 20px;
+ z-index: 2;
+}
+
+.tb-footer-buttons .tb-btn-footer {
+ @include animation(tbMoveFromBottomFade .3s ease both);
+ position: relative !important;
+ display: inline-block !important;
+}
+
+.tb-footer-buttons .tb-btn-footer.ng-hide {
+ @include animation(tbMoveToBottomFade .3s ease both);
+}
+
+._md-toast-open-bottom .tb-footer-buttons {
+ @include transition(all .4s cubic-bezier(.25, .8, .25, 1));
+ @include transform(translate3d(0, -42px, 0));
+}
+
+/***********************
+ * Icons
+ ***********************/
+
+.md-icon-button.tb-md-32 {
+ vertical-align: middle;
+ width: 32px;
+ height: 32px;
+ min-width: 32px;
+ min-height: 32px;
+ margin: 0px !important;
+ padding: 0px !important;
+}
+
+.material-icons.tb-md-20 {
+ font-size: 20px;
+ width: 20px;
+ height: 20px;
+ min-width: 20px;
+ min-height: 20px;
+}
+
+.material-icons.tb-md-96 {
+ font-size: 96px;
+ width: 96px;
+ height: 96px;
+}
+
+/***********************
+ * Layout
+ ***********************/
+
+.tb-absolute-fill {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+}
+
+.tb-progress-cover {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 6;
+ opacity: 1;
+}
+
+/***********************
+ * ACE
+ ***********************/
+
+.ace_editor {
+ font-size: 16px !important;
+}
ui/src/scss/mixins.scss 34(+34 -0)
diff --git a/ui/src/scss/mixins.scss b/ui/src/scss/mixins.scss
new file mode 100644
index 0000000..593cf24
--- /dev/null
+++ b/ui/src/scss/mixins.scss
@@ -0,0 +1,34 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import "~compass-sass-mixins/lib/compass";
+
+@mixin input-placeholder {
+ // replaces compass/css/user-interface/input-placeholder()
+ &::-webkit-input-placeholder {
+ @content;
+ }
+ &:-moz-placeholder {
+ @content;
+ opacity: 1;
+ }
+ &::-moz-placeholder {
+ @content;
+ opacity: 1;
+ }
+ &:-ms-input-placeholder {
+ @content;
+ }
+}
\ No newline at end of file
ui/src/svg/logo_title_white.svg 42(+42 -0)
diff --git a/ui/src/svg/logo_title_white.svg b/ui/src/svg/logo_title_white.svg
new file mode 100644
index 0000000..fff3681
--- /dev/null
+++ b/ui/src/svg/logo_title_white.svg
@@ -0,0 +1,42 @@
+<svg id="svg2" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="320" width="1740" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" viewBox="0 0 1740 320.00002">
+ <g id="layer1" transform="translate(0 -732.36)">
+ <g id="g4652" stroke-width="28">
+ <g stroke-width="28">
+ <g id="g5204-0" transform="matrix(-.66287 .69913 -.66371 -.70001 863.46 1410.6)">
+ <g id="g5175-6" transform="translate(2.5254 3.0305)"></g>
+ <g id="g5175-9-8" transform="translate(41.25 -30.543)" stroke-width="28"></g>
+ </g>
+ <g id="g5175-6-35" transform="matrix(.57657 .52719 -.57729 .52786 584.63 346.6)"></g>
+ <g id="g5204-0-5-0" transform="matrix(.66287 -.69913 .66371 .70001 -543.46 380.47)">
+ <g id="g5175-6-35-9" transform="translate(2.5254 3.0305)"></g>
+ <g id="g5175-9-8-2-6" transform="translate(41.25 -30.543)" stroke-width="28"></g>
+ </g>
+ </g>
+ <g id="text4931">
+ <g id="g4983" fill="#fff">
+ <g id="g5263">
+ <path id="path5235" d="m413.4 820.82v27.185h20.959v14.838h-20.959v69.623q0 6.7444 2.8015 10.168 2.8015 3.3203 9.5459 3.3203 3.3203 0 9.1309-1.2451v15.564q-7.5745 2.0752-14.734 2.0752-12.866 0-19.403-7.782-6.5369-7.782-6.5369-22.101v-69.623h-20.441v-14.838h20.441v-27.185h19.196z"/>
+ <path id="path5237" d="m475.86 861.6q12.762-15.668 33.203-15.668 35.59 0 35.901 40.155v74.188h-19.196v-74.292q-0.10376-12.14-5.603-17.95-5.3955-5.8106-16.913-5.8106-9.3384 0-16.394 4.9805-7.0557 4.9805-10.999 13.074v79.999h-19.196v-159.38h19.196v60.699z"/>
+ <path id="path5239" d="m594.56 960.27h-19.196v-112.27h19.196v112.27zm-20.75-142.05q0-4.6692 2.8015-7.8857 2.9053-3.2166 8.5083-3.2166t8.5083 3.2166 2.9053 7.8857q0 4.6692-2.9053 7.782t-8.5083 3.1128-8.5083-3.1128q-2.8015-3.1128-2.8015-7.782z"/>
+ <path id="path5241" d="m643.33 848 0.62256 14.111q12.866-16.187 33.618-16.187 35.59 0 35.901 40.155v74.188h-19.196v-74.292q-0.10376-12.14-5.603-17.95-5.3955-5.8106-16.913-5.8106-9.3384 0-16.394 4.9805-7.0557 4.9805-10.999 13.074v79.999h-19.196v-112.27h18.158z"/>
+ <path id="path5243" d="m738.06 903.2q0-26.251 12.14-41.711 12.14-15.564 32.166-15.564 20.544 0 32.062 14.526l0.93384-12.451h17.535v109.57q0 21.79-12.97 34.344-12.866 12.555-34.656 12.555-12.14 0-23.761-5.1879-11.621-5.188-17.743-14.215l9.9609-11.517q12.347 15.253 30.194 15.253 14.008 0 21.79-7.8858 7.8857-7.8857 7.8857-22.205v-9.6497q-11.517 13.281-31.439 13.281-19.714 0-31.958-15.875-12.14-15.875-12.14-43.268zm19.299 2.179q0 18.988 7.782 29.883 7.782 10.791 21.79 10.791 18.158 0 26.666-16.498v-51.257q-8.8196-16.083-26.459-16.083-14.008 0-21.893 10.895-7.8857 10.895-7.8857 32.269z"/>
+ <path id="path5245" d="m927.11 930.49q0-7.782-5.9143-12.036-5.8106-4.3579-20.441-7.4707-14.526-3.1128-23.138-7.4707-8.5083-4.3579-12.659-10.376-4.0466-6.0181-4.0466-14.319 0-13.8 11.621-23.346 11.725-9.5459 29.883-9.5459 19.092 0 30.92 9.8572 11.932 9.8572 11.932 25.214h-19.299q0-7.8857-6.7444-13.593-6.6406-5.7068-16.809-5.7068-10.48 0-16.394 4.5654-5.9143 4.5654-5.9143 11.932 0 6.9519 5.4993 10.48 5.4993 3.5278 19.818 6.7444 14.423 3.2166 23.346 7.6782 8.9233 4.4617 13.177 10.791 4.3579 6.2256 4.3579 15.253 0 15.045-12.036 24.176-12.036 9.0271-31.232 9.0271-13.489 0-23.865-4.7729-10.376-4.773-16.29-13.281-5.8105-8.6121-5.8105-18.573h19.196q0.5188 9.6497 7.6782 15.356 7.2632 5.603 19.092 5.603 10.895 0 17.432-4.3579 6.6406-4.4617 6.6406-11.829z"/>
+ <path id="path5247" d="m1066.4 905.38q0 25.732-11.829 41.4-11.829 15.564-31.75 15.564-21.271 0-32.892-15.045l-0.93384 12.97h-17.639v-159.38h19.196v59.454q11.621-14.423 32.062-14.423t32.062 15.46q11.725 15.46 11.725 42.334v1.6602zm-19.196-2.179q0-19.611-7.5745-30.298-7.5744-10.687-21.79-10.687-18.988 0-27.289 17.639v48.56q8.8196 17.639 27.496 17.639 13.8 0 21.478-10.687 7.6782-10.687 7.6782-32.166z"/>
+ <path id="path5249" d="m1085.3 903.1q0-16.498 6.4331-29.675 6.5369-13.177 18.054-20.337 11.621-7.1594 26.459-7.1594 22.931 0 37.042 15.875 14.215 15.875 14.215 42.23v1.3489q0 16.394-6.3293 29.468-6.2256 12.97-17.95 20.233-11.621 7.2632-26.77 7.2632-22.827 0-37.042-15.875-14.111-15.875-14.111-42.023v-1.3489zm19.299 2.2827q0 18.677 8.6121 29.987 8.7158 11.31 23.242 11.31 14.63 0 23.242-11.414 8.6121-11.517 8.6121-32.166 0-18.469-8.8196-29.883-8.7158-11.517-23.242-11.517-14.215 0-22.931 11.31-8.7158 11.31-8.7158 32.373z"/>
+ <path id="path5251" d="m1280.9 960.27q-1.6601-3.3203-2.6977-11.829-13.385 13.904-31.958 13.904-16.602 0-27.289-9.3384-10.584-9.4421-10.584-23.865 0-17.535 13.281-27.185 13.385-9.7534 37.561-9.7534h18.677v-8.8196q0-10.065-6.0181-15.979-6.018-6.0181-17.743-6.0181-10.272 0-17.224 5.188-6.9519 5.188-6.9519 12.555h-19.299q0-8.4045 5.9143-16.187 6.0181-7.8857 16.186-12.451 10.272-4.5654 22.516-4.5654 19.403 0 30.402 9.7534 10.998 9.6496 11.414 26.666v51.672q0 15.46 3.9428 24.591v1.6602h-20.129zm-31.854-14.63q9.0271 0 17.12-4.6692 8.0932-4.6692 11.725-12.14v-23.035h-15.045q-35.278 0-35.278 20.648 0 9.0271 6.0181 14.111 6.0181 5.0842 15.46 5.0842z"/>
+ <path id="path5253" d="m1381.7 865.23q-4.3579-0.72631-9.4422-0.72631-18.884 0-25.629 16.083v79.688h-19.196v-112.27h18.677l0.3113 12.97q9.4421-15.045 26.77-15.045 5.603 0 8.5083 1.4526v17.847z"/>
+ <path id="path5255" d="m1394.5 903.2q0-25.836 12.244-41.504 12.244-15.771 32.062-15.771 19.714 0 31.232 13.489v-58.521h19.196v159.38h-17.639l-0.9338-12.036q-11.517 14.111-32.062 14.111-19.507 0-31.854-15.979-12.244-15.979-12.244-41.711v-1.4526zm19.196 2.179q0 19.092 7.8858 29.883 7.8857 10.791 21.79 10.791 18.262 0 26.666-16.394v-51.569q-8.6121-15.875-26.459-15.875-14.111 0-21.997 10.895-7.8858 10.895-7.8858 32.269z"/>
+ <path id="path5257" d="m1519.6 950.21q0-4.9805 2.9053-8.3008 3.009-3.3203 8.9233-3.3203t8.9234 3.3203q3.1127 3.3203 3.1127 8.3008 0 4.773-3.1127 7.9895-3.0091 3.2166-8.9234 3.2166t-8.9233-3.2166q-2.9053-3.2166-2.9053-7.9895z"/>
+ <path id="path5259" d="m1596 960.27h-19.196v-112.27h19.196v112.27zm-20.752-142.05q0-4.6692 2.8015-7.8857 2.9053-3.2166 8.5083-3.2166t8.5083 3.2166 2.9053 7.8857q0 4.6692-2.9053 7.782t-8.5083 3.1128-8.5083-3.1128q-2.8015-3.1128-2.8015-7.782z"/>
+ <path id="path5261" d="m1621.6 903.1q0-16.498 6.4332-29.675 6.5368-13.177 18.054-20.337 11.621-7.1594 26.459-7.1594 22.931 0 37.042 15.875 14.215 15.875 14.215 42.23v1.3489q0 16.394-6.3293 29.468-6.2256 12.97-17.95 20.233-11.621 7.2632-26.77 7.2632-22.827 0-37.042-15.875-14.111-15.875-14.111-42.023v-1.3489zm19.299 2.2827q0 18.677 8.612 29.987 8.7158 11.31 23.242 11.31 14.63 0 23.242-11.414 8.6121-11.517 8.6121-32.166 0-18.469-8.8196-29.883-8.7158-11.517-23.242-11.517-14.215 0-22.931 11.31-8.7158 11.31-8.7158 32.373z"/>
+ </g>
+ <g id="g4241" transform="translate(-.000061238 .000078147)" stroke-width="28">
+ <path id="path4278" style="color-rendering:auto;text-decoration-color:#000000;color:#000000;isolation:auto;mix-blend-mode:normal;shape-rendering:auto;solid-color:#000000;block-progression:tb;text-decoration-line:none;text-decoration-style:solid;image-rendering:auto;white-space:normal;text-indent:0;text-transform:none" d="m151.13 732.36c-28.363 0-54.915 7.9153-77.613 21.537-6.4723-5.2623-14.605-8.1915-23.067-8.1937a8.7661 8.7661 0 0 0 -0.0035 0c-20.154 0.00073-36.679 16.528-36.678 36.682a8.7661 8.7661 0 0 0 0 0.0106c0.0099 8.4147 2.9267 16.483 8.1033 22.927-13.83 22.83-21.87 49.58-21.87 78.17a8.7661 8.7661 0 1 0 17.531 0c0-24.702 6.7193-47.748 18.378-67.574 4.5663 1.9846 9.4727 3.1496 14.519 3.1573a8.7661 8.7661 0 0 0 0.01241 0c20.155 0.00062 36.683-16.527 36.682-36.682a8.7661 8.7661 0 0 0 0 -0.004c-0.0016-4.9994-1.1387-9.8628-3.0828-14.397 19.717-11.484 42.585-18.095 67.085-18.095a8.7661 8.7661 0 1 0 0 -17.531zm-100.69 30.88c5.9129 0.002 11.191 2.5121 14.836 7.0769a8.7661 8.7661 0 0 0 0.1826 0.21451c2.6715 3.3798 4.1326 7.5531 4.1341 11.863-0.0015 10.677-8.468 19.144-19.144 19.148-4.37-0.008-8.6011-1.5088-12-4.2546a8.7661 8.7661 0 0 0 -0.01241 -0.009c-4.514-3.6331-7.1358-9.0979-7.1442-14.893 0.0025-10.677 8.4701-19.144 19.148-19.146z"/>
+ <path id="path4486-0-9-6-9-7-6-5" style="color-rendering:auto;text-decoration-color:#000000;color:#000000;isolation:auto;mix-blend-mode:normal;shape-rendering:auto;solid-color:#000000;block-progression:tb;text-decoration-line:none;text-decoration-style:solid;image-rendering:auto;white-space:normal;text-indent:0;text-transform:none" d="m66.992 835.19c-1.492 1.5583-2.3663 3.7103-2.2576 6.0713 0.0962 2.0447 0.91322 4.0204 2.3372 5.5177 6.8051 6.8559 20.223 20.223 20.223 20.223l11.844-11.83s-12.973-12.961-20.176-20.171c-1.604-1.6325-3.7498-2.3141-6.012-2.3243-2.3583-0.0112-4.4673 0.95389-5.9592 2.5122zm32.147 19.983-36.639 36.639c-3.9751 3.976-3.9751 10.421 0 14.397l18.156 18.156 31.753 31.753 30.478 30.478c3.9758 3.9759 10.422 3.9763 14.398 0l24.791-24.791 37.914-37.914 36.639-36.639c3.9754-3.9764 3.975-10.422-0.00067-14.398l-18.63-18.63-31.75-31.76-30.01-30c-3.976-3.9751-10.421-3.9751-14.397 0l-24.79 24.79-37.91 37.91zm37.911-37.91s-12.973-12.961-20.176-20.171c-1.604-1.6325-3.7498-2.3141-6.012-2.3243-4.7166-0.0226-8.4341 3.8616-8.2169 8.5834 0.0962 2.0447 0.91322 4.0204 2.3372 5.5177 6.8051 6.8559 20.223 20.223 20.223 20.223l11.844-11.83zm69.193 5.2132s12.961-12.973 20.171-20.176c1.6325-1.604 2.3141-3.7498 2.3243-6.012 0.0225-4.7166-3.8616-8.4341-8.5834-8.2169-2.0447 0.0962-4.0204 0.91322-5.5177 2.3372-6.8559 6.8051-20.223 20.223-20.223 20.223l11.83 11.844zm31.753 31.753s12.961-12.973 20.171-20.176c1.6325-1.604 2.314-3.7498 2.3243-6.012 0.0225-4.7166-3.8616-8.4341-8.5834-8.2169-2.0447 0.0962-4.0204 0.91323-5.5177 2.3372-6.8559 6.8051-20.223 20.223-20.223 20.223l11.83 11.844zm-18.009 69.667s12.973 12.961 20.178 20.169c1.604 1.6324 3.7498 2.314 6.012 2.3243 4.7166 0.0225 8.4342-3.8615 8.2169-8.5834l-0.002 0.002c-0.0962-2.0447-0.91415-4.0214-2.3382-5.5186-6.8051-6.8559-20.222-20.222-20.222-20.222l-11.844 11.83zm-37.914 37.914s12.973 12.961 20.178 20.169c1.604 1.6324 3.7498 2.314 6.012 2.3242 4.7166 0.023 8.4342-3.8615 8.2169-8.5834h-0.002c-0.0962-2.0447-0.91323-4.0205-2.3372-5.5177-6.8051-6.8559-20.223-20.223-20.223-20.223l-11.844 11.83zm-69.667-5.6871s-12.961 12.973-20.169 20.178c-1.6324 1.604-2.314 3.7498-2.3243 6.012-0.02251 4.7166 3.8615 8.4342 8.5834 8.2169h-0.0019c2.0447-0.096 4.0204-0.9132 5.5177-2.3373 6.8561-6.8048 20.223-20.223 20.223-20.223l-11.82-11.84zm-31.743-31.74s-12.961 12.973-20.169 20.178c-1.6324 1.604-2.314 3.7498-2.3243 6.012-0.02251 4.7167 3.8615 8.4342 8.5834 8.2169h-0.0019c2.0447-0.0962 4.0204-0.91322 5.5177-2.3372 6.8561-6.8047 20.223-20.223 20.223-20.223l-11.829-11.85zm87.237-90.554c1.6794-1.7064 3.9669-2.6599 6.2971-2.626 1.6628 0.0237 3.253 0.55012 4.5625 1.5097l16.499 12.1c3.2009 2.297 4.1445 6.6589 2.2308 10.312-1.9137 3.6528-6.1234 5.5243-9.9506 4.4227l6.124 23.948c1.1128 4.3517-1.564 8.9677-5.9833 10.317l-44.642 13.631 8.2456 31.883c1.1728 4.3696-1.5017 9.0445-5.9546 10.407s-8.9756-1.1106-10.068-5.5047l-10.282-39.769c-1.1265-4.3556 1.5493-8.9847 5.9759-10.338l44.661-13.637-4.1219-16.118c-2.7634 3.0643-7.2335 3.8084-10.586 1.7615-3.3531-2.0469-4.6144-6.2896-2.9861-10.047l8.1169-19.454c0.43322-1.0376 1.0673-1.9888 1.8624-2.7973z" fill-rule="evenodd"/>
+ <path id="path4278-5" style="color-rendering:auto;text-decoration-color:#000000;color:#000000;isolation:auto;mix-blend-mode:normal;shape-rendering:auto;solid-color:#000000;block-progression:tb;text-decoration-line:none;text-decoration-style:solid;image-rendering:auto;white-space:normal;text-indent:0;text-transform:none" d="m168.87 1052.4c28.363 0 54.915-7.9155 77.614-21.538 6.4724 5.2624 14.605 8.1917 23.067 8.1939a8.7662 8.7662 0 0 0 0.004 0c20.155-0.0007 36.68-16.528 36.679-36.682a8.7662 8.7662 0 0 0 0 -0.011c-0.01-8.4149-2.9267-16.484-8.1034-22.927 13.825-22.82 21.866-49.572 21.866-78.162a8.7662 8.7662 0 1 0 -17.531 0c0 24.703-6.7194 47.749-18.378 67.575-4.5664-1.9846-9.4728-3.1496-14.519-3.1573a8.7662 8.7662 0 0 0 -0.0124 0c-20.155-0.00062-36.683 16.527-36.682 36.682 0.002 4.9994 1.1387 9.8628 3.0829 14.397-19.717 11.484-42.586 18.095-67.086 18.095a8.7662 8.7662 0 1 0 0 17.531zm100.69-30.875c-5.913 0-11.191-2.5122-14.836-7.0769a8.7662 8.7662 0 0 0 -0.18261 -0.2146c-2.6715-3.3799-4.1327-7.5531-4.1341-11.863 0.002-10.677 8.4681-19.144 19.144-19.148 4.37 0.008 8.6012 1.5088 12 4.2547a8.7662 8.7662 0 0 0 0.0124 0.009c4.5141 3.6332 7.1359 9.098 7.1443 14.893-0.003 10.677-8.4702 19.145-19.148 19.146z"/>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg>
ui/src/svg/logo_white.svg 10(+10 -0)
diff --git a/ui/src/svg/logo_white.svg b/ui/src/svg/logo_white.svg
new file mode 100644
index 0000000..52a38c9
--- /dev/null
+++ b/ui/src/svg/logo_white.svg
@@ -0,0 +1,10 @@
+<svg id="svg2" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="320" width="320" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" viewBox="0 0 320 320.00001">
+ <g id="layer1" transform="translate(0 -732.36)" stroke-width="28">
+ <g id="g5175-6-32" transform="matrix(1.0471 1.0606 -1.0484 1.0619 931.69 -208.55)"></g>
+ <g id="g4241" fill="#fff">
+ <path id="path4278" style="color-rendering:auto;text-decoration-color:#000000;color:#000000;isolation:auto;mix-blend-mode:normal;shape-rendering:auto;solid-color:#000000;block-progression:tb;text-decoration-line:none;text-decoration-style:solid;image-rendering:auto;white-space:normal;text-indent:0;text-transform:none" d="m151.13 732.36c-28.363 0-54.915 7.9153-77.613 21.537-6.4723-5.2623-14.605-8.1915-23.067-8.1937a8.7661 8.7661 0 0 0 -0.0035 0c-20.154 0.00073-36.679 16.528-36.678 36.682a8.7661 8.7661 0 0 0 0 0.0106c0.0099 8.4147 2.9267 16.483 8.1033 22.927-13.83 22.83-21.87 49.58-21.87 78.17a8.7661 8.7661 0 1 0 17.531 0c0-24.702 6.7193-47.748 18.378-67.574 4.5663 1.9846 9.4727 3.1496 14.519 3.1573a8.7661 8.7661 0 0 0 0.01241 0c20.155 0.00062 36.683-16.527 36.682-36.682a8.7661 8.7661 0 0 0 0 -0.004c-0.0016-4.9994-1.1387-9.8628-3.0828-14.397 19.717-11.484 42.585-18.095 67.085-18.095a8.7661 8.7661 0 1 0 0 -17.531zm-100.69 30.88c5.9129 0.002 11.191 2.5121 14.836 7.0769a8.7661 8.7661 0 0 0 0.1826 0.21451c2.6715 3.3798 4.1326 7.5531 4.1341 11.863-0.0015 10.677-8.468 19.144-19.144 19.148-4.37-0.008-8.6011-1.5088-12-4.2546a8.7661 8.7661 0 0 0 -0.01241 -0.009c-4.514-3.6331-7.1358-9.0979-7.1442-14.893 0.0025-10.677 8.4701-19.144 19.148-19.146z"/>
+ <path id="path4486-0-9-6-9-7-6-5" style="color-rendering:auto;text-decoration-color:#000000;color:#000000;isolation:auto;mix-blend-mode:normal;shape-rendering:auto;solid-color:#000000;block-progression:tb;text-decoration-line:none;text-decoration-style:solid;image-rendering:auto;white-space:normal;text-indent:0;text-transform:none" d="m66.992 835.19c-1.492 1.5583-2.3663 3.7103-2.2576 6.0713 0.0962 2.0447 0.91322 4.0204 2.3372 5.5177 6.8051 6.8559 20.223 20.223 20.223 20.223l11.844-11.83s-12.973-12.961-20.176-20.171c-1.604-1.6325-3.7498-2.3141-6.012-2.3243-2.3583-0.0112-4.4673 0.95389-5.9592 2.5122zm32.147 19.983-36.639 36.639c-3.9751 3.976-3.9751 10.421 0 14.397l18.156 18.156 31.753 31.753 30.478 30.478c3.9758 3.9759 10.422 3.9763 14.398 0l24.791-24.791 37.914-37.914 36.639-36.639c3.9754-3.9764 3.975-10.422-0.00067-14.398l-18.63-18.63-31.75-31.76-30.01-30c-3.976-3.9751-10.421-3.9751-14.397 0l-24.79 24.79-37.91 37.91zm37.911-37.91s-12.973-12.961-20.176-20.171c-1.604-1.6325-3.7498-2.3141-6.012-2.3243-4.7166-0.0226-8.4341 3.8616-8.2169 8.5834 0.0962 2.0447 0.91322 4.0204 2.3372 5.5177 6.8051 6.8559 20.223 20.223 20.223 20.223l11.844-11.83zm69.193 5.2132s12.961-12.973 20.171-20.176c1.6325-1.604 2.3141-3.7498 2.3243-6.012 0.0225-4.7166-3.8616-8.4341-8.5834-8.2169-2.0447 0.0962-4.0204 0.91322-5.5177 2.3372-6.8559 6.8051-20.223 20.223-20.223 20.223l11.83 11.844zm31.753 31.753s12.961-12.973 20.171-20.176c1.6325-1.604 2.314-3.7498 2.3243-6.012 0.0225-4.7166-3.8616-8.4341-8.5834-8.2169-2.0447 0.0962-4.0204 0.91323-5.5177 2.3372-6.8559 6.8051-20.223 20.223-20.223 20.223l11.83 11.844zm-18.009 69.667s12.973 12.961 20.178 20.169c1.604 1.6324 3.7498 2.314 6.012 2.3243 4.7166 0.0225 8.4342-3.8615 8.2169-8.5834l-0.002 0.002c-0.0962-2.0447-0.91415-4.0214-2.3382-5.5186-6.8051-6.8559-20.222-20.222-20.222-20.222l-11.844 11.83zm-37.914 37.914s12.973 12.961 20.178 20.169c1.604 1.6324 3.7498 2.314 6.012 2.3242 4.7166 0.023 8.4342-3.8615 8.2169-8.5834h-0.002c-0.0962-2.0447-0.91323-4.0205-2.3372-5.5177-6.8051-6.8559-20.223-20.223-20.223-20.223l-11.844 11.83zm-69.667-5.6871s-12.961 12.973-20.169 20.178c-1.6324 1.604-2.314 3.7498-2.3243 6.012-0.02251 4.7166 3.8615 8.4342 8.5834 8.2169h-0.0019c2.0447-0.096 4.0204-0.9132 5.5177-2.3373 6.8561-6.8048 20.223-20.223 20.223-20.223l-11.82-11.84zm-31.743-31.74s-12.961 12.973-20.169 20.178c-1.6324 1.604-2.314 3.7498-2.3243 6.012-0.02251 4.7167 3.8615 8.4342 8.5834 8.2169h-0.0019c2.0447-0.0962 4.0204-0.91322 5.5177-2.3372 6.8561-6.8047 20.223-20.223 20.223-20.223l-11.829-11.85zm87.237-90.554c1.6794-1.7064 3.9669-2.6599 6.2971-2.626 1.6628 0.0237 3.253 0.55012 4.5625 1.5097l16.499 12.1c3.2009 2.297 4.1445 6.6589 2.2308 10.312-1.9137 3.6528-6.1234 5.5243-9.9506 4.4227l6.124 23.948c1.1128 4.3517-1.564 8.9677-5.9833 10.317l-44.642 13.631 8.2456 31.883c1.1728 4.3696-1.5017 9.0445-5.9546 10.407s-8.9756-1.1106-10.068-5.5047l-10.282-39.769c-1.1265-4.3556 1.5493-8.9847 5.9759-10.338l44.661-13.637-4.1219-16.118c-2.7634 3.0643-7.2335 3.8084-10.586 1.7615-3.3531-2.0469-4.6144-6.2896-2.9861-10.047l8.1169-19.454c0.43322-1.0376 1.0673-1.9888 1.8624-2.7973z" fill-rule="evenodd"/>
+ <path id="path4278-5" style="color-rendering:auto;text-decoration-color:#000000;color:#000000;isolation:auto;mix-blend-mode:normal;shape-rendering:auto;solid-color:#000000;block-progression:tb;text-decoration-line:none;text-decoration-style:solid;image-rendering:auto;white-space:normal;text-indent:0;text-transform:none" d="m168.87 1052.4c28.363 0 54.915-7.9155 77.614-21.538 6.4724 5.2624 14.605 8.1917 23.067 8.1939a8.7662 8.7662 0 0 0 0.004 0c20.155-0.0007 36.68-16.528 36.679-36.682a8.7662 8.7662 0 0 0 0 -0.011c-0.01-8.4149-2.9267-16.484-8.1034-22.927 13.825-22.82 21.866-49.572 21.866-78.162a8.7662 8.7662 0 1 0 -17.531 0c0 24.703-6.7194 47.749-18.378 67.575-4.5664-1.9846-9.4728-3.1496-14.519-3.1573a8.7662 8.7662 0 0 0 -0.0124 0c-20.155-0.00062-36.683 16.527-36.682 36.682 0.002 4.9994 1.1387 9.8628 3.0829 14.397-19.717 11.484-42.586 18.095-67.086 18.095a8.7662 8.7662 0 1 0 0 17.531zm100.69-30.875c-5.913 0-11.191-2.5122-14.836-7.0769a8.7662 8.7662 0 0 0 -0.18261 -0.2146c-2.6715-3.3799-4.1327-7.5531-4.1341-11.863 0.002-10.677 8.4681-19.144 19.144-19.148 4.37 0.008 8.6012 1.5088 12 4.2547a8.7662 8.7662 0 0 0 0.0124 0.009c4.5141 3.6332 7.1359 9.098 7.1443 14.893-0.003 10.677-8.4702 19.145-19.148 19.146z"/>
+ </g>
+ </g>
+</svg>
ui/src/svg/mdi.svg 1(+1 -0)
diff --git a/ui/src/svg/mdi.svg b/ui/src/svg/mdi.svg
new file mode 100644
index 0000000..bb8fa1c
--- /dev/null
+++ b/ui/src/svg/mdi.svg
@@ -0,0 +1 @@
+<svg><defs><g id="access-point"><path d="M4.93,4.93C3.12,6.74 2,9.24 2,12C2,14.76 3.12,17.26 4.93,19.07L6.34,17.66C4.89,16.22 4,14.22 4,12C4,9.79 4.89,7.78 6.34,6.34L4.93,4.93M19.07,4.93L17.66,6.34C19.11,7.78 20,9.79 20,12C20,14.22 19.11,16.22 17.66,17.66L19.07,19.07C20.88,17.26 22,14.76 22,12C22,9.24 20.88,6.74 19.07,4.93M7.76,7.76C6.67,8.85 6,10.35 6,12C6,13.65 6.67,15.15 7.76,16.24L9.17,14.83C8.45,14.11 8,13.11 8,12C8,10.89 8.45,9.89 9.17,9.17L7.76,7.76M16.24,7.76L14.83,9.17C15.55,9.89 16,10.89 16,12C16,13.11 15.55,14.11 14.83,14.83L16.24,16.24C17.33,15.15 18,13.65 18,12C18,10.35 17.33,8.85 16.24,7.76M12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12A2,2 0 0,0 12,10Z" /></g><g id="access-point-network"><path d="M4.93,2.93C3.12,4.74 2,7.24 2,10C2,12.76 3.12,15.26 4.93,17.07L6.34,15.66C4.89,14.22 4,12.22 4,10C4,7.79 4.89,5.78 6.34,4.34L4.93,2.93M19.07,2.93L17.66,4.34C19.11,5.78 20,7.79 20,10C20,12.22 19.11,14.22 17.66,15.66L19.07,17.07C20.88,15.26 22,12.76 22,10C22,7.24 20.88,4.74 19.07,2.93M7.76,5.76C6.67,6.85 6,8.35 6,10C6,11.65 6.67,13.15 7.76,14.24L9.17,12.83C8.45,12.11 8,11.11 8,10C8,8.89 8.45,7.89 9.17,7.17L7.76,5.76M16.24,5.76L14.83,7.17C15.55,7.89 16,8.89 16,10C16,11.11 15.55,12.11 14.83,12.83L16.24,14.24C17.33,13.15 18,11.65 18,10C18,8.35 17.33,6.85 16.24,5.76M12,8A2,2 0 0,0 10,10A2,2 0 0,0 12,12A2,2 0 0,0 14,10A2,2 0 0,0 12,8M11,14V18H10A1,1 0 0,0 9,19H2V21H9A1,1 0 0,0 10,22H14A1,1 0 0,0 15,21H22V19H15A1,1 0 0,0 14,18H13V14H11Z" /></g><g id="account"><path d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z" /></g><g id="account-alert"><path d="M10,4A4,4 0 0,1 14,8A4,4 0 0,1 10,12A4,4 0 0,1 6,8A4,4 0 0,1 10,4M10,14C14.42,14 18,15.79 18,18V20H2V18C2,15.79 5.58,14 10,14M20,12V7H22V12H20M20,16V14H22V16H20Z" /></g><g id="account-box"><path d="M6,17C6,15 10,13.9 12,13.9C14,13.9 18,15 18,17V18H6M15,9A3,3 0 0,1 12,12A3,3 0 0,1 9,9A3,3 0 0,1 12,6A3,3 0 0,1 15,9M3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3H5C3.89,3 3,3.9 3,5Z" /></g><g id="account-box-outline"><path d="M19,19H5V5H19M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3M16.5,16.25C16.5,14.75 13.5,14 12,14C10.5,14 7.5,14.75 7.5,16.25V17H16.5M12,12.25A2.25,2.25 0 0,0 14.25,10A2.25,2.25 0 0,0 12,7.75A2.25,2.25 0 0,0 9.75,10A2.25,2.25 0 0,0 12,12.25Z" /></g><g id="account-card-details"><path d="M2,3H22C23.05,3 24,3.95 24,5V19C24,20.05 23.05,21 22,21H2C0.95,21 0,20.05 0,19V5C0,3.95 0.95,3 2,3M14,6V7H22V6H14M14,8V9H21.5L22,9V8H14M14,10V11H21V10H14M8,13.91C6,13.91 2,15 2,17V18H14V17C14,15 10,13.91 8,13.91M8,6A3,3 0 0,0 5,9A3,3 0 0,0 8,12A3,3 0 0,0 11,9A3,3 0 0,0 8,6Z" /></g><g id="account-check"><path d="M9,5A3.5,3.5 0 0,1 12.5,8.5A3.5,3.5 0 0,1 9,12A3.5,3.5 0 0,1 5.5,8.5A3.5,3.5 0 0,1 9,5M9,13.75C12.87,13.75 16,15.32 16,17.25V19H2V17.25C2,15.32 5.13,13.75 9,13.75M17,12.66L14.25,9.66L15.41,8.5L17,10.09L20.59,6.5L21.75,7.91L17,12.66Z" /></g><g id="account-circle"><path d="M12,19.2C9.5,19.2 7.29,17.92 6,16C6.03,14 10,12.9 12,12.9C14,12.9 17.97,14 18,16C16.71,17.92 14.5,19.2 12,19.2M12,5A3,3 0 0,1 15,8A3,3 0 0,1 12,11A3,3 0 0,1 9,8A3,3 0 0,1 12,5M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z" /></g><g id="account-convert"><path d="M7.5,21.5L8.85,20.16L12.66,23.97L12,24C5.71,24 0.56,19.16 0.05,13H1.55C1.91,16.76 4.25,19.94 7.5,21.5M16.5,2.5L15.15,3.84L11.34,0.03L12,0C18.29,0 23.44,4.84 23.95,11H22.45C22.09,7.24 19.75,4.07 16.5,2.5M6,17C6,15 10,13.9 12,13.9C14,13.9 18,15 18,17V18H6V17M15,9A3,3 0 0,1 12,12A3,3 0 0,1 9,9A3,3 0 0,1 12,6A3,3 0 0,1 15,9Z" /></g><g id="account-key"><path d="M11,10V12H10V14H8V12H5.83C5.42,13.17 4.31,14 3,14A3,3 0 0,1 0,11A3,3 0 0,1 3,8C4.31,8 5.42,8.83 5.83,10H11M3,10A1,1 0 0,0 2,11A1,1 0 0,0 3,12A1,1 0 0,0 4,11A1,1 0 0,0 3,10M16,14C18.67,14 24,15.34 24,18V20H8V18C8,15.34 13.33,14 16,14M16,12A4,4 0 0,1 12,8A4,4 0 0,1 16,4A4,4 0 0,1 20,8A4,4 0 0,1 16,12Z" /></g><g id="account-location"><path d="M18,16H6V15.1C6,13.1 10,12 12,12C14,12 18,13.1 18,15.1M12,5.3C13.5,5.3 14.7,6.5 14.7,8C14.7,9.5 13.5,10.7 12,10.7C10.5,10.7 9.3,9.5 9.3,8C9.3,6.5 10.5,5.3 12,5.3M19,2H5C3.89,2 3,2.89 3,4V18A2,2 0 0,0 5,20H9L12,23L15,20H19A2,2 0 0,0 21,18V4C21,2.89 20.1,2 19,2Z" /></g><g id="account-minus"><path d="M15,14C12.33,14 7,15.33 7,18V20H23V18C23,15.33 17.67,14 15,14M1,10V12H9V10M15,12A4,4 0 0,0 19,8A4,4 0 0,0 15,4A4,4 0 0,0 11,8A4,4 0 0,0 15,12Z" /></g><g id="account-multiple"><path d="M16,13C15.71,13 15.38,13 15.03,13.05C16.19,13.89 17,15 17,16.5V19H23V16.5C23,14.17 18.33,13 16,13M8,13C5.67,13 1,14.17 1,16.5V19H15V16.5C15,14.17 10.33,13 8,13M8,11A3,3 0 0,0 11,8A3,3 0 0,0 8,5A3,3 0 0,0 5,8A3,3 0 0,0 8,11M16,11A3,3 0 0,0 19,8A3,3 0 0,0 16,5A3,3 0 0,0 13,8A3,3 0 0,0 16,11Z" /></g><g id="account-multiple-minus"><path d="M13,13C11,13 7,14 7,16V18H19V16C19,14 15,13 13,13M19.62,13.16C20.45,13.88 21,14.82 21,16V18H24V16C24,14.46 21.63,13.5 19.62,13.16M13,11A3,3 0 0,0 16,8A3,3 0 0,0 13,5A3,3 0 0,0 10,8A3,3 0 0,0 13,11M18,11A3,3 0 0,0 21,8A3,3 0 0,0 18,5C17.68,5 17.37,5.05 17.08,5.14C17.65,5.95 18,6.94 18,8C18,9.06 17.65,10.04 17.08,10.85C17.37,10.95 17.68,11 18,11M8,10H0V12H8V10Z" /></g><g id="account-multiple-outline"><path d="M16.5,6.5A2,2 0 0,1 18.5,8.5A2,2 0 0,1 16.5,10.5A2,2 0 0,1 14.5,8.5A2,2 0 0,1 16.5,6.5M16.5,12A3.5,3.5 0 0,0 20,8.5A3.5,3.5 0 0,0 16.5,5A3.5,3.5 0 0,0 13,8.5A3.5,3.5 0 0,0 16.5,12M7.5,6.5A2,2 0 0,1 9.5,8.5A2,2 0 0,1 7.5,10.5A2,2 0 0,1 5.5,8.5A2,2 0 0,1 7.5,6.5M7.5,12A3.5,3.5 0 0,0 11,8.5A3.5,3.5 0 0,0 7.5,5A3.5,3.5 0 0,0 4,8.5A3.5,3.5 0 0,0 7.5,12M21.5,17.5H14V16.25C14,15.79 13.8,15.39 13.5,15.03C14.36,14.73 15.44,14.5 16.5,14.5C18.94,14.5 21.5,15.71 21.5,16.25M12.5,17.5H2.5V16.25C2.5,15.71 5.06,14.5 7.5,14.5C9.94,14.5 12.5,15.71 12.5,16.25M16.5,13C15.3,13 13.43,13.34 12,14C10.57,13.33 8.7,13 7.5,13C5.33,13 1,14.08 1,16.25V19H23V16.25C23,14.08 18.67,13 16.5,13Z" /></g><g id="account-multiple-plus"><path d="M13,13C11,13 7,14 7,16V18H19V16C19,14 15,13 13,13M19.62,13.16C20.45,13.88 21,14.82 21,16V18H24V16C24,14.46 21.63,13.5 19.62,13.16M13,11A3,3 0 0,0 16,8A3,3 0 0,0 13,5A3,3 0 0,0 10,8A3,3 0 0,0 13,11M18,11A3,3 0 0,0 21,8A3,3 0 0,0 18,5C17.68,5 17.37,5.05 17.08,5.14C17.65,5.95 18,6.94 18,8C18,9.06 17.65,10.04 17.08,10.85C17.37,10.95 17.68,11 18,11M8,10H5V7H3V10H0V12H3V15H5V12H8V10Z" /></g><g id="account-network"><path d="M13,16V18H14A1,1 0 0,1 15,19H22V21H15A1,1 0 0,1 14,22H10A1,1 0 0,1 9,21H2V19H9A1,1 0 0,1 10,18H11V16H5V14.5C5,12.57 8.13,11 12,11C15.87,11 19,12.57 19,14.5V16H13M12,2A3.5,3.5 0 0,1 15.5,5.5A3.5,3.5 0 0,1 12,9A3.5,3.5 0 0,1 8.5,5.5A3.5,3.5 0 0,1 12,2Z" /></g><g id="account-off"><path d="M12,4A4,4 0 0,1 16,8C16,9.95 14.6,11.58 12.75,11.93L8.07,7.25C8.42,5.4 10.05,4 12,4M12.28,14L18.28,20L20,21.72L18.73,23L15.73,20H4V18C4,16.16 6.5,14.61 9.87,14.14L2.78,7.05L4.05,5.78L12.28,14M20,18V19.18L15.14,14.32C18,14.93 20,16.35 20,18Z" /></g><g id="account-outline"><path d="M12,13C9.33,13 4,14.33 4,17V20H20V17C20,14.33 14.67,13 12,13M12,4A4,4 0 0,0 8,8A4,4 0 0,0 12,12A4,4 0 0,0 16,8A4,4 0 0,0 12,4M12,14.9C14.97,14.9 18.1,16.36 18.1,17V18.1H5.9V17C5.9,16.36 9,14.9 12,14.9M12,5.9A2.1,2.1 0 0,1 14.1,8A2.1,2.1 0 0,1 12,10.1A2.1,2.1 0 0,1 9.9,8A2.1,2.1 0 0,1 12,5.9Z" /></g><g id="account-plus"><path d="M15,14C12.33,14 7,15.33 7,18V20H23V18C23,15.33 17.67,14 15,14M6,10V7H4V10H1V12H4V15H6V12H9V10M15,12A4,4 0 0,0 19,8A4,4 0 0,0 15,4A4,4 0 0,0 11,8A4,4 0 0,0 15,12Z" /></g><g id="account-remove"><path d="M15,14C17.67,14 23,15.33 23,18V20H7V18C7,15.33 12.33,14 15,14M15,12A4,4 0 0,1 11,8A4,4 0 0,1 15,4A4,4 0 0,1 19,8A4,4 0 0,1 15,12M5,9.59L7.12,7.46L8.54,8.88L6.41,11L8.54,13.12L7.12,14.54L5,12.41L2.88,14.54L1.46,13.12L3.59,11L1.46,8.88L2.88,7.46L5,9.59Z" /></g><g id="account-search"><path d="M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.43,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.43C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,14C11.11,14 12.5,13.15 13.32,11.88C12.5,10.75 11.11,10 9.5,10C7.89,10 6.5,10.75 5.68,11.88C6.5,13.15 7.89,14 9.5,14M9.5,5A1.75,1.75 0 0,0 7.75,6.75A1.75,1.75 0 0,0 9.5,8.5A1.75,1.75 0 0,0 11.25,6.75A1.75,1.75 0 0,0 9.5,5Z" /></g><g id="account-settings"><path d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14M7,22H9V24H7V22M11,22H13V24H11V22M15,22H17V24H15V22Z" /></g><g id="account-settings-variant"><path d="M9,4A4,4 0 0,0 5,8A4,4 0 0,0 9,12A4,4 0 0,0 13,8A4,4 0 0,0 9,4M9,14C6.33,14 1,15.33 1,18V20H12.08C12.03,19.67 12,19.34 12,19C12,17.5 12.5,16 13.41,14.8C11.88,14.28 10.18,14 9,14M18,14C17.87,14 17.76,14.09 17.74,14.21L17.55,15.53C17.25,15.66 16.96,15.82 16.7,16L15.46,15.5C15.35,15.5 15.22,15.5 15.15,15.63L14.15,17.36C14.09,17.47 14.11,17.6 14.21,17.68L15.27,18.5C15.25,18.67 15.24,18.83 15.24,19C15.24,19.17 15.25,19.33 15.27,19.5L14.21,20.32C14.12,20.4 14.09,20.53 14.15,20.64L15.15,22.37C15.21,22.5 15.34,22.5 15.46,22.5L16.7,22C16.96,22.18 17.24,22.35 17.55,22.47L17.74,23.79C17.76,23.91 17.86,24 18,24H20C20.11,24 20.22,23.91 20.24,23.79L20.43,22.47C20.73,22.34 21,22.18 21.27,22L22.5,22.5C22.63,22.5 22.76,22.5 22.83,22.37L23.83,20.64C23.89,20.53 23.86,20.4 23.77,20.32L22.7,19.5C22.72,19.33 22.74,19.17 22.74,19C22.74,18.83 22.73,18.67 22.7,18.5L23.76,17.68C23.85,17.6 23.88,17.47 23.82,17.36L22.82,15.63C22.76,15.5 22.63,15.5 22.5,15.5L21.27,16C21,15.82 20.73,15.65 20.42,15.53L20.23,14.21C20.22,14.09 20.11,14 20,14M19,17.5A1.5,1.5 0 0,1 20.5,19A1.5,1.5 0 0,1 19,20.5C18.16,20.5 17.5,19.83 17.5,19A1.5,1.5 0 0,1 19,17.5Z" /></g><g id="account-star"><path d="M15,14C12.33,14 7,15.33 7,18V20H23V18C23,15.33 17.67,14 15,14M15,12A4,4 0 0,0 19,8A4,4 0 0,0 15,4A4,4 0 0,0 11,8A4,4 0 0,0 15,12M5,13.28L7.45,14.77L6.8,11.96L9,10.08L6.11,9.83L5,7.19L3.87,9.83L1,10.08L3.18,11.96L2.5,14.77L5,13.28Z" /></g><g id="account-star-variant"><path d="M9,14C11.67,14 17,15.33 17,18V20H1V18C1,15.33 6.33,14 9,14M9,12A4,4 0 0,1 5,8A4,4 0 0,1 9,4A4,4 0 0,1 13,8A4,4 0 0,1 9,12M19,13.28L16.54,14.77L17.2,11.96L15,10.08L17.89,9.83L19,7.19L20.13,9.83L23,10.08L20.82,11.96L21.5,14.77L19,13.28Z" /></g><g id="account-switch"><path d="M16,9C18.33,9 23,10.17 23,12.5V15H17V12.5C17,11 16.19,9.89 15.04,9.05L16,9M8,9C10.33,9 15,10.17 15,12.5V15H1V12.5C1,10.17 5.67,9 8,9M8,7A3,3 0 0,1 5,4A3,3 0 0,1 8,1A3,3 0 0,1 11,4A3,3 0 0,1 8,7M16,7A3,3 0 0,1 13,4A3,3 0 0,1 16,1A3,3 0 0,1 19,4A3,3 0 0,1 16,7M9,16.75V19H15V16.75L18.25,20L15,23.25V21H9V23.25L5.75,20L9,16.75Z" /></g><g id="adjust"><path d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9A3,3 0 0,1 15,12Z" /></g><g id="air-conditioner"><path d="M6.59,0.66C8.93,-1.15 11.47,1.06 12.04,4.5C12.47,4.5 12.89,4.62 13.27,4.84C13.79,4.24 14.25,3.42 14.07,2.5C13.65,0.35 16.06,-1.39 18.35,1.58C20.16,3.92 17.95,6.46 14.5,7.03C14.5,7.46 14.39,7.89 14.16,8.27C14.76,8.78 15.58,9.24 16.5,9.06C18.63,8.64 20.38,11.04 17.41,13.34C15.07,15.15 12.53,12.94 11.96,9.5C11.53,9.5 11.11,9.37 10.74,9.15C10.22,9.75 9.75,10.58 9.93,11.5C10.35,13.64 7.94,15.39 5.65,12.42C3.83,10.07 6.05,7.53 9.5,6.97C9.5,6.54 9.63,6.12 9.85,5.74C9.25,5.23 8.43,4.76 7.5,4.94C5.37,5.36 3.62,2.96 6.59,0.66M5,16H7A2,2 0 0,1 9,18V24H7V22H5V24H3V18A2,2 0 0,1 5,16M5,18V20H7V18H5M12.93,16H15L12.07,24H10L12.93,16M18,16H21V18H18V22H21V24H18A2,2 0 0,1 16,22V18A2,2 0 0,1 18,16Z" /></g><g id="airballoon"><path d="M11,23A2,2 0 0,1 9,21V19H15V21A2,2 0 0,1 13,23H11M12,1C12.71,1 13.39,1.09 14.05,1.26C15.22,2.83 16,5.71 16,9C16,11.28 15.62,13.37 15,16A2,2 0 0,1 13,18H11A2,2 0 0,1 9,16C8.38,13.37 8,11.28 8,9C8,5.71 8.78,2.83 9.95,1.26C10.61,1.09 11.29,1 12,1M20,8C20,11.18 18.15,15.92 15.46,17.21C16.41,15.39 17,11.83 17,9C17,6.17 16.41,3.61 15.46,1.79C18.15,3.08 20,4.82 20,8M4,8C4,4.82 5.85,3.08 8.54,1.79C7.59,3.61 7,6.17 7,9C7,11.83 7.59,15.39 8.54,17.21C5.85,15.92 4,11.18 4,8Z" /></g><g id="airplane"><path d="M21,16V14L13,9V3.5A1.5,1.5 0 0,0 11.5,2A1.5,1.5 0 0,0 10,3.5V9L2,14V16L10,13.5V19L8,20.5V22L11.5,21L15,22V20.5L13,19V13.5L21,16Z" /></g><g id="airplane-landing"><path d="M2.5,19H21.5V21H2.5V19M9.68,13.27L14.03,14.43L19.34,15.85C20.14,16.06 20.96,15.59 21.18,14.79C21.39,14 20.92,13.17 20.12,12.95L14.81,11.53L12.05,2.5L10.12,2V10.28L5.15,8.95L4.22,6.63L2.77,6.24V11.41L4.37,11.84L9.68,13.27Z" /></g><g id="airplane-off"><path d="M3.15,5.27L8.13,10.26L2.15,14V16L10.15,13.5V19L8.15,20.5V22L11.65,21L15.15,22V20.5L13.15,19V15.27L18.87,21L20.15,19.73L4.42,4M13.15,9V3.5A1.5,1.5 0 0,0 11.65,2A1.5,1.5 0 0,0 10.15,3.5V7.18L17.97,15L21.15,16V14L13.15,9Z" /></g><g id="airplane-takeoff"><path d="M2.5,19H21.5V21H2.5V19M22.07,9.64C21.86,8.84 21.03,8.36 20.23,8.58L14.92,10L8,3.57L6.09,4.08L10.23,11.25L5.26,12.58L3.29,11.04L1.84,11.43L3.66,14.59L4.43,15.92L6.03,15.5L11.34,14.07L15.69,12.91L21,11.5C21.81,11.26 22.28,10.44 22.07,9.64Z" /></g><g id="airplay"><path d="M6,22H18L12,16M21,3H3A2,2 0 0,0 1,5V17A2,2 0 0,0 3,19H7V17H3V5H21V17H17V19H21A2,2 0 0,0 23,17V5A2,2 0 0,0 21,3Z" /></g><g id="alarm"><path d="M12,20A7,7 0 0,1 5,13A7,7 0 0,1 12,6A7,7 0 0,1 19,13A7,7 0 0,1 12,20M12,4A9,9 0 0,0 3,13A9,9 0 0,0 12,22A9,9 0 0,0 21,13A9,9 0 0,0 12,4M12.5,8H11V14L15.75,16.85L16.5,15.62L12.5,13.25V8M7.88,3.39L6.6,1.86L2,5.71L3.29,7.24L7.88,3.39M22,5.72L17.4,1.86L16.11,3.39L20.71,7.25L22,5.72Z" /></g><g id="alarm-check"><path d="M10.54,14.53L8.41,12.4L7.35,13.46L10.53,16.64L16.53,10.64L15.47,9.58L10.54,14.53M12,20A7,7 0 0,1 5,13A7,7 0 0,1 12,6A7,7 0 0,1 19,13A7,7 0 0,1 12,20M12,4A9,9 0 0,0 3,13A9,9 0 0,0 12,22A9,9 0 0,0 21,13A9,9 0 0,0 12,4M7.88,3.39L6.6,1.86L2,5.71L3.29,7.24L7.88,3.39M22,5.72L17.4,1.86L16.11,3.39L20.71,7.25L22,5.72Z" /></g><g id="alarm-multiple"><path d="M9.29,3.25L5.16,6.72L4,5.34L8.14,1.87L9.29,3.25M22,5.35L20.84,6.73L16.7,3.25L17.86,1.87L22,5.35M13,4A8,8 0 0,1 21,12A8,8 0 0,1 13,20A8,8 0 0,1 5,12A8,8 0 0,1 13,4M13,6A6,6 0 0,0 7,12A6,6 0 0,0 13,18A6,6 0 0,0 19,12A6,6 0 0,0 13,6M12,7.5H13.5V12.03L16.72,13.5L16.1,14.86L12,13V7.5M1,14C1,11.5 2.13,9.3 3.91,7.83C3.33,9.1 3,10.5 3,12L3.06,13.13L3,14C3,16.28 4.27,18.26 6.14,19.28C7.44,20.5 9.07,21.39 10.89,21.78C10.28,21.92 9.65,22 9,22A8,8 0 0,1 1,14Z" /></g><g id="alarm-off"><path d="M8,3.28L6.6,1.86L5.74,2.57L7.16,4M16.47,18.39C15.26,19.39 13.7,20 12,20A7,7 0 0,1 5,13C5,11.3 5.61,9.74 6.61,8.53M2.92,2.29L1.65,3.57L3,4.9L1.87,5.83L3.29,7.25L4.4,6.31L5.2,7.11C3.83,8.69 3,10.75 3,13A9,9 0 0,0 12,22C14.25,22 16.31,21.17 17.89,19.8L20.09,22L21.36,20.73L3.89,3.27L2.92,2.29M22,5.72L17.4,1.86L16.11,3.39L20.71,7.25L22,5.72M12,6A7,7 0 0,1 19,13C19,13.84 18.84,14.65 18.57,15.4L20.09,16.92C20.67,15.73 21,14.41 21,13A9,9 0 0,0 12,4C10.59,4 9.27,4.33 8.08,4.91L9.6,6.43C10.35,6.16 11.16,6 12,6Z" /></g><g id="alarm-plus"><path d="M13,9H11V12H8V14H11V17H13V14H16V12H13M12,20A7,7 0 0,1 5,13A7,7 0 0,1 12,6A7,7 0 0,1 19,13A7,7 0 0,1 12,20M12,4A9,9 0 0,0 3,13A9,9 0 0,0 12,22A9,9 0 0,0 21,13A9,9 0 0,0 12,4M22,5.72L17.4,1.86L16.11,3.39L20.71,7.25M7.88,3.39L6.6,1.86L2,5.71L3.29,7.24L7.88,3.39Z" /></g><g id="alarm-snooze"><path d="M7.88,3.39L6.6,1.86L2,5.71L3.29,7.24L7.88,3.39M22,5.72L17.4,1.86L16.11,3.39L20.71,7.25L22,5.72M12,4A9,9 0 0,0 3,13A9,9 0 0,0 12,22A9,9 0 0,0 21,13A9,9 0 0,0 12,4M12,20A7,7 0 0,1 5,13A7,7 0 0,1 12,6A7,7 0 0,1 19,13A7,7 0 0,1 12,20M9,11H12.63L9,15.2V17H15V15H11.37L15,10.8V9H9V11Z" /></g><g id="album"><path d="M12,11A1,1 0 0,0 11,12A1,1 0 0,0 12,13A1,1 0 0,0 13,12A1,1 0 0,0 12,11M12,16.5C9.5,16.5 7.5,14.5 7.5,12C7.5,9.5 9.5,7.5 12,7.5C14.5,7.5 16.5,9.5 16.5,12C16.5,14.5 14.5,16.5 12,16.5M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" /></g><g id="alert"><path d="M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z" /></g><g id="alert-box"><path d="M5,3H19A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3M13,13V7H11V13H13M13,17V15H11V17H13Z" /></g><g id="alert-circle"><path d="M13,13H11V7H13M13,17H11V15H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" /></g><g id="alert-circle-outline"><path d="M11,15H13V17H11V15M11,7H13V13H11V7M12,2C6.47,2 2,6.5 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20Z" /></g><g id="alert-octagon"><path d="M13,13H11V7H13M12,17.3A1.3,1.3 0 0,1 10.7,16A1.3,1.3 0 0,1 12,14.7A1.3,1.3 0 0,1 13.3,16A1.3,1.3 0 0,1 12,17.3M15.73,3H8.27L3,8.27V15.73L8.27,21H15.73L21,15.73V8.27L15.73,3Z" /></g><g id="alert-outline"><path d="M12,2L1,21H23M12,6L19.53,19H4.47M11,10V14H13V10M11,16V18H13V16" /></g><g id="all-inclusive"><path d="M18.6,6.62C17.16,6.62 15.8,7.18 14.83,8.15L7.8,14.39C7.16,15.03 6.31,15.38 5.4,15.38C3.53,15.38 2,13.87 2,12C2,10.13 3.53,8.62 5.4,8.62C6.31,8.62 7.16,8.97 7.84,9.65L8.97,10.65L10.5,9.31L9.22,8.2C8.2,7.18 6.84,6.62 5.4,6.62C2.42,6.62 0,9.04 0,12C0,14.96 2.42,17.38 5.4,17.38C6.84,17.38 8.2,16.82 9.17,15.85L16.2,9.61C16.84,8.97 17.69,8.62 18.6,8.62C20.47,8.62 22,10.13 22,12C22,13.87 20.47,15.38 18.6,15.38C17.7,15.38 16.84,15.03 16.16,14.35L15,13.34L13.5,14.68L14.78,15.8C15.8,16.81 17.15,17.37 18.6,17.37C21.58,17.37 24,14.96 24,12C24,9 21.58,6.62 18.6,6.62Z" /></g><g id="alpha"><path d="M18.08,17.8C17.62,17.93 17.21,18 16.85,18C15.65,18 14.84,17.12 14.43,15.35H14.38C13.39,17.26 12,18.21 10.25,18.21C8.94,18.21 7.89,17.72 7.1,16.73C6.31,15.74 5.92,14.5 5.92,13C5.92,11.25 6.37,9.85 7.26,8.76C8.15,7.67 9.36,7.12 10.89,7.12C11.71,7.12 12.45,7.35 13.09,7.8C13.73,8.26 14.22,8.9 14.56,9.73H14.6L15.31,7.33H17.87L15.73,12.65C15.97,13.89 16.22,14.74 16.5,15.19C16.74,15.64 17.08,15.87 17.5,15.87C17.74,15.87 17.93,15.83 18.1,15.76L18.08,17.8M13.82,12.56C13.61,11.43 13.27,10.55 12.81,9.95C12.36,9.34 11.81,9.04 11.18,9.04C10.36,9.04 9.7,9.41 9.21,10.14C8.72,10.88 8.5,11.79 8.5,12.86C8.5,13.84 8.69,14.65 9.12,15.31C9.54,15.97 10.11,16.29 10.82,16.29C11.42,16.29 11.97,16 12.46,15.45C12.96,14.88 13.37,14.05 13.7,12.96L13.82,12.56Z" /></g><g id="alphabetical"><path d="M6,11A2,2 0 0,1 8,13V17H4A2,2 0 0,1 2,15V13A2,2 0 0,1 4,11H6M4,13V15H6V13H4M20,13V15H22V17H20A2,2 0 0,1 18,15V13A2,2 0 0,1 20,11H22V13H20M12,7V11H14A2,2 0 0,1 16,13V15A2,2 0 0,1 14,17H12A2,2 0 0,1 10,15V7H12M12,15H14V13H12V15Z" /></g><g id="altimeter"><path d="M7,3V5H17V3H7M9,7V9H15V7H9M2,7.96V16.04L6.03,12L2,7.96M22.03,7.96L18,12L22.03,16.04V7.96M7,11V13H17V11H7M9,15V17H15V15H9M7,19V21H17V19H7Z" /></g><g id="amazon"><path d="M15.93,17.09C15.75,17.25 15.5,17.26 15.3,17.15C14.41,16.41 14.25,16.07 13.76,15.36C12.29,16.86 11.25,17.31 9.34,17.31C7.09,17.31 5.33,15.92 5.33,13.14C5.33,10.96 6.5,9.5 8.19,8.76C9.65,8.12 11.68,8 13.23,7.83V7.5C13.23,6.84 13.28,6.09 12.9,5.54C12.58,5.05 11.95,4.84 11.4,4.84C10.38,4.84 9.47,5.37 9.25,6.45C9.2,6.69 9,6.93 8.78,6.94L6.18,6.66C5.96,6.61 5.72,6.44 5.78,6.1C6.38,2.95 9.23,2 11.78,2C13.08,2 14.78,2.35 15.81,3.33C17.11,4.55 17,6.18 17,7.95V12.12C17,13.37 17.5,13.93 18,14.6C18.17,14.85 18.21,15.14 18,15.31L15.94,17.09H15.93M13.23,10.56V10C11.29,10 9.24,10.39 9.24,12.67C9.24,13.83 9.85,14.62 10.87,14.62C11.63,14.62 12.3,14.15 12.73,13.4C13.25,12.47 13.23,11.6 13.23,10.56M20.16,19.54C18,21.14 14.82,22 12.1,22C8.29,22 4.85,20.59 2.25,18.24C2.05,18.06 2.23,17.81 2.5,17.95C5.28,19.58 8.75,20.56 12.33,20.56C14.74,20.56 17.4,20.06 19.84,19.03C20.21,18.87 20.5,19.27 20.16,19.54M21.07,18.5C20.79,18.14 19.22,18.33 18.5,18.42C18.31,18.44 18.28,18.26 18.47,18.12C19.71,17.24 21.76,17.5 22,17.79C22.24,18.09 21.93,20.14 20.76,21.11C20.58,21.27 20.41,21.18 20.5,21C20.76,20.33 21.35,18.86 21.07,18.5Z" /></g><g id="amazon-clouddrive"><path d="M4.94,11.12C5.23,11.12 5.5,11.16 5.76,11.23C5.77,9.09 7.5,7.35 9.65,7.35C11.27,7.35 12.67,8.35 13.24,9.77C13.83,9 14.74,8.53 15.76,8.53C17.5,8.53 18.94,9.95 18.94,11.71C18.94,11.95 18.91,12.2 18.86,12.43C19.1,12.34 19.37,12.29 19.65,12.29C20.95,12.29 22,13.35 22,14.65C22,15.95 20.95,17 19.65,17C18.35,17 6.36,17 4.94,17C3.32,17 2,15.68 2,14.06C2,12.43 3.32,11.12 4.94,11.12Z" /></g><g id="ambulance"><path d="M18,18.5A1.5,1.5 0 0,0 19.5,17A1.5,1.5 0 0,0 18,15.5A1.5,1.5 0 0,0 16.5,17A1.5,1.5 0 0,0 18,18.5M19.5,9.5H17V12H21.46L19.5,9.5M6,18.5A1.5,1.5 0 0,0 7.5,17A1.5,1.5 0 0,0 6,15.5A1.5,1.5 0 0,0 4.5,17A1.5,1.5 0 0,0 6,18.5M20,8L23,12V17H21A3,3 0 0,1 18,20A3,3 0 0,1 15,17H9A3,3 0 0,1 6,20A3,3 0 0,1 3,17H1V6C1,4.89 1.89,4 3,4H17V8H20M8,6V9H5V11H8V14H10V11H13V9H10V6H8Z" /></g><g id="amplifier"><path d="M10,2H14A1,1 0 0,1 15,3H21V21H19A1,1 0 0,1 18,22A1,1 0 0,1 17,21H7A1,1 0 0,1 6,22A1,1 0 0,1 5,21H3V3H9A1,1 0 0,1 10,2M5,5V9H19V5H5M7,6A1,1 0 0,1 8,7A1,1 0 0,1 7,8A1,1 0 0,1 6,7A1,1 0 0,1 7,6M12,6H14V7H12V6M15,6H16V8H15V6M17,6H18V8H17V6M12,11A4,4 0 0,0 8,15A4,4 0 0,0 12,19A4,4 0 0,0 16,15A4,4 0 0,0 12,11M10,6A1,1 0 0,1 11,7A1,1 0 0,1 10,8A1,1 0 0,1 9,7A1,1 0 0,1 10,6Z" /></g><g id="anchor"><path d="M12,2A3,3 0 0,0 9,5C9,6.27 9.8,7.4 11,7.83V10H8V12H11V18.92C9.16,18.63 7.53,17.57 6.53,16H8V14H3V19H5V17.3C6.58,19.61 9.2,21 12,21C14.8,21 17.42,19.61 19,17.31V19H21V14H16V16H17.46C16.46,17.56 14.83,18.63 13,18.92V12H16V10H13V7.82C14.2,7.4 15,6.27 15,5A3,3 0 0,0 12,2M12,4A1,1 0 0,1 13,5A1,1 0 0,1 12,6A1,1 0 0,1 11,5A1,1 0 0,1 12,4Z" /></g><g id="android"><path d="M15,5H14V4H15M10,5H9V4H10M15.53,2.16L16.84,0.85C17.03,0.66 17.03,0.34 16.84,0.14C16.64,-0.05 16.32,-0.05 16.13,0.14L14.65,1.62C13.85,1.23 12.95,1 12,1C11.04,1 10.14,1.23 9.34,1.63L7.85,0.14C7.66,-0.05 7.34,-0.05 7.15,0.14C6.95,0.34 6.95,0.66 7.15,0.85L8.46,2.16C6.97,3.26 6,5 6,7H18C18,5 17,3.25 15.53,2.16M20.5,8A1.5,1.5 0 0,0 19,9.5V16.5A1.5,1.5 0 0,0 20.5,18A1.5,1.5 0 0,0 22,16.5V9.5A1.5,1.5 0 0,0 20.5,8M3.5,8A1.5,1.5 0 0,0 2,9.5V16.5A1.5,1.5 0 0,0 3.5,18A1.5,1.5 0 0,0 5,16.5V9.5A1.5,1.5 0 0,0 3.5,8M6,18A1,1 0 0,0 7,19H8V22.5A1.5,1.5 0 0,0 9.5,24A1.5,1.5 0 0,0 11,22.5V19H13V22.5A1.5,1.5 0 0,0 14.5,24A1.5,1.5 0 0,0 16,22.5V19H17A1,1 0 0,0 18,18V8H6V18Z" /></g><g id="android-debug-bridge"><path d="M15,9A1,1 0 0,1 14,8A1,1 0 0,1 15,7A1,1 0 0,1 16,8A1,1 0 0,1 15,9M9,9A1,1 0 0,1 8,8A1,1 0 0,1 9,7A1,1 0 0,1 10,8A1,1 0 0,1 9,9M16.12,4.37L18.22,2.27L17.4,1.44L15.09,3.75C14.16,3.28 13.11,3 12,3C10.88,3 9.84,3.28 8.91,3.75L6.6,1.44L5.78,2.27L7.88,4.37C6.14,5.64 5,7.68 5,10V11H19V10C19,7.68 17.86,5.64 16.12,4.37M5,16C5,19.86 8.13,23 12,23A7,7 0 0,0 19,16V12H5V16Z" /></g><g id="android-studio"><path d="M11,2H13V4H13.5A1.5,1.5 0 0,1 15,5.5V9L14.56,9.44L16.2,12.28C17.31,11.19 18,9.68 18,8H20C20,10.42 18.93,12.59 17.23,14.06L20.37,19.5L20.5,21.72L18.63,20.5L15.56,15.17C14.5,15.7 13.28,16 12,16C10.72,16 9.5,15.7 8.44,15.17L5.37,20.5L3.5,21.72L3.63,19.5L9.44,9.44L9,9V5.5A1.5,1.5 0 0,1 10.5,4H11V2M9.44,13.43C10.22,13.8 11.09,14 12,14C12.91,14 13.78,13.8 14.56,13.43L13.1,10.9H13.09C12.47,11.5 11.53,11.5 10.91,10.9H10.9L9.44,13.43M12,6A1,1 0 0,0 11,7A1,1 0 0,0 12,8A1,1 0 0,0 13,7A1,1 0 0,0 12,6Z" /></g><g id="angular"><path d="M12,2.5L20.84,5.65L19.5,17.35L12,21.5L4.5,17.35L3.16,5.65L12,2.5M12,4.6L6.47,17H8.53L9.64,14.22H14.34L15.45,17H17.5L12,4.6M13.62,12.5H10.39L12,8.63L13.62,12.5Z" /></g><g id="animation"><path d="M4,2C2.89,2 2,2.89 2,4V14H4V4H14V2H4M8,6C6.89,6 6,6.89 6,8V18H8V8H18V6H8M12,10C10.89,10 10,10.89 10,12V20C10,21.11 10.89,22 12,22H20C21.11,22 22,21.11 22,20V12C22,10.89 21.11,10 20,10H12Z" /></g><g id="apple"><path d="M18.71,19.5C17.88,20.74 17,21.95 15.66,21.97C14.32,22 13.89,21.18 12.37,21.18C10.84,21.18 10.37,21.95 9.1,22C7.79,22.05 6.8,20.68 5.96,19.47C4.25,17 2.94,12.45 4.7,9.39C5.57,7.87 7.13,6.91 8.82,6.88C10.1,6.86 11.32,7.75 12.11,7.75C12.89,7.75 14.37,6.68 15.92,6.84C16.57,6.87 18.39,7.1 19.56,8.82C19.47,8.88 17.39,10.1 17.41,12.63C17.44,15.65 20.06,16.66 20.09,16.67C20.06,16.74 19.67,18.11 18.71,19.5M13,3.5C13.73,2.67 14.94,2.04 15.94,2C16.07,3.17 15.6,4.35 14.9,5.19C14.21,6.04 13.07,6.7 11.95,6.61C11.8,5.46 12.36,4.26 13,3.5Z" /></g><g id="apple-finder"><path d="M4,4H11.89C12.46,2.91 13.13,1.88 13.93,1L15.04,2.11C14.61,2.7 14.23,3.34 13.89,4H20A2,2 0 0,1 22,6V19A2,2 0 0,1 20,21H14.93L15.26,22.23L13.43,22.95L12.93,21H4A2,2 0 0,1 2,19V6A2,2 0 0,1 4,4M4,6V19H12.54C12.5,18.67 12.44,18.34 12.4,18C12.27,18 12.13,18 12,18C9.25,18 6.78,17.5 5.13,16.76L6.04,15.12C7,15.64 9.17,16 12,16C12.08,16 12.16,16 12.24,16C12.21,15.33 12.22,14.66 12.27,14H9C9,14 9.4,9.97 11,6H4M20,19V6H13C12.1,8.22 11.58,10.46 11.3,12H14.17C14,13.28 13.97,14.62 14.06,15.93C15.87,15.8 17.25,15.5 17.96,15.12L18.87,16.76C17.69,17.3 16.1,17.7 14.29,17.89C14.35,18.27 14.41,18.64 14.5,19H20M6,8H8V11H6V8M16,8H18V11H16V8Z" /></g><g id="apple-ios"><path d="M20,9V7H16A2,2 0 0,0 14,9V11A2,2 0 0,0 16,13H18V15H14V17H18A2,2 0 0,0 20,15V13A2,2 0 0,0 18,11H16V9M11,15H9V9H11M11,7H9A2,2 0 0,0 7,9V15A2,2 0 0,0 9,17H11A2,2 0 0,0 13,15V9A2,2 0 0,0 11,7M4,17H6V11H4M4,9H6V7H4V9Z" /></g><g id="apple-keyboard-caps"><path d="M15,14V8H17.17L12,2.83L6.83,8H9V14H15M12,0L22,10H17V16H7V10H2L12,0M7,18H17V24H7V18M15,20H9V22H15V20Z" /></g><g id="apple-keyboard-command"><path d="M6,2A4,4 0 0,1 10,6V8H14V6A4,4 0 0,1 18,2A4,4 0 0,1 22,6A4,4 0 0,1 18,10H16V14H18A4,4 0 0,1 22,18A4,4 0 0,1 18,22A4,4 0 0,1 14,18V16H10V18A4,4 0 0,1 6,22A4,4 0 0,1 2,18A4,4 0 0,1 6,14H8V10H6A4,4 0 0,1 2,6A4,4 0 0,1 6,2M16,18A2,2 0 0,0 18,20A2,2 0 0,0 20,18A2,2 0 0,0 18,16H16V18M14,10H10V14H14V10M6,16A2,2 0 0,0 4,18A2,2 0 0,0 6,20A2,2 0 0,0 8,18V16H6M8,6A2,2 0 0,0 6,4A2,2 0 0,0 4,6A2,2 0 0,0 6,8H8V6M18,8A2,2 0 0,0 20,6A2,2 0 0,0 18,4A2,2 0 0,0 16,6V8H18Z" /></g><g id="apple-keyboard-control"><path d="M19.78,11.78L18.36,13.19L12,6.83L5.64,13.19L4.22,11.78L12,4L19.78,11.78Z" /></g><g id="apple-keyboard-option"><path d="M3,4H9.11L16.15,18H21V20H14.88L7.84,6H3V4M14,4H21V6H14V4Z" /></g><g id="apple-keyboard-shift"><path d="M15,18V12H17.17L12,6.83L6.83,12H9V18H15M12,4L22,14H17V20H7V14H2L12,4Z" /></g><g id="apple-mobileme"><path d="M22,15.04C22,17.23 20.24,19 18.07,19H5.93C3.76,19 2,17.23 2,15.04C2,13.07 3.43,11.44 5.31,11.14C5.28,11 5.27,10.86 5.27,10.71C5.27,9.33 6.38,8.2 7.76,8.2C8.37,8.2 8.94,8.43 9.37,8.8C10.14,7.05 11.13,5.44 13.91,5.44C17.28,5.44 18.87,8.06 18.87,10.83C18.87,10.94 18.87,11.06 18.86,11.17C20.65,11.54 22,13.13 22,15.04Z" /></g><g id="apple-safari"><path d="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12C4,14.09 4.8,16 6.11,17.41L9.88,9.88L17.41,6.11C16,4.8 14.09,4 12,4M12,20A8,8 0 0,0 20,12C20,9.91 19.2,8 17.89,6.59L14.12,14.12L6.59,17.89C8,19.2 9.91,20 12,20M12,12L11.23,11.23L9.7,14.3L12.77,12.77L12,12M12,17.5H13V19H12V17.5M15.88,15.89L16.59,15.18L17.65,16.24L16.94,16.95L15.88,15.89M17.5,12V11H19V12H17.5M12,6.5H11V5H12V6.5M8.12,8.11L7.41,8.82L6.35,7.76L7.06,7.05L8.12,8.11M6.5,12V13H5V12H6.5Z" /></g><g id="application"><path d="M19,4C20.11,4 21,4.9 21,6V18A2,2 0 0,1 19,20H5C3.89,20 3,19.1 3,18V6A2,2 0 0,1 5,4H19M19,18V8H5V18H19Z" /></g><g id="appnet"><path d="M14.47,9.14C15.07,7.69 16.18,4.28 16.35,3.68C16.5,3.09 16.95,3 17.2,3H19.25C19.59,3 19.78,3.26 19.7,3.68C17.55,11.27 16.09,13.5 16.09,14C16.09,15.28 17.46,17.67 18.74,17.67C19.5,17.67 19.34,16.56 20.19,16.56H21.81C22.07,16.56 22.32,16.82 22.32,17.25C22.32,17.67 21.85,21 18.61,21C15.36,21 14.15,17.08 14.15,17.08C13.73,17.93 11.23,21 8.16,21C2.7,21 1.68,15.2 1.68,11.79C1.68,8.37 3.3,3 7.91,3C12.5,3 14.47,9.14 14.47,9.14M4.5,11.53C4.5,13.5 4.41,17.59 8,17.67C10.04,17.76 11.91,15.2 12.81,13.15C11.57,8.89 10.72,6.33 8,6.33C4.32,6.41 4.5,11.53 4.5,11.53Z" /></g><g id="apps"><path d="M16,20H20V16H16M16,14H20V10H16M10,8H14V4H10M16,8H20V4H16M10,14H14V10H10M4,14H8V10H4M4,20H8V16H4M10,20H14V16H10M4,8H8V4H4V8Z" /></g><g id="archive"><path d="M3,3H21V7H3V3M4,8H20V21H4V8M9.5,11A0.5,0.5 0 0,0 9,11.5V13H15V11.5A0.5,0.5 0 0,0 14.5,11H9.5Z" /></g><g id="arrange-bring-forward"><path d="M2,2H16V16H2V2M22,8V22H8V18H10V20H20V10H18V8H22Z" /></g><g id="arrange-bring-to-front"><path d="M2,2H11V6H9V4H4V9H6V11H2V2M22,13V22H13V18H15V20H20V15H18V13H22M8,8H16V16H8V8Z" /></g><g id="arrange-send-backward"><path d="M2,2H16V16H2V2M22,8V22H8V18H18V8H22M4,4V14H14V4H4Z" /></g><g id="arrange-send-to-back"><path d="M2,2H11V11H2V2M9,4H4V9H9V4M22,13V22H13V13H22M15,20H20V15H15V20M16,8V11H13V8H16M11,16H8V13H11V16Z" /></g><g id="arrow-all"><path d="M13,11H18L16.5,9.5L17.92,8.08L21.84,12L17.92,15.92L16.5,14.5L18,13H13V18L14.5,16.5L15.92,17.92L12,21.84L8.08,17.92L9.5,16.5L11,18V13H6L7.5,14.5L6.08,15.92L2.16,12L6.08,8.08L7.5,9.5L6,11H11V6L9.5,7.5L8.08,6.08L12,2.16L15.92,6.08L14.5,7.5L13,6V11Z" /></g><g id="arrow-bottom-left"><path d="M19,6.41L17.59,5L7,15.59V9H5V19H15V17H8.41L19,6.41Z" /></g><g id="arrow-bottom-right"><path d="M5,6.41L6.41,5L17,15.59V9H19V19H9V17H15.59L5,6.41Z" /></g><g id="arrow-compress"><path d="M19.5,3.09L15,7.59V4H13V11H20V9H16.41L20.91,4.5L19.5,3.09M4,13V15H7.59L3.09,19.5L4.5,20.91L9,16.41V20H11V13H4Z" /></g><g id="arrow-compress-all"><path d="M19.5,3.09L20.91,4.5L16.41,9H20V11H13V4H15V7.59L19.5,3.09M20.91,19.5L19.5,20.91L15,16.41V20H13V13H20V15H16.41L20.91,19.5M4.5,3.09L9,7.59V4H11V11H4V9H7.59L3.09,4.5L4.5,3.09M3.09,19.5L7.59,15H4V13H11V20H9V16.41L4.5,20.91L3.09,19.5Z" /></g><g id="arrow-down"><path d="M11,4H13V16L18.5,10.5L19.92,11.92L12,19.84L4.08,11.92L5.5,10.5L11,16V4Z" /></g><g id="arrow-down-bold"><path d="M10,4H14V13L17.5,9.5L19.92,11.92L12,19.84L4.08,11.92L6.5,9.5L10,13V4Z" /></g><g id="arrow-down-bold-circle"><path d="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,17L17,12H14V8H10V12H7L12,17Z" /></g><g id="arrow-down-bold-circle-outline"><path d="M12,17L7,12H10V8H14V12H17L12,17M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z" /></g><g id="arrow-down-bold-hexagon-outline"><path d="M12,17L7,12H10V8H14V12H17L12,17M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L5,8.09V15.91L12,19.85L19,15.91V8.09L12,4.15Z" /></g><g id="arrow-down-drop-circle"><path d="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M7,10L12,15L17,10H7Z" /></g><g id="arrow-down-drop-circle-outline"><path d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4M7,10L12,15L17,10H7Z" /></g><g id="arrow-expand"><path d="M10,21V19H6.41L10.91,14.5L9.5,13.09L5,17.59V14H3V21H10M14.5,10.91L19,6.41V10H21V3H14V5H17.59L13.09,9.5L14.5,10.91Z" /></g><g id="arrow-expand-all"><path d="M9.5,13.09L10.91,14.5L6.41,19H10V21H3V14H5V17.59L9.5,13.09M10.91,9.5L9.5,10.91L5,6.41V10H3V3H10V5H6.41L10.91,9.5M14.5,13.09L19,17.59V14H21V21H14V19H17.59L13.09,14.5L14.5,13.09M13.09,9.5L17.59,5H14V3H21V10H19V6.41L14.5,10.91L13.09,9.5Z" /></g><g id="arrow-left"><path d="M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z" /></g><g id="arrow-left-bold"><path d="M20,10V14H11L14.5,17.5L12.08,19.92L4.16,12L12.08,4.08L14.5,6.5L11,10H20Z" /></g><g id="arrow-left-bold-circle"><path d="M22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12M7,12L12,17V14H16V10H12V7L7,12Z" /></g><g id="arrow-left-bold-circle-outline"><path d="M7,12L12,7V10H16V14H12V17L7,12M22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12M20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12Z" /></g><g id="arrow-left-bold-hexagon-outline"><path d="M7,12L12,7V10H16V14H12V17L7,12M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L5,8.09V15.91L12,19.85L19,15.91V8.09L12,4.15Z" /></g><g id="arrow-left-drop-circle"><path d="M22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12M14,7L9,12L14,17V7Z" /></g><g id="arrow-left-drop-circle-outline"><path d="M22,12A10,10 0 0,0 12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12M20,12A8,8 0 0,1 12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12M14,7L9,12L14,17V7Z" /></g><g id="arrow-right"><path d="M4,11V13H16L10.5,18.5L11.92,19.92L19.84,12L11.92,4.08L10.5,5.5L16,11H4Z" /></g><g id="arrow-right-bold"><path d="M4,10V14H13L9.5,17.5L11.92,19.92L19.84,12L11.92,4.08L9.5,6.5L13,10H4Z" /></g><g id="arrow-right-bold-circle"><path d="M2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12M17,12L12,7V10H8V14H12V17L17,12Z" /></g><g id="arrow-right-bold-circle-outline"><path d="M17,12L12,17V14H8V10H12V7L17,12M2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12M4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12Z" /></g><g id="arrow-right-bold-hexagon-outline"><path d="M17,12L12,17V14H8V10H12V7L17,12M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L5,8.09V15.91L12,19.85L19,15.91V8.09L12,4.15Z" /></g><g id="arrow-right-drop-circle"><path d="M2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12M10,17L15,12L10,7V17Z" /></g><g id="arrow-right-drop-circle-outline"><path d="M2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2A10,10 0 0,0 2,12M4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20A8,8 0 0,1 4,12M10,17L15,12L10,7V17Z" /></g><g id="arrow-top-left"><path d="M19,17.59L17.59,19L7,8.41V15H5V5H15V7H8.41L19,17.59Z" /></g><g id="arrow-top-right"><path d="M5,17.59L15.59,7H9V5H19V15H17V8.41L6.41,19L5,17.59Z" /></g><g id="arrow-up"><path d="M13,20H11V8L5.5,13.5L4.08,12.08L12,4.16L19.92,12.08L18.5,13.5L13,8V20Z" /></g><g id="arrow-up-bold"><path d="M14,20H10V11L6.5,14.5L4.08,12.08L12,4.16L19.92,12.08L17.5,14.5L14,11V20Z" /></g><g id="arrow-up-bold-circle"><path d="M12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22M12,7L7,12H10V16H14V12H17L12,7Z" /></g><g id="arrow-up-bold-circle-outline"><path d="M12,7L17,12H14V16H10V12H7L12,7M12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20Z" /></g><g id="arrow-up-bold-hexagon-outline"><path d="M12,7L17,12H14V16H10V12H7L12,7M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L5,8.09V15.91L12,19.85L19,15.91V8.09L12,4.15Z" /></g><g id="arrow-up-drop-circle"><path d="M12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22M17,14L12,9L7,14H17Z" /></g><g id="arrow-up-drop-circle-outline"><path d="M12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22M12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20M17,14L12,9L7,14H17Z" /></g><g id="assistant"><path d="M19,2H5A2,2 0 0,0 3,4V18A2,2 0 0,0 5,20H9L12,23L15,20H19A2,2 0 0,0 21,18V4A2,2 0 0,0 19,2M13.88,12.88L12,17L10.12,12.88L6,11L10.12,9.12L12,5L13.88,9.12L18,11" /></g><g id="at"><path d="M17.42,15C17.79,14.09 18,13.07 18,12C18,8.13 15.31,5 12,5C8.69,5 6,8.13 6,12C6,15.87 8.69,19 12,19C13.54,19 15,19 16,18.22V20.55C15,21 13.46,21 12,21C7.58,21 4,16.97 4,12C4,7.03 7.58,3 12,3C16.42,3 20,7.03 20,12C20,13.85 19.5,15.57 18.65,17H14V15.5C13.36,16.43 12.5,17 11.5,17C9.57,17 8,14.76 8,12C8,9.24 9.57,7 11.5,7C12.5,7 13.36,7.57 14,8.5V8H16V15H17.42M12,9C10.9,9 10,10.34 10,12C10,13.66 10.9,15 12,15C13.1,15 14,13.66 14,12C14,10.34 13.1,9 12,9Z" /></g><g id="attachment"><path d="M7.5,18A5.5,5.5 0 0,1 2,12.5A5.5,5.5 0 0,1 7.5,7H18A4,4 0 0,1 22,11A4,4 0 0,1 18,15H9.5A2.5,2.5 0 0,1 7,12.5A2.5,2.5 0 0,1 9.5,10H17V11.5H9.5A1,1 0 0,0 8.5,12.5A1,1 0 0,0 9.5,13.5H18A2.5,2.5 0 0,0 20.5,11A2.5,2.5 0 0,0 18,8.5H7.5A4,4 0 0,0 3.5,12.5A4,4 0 0,0 7.5,16.5H17V18H7.5Z" /></g><g id="audiobook"><path d="M18,22H6A2,2 0 0,1 4,20V4C4,2.89 4.9,2 6,2H7V9L9.5,7.5L12,9V2H18A2,2 0 0,1 20,4V20A2,2 0 0,1 18,22M13,15A2,2 0 0,0 11,17A2,2 0 0,0 13,19A2,2 0 0,0 15,17V12H18V10H14V15.27C13.71,15.1 13.36,15 13,15Z" /></g><g id="auto-fix"><path d="M7.5,5.6L5,7L6.4,4.5L5,2L7.5,3.4L10,2L8.6,4.5L10,7L7.5,5.6M19.5,15.4L22,14L20.6,16.5L22,19L19.5,17.6L17,19L18.4,16.5L17,14L19.5,15.4M22,2L20.6,4.5L22,7L19.5,5.6L17,7L18.4,4.5L17,2L19.5,3.4L22,2M13.34,12.78L15.78,10.34L13.66,8.22L11.22,10.66L13.34,12.78M14.37,7.29L16.71,9.63C17.1,10 17.1,10.65 16.71,11.04L5.04,22.71C4.65,23.1 4,23.1 3.63,22.71L1.29,20.37C0.9,20 0.9,19.35 1.29,18.96L12.96,7.29C13.35,6.9 14,6.9 14.37,7.29Z" /></g><g id="auto-upload"><path d="M5.35,12.65L6.5,9L7.65,12.65M5.5,7L2.3,16H4.2L4.9,14H8.1L8.8,16H10.7L7.5,7M11,20H22V18H11M14,16H19V11H22L16.5,5.5L11,11H14V16Z" /></g><g id="autorenew"><path d="M12,6V9L16,5L12,1V4A8,8 0 0,0 4,12C4,13.57 4.46,15.03 5.24,16.26L6.7,14.8C6.25,13.97 6,13 6,12A6,6 0 0,1 12,6M18.76,7.74L17.3,9.2C17.74,10.04 18,11 18,12A6,6 0 0,1 12,18V15L8,19L12,23V20A8,8 0 0,0 20,12C20,10.43 19.54,8.97 18.76,7.74Z" /></g><g id="av-timer"><path d="M11,17A1,1 0 0,0 12,18A1,1 0 0,0 13,17A1,1 0 0,0 12,16A1,1 0 0,0 11,17M11,3V7H13V5.08C16.39,5.57 19,8.47 19,12A7,7 0 0,1 12,19A7,7 0 0,1 5,12C5,10.32 5.59,8.78 6.58,7.58L12,13L13.41,11.59L6.61,4.79V4.81C4.42,6.45 3,9.05 3,12A9,9 0 0,0 12,21A9,9 0 0,0 21,12A9,9 0 0,0 12,3M18,12A1,1 0 0,0 17,11A1,1 0 0,0 16,12A1,1 0 0,0 17,13A1,1 0 0,0 18,12M6,12A1,1 0 0,0 7,13A1,1 0 0,0 8,12A1,1 0 0,0 7,11A1,1 0 0,0 6,12Z" /></g><g id="baby"><path d="M18.5,4A2.5,2.5 0 0,1 21,6.5A2.5,2.5 0 0,1 18.5,9A2.5,2.5 0 0,1 16,6.5A2.5,2.5 0 0,1 18.5,4M4.5,20A1.5,1.5 0 0,1 3,18.5A1.5,1.5 0 0,1 4.5,17H11.5A1.5,1.5 0 0,1 13,18.5A1.5,1.5 0 0,1 11.5,20H4.5M16.09,19L14.69,15H11L6.75,10.75C6.75,10.75 9,8.25 12.5,8.25C15.5,8.25 15.85,9.25 16.06,9.87L18.92,18C19.2,18.78 18.78,19.64 18,19.92C17.22,20.19 16.36,19.78 16.09,19Z" /></g><g id="baby-buggy"><path d="M13,2V10H21A8,8 0 0,0 13,2M19.32,15.89C20.37,14.54 21,12.84 21,11H6.44L5.5,9H2V11H4.22C4.22,11 6.11,15.07 6.34,15.42C5.24,16 4.5,17.17 4.5,18.5A3.5,3.5 0 0,0 8,22C9.76,22 11.22,20.7 11.46,19H13.54C13.78,20.7 15.24,22 17,22A3.5,3.5 0 0,0 20.5,18.5C20.5,17.46 20.04,16.53 19.32,15.89M8,20A1.5,1.5 0 0,1 6.5,18.5A1.5,1.5 0 0,1 8,17A1.5,1.5 0 0,1 9.5,18.5A1.5,1.5 0 0,1 8,20M17,20A1.5,1.5 0 0,1 15.5,18.5A1.5,1.5 0 0,1 17,17A1.5,1.5 0 0,1 18.5,18.5A1.5,1.5 0 0,1 17,20Z" /></g><g id="backburger"><path d="M5,13L9,17L7.6,18.42L1.18,12L7.6,5.58L9,7L5,11H21V13H5M21,6V8H11V6H21M21,16V18H11V16H21Z" /></g><g id="backspace"><path d="M22,3H7C6.31,3 5.77,3.35 5.41,3.88L0,12L5.41,20.11C5.77,20.64 6.31,21 7,21H22A2,2 0 0,0 24,19V5A2,2 0 0,0 22,3M19,15.59L17.59,17L14,13.41L10.41,17L9,15.59L12.59,12L9,8.41L10.41,7L14,10.59L17.59,7L19,8.41L15.41,12" /></g><g id="backup-restore"><path d="M12,3A9,9 0 0,0 3,12H0L4,16L8,12H5A7,7 0 0,1 12,5A7,7 0 0,1 19,12A7,7 0 0,1 12,19C10.5,19 9.09,18.5 7.94,17.7L6.5,19.14C8.04,20.3 9.94,21 12,21A9,9 0 0,0 21,12A9,9 0 0,0 12,3M14,12A2,2 0 0,0 12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12Z" /></g><g id="bandcamp"><path d="M22,6L15.5,18H2L8.5,6H22Z" /></g><g id="bank"><path d="M11.5,1L2,6V8H21V6M16,10V17H19V10M2,22H21V19H2M10,10V17H13V10M4,10V17H7V10H4Z" /></g><g id="barcode"><path d="M2,6H4V18H2V6M5,6H6V18H5V6M7,6H10V18H7V6M11,6H12V18H11V6M14,6H16V18H14V6M17,6H20V18H17V6M21,6H22V18H21V6Z" /></g><g id="barcode-scan"><path d="M4,6H6V18H4V6M7,6H8V18H7V6M9,6H12V18H9V6M13,6H14V18H13V6M16,6H18V18H16V6M19,6H20V18H19V6M2,4V8H0V4A2,2 0 0,1 2,2H6V4H2M22,2A2,2 0 0,1 24,4V8H22V4H18V2H22M2,16V20H6V22H2A2,2 0 0,1 0,20V16H2M22,20V16H24V20A2,2 0 0,1 22,22H18V20H22Z" /></g><g id="barley"><path d="M7.33,18.33C6.5,17.17 6.5,15.83 6.5,14.5C8.17,15.5 9.83,16.5 10.67,17.67L11,18.23V15.95C9.5,15.05 8.08,14.13 7.33,13.08C6.5,11.92 6.5,10.58 6.5,9.25C8.17,10.25 9.83,11.25 10.67,12.42L11,13V10.7C9.5,9.8 8.08,8.88 7.33,7.83C6.5,6.67 6.5,5.33 6.5,4C8.17,5 9.83,6 10.67,7.17C10.77,7.31 10.86,7.46 10.94,7.62C10.77,7 10.66,6.42 10.65,5.82C10.64,4.31 11.3,2.76 11.96,1.21C12.65,2.69 13.34,4.18 13.35,5.69C13.36,6.32 13.25,6.96 13.07,7.59C13.15,7.45 13.23,7.31 13.33,7.17C14.17,6 15.83,5 17.5,4C17.5,5.33 17.5,6.67 16.67,7.83C15.92,8.88 14.5,9.8 13,10.7V13L13.33,12.42C14.17,11.25 15.83,10.25 17.5,9.25C17.5,10.58 17.5,11.92 16.67,13.08C15.92,14.13 14.5,15.05 13,15.95V18.23L13.33,17.67C14.17,16.5 15.83,15.5 17.5,14.5C17.5,15.83 17.5,17.17 16.67,18.33C15.92,19.38 14.5,20.3 13,21.2V23H11V21.2C9.5,20.3 8.08,19.38 7.33,18.33Z" /></g><g id="barrel"><path d="M18,19H19V21H5V19H6V13H5V11H6V5H5V3H19V5H18V11H19V13H18V19M9,13A3,3 0 0,0 12,16A3,3 0 0,0 15,13C15,11 12,7.63 12,7.63C12,7.63 9,11 9,13Z" /></g><g id="basecamp"><path d="M3.39,15.64C3.4,15.55 3.42,15.45 3.45,15.36C3.5,15.18 3.54,15 3.6,14.84C3.82,14.19 4.16,13.58 4.5,13C4.7,12.7 4.89,12.41 5.07,12.12C5.26,11.83 5.45,11.54 5.67,11.26C6,10.81 6.45,10.33 7,10.15C7.79,9.9 8.37,10.71 8.82,11.22C9.08,11.5 9.36,11.8 9.71,11.97C9.88,12.04 10.06,12.08 10.24,12.07C10.5,12.05 10.73,11.87 10.93,11.71C11.46,11.27 11.9,10.7 12.31,10.15C12.77,9.55 13.21,8.93 13.73,8.38C13.95,8.15 14.18,7.85 14.5,7.75C14.62,7.71 14.77,7.72 14.91,7.78C15,7.82 15.05,7.87 15.1,7.92C15.17,8 15.25,8.04 15.32,8.09C15.88,8.5 16.4,9 16.89,9.5C17.31,9.93 17.72,10.39 18.1,10.86C18.5,11.32 18.84,11.79 19.15,12.3C19.53,12.93 19.85,13.58 20.21,14.21C20.53,14.79 20.86,15.46 20.53,16.12C20.5,16.15 20.5,16.19 20.5,16.22C19.91,17.19 18.88,17.79 17.86,18.18C16.63,18.65 15.32,18.88 14,18.97C12.66,19.07 11.3,19.06 9.95,18.94C8.73,18.82 7.5,18.6 6.36,18.16C5.4,17.79 4.5,17.25 3.84,16.43C3.69,16.23 3.56,16.03 3.45,15.81C3.43,15.79 3.42,15.76 3.41,15.74C3.39,15.7 3.38,15.68 3.39,15.64M2.08,16.5C2.22,16.73 2.38,16.93 2.54,17.12C2.86,17.5 3.23,17.85 3.62,18.16C4.46,18.81 5.43,19.28 6.44,19.61C7.6,20 8.82,20.19 10.04,20.29C11.45,20.41 12.89,20.41 14.3,20.26C15.6,20.12 16.91,19.85 18.13,19.37C19.21,18.94 20.21,18.32 21.08,17.54C21.31,17.34 21.53,17.13 21.7,16.88C21.86,16.67 22,16.44 22,16.18C22,15.88 22,15.57 21.92,15.27C21.85,14.94 21.76,14.62 21.68,14.3C21.65,14.18 21.62,14.06 21.59,13.94C21.27,12.53 20.78,11.16 20.12,9.87C19.56,8.79 18.87,7.76 18.06,6.84C17.31,6 16.43,5.22 15.43,4.68C14.9,4.38 14.33,4.15 13.75,4C13.44,3.88 13.12,3.81 12.8,3.74C12.71,3.73 12.63,3.71 12.55,3.71C12.44,3.71 12.33,3.71 12.23,3.71C12,3.71 11.82,3.71 11.61,3.71C11.5,3.71 11.43,3.7 11.33,3.71C11.25,3.72 11.16,3.74 11.08,3.75C10.91,3.78 10.75,3.81 10.59,3.85C10.27,3.92 9.96,4 9.65,4.14C9.04,4.38 8.47,4.7 7.93,5.08C6.87,5.8 5.95,6.73 5.18,7.75C4.37,8.83 3.71,10.04 3.21,11.3C2.67,12.64 2.3,14.04 2.07,15.47C2.04,15.65 2,15.84 2,16C2,16.12 2,16.22 2,16.32C2,16.37 2,16.4 2.03,16.44C2.04,16.46 2.06,16.5 2.08,16.5Z" /></g><g id="basket"><path d="M5.5,21C4.72,21 4.04,20.55 3.71,19.9V19.9L1.1,10.44L1,10A1,1 0 0,1 2,9H6.58L11.18,2.43C11.36,2.17 11.66,2 12,2C12.34,2 12.65,2.17 12.83,2.44L17.42,9H22A1,1 0 0,1 23,10L22.96,10.29L20.29,19.9C19.96,20.55 19.28,21 18.5,21H5.5M12,4.74L9,9H15L12,4.74M12,13A2,2 0 0,0 10,15A2,2 0 0,0 12,17A2,2 0 0,0 14,15A2,2 0 0,0 12,13Z" /></g><g id="basket-fill"><path d="M3,2H6V5H3V2M6,7H9V10H6V7M8,2H11V5H8V2M17,11L12,6H15V2H19V6H22L17,11M7.5,22C6.72,22 6.04,21.55 5.71,20.9V20.9L3.1,13.44L3,13A1,1 0 0,1 4,12H20A1,1 0 0,1 21,13L20.96,13.29L18.29,20.9C17.96,21.55 17.28,22 16.5,22H7.5M7.61,20H16.39L18.57,14H5.42L7.61,20Z" /></g><g id="basket-unfill"><path d="M3,10H6V7H3V10M5,5H8V2H5V5M8,10H11V7H8V10M17,1L12,6H15V10H19V6H22L17,1M7.5,22C6.72,22 6.04,21.55 5.71,20.9V20.9L3.1,13.44L3,13A1,1 0 0,1 4,12H20A1,1 0 0,1 21,13L20.96,13.29L18.29,20.9C17.96,21.55 17.28,22 16.5,22H7.5M7.61,20H16.39L18.57,14H5.42L7.61,20Z" /></g><g id="battery"><path d="M16.67,4H15V2H9V4H7.33A1.33,1.33 0 0,0 6,5.33V20.67C6,21.4 6.6,22 7.33,22H16.67A1.33,1.33 0 0,0 18,20.67V5.33C18,4.6 17.4,4 16.67,4Z" /></g><g id="battery-10"><path d="M16,18H8V6H16M16.67,4H15V2H9V4H7.33A1.33,1.33 0 0,0 6,5.33V20.67C6,21.4 6.6,22 7.33,22H16.67A1.33,1.33 0 0,0 18,20.67V5.33C18,4.6 17.4,4 16.67,4Z" /></g><g id="battery-20"><path d="M16,17H8V6H16M16.67,4H15V2H9V4H7.33A1.33,1.33 0 0,0 6,5.33V20.67C6,21.4 6.6,22 7.33,22H16.67A1.33,1.33 0 0,0 18,20.67V5.33C18,4.6 17.4,4 16.67,4Z" /></g><g id="battery-30"><path d="M16,15H8V6H16M16.67,4H15V2H9V4H7.33A1.33,1.33 0 0,0 6,5.33V20.67C6,21.4 6.6,22 7.33,22H16.67A1.33,1.33 0 0,0 18,20.67V5.33C18,4.6 17.4,4 16.67,4Z" /></g><g id="battery-40"><path d="M16,14H8V6H16M16.67,4H15V2H9V4H7.33A1.33,1.33 0 0,0 6,5.33V20.67C6,21.4 6.6,22 7.33,22H16.67A1.33,1.33 0 0,0 18,20.67V5.33C18,4.6 17.4,4 16.67,4Z" /></g><g id="battery-50"><path d="M16,13H8V6H16M16.67,4H15V2H9V4H7.33A1.33,1.33 0 0,0 6,5.33V20.67C6,21.4 6.6,22 7.33,22H16.67A1.33,1.33 0 0,0 18,20.67V5.33C18,4.6 17.4,4 16.67,4Z" /></g><g id="battery-60"><path d="M16,12H8V6H16M16.67,4H15V2H9V4H7.33A1.33,1.33 0 0,0 6,5.33V20.67C6,21.4 6.6,22 7.33,22H16.67A1.33,1.33 0 0,0 18,20.67V5.33C18,4.6 17.4,4 16.67,4Z" /></g><g id="battery-70"><path d="M16,10H8V6H16M16.67,4H15V2H9V4H7.33A1.33,1.33 0 0,0 6,5.33V20.67C6,21.4 6.6,22 7.33,22H16.67A1.33,1.33 0 0,0 18,20.67V5.33C18,4.6 17.4,4 16.67,4Z" /></g><g id="battery-80"><path d="M16,9H8V6H16M16.67,4H15V2H9V4H7.33A1.33,1.33 0 0,0 6,5.33V20.67C6,21.4 6.6,22 7.33,22H16.67A1.33,1.33 0 0,0 18,20.67V5.33C18,4.6 17.4,4 16.67,4Z" /></g><g id="battery-90"><path d="M16,8H8V6H16M16.67,4H15V2H9V4H7.33A1.33,1.33 0 0,0 6,5.33V20.67C6,21.4 6.6,22 7.33,22H16.67A1.33,1.33 0 0,0 18,20.67V5.33C18,4.6 17.4,4 16.67,4Z" /></g><g id="battery-alert"><path d="M13,14H11V9H13M13,18H11V16H13M16.67,4H15V2H9V4H7.33A1.33,1.33 0 0,0 6,5.33V20.67C6,21.4 6.6,22 7.33,22H16.67A1.33,1.33 0 0,0 18,20.67V5.33C18,4.6 17.4,4 16.67,4Z" /></g><g id="battery-charging"><path d="M16.67,4H15V2H9V4H7.33A1.33,1.33 0 0,0 6,5.33V20.66C6,21.4 6.6,22 7.33,22H16.66C17.4,22 18,21.4 18,20.67V5.33C18,4.6 17.4,4 16.67,4M11,20V14.5H9L13,7V12.5H15" /></g><g id="battery-charging-100"><path d="M23,11H20V4L15,14H18V22M12.67,4H11V2H5V4H3.33A1.33,1.33 0 0,0 2,5.33V20.67C2,21.4 2.6,22 3.33,22H12.67C13.4,22 14,21.4 14,20.67V5.33A1.33,1.33 0 0,0 12.67,4Z" /></g><g id="battery-charging-20"><path d="M23.05,11H20.05V4L15.05,14H18.05V22M12.05,17H4.05V6H12.05M12.72,4H11.05V2H5.05V4H3.38A1.33,1.33 0 0,0 2.05,5.33V20.67C2.05,21.4 2.65,22 3.38,22H12.72C13.45,22 14.05,21.4 14.05,20.67V5.33A1.33,1.33 0 0,0 12.72,4Z" /></g><g id="battery-charging-30"><path d="M12,15H4V6H12M12.67,4H11V2H5V4H3.33A1.33,1.33 0 0,0 2,5.33V20.67C2,21.4 2.6,22 3.33,22H12.67C13.4,22 14,21.4 14,20.67V5.33A1.33,1.33 0 0,0 12.67,4M23,11H20V4L15,14H18V22L23,11Z" /></g><g id="battery-charging-40"><path d="M23,11H20V4L15,14H18V22M12,13H4V6H12M12.67,4H11V2H5V4H3.33A1.33,1.33 0 0,0 2,5.33V20.67C2,21.4 2.6,22 3.33,22H12.67C13.4,22 14,21.4 14,20.67V5.33A1.33,1.33 0 0,0 12.67,4Z" /></g><g id="battery-charging-60"><path d="M12,11H4V6H12M12.67,4H11V2H5V4H3.33A1.33,1.33 0 0,0 2,5.33V20.67C2,21.4 2.6,22 3.33,22H12.67C13.4,22 14,21.4 14,20.67V5.33A1.33,1.33 0 0,0 12.67,4M23,11H20V4L15,14H18V22L23,11Z" /></g><g id="battery-charging-80"><path d="M23,11H20V4L15,14H18V22M12,9H4V6H12M12.67,4H11V2H5V4H3.33A1.33,1.33 0 0,0 2,5.33V20.67C2,21.4 2.6,22 3.33,22H12.67C13.4,22 14,21.4 14,20.67V5.33A1.33,1.33 0 0,0 12.67,4Z" /></g><g id="battery-charging-90"><path d="M23,11H20V4L15,14H18V22M12,8H4V6H12M12.67,4H11V2H5V4H3.33A1.33,1.33 0 0,0 2,5.33V20.67C2,21.4 2.6,22 3.33,22H12.67C13.4,22 14,21.4 14,20.67V5.33A1.33,1.33 0 0,0 12.67,4Z" /></g><g id="battery-minus"><path d="M16.67,4C17.4,4 18,4.6 18,5.33V20.67A1.33,1.33 0 0,1 16.67,22H7.33C6.6,22 6,21.4 6,20.67V5.33A1.33,1.33 0 0,1 7.33,4H9V2H15V4H16.67M8,12V14H16V12" /></g><g id="battery-negative"><path d="M11.67,4A1.33,1.33 0 0,1 13,5.33V20.67C13,21.4 12.4,22 11.67,22H2.33C1.6,22 1,21.4 1,20.67V5.33A1.33,1.33 0 0,1 2.33,4H4V2H10V4H11.67M15,12H23V14H15V12M3,13H11V6H3V13Z" /></g><g id="battery-outline"><path d="M16,20H8V6H16M16.67,4H15V2H9V4H7.33A1.33,1.33 0 0,0 6,5.33V20.67C6,21.4 6.6,22 7.33,22H16.67A1.33,1.33 0 0,0 18,20.67V5.33C18,4.6 17.4,4 16.67,4Z" /></g><g id="battery-plus"><path d="M16.67,4C17.4,4 18,4.6 18,5.33V20.67A1.33,1.33 0 0,1 16.67,22H7.33C6.6,22 6,21.4 6,20.67V5.33A1.33,1.33 0 0,1 7.33,4H9V2H15V4H16.67M16,14V12H13V9H11V12H8V14H11V17H13V14H16Z" /></g><g id="battery-positive"><path d="M11.67,4A1.33,1.33 0 0,1 13,5.33V20.67C13,21.4 12.4,22 11.67,22H2.33C1.6,22 1,21.4 1,20.67V5.33A1.33,1.33 0 0,1 2.33,4H4V2H10V4H11.67M23,14H20V17H18V14H15V12H18V9H20V12H23V14M3,13H11V6H3V13Z" /></g><g id="battery-unknown"><path d="M15.07,12.25L14.17,13.17C13.63,13.71 13.25,14.18 13.09,15H11.05C11.16,14.1 11.56,13.28 12.17,12.67L13.41,11.41C13.78,11.05 14,10.55 14,10C14,8.89 13.1,8 12,8A2,2 0 0,0 10,10H8A4,4 0 0,1 12,6A4,4 0 0,1 16,10C16,10.88 15.64,11.68 15.07,12.25M13,19H11V17H13M16.67,4H15V2H9V4H7.33A1.33,1.33 0 0,0 6,5.33V20.66C6,21.4 6.6,22 7.33,22H16.67C17.4,22 18,21.4 18,20.66V5.33C18,4.59 17.4,4 16.67,4Z" /></g><g id="beach"><path d="M15,18.54C17.13,18.21 19.5,18 22,18V22H5C5,21.35 8.2,19.86 13,18.9V12.4C12.16,12.65 11.45,13.21 11,13.95C10.39,12.93 9.27,12.25 8,12.25C6.73,12.25 5.61,12.93 5,13.95C5.03,10.37 8.5,7.43 13,7.04V7A1,1 0 0,1 14,6A1,1 0 0,1 15,7V7.04C19.5,7.43 22.96,10.37 23,13.95C22.39,12.93 21.27,12.25 20,12.25C18.73,12.25 17.61,12.93 17,13.95C16.55,13.21 15.84,12.65 15,12.39V18.54M7,2A5,5 0 0,1 2,7V2H7Z" /></g><g id="beaker"><path d="M3,3H21V5A2,2 0 0,0 19,7V19A2,2 0 0,1 17,21H7A2,2 0 0,1 5,19V7A2,2 0 0,0 3,5V3M7,5V7H12V8H7V9H10V10H7V11H10V12H7V13H12V14H7V15H10V16H7V19H17V5H7Z" /></g><g id="beats"><path d="M7,12A5,5 0 0,0 12,17A5,5 0 0,0 17,12A5,5 0 0,0 12,7C10.87,7 9.84,7.37 9,8V2.46C9.95,2.16 10.95,2 12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12C2,8.3 4,5.07 7,3.34V12M14.5,12C14.5,12.37 14.3,12.68 14,12.86L12.11,14.29C11.94,14.42 11.73,14.5 11.5,14.5A1,1 0 0,1 10.5,13.5V10.5A1,1 0 0,1 11.5,9.5C11.73,9.5 11.94,9.58 12.11,9.71L14,11.14C14.3,11.32 14.5,11.63 14.5,12Z" /></g><g id="beer"><path d="M4,2H19L17,22H6L4,2M6.2,4L7.8,20H8.8L7.43,6.34C8.5,6 9.89,5.89 11,7C12.56,8.56 15.33,7.69 16.5,7.23L16.8,4H6.2Z" /></g><g id="behance"><path d="M19.58,12.27C19.54,11.65 19.33,11.18 18.96,10.86C18.59,10.54 18.13,10.38 17.58,10.38C17,10.38 16.5,10.55 16.19,10.89C15.86,11.23 15.65,11.69 15.57,12.27M21.92,12.04C22,12.45 22,13.04 22,13.81H15.5C15.55,14.71 15.85,15.33 16.44,15.69C16.79,15.92 17.22,16.03 17.73,16.03C18.26,16.03 18.69,15.89 19,15.62C19.2,15.47 19.36,15.27 19.5,15H21.88C21.82,15.54 21.53,16.07 21,16.62C20.22,17.5 19.1,17.92 17.66,17.92C16.47,17.92 15.43,17.55 14.5,16.82C13.62,16.09 13.16,14.9 13.16,13.25C13.16,11.7 13.57,10.5 14.39,9.7C15.21,8.87 16.27,8.46 17.58,8.46C18.35,8.46 19.05,8.6 19.67,8.88C20.29,9.16 20.81,9.59 21.21,10.2C21.58,10.73 21.81,11.34 21.92,12.04M9.58,14.07C9.58,13.42 9.31,12.97 8.79,12.73C8.5,12.6 8.08,12.53 7.54,12.5H4.87V15.84H7.5C8.04,15.84 8.46,15.77 8.76,15.62C9.31,15.35 9.58,14.83 9.58,14.07M4.87,10.46H7.5C8.04,10.46 8.5,10.36 8.82,10.15C9.16,9.95 9.32,9.58 9.32,9.06C9.32,8.5 9.1,8.1 8.66,7.91C8.27,7.78 7.78,7.72 7.19,7.72H4.87M11.72,12.42C12.04,12.92 12.2,13.53 12.2,14.24C12.2,15 12,15.64 11.65,16.23C11.41,16.62 11.12,16.94 10.77,17.21C10.37,17.5 9.9,17.72 9.36,17.83C8.82,17.94 8.24,18 7.61,18H2V5.55H8C9.53,5.58 10.6,6 11.23,6.88C11.61,7.41 11.8,8.04 11.8,8.78C11.8,9.54 11.61,10.15 11.23,10.61C11,10.87 10.7,11.11 10.28,11.32C10.91,11.55 11.39,11.92 11.72,12.42M20.06,7.32H15.05V6.07H20.06V7.32Z" /></g><g id="bell"><path d="M14,20A2,2 0 0,1 12,22A2,2 0 0,1 10,20H14M12,2A1,1 0 0,1 13,3V4.08C15.84,4.56 18,7.03 18,10V16L21,19H3L6,16V10C6,7.03 8.16,4.56 11,4.08V3A1,1 0 0,1 12,2Z" /></g><g id="bell-off"><path d="M14,20A2,2 0 0,1 12,22A2,2 0 0,1 10,20H14M19.74,21.57L17.17,19H3L6,16V10C6,9.35 6.1,8.72 6.3,8.13L3.47,5.3L4.89,3.89L7.29,6.29L21.15,20.15L19.74,21.57M11,4.08V3A1,1 0 0,1 12,2A1,1 0 0,1 13,3V4.08C15.84,4.56 18,7.03 18,10V14.17L8.77,4.94C9.44,4.5 10.19,4.22 11,4.08Z" /></g><g id="bell-outline"><path d="M16,17H7V10.5C7,8 9,6 11.5,6C14,6 16,8 16,10.5M18,16V10.5C18,7.43 15.86,4.86 13,4.18V3.5A1.5,1.5 0 0,0 11.5,2A1.5,1.5 0 0,0 10,3.5V4.18C7.13,4.86 5,7.43 5,10.5V16L3,18V19H20V18M11.5,22A2,2 0 0,0 13.5,20H9.5A2,2 0 0,0 11.5,22Z" /></g><g id="bell-plus"><path d="M10,21C10,22.11 10.9,23 12,23A2,2 0 0,0 14,21M18.88,16.82V11C18.88,7.75 16.63,5.03 13.59,4.31V3.59A1.59,1.59 0 0,0 12,2A1.59,1.59 0 0,0 10.41,3.59V4.31C7.37,5.03 5.12,7.75 5.12,11V16.82L3,18.94V20H21V18.94M16,13H13V16H11V13H8V11H11V8H13V11H16" /></g><g id="bell-ring"><path d="M11.5,22C11.64,22 11.77,22 11.9,21.96C12.55,21.82 13.09,21.38 13.34,20.78C13.44,20.54 13.5,20.27 13.5,20H9.5A2,2 0 0,0 11.5,22M18,10.5C18,7.43 15.86,4.86 13,4.18V3.5A1.5,1.5 0 0,0 11.5,2A1.5,1.5 0 0,0 10,3.5V4.18C7.13,4.86 5,7.43 5,10.5V16L3,18V19H20V18L18,16M19.97,10H21.97C21.82,6.79 20.24,3.97 17.85,2.15L16.42,3.58C18.46,5 19.82,7.35 19.97,10M6.58,3.58L5.15,2.15C2.76,3.97 1.18,6.79 1,10H3C3.18,7.35 4.54,5 6.58,3.58Z" /></g><g id="bell-ring-outline"><path d="M16,17V10.5C16,8 14,6 11.5,6C9,6 7,8 7,10.5V17H16M18,16L20,18V19H3V18L5,16V10.5C5,7.43 7.13,4.86 10,4.18V3.5A1.5,1.5 0 0,1 11.5,2A1.5,1.5 0 0,1 13,3.5V4.18C15.86,4.86 18,7.43 18,10.5V16M11.5,22A2,2 0 0,1 9.5,20H13.5A2,2 0 0,1 11.5,22M19.97,10C19.82,7.35 18.46,5 16.42,3.58L17.85,2.15C20.24,3.97 21.82,6.79 21.97,10H19.97M6.58,3.58C4.54,5 3.18,7.35 3,10H1C1.18,6.79 2.76,3.97 5.15,2.15L6.58,3.58Z" /></g><g id="bell-sleep"><path d="M14,9.8L11.2,13.2H14V15H9V13.2L11.8,9.8H9V8H14M18,16V10.5C18,7.43 15.86,4.86 13,4.18V3.5A1.5,1.5 0 0,0 11.5,2A1.5,1.5 0 0,0 10,3.5V4.18C7.13,4.86 5,7.43 5,10.5V16L3,18V19H20V18M11.5,22A2,2 0 0,0 13.5,20H9.5A2,2 0 0,0 11.5,22Z" /></g><g id="beta"><path d="M9.23,17.59V23.12H6.88V6.72C6.88,5.27 7.31,4.13 8.16,3.28C9,2.43 10.17,2 11.61,2C13,2 14.07,2.34 14.87,3C15.66,3.68 16.05,4.62 16.05,5.81C16.05,6.63 15.79,7.4 15.27,8.11C14.75,8.82 14.08,9.31 13.25,9.58V9.62C14.5,9.82 15.47,10.27 16.13,11C16.79,11.71 17.12,12.62 17.12,13.74C17.12,15.06 16.66,16.14 15.75,16.97C14.83,17.8 13.63,18.21 12.13,18.21C11.07,18.21 10.1,18 9.23,17.59M10.72,10.75V8.83C11.59,8.72 12.3,8.4 12.87,7.86C13.43,7.31 13.71,6.7 13.71,6C13.71,4.62 13,3.92 11.6,3.92C10.84,3.92 10.25,4.16 9.84,4.65C9.43,5.14 9.23,5.82 9.23,6.71V15.5C10.14,16.03 11.03,16.29 11.89,16.29C12.73,16.29 13.39,16.07 13.86,15.64C14.33,15.2 14.56,14.58 14.56,13.79C14.56,12 13.28,11 10.72,10.75Z" /></g><g id="bible"><path d="M5.81,2H7V9L9.5,7.5L12,9V2H18A2,2 0 0,1 20,4V20C20,21.05 19.05,22 18,22H6C4.95,22 4,21.05 4,20V4C4,3 4.83,2.09 5.81,2M13,10V13H10V15H13V20H15V15H18V13H15V10H13Z" /></g><g id="bike"><path d="M5,20.5A3.5,3.5 0 0,1 1.5,17A3.5,3.5 0 0,1 5,13.5A3.5,3.5 0 0,1 8.5,17A3.5,3.5 0 0,1 5,20.5M5,12A5,5 0 0,0 0,17A5,5 0 0,0 5,22A5,5 0 0,0 10,17A5,5 0 0,0 5,12M14.8,10H19V8.2H15.8L13.86,4.93C13.57,4.43 13,4.1 12.4,4.1C11.93,4.1 11.5,4.29 11.2,4.6L7.5,8.29C7.19,8.6 7,9 7,9.5C7,10.13 7.33,10.66 7.85,10.97L11.2,13V18H13V11.5L10.75,9.85L13.07,7.5M19,20.5A3.5,3.5 0 0,1 15.5,17A3.5,3.5 0 0,1 19,13.5A3.5,3.5 0 0,1 22.5,17A3.5,3.5 0 0,1 19,20.5M19,12A5,5 0 0,0 14,17A5,5 0 0,0 19,22A5,5 0 0,0 24,17A5,5 0 0,0 19,12M16,4.8C17,4.8 17.8,4 17.8,3C17.8,2 17,1.2 16,1.2C15,1.2 14.2,2 14.2,3C14.2,4 15,4.8 16,4.8Z" /></g><g id="bing"><path d="M5,3V19L8.72,21L18,15.82V11.73H18L9.77,8.95L11.38,12.84L13.94,14L8.7,16.92V4.27L5,3" /></g><g id="binoculars"><path d="M11,6H13V13H11V6M9,20A1,1 0 0,1 8,21H5A1,1 0 0,1 4,20V15L6,6H10V13A1,1 0 0,1 9,14V20M10,5H7V3H10V5M15,20V14A1,1 0 0,1 14,13V6H18L20,15V20A1,1 0 0,1 19,21H16A1,1 0 0,1 15,20M14,5V3H17V5H14Z" /></g><g id="bio"><path d="M17,12H20A2,2 0 0,1 22,14V17A2,2 0 0,1 20,19H17A2,2 0 0,1 15,17V14A2,2 0 0,1 17,12M17,14V17H20V14H17M2,7H7A2,2 0 0,1 9,9V11A2,2 0 0,1 7,13A2,2 0 0,1 9,15V17A2,2 0 0,1 7,19H2V13L2,7M4,9V12H7V9H4M4,17H7V14H4V17M11,13H13V19H11V13M11,9H13V11H11V9Z" /></g><g id="biohazard"><path d="M23,16.06C23,16.29 23,16.5 22.96,16.7C22.78,14.14 20.64,12.11 18,12.11C17.63,12.11 17.27,12.16 16.92,12.23C16.96,12.5 17,12.73 17,13C17,15.35 15.31,17.32 13.07,17.81C13.42,20.05 15.31,21.79 17.65,21.96C17.43,22 17.22,22 17,22C14.92,22 13.07,20.94 12,19.34C10.93,20.94 9.09,22 7,22C6.78,22 6.57,22 6.35,21.96C8.69,21.79 10.57,20.06 10.93,17.81C8.68,17.32 7,15.35 7,13C7,12.73 7.04,12.5 7.07,12.23C6.73,12.16 6.37,12.11 6,12.11C3.36,12.11 1.22,14.14 1.03,16.7C1,16.5 1,16.29 1,16.06C1,12.85 3.59,10.24 6.81,10.14C6.3,9.27 6,8.25 6,7.17C6,4.94 7.23,3 9.06,2C7.81,2.9 7,4.34 7,6C7,7.35 7.56,8.59 8.47,9.5C9.38,8.59 10.62,8.04 12,8.04C13.37,8.04 14.62,8.59 15.5,9.5C16.43,8.59 17,7.35 17,6C17,4.34 16.18,2.9 14.94,2C16.77,3 18,4.94 18,7.17C18,8.25 17.7,9.27 17.19,10.14C20.42,10.24 23,12.85 23,16.06M9.27,10.11C10.05,10.62 11,10.92 12,10.92C13,10.92 13.95,10.62 14.73,10.11C14,9.45 13.06,9.03 12,9.03C10.94,9.03 10,9.45 9.27,10.11M12,14.47C12.82,14.47 13.5,13.8 13.5,13A1.5,1.5 0 0,0 12,11.5A1.5,1.5 0 0,0 10.5,13C10.5,13.8 11.17,14.47 12,14.47M10.97,16.79C10.87,14.9 9.71,13.29 8.05,12.55C8.03,12.7 8,12.84 8,13C8,14.82 9.27,16.34 10.97,16.79M15.96,12.55C14.29,13.29 13.12,14.9 13,16.79C14.73,16.34 16,14.82 16,13C16,12.84 15.97,12.7 15.96,12.55Z" /></g><g id="bitbucket"><path d="M12,5.76C15.06,5.77 17.55,5.24 17.55,4.59C17.55,3.94 15.07,3.41 12,3.4C8.94,3.4 6.45,3.92 6.45,4.57C6.45,5.23 8.93,5.76 12,5.76M12,14.4C13.5,14.4 14.75,13.16 14.75,11.64A2.75,2.75 0 0,0 12,8.89C10.5,8.89 9.25,10.12 9.25,11.64C9.25,13.16 10.5,14.4 12,14.4M12,2C16.77,2 20.66,3.28 20.66,4.87C20.66,5.29 19.62,11.31 19.21,13.69C19.03,14.76 16.26,16.33 12,16.33V16.31L12,16.33C7.74,16.33 4.97,14.76 4.79,13.69C4.38,11.31 3.34,5.29 3.34,4.87C3.34,3.28 7.23,2 12,2M18.23,16.08C18.38,16.08 18.53,16.19 18.53,16.42V16.5C18.19,18.26 17.95,19.5 17.91,19.71C17.62,21 15.07,22 12,22V22C8.93,22 6.38,21 6.09,19.71C6.05,19.5 5.81,18.26 5.47,16.5V16.42C5.47,16.19 5.62,16.08 5.77,16.08C5.91,16.08 6,16.17 6,16.17C6,16.17 8.14,17.86 12,17.86C15.86,17.86 18,16.17 18,16.17C18,16.17 18.09,16.08 18.23,16.08M13.38,11.64C13.38,12.4 12.76,13 12,13C11.24,13 10.62,12.4 10.62,11.64A1.38,1.38 0 0,1 12,10.26A1.38,1.38 0 0,1 13.38,11.64Z" /></g><g id="black-mesa"><path d="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12C4,14.39 5.05,16.53 6.71,18H9V12H17L19.15,15.59C19.69,14.5 20,13.29 20,12A8,8 0 0,0 12,4Z" /></g><g id="blackberry"><path d="M5.45,10.28C6.4,10.28 7.5,11.05 7.5,12C7.5,12.95 6.4,13.72 5.45,13.72H2L2.69,10.28H5.45M6.14,4.76C7.09,4.76 8.21,5.53 8.21,6.5C8.21,7.43 7.09,8.21 6.14,8.21H2.69L3.38,4.76H6.14M13.03,4.76C14,4.76 15.1,5.53 15.1,6.5C15.1,7.43 14,8.21 13.03,8.21H9.41L10.1,4.76H13.03M12.34,10.28C13.3,10.28 14.41,11.05 14.41,12C14.41,12.95 13.3,13.72 12.34,13.72H8.72L9.41,10.28H12.34M10.97,15.79C11.92,15.79 13.03,16.57 13.03,17.5C13.03,18.47 11.92,19.24 10.97,19.24H7.5L8.21,15.79H10.97M18.55,13.72C19.5,13.72 20.62,14.5 20.62,15.45C20.62,16.4 19.5,17.17 18.55,17.17H15.1L15.79,13.72H18.55M19.93,8.21C20.88,8.21 22,9 22,9.93C22,10.88 20.88,11.66 19.93,11.66H16.5L17.17,8.21H19.93Z" /></g><g id="blender"><path d="M8,3C8,3.34 8.17,3.69 8.5,3.88L12,6H2.5A1.5,1.5 0 0,0 1,7.5A1.5,1.5 0 0,0 2.5,9H8.41L2,13C1.16,13.5 1,14.22 1,15C1,16 1.77,17 3,17C3.69,17 4.39,16.5 5,16L7,14.38C7.2,18.62 10.71,22 15,22A8,8 0 0,0 23,14C23,11.08 21.43,8.5 19.09,7.13C19.06,7.11 19.03,7.08 19,7.06C19,7.06 18.92,7 18.86,6.97C15.76,4.88 13.03,3.72 9.55,2.13C9.34,2.04 9.16,2 9,2C8.4,2 8,2.46 8,3M15,9A5,5 0 0,1 20,14A5,5 0 0,1 15,19A5,5 0 0,1 10,14A5,5 0 0,1 15,9M15,10.5A3.5,3.5 0 0,0 11.5,14A3.5,3.5 0 0,0 15,17.5A3.5,3.5 0 0,0 18.5,14A3.5,3.5 0 0,0 15,10.5Z" /></g><g id="blinds"><path d="M3,2H21A1,1 0 0,1 22,3V5A1,1 0 0,1 21,6H20V13A1,1 0 0,1 19,14H13V16.17C14.17,16.58 15,17.69 15,19A3,3 0 0,1 12,22A3,3 0 0,1 9,19C9,17.69 9.83,16.58 11,16.17V14H5A1,1 0 0,1 4,13V6H3A1,1 0 0,1 2,5V3A1,1 0 0,1 3,2M12,18A1,1 0 0,0 11,19A1,1 0 0,0 12,20A1,1 0 0,0 13,19A1,1 0 0,0 12,18Z" /></g><g id="block-helper"><path d="M12,0A12,12 0 0,1 24,12A12,12 0 0,1 12,24A12,12 0 0,1 0,12A12,12 0 0,1 12,0M12,2A10,10 0 0,0 2,12C2,14.4 2.85,16.6 4.26,18.33L18.33,4.26C16.6,2.85 14.4,2 12,2M12,22A10,10 0 0,0 22,12C22,9.6 21.15,7.4 19.74,5.67L5.67,19.74C7.4,21.15 9.6,22 12,22Z" /></g><g id="blogger"><path d="M14,13H9.95A1,1 0 0,0 8.95,14A1,1 0 0,0 9.95,15H14A1,1 0 0,0 15,14A1,1 0 0,0 14,13M9.95,10H12.55A1,1 0 0,0 13.55,9A1,1 0 0,0 12.55,8H9.95A1,1 0 0,0 8.95,9A1,1 0 0,0 9.95,10M16,9V10A1,1 0 0,0 17,11A1,1 0 0,1 18,12V15A3,3 0 0,1 15,18H9A3,3 0 0,1 6,15V8A3,3 0 0,1 9,5H13A3,3 0 0,1 16,8M20,2H4C2.89,2 2,2.89 2,4V20A2,2 0 0,0 4,22H20A2,2 0 0,0 22,20V4C22,2.89 21.1,2 20,2Z" /></g><g id="bluetooth"><path d="M14.88,16.29L13,18.17V14.41M13,5.83L14.88,7.71L13,9.58M17.71,7.71L12,2H11V9.58L6.41,5L5,6.41L10.59,12L5,17.58L6.41,19L11,14.41V22H12L17.71,16.29L13.41,12L17.71,7.71Z" /></g><g id="bluetooth-audio"><path d="M12.88,16.29L11,18.17V14.41M11,5.83L12.88,7.71L11,9.58M15.71,7.71L10,2H9V9.58L4.41,5L3,6.41L8.59,12L3,17.58L4.41,19L9,14.41V22H10L15.71,16.29L11.41,12M19.53,6.71L18.26,8C18.89,9.18 19.25,10.55 19.25,12C19.25,13.45 18.89,14.82 18.26,16L19.46,17.22C20.43,15.68 21,13.87 21,11.91C21,10 20.46,8.23 19.53,6.71M14.24,12L16.56,14.33C16.84,13.6 17,12.82 17,12C17,11.18 16.84,10.4 16.57,9.68L14.24,12Z" /></g><g id="bluetooth-connect"><path d="M19,10L17,12L19,14L21,12M14.88,16.29L13,18.17V14.41M13,5.83L14.88,7.71L13,9.58M17.71,7.71L12,2H11V9.58L6.41,5L5,6.41L10.59,12L5,17.58L6.41,19L11,14.41V22H12L17.71,16.29L13.41,12M7,12L5,10L3,12L5,14L7,12Z" /></g><g id="bluetooth-off"><path d="M13,5.83L14.88,7.71L13.28,9.31L14.69,10.72L17.71,7.7L12,2H11V7.03L13,9.03M5.41,4L4,5.41L10.59,12L5,17.59L6.41,19L11,14.41V22H12L16.29,17.71L18.59,20L20,18.59M13,18.17V14.41L14.88,16.29" /></g><g id="bluetooth-settings"><path d="M14.88,14.29L13,16.17V12.41L14.88,14.29M13,3.83L14.88,5.71L13,7.59M17.71,5.71L12,0H11V7.59L6.41,3L5,4.41L10.59,10L5,15.59L6.41,17L11,12.41V20H12L17.71,14.29L13.41,10L17.71,5.71M15,24H17V22H15M7,24H9V22H7M11,24H13V22H11V24Z" /></g><g id="bluetooth-transfer"><path d="M14.71,7.71L10.41,12L14.71,16.29L9,22H8V14.41L3.41,19L2,17.59L7.59,12L2,6.41L3.41,5L8,9.59V2H9L14.71,7.71M10,5.83V9.59L11.88,7.71L10,5.83M11.88,16.29L10,14.41V18.17L11.88,16.29M22,8H20V11H18V8H16L19,4L22,8M22,16L19,20L16,16H18V13H20V16H22Z" /></g><g id="blur"><path d="M14,8.5A1.5,1.5 0 0,0 12.5,10A1.5,1.5 0 0,0 14,11.5A1.5,1.5 0 0,0 15.5,10A1.5,1.5 0 0,0 14,8.5M14,12.5A1.5,1.5 0 0,0 12.5,14A1.5,1.5 0 0,0 14,15.5A1.5,1.5 0 0,0 15.5,14A1.5,1.5 0 0,0 14,12.5M10,17A1,1 0 0,0 9,18A1,1 0 0,0 10,19A1,1 0 0,0 11,18A1,1 0 0,0 10,17M10,8.5A1.5,1.5 0 0,0 8.5,10A1.5,1.5 0 0,0 10,11.5A1.5,1.5 0 0,0 11.5,10A1.5,1.5 0 0,0 10,8.5M14,20.5A0.5,0.5 0 0,0 13.5,21A0.5,0.5 0 0,0 14,21.5A0.5,0.5 0 0,0 14.5,21A0.5,0.5 0 0,0 14,20.5M14,17A1,1 0 0,0 13,18A1,1 0 0,0 14,19A1,1 0 0,0 15,18A1,1 0 0,0 14,17M21,13.5A0.5,0.5 0 0,0 20.5,14A0.5,0.5 0 0,0 21,14.5A0.5,0.5 0 0,0 21.5,14A0.5,0.5 0 0,0 21,13.5M18,5A1,1 0 0,0 17,6A1,1 0 0,0 18,7A1,1 0 0,0 19,6A1,1 0 0,0 18,5M18,9A1,1 0 0,0 17,10A1,1 0 0,0 18,11A1,1 0 0,0 19,10A1,1 0 0,0 18,9M18,17A1,1 0 0,0 17,18A1,1 0 0,0 18,19A1,1 0 0,0 19,18A1,1 0 0,0 18,17M18,13A1,1 0 0,0 17,14A1,1 0 0,0 18,15A1,1 0 0,0 19,14A1,1 0 0,0 18,13M10,12.5A1.5,1.5 0 0,0 8.5,14A1.5,1.5 0 0,0 10,15.5A1.5,1.5 0 0,0 11.5,14A1.5,1.5 0 0,0 10,12.5M10,7A1,1 0 0,0 11,6A1,1 0 0,0 10,5A1,1 0 0,0 9,6A1,1 0 0,0 10,7M10,3.5A0.5,0.5 0 0,0 10.5,3A0.5,0.5 0 0,0 10,2.5A0.5,0.5 0 0,0 9.5,3A0.5,0.5 0 0,0 10,3.5M10,20.5A0.5,0.5 0 0,0 9.5,21A0.5,0.5 0 0,0 10,21.5A0.5,0.5 0 0,0 10.5,21A0.5,0.5 0 0,0 10,20.5M3,13.5A0.5,0.5 0 0,0 2.5,14A0.5,0.5 0 0,0 3,14.5A0.5,0.5 0 0,0 3.5,14A0.5,0.5 0 0,0 3,13.5M14,3.5A0.5,0.5 0 0,0 14.5,3A0.5,0.5 0 0,0 14,2.5A0.5,0.5 0 0,0 13.5,3A0.5,0.5 0 0,0 14,3.5M14,7A1,1 0 0,0 15,6A1,1 0 0,0 14,5A1,1 0 0,0 13,6A1,1 0 0,0 14,7M21,10.5A0.5,0.5 0 0,0 21.5,10A0.5,0.5 0 0,0 21,9.5A0.5,0.5 0 0,0 20.5,10A0.5,0.5 0 0,0 21,10.5M6,5A1,1 0 0,0 5,6A1,1 0 0,0 6,7A1,1 0 0,0 7,6A1,1 0 0,0 6,5M3,9.5A0.5,0.5 0 0,0 2.5,10A0.5,0.5 0 0,0 3,10.5A0.5,0.5 0 0,0 3.5,10A0.5,0.5 0 0,0 3,9.5M6,9A1,1 0 0,0 5,10A1,1 0 0,0 6,11A1,1 0 0,0 7,10A1,1 0 0,0 6,9M6,17A1,1 0 0,0 5,18A1,1 0 0,0 6,19A1,1 0 0,0 7,18A1,1 0 0,0 6,17M6,13A1,1 0 0,0 5,14A1,1 0 0,0 6,15A1,1 0 0,0 7,14A1,1 0 0,0 6,13Z" /></g><g id="blur-linear"><path d="M13,17A1,1 0 0,0 14,16A1,1 0 0,0 13,15A1,1 0 0,0 12,16A1,1 0 0,0 13,17M13,13A1,1 0 0,0 14,12A1,1 0 0,0 13,11A1,1 0 0,0 12,12A1,1 0 0,0 13,13M13,9A1,1 0 0,0 14,8A1,1 0 0,0 13,7A1,1 0 0,0 12,8A1,1 0 0,0 13,9M17,12.5A0.5,0.5 0 0,0 17.5,12A0.5,0.5 0 0,0 17,11.5A0.5,0.5 0 0,0 16.5,12A0.5,0.5 0 0,0 17,12.5M17,8.5A0.5,0.5 0 0,0 17.5,8A0.5,0.5 0 0,0 17,7.5A0.5,0.5 0 0,0 16.5,8A0.5,0.5 0 0,0 17,8.5M3,3V5H21V3M17,16.5A0.5,0.5 0 0,0 17.5,16A0.5,0.5 0 0,0 17,15.5A0.5,0.5 0 0,0 16.5,16A0.5,0.5 0 0,0 17,16.5M9,17A1,1 0 0,0 10,16A1,1 0 0,0 9,15A1,1 0 0,0 8,16A1,1 0 0,0 9,17M5,13.5A1.5,1.5 0 0,0 6.5,12A1.5,1.5 0 0,0 5,10.5A1.5,1.5 0 0,0 3.5,12A1.5,1.5 0 0,0 5,13.5M5,9.5A1.5,1.5 0 0,0 6.5,8A1.5,1.5 0 0,0 5,6.5A1.5,1.5 0 0,0 3.5,8A1.5,1.5 0 0,0 5,9.5M3,21H21V19H3M9,9A1,1 0 0,0 10,8A1,1 0 0,0 9,7A1,1 0 0,0 8,8A1,1 0 0,0 9,9M9,13A1,1 0 0,0 10,12A1,1 0 0,0 9,11A1,1 0 0,0 8,12A1,1 0 0,0 9,13M5,17.5A1.5,1.5 0 0,0 6.5,16A1.5,1.5 0 0,0 5,14.5A1.5,1.5 0 0,0 3.5,16A1.5,1.5 0 0,0 5,17.5Z" /></g><g id="blur-off"><path d="M3,13.5A0.5,0.5 0 0,0 2.5,14A0.5,0.5 0 0,0 3,14.5A0.5,0.5 0 0,0 3.5,14A0.5,0.5 0 0,0 3,13.5M6,17A1,1 0 0,0 5,18A1,1 0 0,0 6,19A1,1 0 0,0 7,18A1,1 0 0,0 6,17M10,20.5A0.5,0.5 0 0,0 9.5,21A0.5,0.5 0 0,0 10,21.5A0.5,0.5 0 0,0 10.5,21A0.5,0.5 0 0,0 10,20.5M3,9.5A0.5,0.5 0 0,0 2.5,10A0.5,0.5 0 0,0 3,10.5A0.5,0.5 0 0,0 3.5,10A0.5,0.5 0 0,0 3,9.5M6,13A1,1 0 0,0 5,14A1,1 0 0,0 6,15A1,1 0 0,0 7,14A1,1 0 0,0 6,13M21,13.5A0.5,0.5 0 0,0 20.5,14A0.5,0.5 0 0,0 21,14.5A0.5,0.5 0 0,0 21.5,14A0.5,0.5 0 0,0 21,13.5M10,17A1,1 0 0,0 9,18A1,1 0 0,0 10,19A1,1 0 0,0 11,18A1,1 0 0,0 10,17M2.5,5.27L6.28,9.05L6,9A1,1 0 0,0 5,10A1,1 0 0,0 6,11A1,1 0 0,0 7,10C7,9.9 6.97,9.81 6.94,9.72L9.75,12.53C9.04,12.64 8.5,13.26 8.5,14A1.5,1.5 0 0,0 10,15.5C10.74,15.5 11.36,14.96 11.47,14.25L14.28,17.06C14.19,17.03 14.1,17 14,17A1,1 0 0,0 13,18A1,1 0 0,0 14,19A1,1 0 0,0 15,18C15,17.9 14.97,17.81 14.94,17.72L18.72,21.5L20,20.23L3.77,4L2.5,5.27M14,20.5A0.5,0.5 0 0,0 13.5,21A0.5,0.5 0 0,0 14,21.5A0.5,0.5 0 0,0 14.5,21A0.5,0.5 0 0,0 14,20.5M18,7A1,1 0 0,0 19,6A1,1 0 0,0 18,5A1,1 0 0,0 17,6A1,1 0 0,0 18,7M18,11A1,1 0 0,0 19,10A1,1 0 0,0 18,9A1,1 0 0,0 17,10A1,1 0 0,0 18,11M18,15A1,1 0 0,0 19,14A1,1 0 0,0 18,13A1,1 0 0,0 17,14A1,1 0 0,0 18,15M10,7A1,1 0 0,0 11,6A1,1 0 0,0 10,5A1,1 0 0,0 9,6A1,1 0 0,0 10,7M21,10.5A0.5,0.5 0 0,0 21.5,10A0.5,0.5 0 0,0 21,9.5A0.5,0.5 0 0,0 20.5,10A0.5,0.5 0 0,0 21,10.5M10,3.5A0.5,0.5 0 0,0 10.5,3A0.5,0.5 0 0,0 10,2.5A0.5,0.5 0 0,0 9.5,3A0.5,0.5 0 0,0 10,3.5M14,3.5A0.5,0.5 0 0,0 14.5,3A0.5,0.5 0 0,0 14,2.5A0.5,0.5 0 0,0 13.5,3A0.5,0.5 0 0,0 14,3.5M13.8,11.5H14A1.5,1.5 0 0,0 15.5,10A1.5,1.5 0 0,0 14,8.5A1.5,1.5 0 0,0 12.5,10V10.2C12.61,10.87 13.13,11.39 13.8,11.5M14,7A1,1 0 0,0 15,6A1,1 0 0,0 14,5A1,1 0 0,0 13,6A1,1 0 0,0 14,7Z" /></g><g id="blur-radial"><path d="M14,13A1,1 0 0,0 13,14A1,1 0 0,0 14,15A1,1 0 0,0 15,14A1,1 0 0,0 14,13M14,16.5A0.5,0.5 0 0,0 13.5,17A0.5,0.5 0 0,0 14,17.5A0.5,0.5 0 0,0 14.5,17A0.5,0.5 0 0,0 14,16.5M12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M17,9.5A0.5,0.5 0 0,0 16.5,10A0.5,0.5 0 0,0 17,10.5A0.5,0.5 0 0,0 17.5,10A0.5,0.5 0 0,0 17,9.5M17,13.5A0.5,0.5 0 0,0 16.5,14A0.5,0.5 0 0,0 17,14.5A0.5,0.5 0 0,0 17.5,14A0.5,0.5 0 0,0 17,13.5M14,7.5A0.5,0.5 0 0,0 14.5,7A0.5,0.5 0 0,0 14,6.5A0.5,0.5 0 0,0 13.5,7A0.5,0.5 0 0,0 14,7.5M14,9A1,1 0 0,0 13,10A1,1 0 0,0 14,11A1,1 0 0,0 15,10A1,1 0 0,0 14,9M10,7.5A0.5,0.5 0 0,0 10.5,7A0.5,0.5 0 0,0 10,6.5A0.5,0.5 0 0,0 9.5,7A0.5,0.5 0 0,0 10,7.5M7,13.5A0.5,0.5 0 0,0 6.5,14A0.5,0.5 0 0,0 7,14.5A0.5,0.5 0 0,0 7.5,14A0.5,0.5 0 0,0 7,13.5M10,16.5A0.5,0.5 0 0,0 9.5,17A0.5,0.5 0 0,0 10,17.5A0.5,0.5 0 0,0 10.5,17A0.5,0.5 0 0,0 10,16.5M7,9.5A0.5,0.5 0 0,0 6.5,10A0.5,0.5 0 0,0 7,10.5A0.5,0.5 0 0,0 7.5,10A0.5,0.5 0 0,0 7,9.5M10,13A1,1 0 0,0 9,14A1,1 0 0,0 10,15A1,1 0 0,0 11,14A1,1 0 0,0 10,13M10,9A1,1 0 0,0 9,10A1,1 0 0,0 10,11A1,1 0 0,0 11,10A1,1 0 0,0 10,9Z" /></g><g id="bomb"><path d="M11.25,6A3.25,3.25 0 0,1 14.5,2.75A3.25,3.25 0 0,1 17.75,6C17.75,6.42 18.08,6.75 18.5,6.75C18.92,6.75 19.25,6.42 19.25,6V5.25H20.75V6A2.25,2.25 0 0,1 18.5,8.25A2.25,2.25 0 0,1 16.25,6A1.75,1.75 0 0,0 14.5,4.25A1.75,1.75 0 0,0 12.75,6H14V7.29C16.89,8.15 19,10.83 19,14A7,7 0 0,1 12,21A7,7 0 0,1 5,14C5,10.83 7.11,8.15 10,7.29V6H11.25M22,6H24V7H22V6M19,4V2H20V4H19M20.91,4.38L22.33,2.96L23.04,3.67L21.62,5.09L20.91,4.38Z" /></g><g id="bone"><path d="M8,14A3,3 0 0,1 5,17A3,3 0 0,1 2,14C2,13.23 2.29,12.53 2.76,12C2.29,11.47 2,10.77 2,10A3,3 0 0,1 5,7A3,3 0 0,1 8,10C9.33,10.08 10.67,10.17 12,10.17C13.33,10.17 14.67,10.08 16,10A3,3 0 0,1 19,7A3,3 0 0,1 22,10C22,10.77 21.71,11.47 21.24,12C21.71,12.53 22,13.23 22,14A3,3 0 0,1 19,17A3,3 0 0,1 16,14C14.67,13.92 13.33,13.83 12,13.83C10.67,13.83 9.33,13.92 8,14Z" /></g><g id="book"><path d="M18,22A2,2 0 0,0 20,20V4C20,2.89 19.1,2 18,2H12V9L9.5,7.5L7,9V2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18Z" /></g><g id="book-minus"><path d="M18,22H6A2,2 0 0,1 4,20V4C4,2.89 4.9,2 6,2H7V9L9.5,7.5L12,9V2H18A2,2 0 0,1 20,4V20A2,2 0 0,1 18,22M18,18V16H12V18H18Z" /></g><g id="book-multiple"><path d="M19,18H9A2,2 0 0,1 7,16V4A2,2 0 0,1 9,2H10V7L12,5.5L14,7V2H19A2,2 0 0,1 21,4V16A2,2 0 0,1 19,18M17,20V22H5A2,2 0 0,1 3,20V6H5V20H17Z" /></g><g id="book-multiple-variant"><path d="M19,18H9A2,2 0 0,1 7,16V4A2,2 0 0,1 9,2H19A2,2 0 0,1 21,4V16A2,2 0 0,1 19,18M10,9L12,7.5L14,9V4H10V9M17,20V22H5A2,2 0 0,1 3,20V6H5V20H17Z" /></g><g id="book-open"><path d="M13,12H20V13.5H13M13,9.5H20V11H13M13,14.5H20V16H13M21,4H3A2,2 0 0,0 1,6V19A2,2 0 0,0 3,21H21A2,2 0 0,0 23,19V6A2,2 0 0,0 21,4M21,19H12V6H21" /></g><g id="book-open-page-variant"><path d="M19,2L14,6.5V17.5L19,13V2M6.5,5C4.55,5 2.45,5.4 1,6.5V21.16C1,21.41 1.25,21.66 1.5,21.66C1.6,21.66 1.65,21.59 1.75,21.59C3.1,20.94 5.05,20.5 6.5,20.5C8.45,20.5 10.55,20.9 12,22C13.35,21.15 15.8,20.5 17.5,20.5C19.15,20.5 20.85,20.81 22.25,21.56C22.35,21.61 22.4,21.59 22.5,21.59C22.75,21.59 23,21.34 23,21.09V6.5C22.4,6.05 21.75,5.75 21,5.5V7.5L21,13V19C19.9,18.65 18.7,18.5 17.5,18.5C15.8,18.5 13.35,19.15 12,20V13L12,8.5V6.5C10.55,5.4 8.45,5 6.5,5V5Z" /></g><g id="book-open-variant"><path d="M21,5C19.89,4.65 18.67,4.5 17.5,4.5C15.55,4.5 13.45,4.9 12,6C10.55,4.9 8.45,4.5 6.5,4.5C4.55,4.5 2.45,4.9 1,6V20.65C1,20.9 1.25,21.15 1.5,21.15C1.6,21.15 1.65,21.1 1.75,21.1C3.1,20.45 5.05,20 6.5,20C8.45,20 10.55,20.4 12,21.5C13.35,20.65 15.8,20 17.5,20C19.15,20 20.85,20.3 22.25,21.05C22.35,21.1 22.4,21.1 22.5,21.1C22.75,21.1 23,20.85 23,20.6V6C22.4,5.55 21.75,5.25 21,5M21,18.5C19.9,18.15 18.7,18 17.5,18C15.8,18 13.35,18.65 12,19.5V8C13.35,7.15 15.8,6.5 17.5,6.5C18.7,6.5 19.9,6.65 21,7V18.5Z" /></g><g id="book-plus"><path d="M18,22H6A2,2 0 0,1 4,20V4C4,2.89 4.9,2 6,2H7V9L9.5,7.5L12,9V2H18A2,2 0 0,1 20,4V20A2,2 0 0,1 18,22M14,20H16V18H18V16H16V14H14V16H12V18H14V20Z" /></g><g id="book-variant"><path d="M6,4H11V12L8.5,10.5L6,12M18,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V4A2,2 0 0,0 18,2Z" /></g><g id="bookmark"><path d="M17,3H7A2,2 0 0,0 5,5V21L12,18L19,21V5C19,3.89 18.1,3 17,3Z" /></g><g id="bookmark-check"><path d="M17,3A2,2 0 0,1 19,5V21L12,18L5,21V5C5,3.89 5.9,3 7,3H17M11,14L17.25,7.76L15.84,6.34L11,11.18L8.41,8.59L7,10L11,14Z" /></g><g id="bookmark-music"><path d="M17,3A2,2 0 0,1 19,5V21L12,18L5,21V5C5,3.89 5.9,3 7,3H17M11,11A2,2 0 0,0 9,13A2,2 0 0,0 11,15A2,2 0 0,0 13,13V8H16V6H12V11.27C11.71,11.1 11.36,11 11,11Z" /></g><g id="bookmark-outline"><path d="M17,18L12,15.82L7,18V5H17M17,3H7A2,2 0 0,0 5,5V21L12,18L19,21V5C19,3.89 18.1,3 17,3Z" /></g><g id="bookmark-plus"><path d="M17,3A2,2 0 0,1 19,5V21L12,18L5,21V5C5,3.89 5.9,3 7,3H17M11,7V9H9V11H11V13H13V11H15V9H13V7H11Z" /></g><g id="bookmark-plus-outline"><path d="M17,18V5H7V18L12,15.82L17,18M17,3A2,2 0 0,1 19,5V21L12,18L5,21V5C5,3.89 5.9,3 7,3H17M11,7H13V9H15V11H13V13H11V11H9V9H11V7Z" /></g><g id="bookmark-remove"><path d="M17,3A2,2 0 0,1 19,5V21L12,18L5,21V5C5,3.89 5.9,3 7,3H17M8.17,8.58L10.59,11L8.17,13.41L9.59,14.83L12,12.41L14.41,14.83L15.83,13.41L13.41,11L15.83,8.58L14.41,7.17L12,9.58L9.59,7.17L8.17,8.58Z" /></g><g id="boombox"><path d="M7,5L5,7V8H3A1,1 0 0,0 2,9V17A1,1 0 0,0 3,18H21A1,1 0 0,0 22,17V9A1,1 0 0,0 21,8H19V7L17,5H7M7,7H17V8H7V7M11,9H13A0.5,0.5 0 0,1 13.5,9.5A0.5,0.5 0 0,1 13,10H11A0.5,0.5 0 0,1 10.5,9.5A0.5,0.5 0 0,1 11,9M7.5,10.5A3,3 0 0,1 10.5,13.5A3,3 0 0,1 7.5,16.5A3,3 0 0,1 4.5,13.5A3,3 0 0,1 7.5,10.5M16.5,10.5A3,3 0 0,1 19.5,13.5A3,3 0 0,1 16.5,16.5A3,3 0 0,1 13.5,13.5A3,3 0 0,1 16.5,10.5M7.5,12A1.5,1.5 0 0,0 6,13.5A1.5,1.5 0 0,0 7.5,15A1.5,1.5 0 0,0 9,13.5A1.5,1.5 0 0,0 7.5,12M16.5,12A1.5,1.5 0 0,0 15,13.5A1.5,1.5 0 0,0 16.5,15A1.5,1.5 0 0,0 18,13.5A1.5,1.5 0 0,0 16.5,12Z" /></g><g id="border-all"><path d="M19,11H13V5H19M19,19H13V13H19M11,11H5V5H11M11,19H5V13H11M3,21H21V3H3V21Z" /></g><g id="border-bottom"><path d="M5,15H3V17H5M3,21H21V19H3M5,11H3V13H5M19,9H21V7H19M19,5H21V3H19M5,7H3V9H5M19,17H21V15H19M19,13H21V11H19M17,3H15V5H17M13,3H11V5H13M17,11H15V13H17M13,7H11V9H13M5,3H3V5H5M13,11H11V13H13M9,3H7V5H9M13,15H11V17H13M9,11H7V13H9V11Z" /></g><g id="border-color"><path d="M20.71,4.04C21.1,3.65 21.1,3 20.71,2.63L18.37,0.29C18,-0.1 17.35,-0.1 16.96,0.29L15,2.25L18.75,6M17.75,7L14,3.25L4,13.25V17H7.75L17.75,7Z" /></g><g id="border-horizontal"><path d="M19,21H21V19H19M15,21H17V19H15M11,17H13V15H11M19,9H21V7H19M19,5H21V3H19M3,13H21V11H3M11,21H13V19H11M19,17H21V15H19M13,3H11V5H13M13,7H11V9H13M17,3H15V5H17M9,3H7V5H9M5,3H3V5H5M7,21H9V19H7M3,17H5V15H3M5,7H3V9H5M3,21H5V19H3V21Z" /></g><g id="border-inside"><path d="M19,17H21V15H19M19,21H21V19H19M13,3H11V11H3V13H11V21H13V13H21V11H13M15,21H17V19H15M19,5H21V3H19M19,9H21V7H19M17,3H15V5H17M5,3H3V5H5M9,3H7V5H9M3,17H5V15H3M5,7H3V9H5M7,21H9V19H7M3,21H5V19H3V21Z" /></g><g id="border-left"><path d="M15,5H17V3H15M15,13H17V11H15M19,21H21V19H19M19,13H21V11H19M19,5H21V3H19M19,17H21V15H19M15,21H17V19H15M19,9H21V7H19M3,21H5V3H3M7,13H9V11H7M7,5H9V3H7M7,21H9V19H7M11,13H13V11H11M11,9H13V7H11M11,5H13V3H11M11,17H13V15H11M11,21H13V19H11V21Z" /></g><g id="border-none"><path d="M15,5H17V3H15M15,13H17V11H15M15,21H17V19H15M11,5H13V3H11M19,5H21V3H19M11,9H13V7H11M19,9H21V7H19M19,21H21V19H19M19,13H21V11H19M19,17H21V15H19M11,13H13V11H11M3,5H5V3H3M3,9H5V7H3M3,13H5V11H3M3,17H5V15H3M3,21H5V19H3M11,21H13V19H11M11,17H13V15H11M7,21H9V19H7M7,13H9V11H7M7,5H9V3H7V5Z" /></g><g id="border-outside"><path d="M9,11H7V13H9M13,15H11V17H13M19,19H5V5H19M3,21H21V3H3M17,11H15V13H17M13,11H11V13H13M13,7H11V9H13V7Z" /></g><g id="border-right"><path d="M11,9H13V7H11M11,5H13V3H11M11,13H13V11H11M15,5H17V3H15M15,21H17V19H15M19,21H21V3H19M15,13H17V11H15M11,17H13V15H11M3,9H5V7H3M3,17H5V15H3M3,13H5V11H3M11,21H13V19H11M3,21H5V19H3M7,13H9V11H7M7,5H9V3H7M3,5H5V3H3M7,21H9V19H7V21Z" /></g><g id="border-style"><path d="M15,21H17V19H15M19,21H21V19H19M7,21H9V19H7M11,21H13V19H11M19,17H21V15H19M19,13H21V11H19M3,3V21H5V5H21V3M19,9H21V7H19" /></g><g id="border-top"><path d="M15,13H17V11H15M19,21H21V19H19M11,9H13V7H11M15,21H17V19H15M19,17H21V15H19M3,5H21V3H3M19,13H21V11H19M19,9H21V7H19M11,17H13V15H11M3,9H5V7H3M3,13H5V11H3M3,21H5V19H3M3,17H5V15H3M11,21H13V19H11M11,13H13V11H11M7,13H9V11H7M7,21H9V19H7V21Z" /></g><g id="border-vertical"><path d="M15,13H17V11H15M15,21H17V19H15M15,5H17V3H15M19,9H21V7H19M19,5H21V3H19M19,13H21V11H19M19,21H21V19H19M11,21H13V3H11M19,17H21V15H19M7,5H9V3H7M3,17H5V15H3M3,21H5V19H3M3,13H5V11H3M7,13H9V11H7M7,21H9V19H7M3,5H5V3H3M3,9H5V7H3V9Z" /></g><g id="bow-tie"><path d="M15,14L21,17V7L15,10V14M9,14L3,17V7L9,10V14M10,10H14V14H10V10Z" /></g><g id="bowl"><path d="M22,15A7,7 0 0,1 15,22H9A7,7 0 0,1 2,15V12H15.58L20.3,4.44L22,5.5L17.94,12H22V15Z" /></g><g id="bowling"><path d="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12.5,11A1.5,1.5 0 0,0 11,12.5A1.5,1.5 0 0,0 12.5,14A1.5,1.5 0 0,0 14,12.5A1.5,1.5 0 0,0 12.5,11M12,5A2,2 0 0,0 10,7A2,2 0 0,0 12,9A2,2 0 0,0 14,7A2,2 0 0,0 12,5M5.93,8.5C5.38,9.45 5.71,10.67 6.66,11.22C7.62,11.78 8.84,11.45 9.4,10.5C9.95,9.53 9.62,8.31 8.66,7.76C7.71,7.21 6.5,7.53 5.93,8.5Z" /></g><g id="box"><path d="M15.39,14.04V14.04C15.39,12.62 14.24,11.47 12.82,11.47C11.41,11.47 10.26,12.62 10.26,14.04V14.04C10.26,15.45 11.41,16.6 12.82,16.6C14.24,16.6 15.39,15.45 15.39,14.04M17.1,14.04C17.1,16.4 15.18,18.31 12.82,18.31C11.19,18.31 9.77,17.39 9.05,16.04C8.33,17.39 6.91,18.31 5.28,18.31C2.94,18.31 1.04,16.43 1,14.11V14.11H1V7H1V7C1,6.56 1.39,6.18 1.86,6.18C2.33,6.18 2.7,6.56 2.71,7V7H2.71V10.62C3.43,10.08 4.32,9.76 5.28,9.76C6.91,9.76 8.33,10.68 9.05,12.03C9.77,10.68 11.19,9.76 12.82,9.76C15.18,9.76 17.1,11.68 17.1,14.04V14.04M7.84,14.04V14.04C7.84,12.62 6.69,11.47 5.28,11.47C3.86,11.47 2.71,12.62 2.71,14.04V14.04C2.71,15.45 3.86,16.6 5.28,16.6C6.69,16.6 7.84,15.45 7.84,14.04M22.84,16.96V16.96C22.95,17.12 23,17.3 23,17.47C23,17.73 22.88,18 22.66,18.15C22.5,18.26 22.33,18.32 22.15,18.32C21.9,18.32 21.65,18.21 21.5,18L19.59,15.47L17.7,18V18C17.53,18.21 17.28,18.32 17.03,18.32C16.85,18.32 16.67,18.26 16.5,18.15C16.29,18 16.17,17.72 16.17,17.46C16.17,17.29 16.23,17.11 16.33,16.96V16.96H16.33V16.96L18.5,14.04L16.33,11.11V11.11H16.33V11.11C16.22,10.96 16.17,10.79 16.17,10.61C16.17,10.35 16.29,10.1 16.5,9.93C16.89,9.65 17.41,9.72 17.7,10.09V10.09L19.59,12.61L21.5,10.09C21.76,9.72 22.29,9.65 22.66,9.93C22.89,10.1 23,10.36 23,10.63C23,10.8 22.95,10.97 22.84,11.11V11.11H22.84V11.11L20.66,14.04L22.84,16.96V16.96H22.84Z" /></g><g id="box-cutter"><path d="M7.22,11.91C6.89,12.24 6.71,12.65 6.66,13.08L12.17,15.44L20.66,6.96C21.44,6.17 21.44,4.91 20.66,4.13L19.24,2.71C18.46,1.93 17.2,1.93 16.41,2.71L7.22,11.91M5,16V21.75L10.81,16.53L5.81,14.53L5,16M17.12,4.83C17.5,4.44 18.15,4.44 18.54,4.83C18.93,5.23 18.93,5.86 18.54,6.25C18.15,6.64 17.5,6.64 17.12,6.25C16.73,5.86 16.73,5.23 17.12,4.83Z" /></g><g id="box-shadow"><path d="M3,3H18V18H3V3M19,19H21V21H19V19M19,16H21V18H19V16M19,13H21V15H19V13M19,10H21V12H19V10M19,7H21V9H19V7M16,19H18V21H16V19M13,19H15V21H13V19M10,19H12V21H10V19M7,19H9V21H7V19Z" /></g><g id="bridge"><path d="M7,14V10.91C6.28,10.58 5.61,10.18 5,9.71V14H7M5,18H3V16H1V14H3V7H5V8.43C6.8,10 9.27,11 12,11C14.73,11 17.2,10 19,8.43V7H21V14H23V16H21V18H19V16H5V18M17,10.91V14H19V9.71C18.39,10.18 17.72,10.58 17,10.91M16,14V11.32C15.36,11.55 14.69,11.72 14,11.84V14H16M13,14V11.96L12,12L11,11.96V14H13M10,14V11.84C9.31,11.72 8.64,11.55 8,11.32V14H10Z" /></g><g id="briefcase"><path d="M14,6H10V4H14M20,6H16V4L14,2H10L8,4V6H4C2.89,6 2,6.89 2,8V19A2,2 0 0,0 4,21H20A2,2 0 0,0 22,19V8C22,6.89 21.1,6 20,6Z" /></g><g id="briefcase-check"><path d="M10.5,17.5L7,14L8.41,12.59L10.5,14.67L15.68,9.5L17.09,10.91M10,4H14V6H10M20,6H16V4L14,2H10L8,4V6H4C2.89,6 2,6.89 2,8V19C2,20.11 2.89,21 4,21H20C21.11,21 22,20.11 22,19V8C22,6.89 21.11,6 20,6Z" /></g><g id="briefcase-download"><path d="M12,19L7,14H10V10H14V14H17M10,4H14V6H10M20,6H16V4L14,2H10L8,4V6H4C2.89,6 2,6.89 2,8V19A2,2 0 0,0 4,21H20A2,2 0 0,0 22,19V8C22,6.89 21.1,6 20,6Z" /></g><g id="briefcase-upload"><path d="M20,6A2,2 0 0,1 22,8V19A2,2 0 0,1 20,21H4C2.89,21 2,20.1 2,19V8C2,6.89 2.89,6 4,6H8V4L10,2H14L16,4V6H20M10,4V6H14V4H10M12,9L7,14H10V18H14V14H17L12,9Z" /></g><g id="brightness-1"><path d="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z" /></g><g id="brightness-2"><path d="M10,2C8.18,2 6.47,2.5 5,3.35C8,5.08 10,8.3 10,12C10,15.7 8,18.92 5,20.65C6.47,21.5 8.18,22 10,22A10,10 0 0,0 20,12A10,10 0 0,0 10,2Z" /></g><g id="brightness-3"><path d="M9,2C7.95,2 6.95,2.16 6,2.46C10.06,3.73 13,7.5 13,12C13,16.5 10.06,20.27 6,21.54C6.95,21.84 7.95,22 9,22A10,10 0 0,0 19,12A10,10 0 0,0 9,2Z" /></g><g id="brightness-4"><path d="M12,18C11.11,18 10.26,17.8 9.5,17.45C11.56,16.5 13,14.42 13,12C13,9.58 11.56,7.5 9.5,6.55C10.26,6.2 11.11,6 12,6A6,6 0 0,1 18,12A6,6 0 0,1 12,18M20,8.69V4H15.31L12,0.69L8.69,4H4V8.69L0.69,12L4,15.31V20H8.69L12,23.31L15.31,20H20V15.31L23.31,12L20,8.69Z" /></g><g id="brightness-5"><path d="M12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6A6,6 0 0,1 18,12A6,6 0 0,1 12,18M20,15.31L23.31,12L20,8.69V4H15.31L12,0.69L8.69,4H4V8.69L0.69,12L4,15.31V20H8.69L12,23.31L15.31,20H20V15.31Z" /></g><g id="brightness-6"><path d="M12,18V6A6,6 0 0,1 18,12A6,6 0 0,1 12,18M20,15.31L23.31,12L20,8.69V4H15.31L12,0.69L8.69,4H4V8.69L0.69,12L4,15.31V20H8.69L12,23.31L15.31,20H20V15.31Z" /></g><g id="brightness-7"><path d="M12,8A4,4 0 0,0 8,12A4,4 0 0,0 12,16A4,4 0 0,0 16,12A4,4 0 0,0 12,8M12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6A6,6 0 0,1 18,12A6,6 0 0,1 12,18M20,8.69V4H15.31L12,0.69L8.69,4H4V8.69L0.69,12L4,15.31V20H8.69L12,23.31L15.31,20H20V15.31L23.31,12L20,8.69Z" /></g><g id="brightness-auto"><path d="M14.3,16L13.6,14H10.4L9.7,16H7.8L11,7H13L16.2,16H14.3M20,8.69V4H15.31L12,0.69L8.69,4H4V8.69L0.69,12L4,15.31V20H8.69L12,23.31L15.31,20H20V15.31L23.31,12L20,8.69M10.85,12.65H13.15L12,9L10.85,12.65Z" /></g><g id="broom"><path d="M19.36,2.72L20.78,4.14L15.06,9.85C16.13,11.39 16.28,13.24 15.38,14.44L9.06,8.12C10.26,7.22 12.11,7.37 13.65,8.44L19.36,2.72M5.93,17.57C3.92,15.56 2.69,13.16 2.35,10.92L7.23,8.83L14.67,16.27L12.58,21.15C10.34,20.81 7.94,19.58 5.93,17.57Z" /></g><g id="brush"><path d="M20.71,4.63L19.37,3.29C19,2.9 18.35,2.9 17.96,3.29L9,12.25L11.75,15L20.71,6.04C21.1,5.65 21.1,5 20.71,4.63M7,14A3,3 0 0,0 4,17C4,18.31 2.84,19 2,19C2.92,20.22 4.5,21 6,21A4,4 0 0,0 10,17A3,3 0 0,0 7,14Z" /></g><g id="buffer"><path d="M12.6,2.86C15.27,4.1 18,5.39 20.66,6.63C20.81,6.7 21,6.75 21,6.95C21,7.15 20.81,7.19 20.66,7.26C18,8.5 15.3,9.77 12.62,11C12.21,11.21 11.79,11.21 11.38,11C8.69,9.76 6,8.5 3.32,7.25C3.18,7.19 3,7.14 3,6.94C3,6.76 3.18,6.71 3.31,6.65C6,5.39 8.74,4.1 11.44,2.85C11.73,2.72 12.3,2.73 12.6,2.86M12,21.15C11.8,21.15 11.66,21.07 11.38,20.97C8.69,19.73 6,18.47 3.33,17.22C3.19,17.15 3,17.11 3,16.9C3,16.7 3.19,16.66 3.34,16.59C3.78,16.38 4.23,16.17 4.67,15.96C5.12,15.76 5.56,15.76 6,15.97C7.79,16.8 9.57,17.63 11.35,18.46C11.79,18.67 12.23,18.66 12.67,18.46C14.45,17.62 16.23,16.79 18,15.96C18.44,15.76 18.87,15.75 19.29,15.95C19.77,16.16 20.24,16.39 20.71,16.61C20.78,16.64 20.85,16.68 20.91,16.73C21.04,16.83 21.04,17 20.91,17.08C20.83,17.14 20.74,17.19 20.65,17.23C18,18.5 15.33,19.72 12.66,20.95C12.46,21.05 12.19,21.15 12,21.15M12,16.17C11.9,16.17 11.55,16.07 11.36,16C8.68,14.74 6,13.5 3.34,12.24C3.2,12.18 3,12.13 3,11.93C3,11.72 3.2,11.68 3.35,11.61C3.8,11.39 4.25,11.18 4.7,10.97C5.13,10.78 5.56,10.78 6,11C7.78,11.82 9.58,12.66 11.38,13.5C11.79,13.69 12.21,13.69 12.63,13.5C14.43,12.65 16.23,11.81 18.04,10.97C18.45,10.78 18.87,10.78 19.29,10.97C19.76,11.19 20.24,11.41 20.71,11.63C20.77,11.66 20.84,11.69 20.9,11.74C21.04,11.85 21.04,12 20.89,12.12C20.84,12.16 20.77,12.19 20.71,12.22C18,13.5 15.31,14.75 12.61,16C12.42,16.09 12.08,16.17 12,16.17Z" /></g><g id="bug"><path d="M14,12H10V10H14M14,16H10V14H14M20,8H17.19C16.74,7.22 16.12,6.55 15.37,6.04L17,4.41L15.59,3L13.42,5.17C12.96,5.06 12.5,5 12,5C11.5,5 11.04,5.06 10.59,5.17L8.41,3L7,4.41L8.62,6.04C7.88,6.55 7.26,7.22 6.81,8H4V10H6.09C6.04,10.33 6,10.66 6,11V12H4V14H6V15C6,15.34 6.04,15.67 6.09,16H4V18H6.81C7.85,19.79 9.78,21 12,21C14.22,21 16.15,19.79 17.19,18H20V16H17.91C17.96,15.67 18,15.34 18,15V14H20V12H18V11C18,10.66 17.96,10.33 17.91,10H20V8Z" /></g><g id="bulletin-board"><path d="M12.04,2.5L9.53,5H14.53L12.04,2.5M4,7V20H20V7H4M12,0L17,5V5H20A2,2 0 0,1 22,7V20A2,2 0 0,1 20,22H4A2,2 0 0,1 2,20V7A2,2 0 0,1 4,5H7V5L12,0M7,18V14H12V18H7M14,17V10H18V17H14M6,12V9H11V12H6Z" /></g><g id="bullhorn"><path d="M16,12V16A1,1 0 0,1 15,17C14.83,17 14.67,17 14.06,16.5C13.44,16 12.39,15 11.31,14.5C10.31,14.04 9.28,14 8.26,14L9.47,17.32L9.5,17.5A0.5,0.5 0 0,1 9,18H7C6.78,18 6.59,17.86 6.53,17.66L5.19,14H5A1,1 0 0,1 4,13A2,2 0 0,1 2,11A2,2 0 0,1 4,9A1,1 0 0,1 5,8H8C9.11,8 10.22,8 11.31,7.5C12.39,7 13.44,6 14.06,5.5C14.67,5 14.83,5 15,5A1,1 0 0,1 16,6V10A1,1 0 0,1 17,11A1,1 0 0,1 16,12M21,11C21,12.38 20.44,13.63 19.54,14.54L18.12,13.12C18.66,12.58 19,11.83 19,11C19,10.17 18.66,9.42 18.12,8.88L19.54,7.46C20.44,8.37 21,9.62 21,11Z" /></g><g id="bullseye"><path d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4M12,6A6,6 0 0,0 6,12A6,6 0 0,0 12,18A6,6 0 0,0 18,12A6,6 0 0,0 12,6M12,8A4,4 0 0,1 16,12A4,4 0 0,1 12,16A4,4 0 0,1 8,12A4,4 0 0,1 12,8M12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12A2,2 0 0,0 12,10Z" /></g><g id="burst-mode"><path d="M1,5H3V19H1V5M5,5H7V19H5V5M22,5H10A1,1 0 0,0 9,6V18A1,1 0 0,0 10,19H22A1,1 0 0,0 23,18V6A1,1 0 0,0 22,5M11,17L13.5,13.85L15.29,16L17.79,12.78L21,17H11Z" /></g><g id="bus"><path d="M18,11H6V6H18M16.5,17A1.5,1.5 0 0,1 15,15.5A1.5,1.5 0 0,1 16.5,14A1.5,1.5 0 0,1 18,15.5A1.5,1.5 0 0,1 16.5,17M7.5,17A1.5,1.5 0 0,1 6,15.5A1.5,1.5 0 0,1 7.5,14A1.5,1.5 0 0,1 9,15.5A1.5,1.5 0 0,1 7.5,17M4,16C4,16.88 4.39,17.67 5,18.22V20A1,1 0 0,0 6,21H7A1,1 0 0,0 8,20V19H16V20A1,1 0 0,0 17,21H18A1,1 0 0,0 19,20V18.22C19.61,17.67 20,16.88 20,16V6C20,2.5 16.42,2 12,2C7.58,2 4,2.5 4,6V16Z" /></g><g id="cached"><path d="M19,8L15,12H18A6,6 0 0,1 12,18C11,18 10.03,17.75 9.2,17.3L7.74,18.76C8.97,19.54 10.43,20 12,20A8,8 0 0,0 20,12H23M6,12A6,6 0 0,1 12,6C13,6 13.97,6.25 14.8,6.7L16.26,5.24C15.03,4.46 13.57,4 12,4A8,8 0 0,0 4,12H1L5,16L9,12" /></g><g id="cake"><path d="M11.5,0.5C12,0.75 13,2.4 13,3.5C13,4.6 12.33,5 11.5,5C10.67,5 10,4.85 10,3.75C10,2.65 11,2 11.5,0.5M18.5,9C21,9 23,11 23,13.5C23,15.06 22.21,16.43 21,17.24V23H12L3,23V17.24C1.79,16.43 1,15.06 1,13.5C1,11 3,9 5.5,9H10V6H13V9H18.5M12,16A2.5,2.5 0 0,0 14.5,13.5H16A2.5,2.5 0 0,0 18.5,16A2.5,2.5 0 0,0 21,13.5A2.5,2.5 0 0,0 18.5,11H5.5A2.5,2.5 0 0,0 3,13.5A2.5,2.5 0 0,0 5.5,16A2.5,2.5 0 0,0 8,13.5H9.5A2.5,2.5 0 0,0 12,16Z" /></g><g id="cake-layered"><path d="M21,21V17C21,15.89 20.1,15 19,15H18V12C18,10.89 17.1,10 16,10H13V8H11V10H8C6.89,10 6,10.89 6,12V15H5C3.89,15 3,15.89 3,17V21H1V23H23V21M12,7A2,2 0 0,0 14,5C14,4.62 13.9,4.27 13.71,3.97L12,1L10.28,3.97C10.1,4.27 10,4.62 10,5A2,2 0 0,0 12,7Z" /></g><g id="cake-variant"><path d="M12,6C13.11,6 14,5.1 14,4C14,3.62 13.9,3.27 13.71,2.97L12,0L10.29,2.97C10.1,3.27 10,3.62 10,4A2,2 0 0,0 12,6M16.6,16L15.53,14.92L14.45,16C13.15,17.29 10.87,17.3 9.56,16L8.5,14.92L7.4,16C6.75,16.64 5.88,17 4.96,17C4.23,17 3.56,16.77 3,16.39V21A1,1 0 0,0 4,22H20A1,1 0 0,0 21,21V16.39C20.44,16.77 19.77,17 19.04,17C18.12,17 17.25,16.64 16.6,16M18,9H13V7H11V9H6A3,3 0 0,0 3,12V13.54C3,14.62 3.88,15.5 4.96,15.5C5.5,15.5 6,15.3 6.34,14.93L8.5,12.8L10.61,14.93C11.35,15.67 12.64,15.67 13.38,14.93L15.5,12.8L17.65,14.93C18,15.3 18.5,15.5 19.03,15.5C20.11,15.5 21,14.62 21,13.54V12A3,3 0 0,0 18,9Z" /></g><g id="calculator"><path d="M7,2H17A2,2 0 0,1 19,4V20A2,2 0 0,1 17,22H7A2,2 0 0,1 5,20V4A2,2 0 0,1 7,2M7,4V8H17V4H7M7,10V12H9V10H7M11,10V12H13V10H11M15,10V12H17V10H15M7,14V16H9V14H7M11,14V16H13V14H11M15,14V16H17V14H15M7,18V20H9V18H7M11,18V20H13V18H11M15,18V20H17V18H15Z" /></g><g id="calendar"><path d="M19,19H5V8H19M16,1V3H8V1H6V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3H18V1M17,12H12V17H17V12Z" /></g><g id="calendar-blank"><path d="M19,19H5V8H19M16,1V3H8V1H6V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3H18V1" /></g><g id="calendar-check"><path d="M19,19H5V8H19M19,3H18V1H16V3H8V1H6V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3M16.53,11.06L15.47,10L10.59,14.88L8.47,12.76L7.41,13.82L10.59,17L16.53,11.06Z" /></g><g id="calendar-clock"><path d="M15,13H16.5V15.82L18.94,17.23L18.19,18.53L15,16.69V13M19,8H5V19H9.67C9.24,18.09 9,17.07 9,16A7,7 0 0,1 16,9C17.07,9 18.09,9.24 19,9.67V8M5,21C3.89,21 3,20.1 3,19V5C3,3.89 3.89,3 5,3H6V1H8V3H16V1H18V3H19A2,2 0 0,1 21,5V11.1C22.24,12.36 23,14.09 23,16A7,7 0 0,1 16,23C14.09,23 12.36,22.24 11.1,21H5M16,11.15A4.85,4.85 0 0,0 11.15,16C11.15,18.68 13.32,20.85 16,20.85A4.85,4.85 0 0,0 20.85,16C20.85,13.32 18.68,11.15 16,11.15Z" /></g><g id="calendar-multiple"><path d="M21,17V8H7V17H21M21,3A2,2 0 0,1 23,5V17A2,2 0 0,1 21,19H7C5.89,19 5,18.1 5,17V5A2,2 0 0,1 7,3H8V1H10V3H18V1H20V3H21M3,21H17V23H3C1.89,23 1,22.1 1,21V9H3V21M19,15H15V11H19V15Z" /></g><g id="calendar-multiple-check"><path d="M21,17V8H7V17H21M21,3A2,2 0 0,1 23,5V17A2,2 0 0,1 21,19H7C5.89,19 5,18.1 5,17V5A2,2 0 0,1 7,3H8V1H10V3H18V1H20V3H21M17.53,11.06L13.09,15.5L10.41,12.82L11.47,11.76L13.09,13.38L16.47,10L17.53,11.06M3,21H17V23H3C1.89,23 1,22.1 1,21V9H3V21Z" /></g><g id="calendar-plus"><path d="M19,19V7H5V19H19M16,1H18V3H19A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5C3.89,21 3,20.1 3,19V5C3,3.89 3.89,3 5,3H6V1H8V3H16V1M11,9H13V12H16V14H13V17H11V14H8V12H11V9Z" /></g><g id="calendar-question"><path d="M6,1V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3H18V1H16V3H8V1H6M5,8H19V19H5V8M12.19,9C11.32,9 10.62,9.2 10.08,9.59C9.56,10 9.3,10.57 9.31,11.36L9.32,11.39H11.25C11.26,11.09 11.35,10.86 11.53,10.7C11.71,10.55 11.93,10.47 12.19,10.47C12.5,10.47 12.76,10.57 12.94,10.75C13.12,10.94 13.2,11.2 13.2,11.5C13.2,11.82 13.13,12.09 12.97,12.32C12.83,12.55 12.62,12.75 12.36,12.91C11.85,13.25 11.5,13.55 11.31,13.82C11.11,14.08 11,14.5 11,15H13C13,14.69 13.04,14.44 13.13,14.26C13.22,14.08 13.39,13.9 13.64,13.74C14.09,13.5 14.46,13.21 14.75,12.81C15.04,12.41 15.19,12 15.19,11.5C15.19,10.74 14.92,10.13 14.38,9.68C13.85,9.23 13.12,9 12.19,9M11,16V18H13V16H11Z" /></g><g id="calendar-range"><path d="M9,11H7V13H9V11M13,11H11V13H13V11M17,11H15V13H17V11M19,4H18V2H16V4H8V2H6V4H5C3.89,4 3,4.9 3,6V20A2,2 0 0,0 5,22H19A2,2 0 0,0 21,20V6A2,2 0 0,0 19,4M19,20H5V9H19V20Z" /></g><g id="calendar-remove"><path d="M19,19H5V8H19M19,3H18V1H16V3H8V1H6V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3M9.31,17L11.75,14.56L14.19,17L15.25,15.94L12.81,13.5L15.25,11.06L14.19,10L11.75,12.44L9.31,10L8.25,11.06L10.69,13.5L8.25,15.94L9.31,17Z" /></g><g id="calendar-text"><path d="M14,14H7V16H14M19,19H5V8H19M19,3H18V1H16V3H8V1H6V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3M17,10H7V12H17V10Z" /></g><g id="calendar-today"><path d="M7,10H12V15H7M19,19H5V8H19M19,3H18V1H16V3H8V1H6V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3Z" /></g><g id="call-made"><path d="M9,5V7H15.59L4,18.59L5.41,20L17,8.41V15H19V5" /></g><g id="call-merge"><path d="M17,20.41L18.41,19L15,15.59L13.59,17M7.5,8H11V13.59L5.59,19L7,20.41L13,14.41V8H16.5L12,3.5" /></g><g id="call-missed"><path d="M19.59,7L12,14.59L6.41,9H11V7H3V15H5V10.41L12,17.41L21,8.41" /></g><g id="call-received"><path d="M20,5.41L18.59,4L7,15.59V9H5V19H15V17H8.41" /></g><g id="call-split"><path d="M14,4L16.29,6.29L13.41,9.17L14.83,10.59L17.71,7.71L20,10V4M10,4H4V10L6.29,7.71L11,12.41V20H13V11.59L7.71,6.29" /></g><g id="camcorder"><path d="M17,10.5V7A1,1 0 0,0 16,6H4A1,1 0 0,0 3,7V17A1,1 0 0,0 4,18H16A1,1 0 0,0 17,17V13.5L21,17.5V6.5L17,10.5Z" /></g><g id="camcorder-box"><path d="M18,16L14,12.8V16H6V8H14V11.2L18,8M20,4H4A2,2 0 0,0 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V6C22,4.89 21.1,4 20,4Z" /></g><g id="camcorder-box-off"><path d="M6,8H6.73L14,15.27V16H6M2.27,1L1,2.27L3,4.28C2.41,4.62 2,5.26 2,6V18A2,2 0 0,0 4,20H18.73L20.73,22L22,20.73M20,4H7.82L11.82,8H14V10.18L14.57,10.75L18,8V14.18L22,18.17C22,18.11 22,18.06 22,18V6A2,2 0 0,0 20,4Z" /></g><g id="camcorder-off"><path d="M3.27,2L2,3.27L4.73,6H4A1,1 0 0,0 3,7V17A1,1 0 0,0 4,18H16C16.2,18 16.39,17.92 16.54,17.82L19.73,21L21,19.73M21,6.5L17,10.5V7A1,1 0 0,0 16,6H9.82L21,17.18V6.5Z" /></g><g id="camera"><path d="M4,4H7L9,2H15L17,4H20A2,2 0 0,1 22,6V18A2,2 0 0,1 20,20H4A2,2 0 0,1 2,18V6A2,2 0 0,1 4,4M12,7A5,5 0 0,0 7,12A5,5 0 0,0 12,17A5,5 0 0,0 17,12A5,5 0 0,0 12,7M12,9A3,3 0 0,1 15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9Z" /></g><g id="camera-burst"><path d="M1,5H3V19H1V5M5,5H7V19H5V5M22,5H10A1,1 0 0,0 9,6V18A1,1 0 0,0 10,19H22A1,1 0 0,0 23,18V6A1,1 0 0,0 22,5M11,17L13.5,13.85L15.29,16L17.79,12.78L21,17H11Z" /></g><g id="camera-enhance"><path d="M9,3L7.17,5H4A2,2 0 0,0 2,7V19A2,2 0 0,0 4,21H20A2,2 0 0,0 22,19V7A2,2 0 0,0 20,5H16.83L15,3M12,18A5,5 0 0,1 7,13A5,5 0 0,1 12,8A5,5 0 0,1 17,13A5,5 0 0,1 12,18M12,17L13.25,14.25L16,13L13.25,11.75L12,9L10.75,11.75L8,13L10.75,14.25" /></g><g id="camera-front"><path d="M7,2H17V12.5C17,10.83 13.67,10 12,10C10.33,10 7,10.83 7,12.5M17,0H7A2,2 0 0,0 5,2V16A2,2 0 0,0 7,18H17A2,2 0 0,0 19,16V2A2,2 0 0,0 17,0M12,8A2,2 0 0,0 14,6A2,2 0 0,0 12,4A2,2 0 0,0 10,6A2,2 0 0,0 12,8M14,20V22H19V20M10,20H5V22H10V24L13,21L10,18V20Z" /></g><g id="camera-front-variant"><path d="M6,0H18A2,2 0 0,1 20,2V22A2,2 0 0,1 18,24H6A2,2 0 0,1 4,22V2A2,2 0 0,1 6,0M12,6A3,3 0 0,1 15,9A3,3 0 0,1 12,12A3,3 0 0,1 9,9A3,3 0 0,1 12,6M11,1V3H13V1H11M6,4V16.5C6,15.12 8.69,14 12,14C15.31,14 18,15.12 18,16.5V4H6M13,18H9V20H13V22L16,19L13,16V18Z" /></g><g id="camera-iris"><path d="M13.73,15L9.83,21.76C10.53,21.91 11.25,22 12,22C14.4,22 16.6,21.15 18.32,19.75L14.66,13.4M2.46,15C3.38,17.92 5.61,20.26 8.45,21.34L12.12,15M8.54,12L4.64,5.25C3,7 2,9.39 2,12C2,12.68 2.07,13.35 2.2,14H9.69M21.8,10H14.31L14.6,10.5L19.36,18.75C21,16.97 22,14.6 22,12C22,11.31 21.93,10.64 21.8,10M21.54,9C20.62,6.07 18.39,3.74 15.55,2.66L11.88,9M9.4,10.5L14.17,2.24C13.47,2.09 12.75,2 12,2C9.6,2 7.4,2.84 5.68,4.25L9.34,10.6L9.4,10.5Z" /></g><g id="camera-off"><path d="M1.2,4.47L2.5,3.2L20,20.72L18.73,22L16.73,20H4A2,2 0 0,1 2,18V6C2,5.78 2.04,5.57 2.1,5.37L1.2,4.47M7,4L9,2H15L17,4H20A2,2 0 0,1 22,6V18C22,18.6 21.74,19.13 21.32,19.5L16.33,14.5C16.76,13.77 17,12.91 17,12A5,5 0 0,0 12,7C11.09,7 10.23,7.24 9.5,7.67L5.82,4H7M7,12A5,5 0 0,0 12,17C12.5,17 13.03,16.92 13.5,16.77L11.72,15C10.29,14.85 9.15,13.71 9,12.28L7.23,10.5C7.08,10.97 7,11.5 7,12M12,9A3,3 0 0,1 15,12C15,12.35 14.94,12.69 14.83,13L11,9.17C11.31,9.06 11.65,9 12,9Z" /></g><g id="camera-party-mode"><path d="M12,17C10.37,17 8.94,16.21 8,15H12A3,3 0 0,0 15,12C15,11.65 14.93,11.31 14.82,11H16.9C16.96,11.32 17,11.66 17,12A5,5 0 0,1 12,17M12,7C13.63,7 15.06,7.79 16,9H12A3,3 0 0,0 9,12C9,12.35 9.07,12.68 9.18,13H7.1C7.03,12.68 7,12.34 7,12A5,5 0 0,1 12,7M20,4H16.83L15,2H9L7.17,4H4A2,2 0 0,0 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V6C22,4.89 21.1,4 20,4Z" /></g><g id="camera-rear"><path d="M12,6C10.89,6 10,5.1 10,4A2,2 0 0,1 12,2C13.09,2 14,2.9 14,4A2,2 0 0,1 12,6M17,0H7A2,2 0 0,0 5,2V16A2,2 0 0,0 7,18H17A2,2 0 0,0 19,16V2A2,2 0 0,0 17,0M14,20V22H19V20M10,20H5V22H10V24L13,21L10,18V20Z" /></g><g id="camera-rear-variant"><path d="M6,0H18A2,2 0 0,1 20,2V22A2,2 0 0,1 18,24H6A2,2 0 0,1 4,22V2A2,2 0 0,1 6,0M12,2A2,2 0 0,0 10,4A2,2 0 0,0 12,6A2,2 0 0,0 14,4A2,2 0 0,0 12,2M13,18H9V20H13V22L16,19L13,16V18Z" /></g><g id="camera-switch"><path d="M15,15.5V13H9V15.5L5.5,12L9,8.5V11H15V8.5L18.5,12M20,4H16.83L15,2H9L7.17,4H4A2,2 0 0,0 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V6C22,4.89 21.1,4 20,4Z" /></g><g id="camera-timer"><path d="M4.94,6.35C4.55,5.96 4.55,5.32 4.94,4.93C5.33,4.54 5.96,4.54 6.35,4.93L13.07,10.31L13.42,10.59C14.2,11.37 14.2,12.64 13.42,13.42C12.64,14.2 11.37,14.2 10.59,13.42L10.31,13.07L4.94,6.35M12,20A8,8 0 0,0 20,12C20,9.79 19.1,7.79 17.66,6.34L19.07,4.93C20.88,6.74 22,9.24 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12H4A8,8 0 0,0 12,20M12,1A2,2 0 0,1 14,3A2,2 0 0,1 12,5A2,2 0 0,1 10,3A2,2 0 0,1 12,1Z" /></g><g id="candle"><path d="M12.5,2C10.84,2 9.5,5.34 9.5,7A3,3 0 0,0 12.5,10A3,3 0 0,0 15.5,7C15.5,5.34 14.16,2 12.5,2M12.5,6.5A1,1 0 0,1 13.5,7.5A1,1 0 0,1 12.5,8.5A1,1 0 0,1 11.5,7.5A1,1 0 0,1 12.5,6.5M10,11A1,1 0 0,0 9,12V20H7A1,1 0 0,1 6,19V18A1,1 0 0,0 5,17A1,1 0 0,0 4,18V19A3,3 0 0,0 7,22H19A1,1 0 0,0 20,21A1,1 0 0,0 19,20H16V12A1,1 0 0,0 15,11H10Z" /></g><g id="candycane"><path d="M10,10A2,2 0 0,1 8,12A2,2 0 0,1 6,10V8C6,7.37 6.1,6.77 6.27,6.2L10,9.93V10M12,2C12.74,2 13.44,2.13 14.09,2.38L11.97,6C11.14,6 10.44,6.5 10.15,7.25L7.24,4.34C8.34,2.92 10.06,2 12,2M17.76,6.31L14,10.07V8C14,7.62 13.9,7.27 13.72,6.97L15.83,3.38C16.74,4.13 17.42,5.15 17.76,6.31M18,13.09L14,17.09V12.9L18,8.9V13.09M18,20A2,2 0 0,1 16,22A2,2 0 0,1 14,20V19.91L18,15.91V20Z" /></g><g id="car"><path d="M5,11L6.5,6.5H17.5L19,11M17.5,16A1.5,1.5 0 0,1 16,14.5A1.5,1.5 0 0,1 17.5,13A1.5,1.5 0 0,1 19,14.5A1.5,1.5 0 0,1 17.5,16M6.5,16A1.5,1.5 0 0,1 5,14.5A1.5,1.5 0 0,1 6.5,13A1.5,1.5 0 0,1 8,14.5A1.5,1.5 0 0,1 6.5,16M18.92,6C18.72,5.42 18.16,5 17.5,5H6.5C5.84,5 5.28,5.42 5.08,6L3,12V20A1,1 0 0,0 4,21H5A1,1 0 0,0 6,20V19H18V20A1,1 0 0,0 19,21H20A1,1 0 0,0 21,20V12L18.92,6Z" /></g><g id="car-battery"><path d="M4,3V6H1V20H23V6H20V3H14V6H10V3H4M3,8H21V18H3V8M15,10V12H13V14H15V16H17V14H19V12H17V10H15M5,12V14H11V12H5Z" /></g><g id="car-connected"><path d="M5,14H19L17.5,9.5H6.5L5,14M17.5,19A1.5,1.5 0 0,0 19,17.5A1.5,1.5 0 0,0 17.5,16A1.5,1.5 0 0,0 16,17.5A1.5,1.5 0 0,0 17.5,19M6.5,19A1.5,1.5 0 0,0 8,17.5A1.5,1.5 0 0,0 6.5,16A1.5,1.5 0 0,0 5,17.5A1.5,1.5 0 0,0 6.5,19M18.92,9L21,15V23A1,1 0 0,1 20,24H19A1,1 0 0,1 18,23V22H6V23A1,1 0 0,1 5,24H4A1,1 0 0,1 3,23V15L5.08,9C5.28,8.42 5.85,8 6.5,8H17.5C18.15,8 18.72,8.42 18.92,9M12,0C14.12,0 16.15,0.86 17.65,2.35L16.23,3.77C15.11,2.65 13.58,2 12,2C10.42,2 8.89,2.65 7.77,3.77L6.36,2.35C7.85,0.86 9.88,0 12,0M12,4C13.06,4 14.07,4.44 14.82,5.18L13.4,6.6C13.03,6.23 12.53,6 12,6C11.5,6 10.97,6.23 10.6,6.6L9.18,5.18C9.93,4.44 10.94,4 12,4Z" /></g><g id="car-wash"><path d="M5,13L6.5,8.5H17.5L19,13M17.5,18A1.5,1.5 0 0,1 16,16.5A1.5,1.5 0 0,1 17.5,15A1.5,1.5 0 0,1 19,16.5A1.5,1.5 0 0,1 17.5,18M6.5,18A1.5,1.5 0 0,1 5,16.5A1.5,1.5 0 0,1 6.5,15A1.5,1.5 0 0,1 8,16.5A1.5,1.5 0 0,1 6.5,18M18.92,8C18.72,7.42 18.16,7 17.5,7H6.5C5.84,7 5.28,7.42 5.08,8L3,14V22A1,1 0 0,0 4,23H5A1,1 0 0,0 6,22V21H18V22A1,1 0 0,0 19,23H20A1,1 0 0,0 21,22V14M7,5A1.5,1.5 0 0,0 8.5,3.5C8.5,2.5 7,0.8 7,0.8C7,0.8 5.5,2.5 5.5,3.5A1.5,1.5 0 0,0 7,5M12,5A1.5,1.5 0 0,0 13.5,3.5C13.5,2.5 12,0.8 12,0.8C12,0.8 10.5,2.5 10.5,3.5A1.5,1.5 0 0,0 12,5M17,5A1.5,1.5 0 0,0 18.5,3.5C18.5,2.5 17,0.8 17,0.8C17,0.8 15.5,2.5 15.5,3.5A1.5,1.5 0 0,0 17,5Z" /></g><g id="cards"><path d="M21.47,4.35L20.13,3.79V12.82L22.56,6.96C22.97,5.94 22.5,4.77 21.47,4.35M1.97,8.05L6.93,20C7.24,20.77 7.97,21.24 8.74,21.26C9,21.26 9.27,21.21 9.53,21.1L16.9,18.05C17.65,17.74 18.11,17 18.13,16.26C18.14,16 18.09,15.71 18,15.45L13,3.5C12.71,2.73 11.97,2.26 11.19,2.25C10.93,2.25 10.67,2.31 10.42,2.4L3.06,5.45C2.04,5.87 1.55,7.04 1.97,8.05M18.12,4.25A2,2 0 0,0 16.12,2.25H14.67L18.12,10.59" /></g><g id="cards-outline"><path d="M11.19,2.25C10.93,2.25 10.67,2.31 10.42,2.4L3.06,5.45C2.04,5.87 1.55,7.04 1.97,8.05L6.93,20C7.24,20.77 7.97,21.23 8.74,21.25C9,21.25 9.27,21.22 9.53,21.1L16.9,18.05C17.65,17.74 18.11,17 18.13,16.25C18.14,16 18.09,15.71 18,15.45L13,3.5C12.71,2.73 11.97,2.26 11.19,2.25M14.67,2.25L18.12,10.6V4.25A2,2 0 0,0 16.12,2.25M20.13,3.79V12.82L22.56,6.96C22.97,5.94 22.5,4.78 21.47,4.36M11.19,4.22L16.17,16.24L8.78,19.3L3.8,7.29" /></g><g id="cards-playing-outline"><path d="M11.19,2.25C11.97,2.26 12.71,2.73 13,3.5L18,15.45C18.09,15.71 18.14,16 18.13,16.25C18.11,17 17.65,17.74 16.9,18.05L9.53,21.1C9.27,21.22 9,21.25 8.74,21.25C7.97,21.23 7.24,20.77 6.93,20L1.97,8.05C1.55,7.04 2.04,5.87 3.06,5.45L10.42,2.4C10.67,2.31 10.93,2.25 11.19,2.25M14.67,2.25H16.12A2,2 0 0,1 18.12,4.25V10.6L14.67,2.25M20.13,3.79L21.47,4.36C22.5,4.78 22.97,5.94 22.56,6.96L20.13,12.82V3.79M11.19,4.22L3.8,7.29L8.77,19.3L16.17,16.24L11.19,4.22M8.65,8.54L11.88,10.95L11.44,14.96L8.21,12.54L8.65,8.54Z" /></g><g id="carrot"><path d="M16,10L15.8,11H13.5A0.5,0.5 0 0,0 13,11.5A0.5,0.5 0 0,0 13.5,12H15.6L14.6,17H12.5A0.5,0.5 0 0,0 12,17.5A0.5,0.5 0 0,0 12.5,18H14.4L14,20A2,2 0 0,1 12,22A2,2 0 0,1 10,20L9,15H10.5A0.5,0.5 0 0,0 11,14.5A0.5,0.5 0 0,0 10.5,14H8.8L8,10C8,8.8 8.93,7.77 10.29,7.29L8.9,5.28C8.59,4.82 8.7,4.2 9.16,3.89C9.61,3.57 10.23,3.69 10.55,4.14L11,4.8V3A1,1 0 0,1 12,2A1,1 0 0,1 13,3V5.28L14.5,3.54C14.83,3.12 15.47,3.07 15.89,3.43C16.31,3.78 16.36,4.41 16,4.84L13.87,7.35C15.14,7.85 16,8.85 16,10Z" /></g><g id="cart"><path d="M17,18C15.89,18 15,18.89 15,20A2,2 0 0,0 17,22A2,2 0 0,0 19,20C19,18.89 18.1,18 17,18M1,2V4H3L6.6,11.59L5.24,14.04C5.09,14.32 5,14.65 5,15A2,2 0 0,0 7,17H19V15H7.42A0.25,0.25 0 0,1 7.17,14.75C7.17,14.7 7.18,14.66 7.2,14.63L8.1,13H15.55C16.3,13 16.96,12.58 17.3,11.97L20.88,5.5C20.95,5.34 21,5.17 21,5A1,1 0 0,0 20,4H5.21L4.27,2M7,18C5.89,18 5,18.89 5,20A2,2 0 0,0 7,22A2,2 0 0,0 9,20C9,18.89 8.1,18 7,18Z" /></g><g id="cart-off"><path d="M22.73,22.73L1.27,1.27L0,2.54L4.39,6.93L6.6,11.59L5.25,14.04C5.09,14.32 5,14.65 5,15A2,2 0 0,0 7,17H14.46L15.84,18.38C15.34,18.74 15,19.33 15,20A2,2 0 0,0 17,22C17.67,22 18.26,21.67 18.62,21.16L21.46,24L22.73,22.73M7.42,15A0.25,0.25 0 0,1 7.17,14.75L7.2,14.63L8.1,13H10.46L12.46,15H7.42M15.55,13C16.3,13 16.96,12.59 17.3,11.97L20.88,5.5C20.96,5.34 21,5.17 21,5A1,1 0 0,0 20,4H6.54L15.55,13M7,18A2,2 0 0,0 5,20A2,2 0 0,0 7,22A2,2 0 0,0 9,20A2,2 0 0,0 7,18Z" /></g><g id="cart-outline"><path d="M17,18A2,2 0 0,1 19,20A2,2 0 0,1 17,22C15.89,22 15,21.1 15,20C15,18.89 15.89,18 17,18M1,2H4.27L5.21,4H20A1,1 0 0,1 21,5C21,5.17 20.95,5.34 20.88,5.5L17.3,11.97C16.96,12.58 16.3,13 15.55,13H8.1L7.2,14.63L7.17,14.75A0.25,0.25 0 0,0 7.42,15H19V17H7C5.89,17 5,16.1 5,15C5,14.65 5.09,14.32 5.24,14.04L6.6,11.59L3,4H1V2M7,18A2,2 0 0,1 9,20A2,2 0 0,1 7,22C5.89,22 5,21.1 5,20C5,18.89 5.89,18 7,18M16,11L18.78,6H6.14L8.5,11H16Z" /></g><g id="cart-plus"><path d="M11,9H13V6H16V4H13V1H11V4H8V6H11M7,18A2,2 0 0,0 5,20A2,2 0 0,0 7,22A2,2 0 0,0 9,20A2,2 0 0,0 7,18M17,18A2,2 0 0,0 15,20A2,2 0 0,0 17,22A2,2 0 0,0 19,20A2,2 0 0,0 17,18M7.17,14.75L7.2,14.63L8.1,13H15.55C16.3,13 16.96,12.59 17.3,11.97L21.16,4.96L19.42,4H19.41L18.31,6L15.55,11H8.53L8.4,10.73L6.16,6L5.21,4L4.27,2H1V4H3L6.6,11.59L5.25,14.04C5.09,14.32 5,14.65 5,15A2,2 0 0,0 7,17H19V15H7.42C7.29,15 7.17,14.89 7.17,14.75Z" /></g><g id="case-sensitive-alt"><path d="M20,14C20,12.5 19.5,12 18,12H16V11C16,10 16,10 14,10V15.4L14,19H16L18,19C19.5,19 20,18.47 20,17V14M12,12C12,10.5 11.47,10 10,10H6C4.5,10 4,10.5 4,12V19H6V16H10V19H12V12M10,7H14V5H10V7M22,9V20C22,21.11 21.11,22 20,22H4A2,2 0 0,1 2,20V9C2,7.89 2.89,7 4,7H8V5L10,3H14L16,5V7H20A2,2 0 0,1 22,9H22M16,17H18V14H16V17M6,12H10V14H6V12Z" /></g><g id="cash"><path d="M3,6H21V18H3V6M12,9A3,3 0 0,1 15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9M7,8A2,2 0 0,1 5,10V14A2,2 0 0,1 7,16H17A2,2 0 0,1 19,14V10A2,2 0 0,1 17,8H7Z" /></g><g id="cash-100"><path d="M2,5H22V20H2V5M20,18V7H4V18H20M17,8A2,2 0 0,0 19,10V15A2,2 0 0,0 17,17H7A2,2 0 0,0 5,15V10A2,2 0 0,0 7,8H17M17,13V12C17,10.9 16.33,10 15.5,10C14.67,10 14,10.9 14,12V13C14,14.1 14.67,15 15.5,15C16.33,15 17,14.1 17,13M15.5,11A0.5,0.5 0 0,1 16,11.5V13.5A0.5,0.5 0 0,1 15.5,14A0.5,0.5 0 0,1 15,13.5V11.5A0.5,0.5 0 0,1 15.5,11M13,13V12C13,10.9 12.33,10 11.5,10C10.67,10 10,10.9 10,12V13C10,14.1 10.67,15 11.5,15C12.33,15 13,14.1 13,13M11.5,11A0.5,0.5 0 0,1 12,11.5V13.5A0.5,0.5 0 0,1 11.5,14A0.5,0.5 0 0,1 11,13.5V11.5A0.5,0.5 0 0,1 11.5,11M8,15H9V10H8L7,10.5V11.5L8,11V15Z" /></g><g id="cash-multiple"><path d="M5,6H23V18H5V6M14,9A3,3 0 0,1 17,12A3,3 0 0,1 14,15A3,3 0 0,1 11,12A3,3 0 0,1 14,9M9,8A2,2 0 0,1 7,10V14A2,2 0 0,1 9,16H19A2,2 0 0,1 21,14V10A2,2 0 0,1 19,8H9M1,10H3V20H19V22H1V10Z" /></g><g id="cash-usd"><path d="M20,18H4V6H20M20,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V6C22,4.89 21.1,4 20,4M11,17H13V16H14A1,1 0 0,0 15,15V12A1,1 0 0,0 14,11H11V10H15V8H13V7H11V8H10A1,1 0 0,0 9,9V12A1,1 0 0,0 10,13H13V14H9V16H11V17Z" /></g><g id="cast"><path d="M1,10V12A9,9 0 0,1 10,21H12C12,14.92 7.07,10 1,10M1,14V16A5,5 0 0,1 6,21H8A7,7 0 0,0 1,14M1,18V21H4A3,3 0 0,0 1,18M21,3H3C1.89,3 1,3.89 1,5V8H3V5H21V19H14V21H21A2,2 0 0,0 23,19V5C23,3.89 22.1,3 21,3Z" /></g><g id="cast-connected"><path d="M21,3H3C1.89,3 1,3.89 1,5V8H3V5H21V19H14V21H21A2,2 0 0,0 23,19V5C23,3.89 22.1,3 21,3M1,10V12A9,9 0 0,1 10,21H12C12,14.92 7.07,10 1,10M19,7H5V8.63C8.96,9.91 12.09,13.04 13.37,17H19M1,14V16A5,5 0 0,1 6,21H8A7,7 0 0,0 1,14M1,18V21H4A3,3 0 0,0 1,18Z" /></g><g id="castle"><path d="M2,13H4V15H6V13H8V15H10V13H12V15H14V10L17,7V1H19L23,3L19,5V7L22,10V22H11V19A2,2 0 0,0 9,17A2,2 0 0,0 7,19V22H2V13M18,10C17.45,10 17,10.54 17,11.2V13H19V11.2C19,10.54 18.55,10 18,10Z" /></g><g id="cat"><path d="M12,8L10.67,8.09C9.81,7.07 7.4,4.5 5,4.5C5,4.5 3.03,7.46 4.96,11.41C4.41,12.24 4.07,12.67 4,13.66L2.07,13.95L2.28,14.93L4.04,14.67L4.18,15.38L2.61,16.32L3.08,17.21L4.53,16.32C5.68,18.76 8.59,20 12,20C15.41,20 18.32,18.76 19.47,16.32L20.92,17.21L21.39,16.32L19.82,15.38L19.96,14.67L21.72,14.93L21.93,13.95L20,13.66C19.93,12.67 19.59,12.24 19.04,11.41C20.97,7.46 19,4.5 19,4.5C16.6,4.5 14.19,7.07 13.33,8.09L12,8M9,11A1,1 0 0,1 10,12A1,1 0 0,1 9,13A1,1 0 0,1 8,12A1,1 0 0,1 9,11M15,11A1,1 0 0,1 16,12A1,1 0 0,1 15,13A1,1 0 0,1 14,12A1,1 0 0,1 15,11M11,14H13L12.3,15.39C12.5,16.03 13.06,16.5 13.75,16.5A1.5,1.5 0 0,0 15.25,15H15.75A2,2 0 0,1 13.75,17C13,17 12.35,16.59 12,16V16H12C11.65,16.59 11,17 10.25,17A2,2 0 0,1 8.25,15H8.75A1.5,1.5 0 0,0 10.25,16.5C10.94,16.5 11.5,16.03 11.7,15.39L11,14Z" /></g><g id="cellphone"><path d="M17,19H7V5H17M17,1H7C5.89,1 5,1.89 5,3V21A2,2 0 0,0 7,23H17A2,2 0 0,0 19,21V3C19,1.89 18.1,1 17,1Z" /></g><g id="cellphone-android"><path d="M17.25,18H6.75V4H17.25M14,21H10V20H14M16,1H8A3,3 0 0,0 5,4V20A3,3 0 0,0 8,23H16A3,3 0 0,0 19,20V4A3,3 0 0,0 16,1Z" /></g><g id="cellphone-basic"><path d="M15,2A1,1 0 0,0 14,3V6H10C8.89,6 8,6.89 8,8V20C8,21.11 8.89,22 10,22H15C16.11,22 17,21.11 17,20V8C17,7.26 16.6,6.62 16,6.28V3A1,1 0 0,0 15,2M10,8H15V13H10V8M10,15H11V16H10V15M12,15H13V16H12V15M14,15H15V16H14V15M10,17H11V18H10V17M12,17H13V18H12V17M14,17H15V18H14V17M10,19H11V20H10V19M12,19H13V20H12V19M14,19H15V20H14V19Z" /></g><g id="cellphone-dock"><path d="M16,15H8V5H16M16,1H8C6.89,1 6,1.89 6,3V17A2,2 0 0,0 8,19H16A2,2 0 0,0 18,17V3C18,1.89 17.1,1 16,1M8,23H16V21H8V23Z" /></g><g id="cellphone-iphone"><path d="M16,18H7V4H16M11.5,22A1.5,1.5 0 0,1 10,20.5A1.5,1.5 0 0,1 11.5,19A1.5,1.5 0 0,1 13,20.5A1.5,1.5 0 0,1 11.5,22M15.5,1H7.5A2.5,2.5 0 0,0 5,3.5V20.5A2.5,2.5 0 0,0 7.5,23H15.5A2.5,2.5 0 0,0 18,20.5V3.5A2.5,2.5 0 0,0 15.5,1Z" /></g><g id="cellphone-link"><path d="M22,17H18V10H22M23,8H17A1,1 0 0,0 16,9V19A1,1 0 0,0 17,20H23A1,1 0 0,0 24,19V9A1,1 0 0,0 23,8M4,6H22V4H4A2,2 0 0,0 2,6V17H0V20H14V17H4V6Z" /></g><g id="cellphone-link-off"><path d="M23,8H17A1,1 0 0,0 16,9V13.18L18,15.18V10H22V17H19.82L22.82,20H23A1,1 0 0,0 24,19V9A1,1 0 0,0 23,8M4,6.27L14.73,17H4V6.27M1.92,1.65L0.65,2.92L2.47,4.74C2.18,5.08 2,5.5 2,6V17H0V20H17.73L20.08,22.35L21.35,21.08L3.89,3.62L1.92,1.65M22,6V4H6.82L8.82,6H22Z" /></g><g id="cellphone-settings"><path d="M16,16H8V4H16M16,0H8A2,2 0 0,0 6,2V18A2,2 0 0,0 8,20H16A2,2 0 0,0 18,18V2A2,2 0 0,0 16,0M15,24H17V22H15M11,24H13V22H11M7,24H9V22H7V24Z" /></g><g id="certificate"><path d="M4,3C2.89,3 2,3.89 2,5V15A2,2 0 0,0 4,17H12V22L15,19L18,22V17H20A2,2 0 0,0 22,15V8L22,6V5A2,2 0 0,0 20,3H16V3H4M12,5L15,7L18,5V8.5L21,10L18,11.5V15L15,13L12,15V11.5L9,10L12,8.5V5M4,5H9V7H4V5M4,9H7V11H4V9M4,13H9V15H4V13Z" /></g><g id="chair-school"><path d="M22,5V7H17L13.53,12H16V14H14.46L18.17,22H15.97L15.04,20H6.38L5.35,22H3.1L7.23,14H7C6.55,14 6.17,13.7 6.04,13.3L2.87,3.84L3.82,3.5C4.34,3.34 4.91,3.63 5.08,4.15L7.72,12H12.1L15.57,7H12V5H22M9.5,14L7.42,18H14.11L12.26,14H9.5Z" /></g><g id="chart-arc"><path d="M16.18,19.6L14.17,16.12C15.15,15.4 15.83,14.28 15.97,13H20C19.83,15.76 18.35,18.16 16.18,19.6M13,7.03V3C17.3,3.26 20.74,6.7 21,11H16.97C16.74,8.91 15.09,7.26 13,7.03M7,12.5C7,13.14 7.13,13.75 7.38,14.3L3.9,16.31C3.32,15.16 3,13.87 3,12.5C3,7.97 6.54,4.27 11,4V8.03C8.75,8.28 7,10.18 7,12.5M11.5,21C8.53,21 5.92,19.5 4.4,17.18L7.88,15.17C8.7,16.28 10,17 11.5,17C12.14,17 12.75,16.87 13.3,16.62L15.31,20.1C14.16,20.68 12.87,21 11.5,21Z" /></g><g id="chart-areaspline"><path d="M17.45,15.18L22,7.31V19L22,21H2V3H4V15.54L9.5,6L16,9.78L20.24,2.45L21.97,3.45L16.74,12.5L10.23,8.75L4.31,19H6.57L10.96,11.44L17.45,15.18Z" /></g><g id="chart-bar"><path d="M22,21H2V3H4V19H6V10H10V19H12V6H16V19H18V14H22V21Z" /></g><g id="chart-bubble"><path d="M7.2,11.2C8.97,11.2 10.4,12.63 10.4,14.4C10.4,16.17 8.97,17.6 7.2,17.6C5.43,17.6 4,16.17 4,14.4C4,12.63 5.43,11.2 7.2,11.2M14.8,16A2,2 0 0,1 16.8,18A2,2 0 0,1 14.8,20A2,2 0 0,1 12.8,18A2,2 0 0,1 14.8,16M15.2,4A4.8,4.8 0 0,1 20,8.8C20,11.45 17.85,13.6 15.2,13.6A4.8,4.8 0 0,1 10.4,8.8C10.4,6.15 12.55,4 15.2,4Z" /></g><g id="chart-gantt"><path d="M2,5H10V2H12V22H10V18H6V15H10V13H4V10H10V8H2V5M14,5H17V8H14V5M14,10H19V13H14V10M14,15H22V18H14V15Z" /></g><g id="chart-histogram"><path d="M3,3H5V13H9V7H13V11H17V15H21V21H3V3Z" /></g><g id="chart-line"><path d="M16,11.78L20.24,4.45L21.97,5.45L16.74,14.5L10.23,10.75L5.46,19H22V21H2V3H4V17.54L9.5,8L16,11.78Z" /></g><g id="chart-pie"><path d="M21,11H13V3A8,8 0 0,1 21,11M19,13C19,15.78 17.58,18.23 15.43,19.67L11.58,13H19M11,21C8.22,21 5.77,19.58 4.33,17.43L10.82,13.68L14.56,20.17C13.5,20.7 12.28,21 11,21M3,13A8,8 0 0,1 11,5V12.42L3.83,16.56C3.3,15.5 3,14.28 3,13Z" /></g><g id="chart-scatterplot-hexbin"><path d="M2,2H4V20H22V22H2V2M14,14.5L12,18H7.94L5.92,14.5L7.94,11H12L14,14.5M14.08,6.5L12.06,10H8L6,6.5L8,3H12.06L14.08,6.5M21.25,10.5L19.23,14H15.19L13.17,10.5L15.19,7H19.23L21.25,10.5Z" /></g><g id="chart-timeline"><path d="M2,2H4V20H22V22H2V2M7,10H17V13H7V10M11,15H21V18H11V15M6,4H22V8H20V6H8V8H6V4Z" /></g><g id="check"><path d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z" /></g><g id="check-all"><path d="M0.41,13.41L6,19L7.41,17.58L1.83,12M22.24,5.58L11.66,16.17L7.5,12L6.07,13.41L11.66,19L23.66,7M18,7L16.59,5.58L10.24,11.93L11.66,13.34L18,7Z" /></g><g id="check-circle"><path d="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M11,16.5L18,9.5L16.59,8.09L11,13.67L7.91,10.59L6.5,12L11,16.5Z" /></g><g id="check-circle-outline"><path d="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4M11,16.5L6.5,12L7.91,10.59L11,13.67L16.59,8.09L18,9.5L11,16.5Z" /></g><g id="checkbox-blank"><path d="M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3Z" /></g><g id="checkbox-blank-circle"><path d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" /></g><g id="checkbox-blank-circle-outline"><path d="M12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" /></g><g id="checkbox-blank-outline"><path d="M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3M19,5V19H5V5H19Z" /></g><g id="checkbox-marked"><path d="M10,17L5,12L6.41,10.58L10,14.17L17.59,6.58L19,8M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3Z" /></g><g id="checkbox-marked-circle"><path d="M10,17L5,12L6.41,10.58L10,14.17L17.59,6.58L19,8M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" /></g><g id="checkbox-marked-circle-outline"><path d="M20,12A8,8 0 0,1 12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4C12.76,4 13.5,4.11 14.2,4.31L15.77,2.74C14.61,2.26 13.34,2 12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12M7.91,10.08L6.5,11.5L11,16L21,6L19.59,4.58L11,13.17L7.91,10.08Z" /></g><g id="checkbox-marked-outline"><path d="M19,19H5V5H15V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V11H19M7.91,10.08L6.5,11.5L11,16L21,6L19.59,4.58L11,13.17L7.91,10.08Z" /></g><g id="checkbox-multiple-blank"><path d="M22,16A2,2 0 0,1 20,18H8C6.89,18 6,17.1 6,16V4C6,2.89 6.89,2 8,2H20A2,2 0 0,1 22,4V16M16,20V22H4A2,2 0 0,1 2,20V7H4V20H16Z" /></g><g id="checkbox-multiple-blank-circle"><path d="M14,2A8,8 0 0,0 6,10A8,8 0 0,0 14,18A8,8 0 0,0 22,10A8,8 0 0,0 14,2M4.93,5.82C3.08,7.34 2,9.61 2,12A8,8 0 0,0 10,20C10.64,20 11.27,19.92 11.88,19.77C10.12,19.38 8.5,18.5 7.17,17.29C5.22,16.25 4,14.21 4,12C4,11.7 4.03,11.41 4.07,11.11C4.03,10.74 4,10.37 4,10C4,8.56 4.32,7.13 4.93,5.82Z" /></g><g id="checkbox-multiple-blank-circle-outline"><path d="M14,2A8,8 0 0,0 6,10A8,8 0 0,0 14,18A8,8 0 0,0 22,10A8,8 0 0,0 14,2M14,4C17.32,4 20,6.69 20,10C20,13.32 17.32,16 14,16A6,6 0 0,1 8,10A6,6 0 0,1 14,4M4.93,5.82C3.08,7.34 2,9.61 2,12A8,8 0 0,0 10,20C10.64,20 11.27,19.92 11.88,19.77C10.12,19.38 8.5,18.5 7.17,17.29C5.22,16.25 4,14.21 4,12C4,11.7 4.03,11.41 4.07,11.11C4.03,10.74 4,10.37 4,10C4,8.56 4.32,7.13 4.93,5.82Z" /></g><g id="checkbox-multiple-blank-outline"><path d="M20,16V4H8V16H20M22,16A2,2 0 0,1 20,18H8C6.89,18 6,17.1 6,16V4C6,2.89 6.89,2 8,2H20A2,2 0 0,1 22,4V16M16,20V22H4A2,2 0 0,1 2,20V7H4V20H16Z" /></g><g id="checkbox-multiple-marked"><path d="M22,16A2,2 0 0,1 20,18H8C6.89,18 6,17.1 6,16V4C6,2.89 6.89,2 8,2H20A2,2 0 0,1 22,4V16M16,20V22H4A2,2 0 0,1 2,20V7H4V20H16M13,14L20,7L18.59,5.59L13,11.17L9.91,8.09L8.5,9.5L13,14Z" /></g><g id="checkbox-multiple-marked-circle"><path d="M14,2A8,8 0 0,0 6,10A8,8 0 0,0 14,18A8,8 0 0,0 22,10A8,8 0 0,0 14,2M4.93,5.82C3.08,7.34 2,9.61 2,12A8,8 0 0,0 10,20C10.64,20 11.27,19.92 11.88,19.77C10.12,19.38 8.5,18.5 7.17,17.29C5.22,16.25 4,14.21 4,12C4,11.7 4.03,11.41 4.07,11.11C4.03,10.74 4,10.37 4,10C4,8.56 4.32,7.13 4.93,5.82M18.09,6.08L19.5,7.5L13,14L9.21,10.21L10.63,8.79L13,11.17" /></g><g id="checkbox-multiple-marked-circle-outline"><path d="M14,2A8,8 0 0,0 6,10A8,8 0 0,0 14,18A8,8 0 0,0 22,10H20C20,13.32 17.32,16 14,16A6,6 0 0,1 8,10A6,6 0 0,1 14,4C14.43,4 14.86,4.05 15.27,4.14L16.88,2.54C15.96,2.18 15,2 14,2M20.59,3.58L14,10.17L11.62,7.79L10.21,9.21L14,13L22,5M4.93,5.82C3.08,7.34 2,9.61 2,12A8,8 0 0,0 10,20C10.64,20 11.27,19.92 11.88,19.77C10.12,19.38 8.5,18.5 7.17,17.29C5.22,16.25 4,14.21 4,12C4,11.7 4.03,11.41 4.07,11.11C4.03,10.74 4,10.37 4,10C4,8.56 4.32,7.13 4.93,5.82Z" /></g><g id="checkbox-multiple-marked-outline"><path d="M20,16V10H22V16A2,2 0 0,1 20,18H8C6.89,18 6,17.1 6,16V4C6,2.89 6.89,2 8,2H16V4H8V16H20M10.91,7.08L14,10.17L20.59,3.58L22,5L14,13L9.5,8.5L10.91,7.08M16,20V22H4A2,2 0 0,1 2,20V7H4V20H16Z" /></g><g id="checkerboard"><path d="M3,3H21V21H3V3M5,5V12H12V19H19V12H12V5H5Z" /></g><g id="chemical-weapon"><path d="M11,7.83C9.83,7.42 9,6.3 9,5A3,3 0 0,1 12,2A3,3 0 0,1 15,5C15,6.31 14.16,7.42 13,7.83V10.64C12.68,10.55 12.35,10.5 12,10.5C11.65,10.5 11.32,10.55 11,10.64V7.83M18.3,21.1C17.16,20.45 16.62,19.18 16.84,17.96L14.4,16.55C14.88,16.09 15.24,15.5 15.4,14.82L17.84,16.23C18.78,15.42 20.16,15.26 21.29,15.91C22.73,16.74 23.22,18.57 22.39,20C21.56,21.44 19.73,21.93 18.3,21.1M2.7,15.9C3.83,15.25 5.21,15.42 6.15,16.22L8.6,14.81C8.76,15.5 9.11,16.08 9.6,16.54L7.15,17.95C7.38,19.17 6.83,20.45 5.7,21.1C4.26,21.93 2.43,21.44 1.6,20C0.77,18.57 1.26,16.73 2.7,15.9M14,14A2,2 0 0,1 12,16C10.89,16 10,15.1 10,14A2,2 0 0,1 12,12C13.11,12 14,12.9 14,14M17,14L16.97,14.57L15.5,13.71C15.4,12.64 14.83,11.71 14,11.12V9.41C15.77,10.19 17,11.95 17,14M14.97,18.03C14.14,18.64 13.11,19 12,19C10.89,19 9.86,18.64 9.03,18L10.5,17.17C10.96,17.38 11.47,17.5 12,17.5C12.53,17.5 13.03,17.38 13.5,17.17L14.97,18.03M7.03,14.56L7,14C7,11.95 8.23,10.19 10,9.42V11.13C9.17,11.71 8.6,12.64 8.5,13.7L7.03,14.56Z" /></g><g id="chevron-double-down"><path d="M16.59,5.59L18,7L12,13L6,7L7.41,5.59L12,10.17L16.59,5.59M16.59,11.59L18,13L12,19L6,13L7.41,11.59L12,16.17L16.59,11.59Z" /></g><g id="chevron-double-left"><path d="M18.41,7.41L17,6L11,12L17,18L18.41,16.59L13.83,12L18.41,7.41M12.41,7.41L11,6L5,12L11,18L12.41,16.59L7.83,12L12.41,7.41Z" /></g><g id="chevron-double-right"><path d="M5.59,7.41L7,6L13,12L7,18L5.59,16.59L10.17,12L5.59,7.41M11.59,7.41L13,6L19,12L13,18L11.59,16.59L16.17,12L11.59,7.41Z" /></g><g id="chevron-double-up"><path d="M7.41,18.41L6,17L12,11L18,17L16.59,18.41L12,13.83L7.41,18.41M7.41,12.41L6,11L12,5L18,11L16.59,12.41L12,7.83L7.41,12.41Z" /></g><g id="chevron-down"><path d="M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z" /></g><g id="chevron-left"><path d="M15.41,16.58L10.83,12L15.41,7.41L14,6L8,12L14,18L15.41,16.58Z" /></g><g id="chevron-right"><path d="M8.59,16.58L13.17,12L8.59,7.41L10,6L16,12L10,18L8.59,16.58Z" /></g><g id="chevron-up"><path d="M7.41,15.41L12,10.83L16.59,15.41L18,14L12,8L6,14L7.41,15.41Z" /></g><g id="chip"><path d="M6,4H18V5H21V7H18V9H21V11H18V13H21V15H18V17H21V19H18V20H6V19H3V17H6V15H3V13H6V11H3V9H6V7H3V5H6V4M11,15V18H12V15H11M13,15V18H14V15H13M15,15V18H16V15H15Z" /></g><g id="church"><path d="M11,2H13V4H15V6H13V9.4L22,13V15L20,14.2V22H14V17A2,2 0 0,0 12,15A2,2 0 0,0 10,17V22H4V14.2L2,15V13L11,9.4V6H9V4H11V2M6,20H8V15L7,14L6,15V20M16,20H18V15L17,14L16,15V20Z" /></g><g id="cisco-webex"><path d="M12,3A9,9 0 0,1 21,12A9,9 0 0,1 12,21A9,9 0 0,1 3,12A9,9 0 0,1 12,3M5.94,8.5C4,11.85 5.15,16.13 8.5,18.06C11.85,20 18.85,7.87 15.5,5.94C12.15,4 7.87,5.15 5.94,8.5Z" /></g><g id="city"><path d="M19,15H17V13H19M19,19H17V17H19M13,7H11V5H13M13,11H11V9H13M13,15H11V13H13M13,19H11V17H13M7,11H5V9H7M7,15H5V13H7M7,19H5V17H7M15,11V5L12,2L9,5V7H3V21H21V11H15Z" /></g><g id="clipboard"><path d="M9,4A3,3 0 0,1 12,1A3,3 0 0,1 15,4H19A2,2 0 0,1 21,6V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V6A2,2 0 0,1 5,4H9M12,3A1,1 0 0,0 11,4A1,1 0 0,0 12,5A1,1 0 0,0 13,4A1,1 0 0,0 12,3Z" /></g><g id="clipboard-account"><path d="M18,19H6V17.6C6,15.6 10,14.5 12,14.5C14,14.5 18,15.6 18,17.6M12,7A3,3 0 0,1 15,10A3,3 0 0,1 12,13A3,3 0 0,1 9,10A3,3 0 0,1 12,7M12,3A1,1 0 0,1 13,4A1,1 0 0,1 12,5A1,1 0 0,1 11,4A1,1 0 0,1 12,3M19,3H14.82C14.4,1.84 13.3,1 12,1C10.7,1 9.6,1.84 9.18,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3Z" /></g><g id="clipboard-alert"><path d="M12,5A1,1 0 0,1 11,4A1,1 0 0,1 12,3A1,1 0 0,1 13,4A1,1 0 0,1 12,5M13,14H11V8H13M13,18H11V16H13M19,3H14.82C14.4,1.84 13.3,1 12,1C10.7,1 9.6,1.84 9.18,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3Z" /></g><g id="clipboard-arrow-down"><path d="M12,18L7,13H10V9H14V13H17M12,3A1,1 0 0,1 13,4A1,1 0 0,1 12,5A1,1 0 0,1 11,4A1,1 0 0,1 12,3M19,3H14.82C14.4,1.84 13.3,1 12,1C10.7,1 9.6,1.84 9.18,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3Z" /></g><g id="clipboard-arrow-left"><path d="M16,15H12V18L7,13L12,8V11H16M12,3A1,1 0 0,1 13,4A1,1 0 0,1 12,5A1,1 0 0,1 11,4A1,1 0 0,1 12,3M19,3H14.82C14.4,1.84 13.3,1 12,1C10.7,1 9.6,1.84 9.18,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3Z" /></g><g id="clipboard-check"><path d="M10,17L6,13L7.41,11.59L10,14.17L16.59,7.58L18,9M12,3A1,1 0 0,1 13,4A1,1 0 0,1 12,5A1,1 0 0,1 11,4A1,1 0 0,1 12,3M19,3H14.82C14.4,1.84 13.3,1 12,1C10.7,1 9.6,1.84 9.18,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3Z" /></g><g id="clipboard-outline"><path d="M7,8V6H5V19H19V6H17V8H7M9,4A3,3 0 0,1 12,1A3,3 0 0,1 15,4H19A2,2 0 0,1 21,6V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V6A2,2 0 0,1 5,4H9M12,3A1,1 0 0,0 11,4A1,1 0 0,0 12,5A1,1 0 0,0 13,4A1,1 0 0,0 12,3Z" /></g><g id="clipboard-text"><path d="M17,9H7V7H17M17,13H7V11H17M14,17H7V15H14M12,3A1,1 0 0,1 13,4A1,1 0 0,1 12,5A1,1 0 0,1 11,4A1,1 0 0,1 12,3M19,3H14.82C14.4,1.84 13.3,1 12,1C10.7,1 9.6,1.84 9.18,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3Z" /></g><g id="clippy"><path d="M15,15.5A2.5,2.5 0 0,1 12.5,18A2.5,2.5 0 0,1 10,15.5V13.75A0.75,0.75 0 0,1 10.75,13A0.75,0.75 0 0,1 11.5,13.75V15.5A1,1 0 0,0 12.5,16.5A1,1 0 0,0 13.5,15.5V11.89C12.63,11.61 12,10.87 12,10C12,8.9 13,8 14.25,8C15.5,8 16.5,8.9 16.5,10C16.5,10.87 15.87,11.61 15,11.89V15.5M8.25,8C9.5,8 10.5,8.9 10.5,10C10.5,10.87 9.87,11.61 9,11.89V17.25A3.25,3.25 0 0,0 12.25,20.5A3.25,3.25 0 0,0 15.5,17.25V13.75A0.75,0.75 0 0,1 16.25,13A0.75,0.75 0 0,1 17,13.75V17.25A4.75,4.75 0 0,1 12.25,22A4.75,4.75 0 0,1 7.5,17.25V11.89C6.63,11.61 6,10.87 6,10C6,8.9 7,8 8.25,8M10.06,6.13L9.63,7.59C9.22,7.37 8.75,7.25 8.25,7.25C7.34,7.25 6.53,7.65 6.03,8.27L4.83,7.37C5.46,6.57 6.41,6 7.5,5.81V5.75A3.75,3.75 0 0,1 11.25,2A3.75,3.75 0 0,1 15,5.75V5.81C16.09,6 17.04,6.57 17.67,7.37L16.47,8.27C15.97,7.65 15.16,7.25 14.25,7.25C13.75,7.25 13.28,7.37 12.87,7.59L12.44,6.13C12.77,6 13.13,5.87 13.5,5.81V5.75C13.5,4.5 12.5,3.5 11.25,3.5C10,3.5 9,4.5 9,5.75V5.81C9.37,5.87 9.73,6 10.06,6.13M14.25,9.25C13.7,9.25 13.25,9.59 13.25,10C13.25,10.41 13.7,10.75 14.25,10.75C14.8,10.75 15.25,10.41 15.25,10C15.25,9.59 14.8,9.25 14.25,9.25M8.25,9.25C7.7,9.25 7.25,9.59 7.25,10C7.25,10.41 7.7,10.75 8.25,10.75C8.8,10.75 9.25,10.41 9.25,10C9.25,9.59 8.8,9.25 8.25,9.25Z" /></g><g id="clock"><path d="M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22C6.47,22 2,17.5 2,12A10,10 0 0,1 12,2M12.5,7V12.25L17,14.92L16.25,16.15L11,13V7H12.5Z" /></g><g id="clock-alert"><path d="M12,2C6.47,2 2,6.5 2,12A10,10 0 0,0 12,22C14.25,22 16.33,21.24 18,20V17.28C16.53,18.94 14.39,20 12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4C15.36,4 18.23,6.07 19.41,9H21.54C20.27,4.94 16.5,2 12,2M11,7V13L16.25,16.15L17,14.92L12.5,12.25V7H11M20,11V18H22V11H20M20,20V22H22V20H20Z" /></g><g id="clock-end"><path d="M12,1C8.14,1 5,4.14 5,8A7,7 0 0,0 12,15C15.86,15 19,11.87 19,8C19,4.14 15.86,1 12,1M12,3.15C14.67,3.15 16.85,5.32 16.85,8C16.85,10.68 14.67,12.85 12,12.85A4.85,4.85 0 0,1 7.15,8A4.85,4.85 0 0,1 12,3.15M11,5V8.69L14.19,10.53L14.94,9.23L12.5,7.82V5M15,16V19H3V21H15V24L19,20M19,20V24H21V16H19" /></g><g id="clock-fast"><path d="M15,4A8,8 0 0,1 23,12A8,8 0 0,1 15,20A8,8 0 0,1 7,12A8,8 0 0,1 15,4M15,6A6,6 0 0,0 9,12A6,6 0 0,0 15,18A6,6 0 0,0 21,12A6,6 0 0,0 15,6M14,8H15.5V11.78L17.83,14.11L16.77,15.17L14,12.4V8M2,18A1,1 0 0,1 1,17A1,1 0 0,1 2,16H5.83C6.14,16.71 6.54,17.38 7,18H2M3,13A1,1 0 0,1 2,12A1,1 0 0,1 3,11H5.05L5,12L5.05,13H3M4,8A1,1 0 0,1 3,7A1,1 0 0,1 4,6H7C6.54,6.62 6.14,7.29 5.83,8H4Z" /></g><g id="clock-in"><path d="M2.21,0.79L0.79,2.21L4.8,6.21L3,8H8V3L6.21,4.8M12,8C8.14,8 5,11.13 5,15A7,7 0 0,0 12,22C15.86,22 19,18.87 19,15A7,7 0 0,0 12,8M12,10.15C14.67,10.15 16.85,12.32 16.85,15A4.85,4.85 0 0,1 12,19.85C9.32,19.85 7.15,17.68 7.15,15A4.85,4.85 0 0,1 12,10.15M11,12V15.69L14.19,17.53L14.94,16.23L12.5,14.82V12" /></g><g id="clock-out"><path d="M18,1L19.8,2.79L15.79,6.79L17.21,8.21L21.21,4.21L23,6V1M12,8C8.14,8 5,11.13 5,15A7,7 0 0,0 12,22C15.86,22 19,18.87 19,15A7,7 0 0,0 12,8M12,10.15C14.67,10.15 16.85,12.32 16.85,15A4.85,4.85 0 0,1 12,19.85C9.32,19.85 7.15,17.68 7.15,15A4.85,4.85 0 0,1 12,10.15M11,12V15.69L14.19,17.53L14.94,16.23L12.5,14.82V12" /></g><g id="clock-start"><path d="M12,1C8.14,1 5,4.14 5,8A7,7 0 0,0 12,15C15.86,15 19,11.87 19,8C19,4.14 15.86,1 12,1M12,3.15C14.67,3.15 16.85,5.32 16.85,8C16.85,10.68 14.67,12.85 12,12.85A4.85,4.85 0 0,1 7.15,8A4.85,4.85 0 0,1 12,3.15M11,5V8.69L14.19,10.53L14.94,9.23L12.5,7.82V5M4,16V24H6V21H18V24L22,20L18,16V19H6V16" /></g><g id="close"><path d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" /></g><g id="close-box"><path d="M19,3H16.3H7.7H5A2,2 0 0,0 3,5V7.7V16.4V19A2,2 0 0,0 5,21H7.7H16.4H19A2,2 0 0,0 21,19V16.3V7.7V5A2,2 0 0,0 19,3M15.6,17L12,13.4L8.4,17L7,15.6L10.6,12L7,8.4L8.4,7L12,10.6L15.6,7L17,8.4L13.4,12L17,15.6L15.6,17Z" /></g><g id="close-box-outline"><path d="M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3M19,19H5V5H19V19M17,8.4L13.4,12L17,15.6L15.6,17L12,13.4L8.4,17L7,15.6L10.6,12L7,8.4L8.4,7L12,10.6L15.6,7L17,8.4Z" /></g><g id="close-circle"><path d="M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z" /></g><g id="close-circle-outline"><path d="M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2C6.47,2 2,6.47 2,12C2,17.53 6.47,22 12,22C17.53,22 22,17.53 22,12C22,6.47 17.53,2 12,2M14.59,8L12,10.59L9.41,8L8,9.41L10.59,12L8,14.59L9.41,16L12,13.41L14.59,16L16,14.59L13.41,12L16,9.41L14.59,8Z" /></g><g id="close-network"><path d="M14.59,6L12,8.59L9.41,6L8,7.41L10.59,10L8,12.59L9.41,14L12,11.41L14.59,14L16,12.59L13.41,10L16,7.41L14.59,6M17,3A2,2 0 0,1 19,5V15A2,2 0 0,1 17,17H13V19H14A1,1 0 0,1 15,20H22V22H15A1,1 0 0,1 14,23H10A1,1 0 0,1 9,22H2V20H9A1,1 0 0,1 10,19H11V17H7C5.89,17 5,16.1 5,15V5A2,2 0 0,1 7,3H17Z" /></g><g id="close-octagon"><path d="M8.27,3L3,8.27V15.73L8.27,21H15.73L21,15.73V8.27L15.73,3M8.41,7L12,10.59L15.59,7L17,8.41L13.41,12L17,15.59L15.59,17L12,13.41L8.41,17L7,15.59L10.59,12L7,8.41" /></g><g id="close-octagon-outline"><path d="M8.27,3L3,8.27V15.73L8.27,21H15.73C17.5,19.24 21,15.73 21,15.73V8.27L15.73,3M9.1,5H14.9L19,9.1V14.9L14.9,19H9.1L5,14.9V9.1M9.12,7.71L7.71,9.12L10.59,12L7.71,14.88L9.12,16.29L12,13.41L14.88,16.29L16.29,14.88L13.41,12L16.29,9.12L14.88,7.71L12,10.59" /></g><g id="closed-caption"><path d="M18,11H16.5V10.5H14.5V13.5H16.5V13H18V14A1,1 0 0,1 17,15H14A1,1 0 0,1 13,14V10A1,1 0 0,1 14,9H17A1,1 0 0,1 18,10M11,11H9.5V10.5H7.5V13.5H9.5V13H11V14A1,1 0 0,1 10,15H7A1,1 0 0,1 6,14V10A1,1 0 0,1 7,9H10A1,1 0 0,1 11,10M19,4H5C3.89,4 3,4.89 3,6V18A2,2 0 0,0 5,20H19A2,2 0 0,0 21,18V6C21,4.89 20.1,4 19,4Z" /></g><g id="cloud"><path d="M19.35,10.03C18.67,6.59 15.64,4 12,4C9.11,4 6.6,5.64 5.35,8.03C2.34,8.36 0,10.9 0,14A6,6 0 0,0 6,20H19A5,5 0 0,0 24,15C24,12.36 21.95,10.22 19.35,10.03Z" /></g><g id="cloud-check"><path d="M10,17L6.5,13.5L7.91,12.08L10,14.17L15.18,9L16.59,10.41M19.35,10.03C18.67,6.59 15.64,4 12,4C9.11,4 6.6,5.64 5.35,8.03C2.34,8.36 0,10.9 0,14A6,6 0 0,0 6,20H19A5,5 0 0,0 24,15C24,12.36 21.95,10.22 19.35,10.03Z" /></g><g id="cloud-circle"><path d="M16.5,16H8A3,3 0 0,1 5,13A3,3 0 0,1 8,10C8.05,10 8.09,10 8.14,10C8.58,8.28 10.13,7 12,7A4,4 0 0,1 16,11H16.5A2.5,2.5 0 0,1 19,13.5A2.5,2.5 0 0,1 16.5,16M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" /></g><g id="cloud-download"><path d="M17,13L12,18L7,13H10V9H14V13M19.35,10.03C18.67,6.59 15.64,4 12,4C9.11,4 6.6,5.64 5.35,8.03C2.34,8.36 0,10.9 0,14A6,6 0 0,0 6,20H19A5,5 0 0,0 24,15C24,12.36 21.95,10.22 19.35,10.03Z" /></g><g id="cloud-outline"><path d="M19,18H6A4,4 0 0,1 2,14A4,4 0 0,1 6,10H6.71C7.37,7.69 9.5,6 12,6A5.5,5.5 0 0,1 17.5,11.5V12H19A3,3 0 0,1 22,15A3,3 0 0,1 19,18M19.35,10.03C18.67,6.59 15.64,4 12,4C9.11,4 6.6,5.64 5.35,8.03C2.34,8.36 0,10.9 0,14A6,6 0 0,0 6,20H19A5,5 0 0,0 24,15C24,12.36 21.95,10.22 19.35,10.03Z" /></g><g id="cloud-outline-off"><path d="M7.73,10L15.73,18H6A4,4 0 0,1 2,14A4,4 0 0,1 6,10M3,5.27L5.75,8C2.56,8.15 0,10.77 0,14A6,6 0 0,0 6,20H17.73L19.73,22L21,20.73L4.27,4M19.35,10.03C18.67,6.59 15.64,4 12,4C10.5,4 9.15,4.43 8,5.17L9.45,6.63C10.21,6.23 11.08,6 12,6A5.5,5.5 0 0,1 17.5,11.5V12H19A3,3 0 0,1 22,15C22,16.13 21.36,17.11 20.44,17.62L21.89,19.07C23.16,18.16 24,16.68 24,15C24,12.36 21.95,10.22 19.35,10.03Z" /></g><g id="cloud-print"><path d="M12,2C9.11,2 6.6,3.64 5.35,6.04C2.34,6.36 0,8.91 0,12A6,6 0 0,0 6,18V22H18V18H19A5,5 0 0,0 24,13C24,10.36 21.95,8.22 19.35,8.04C18.67,4.59 15.64,2 12,2M8,13H16V20H8V13M9,14V15H15V14H9M9,16V17H15V16H9M9,18V19H15V18H9Z" /></g><g id="cloud-print-outline"><path d="M19,16A3,3 0 0,0 22,13A3,3 0 0,0 19,10H17.5V9.5A5.5,5.5 0 0,0 12,4C9.5,4 7.37,5.69 6.71,8H6A4,4 0 0,0 2,12A4,4 0 0,0 6,16V11H18V16H19M19.36,8.04C21.95,8.22 24,10.36 24,13A5,5 0 0,1 19,18H18V22H6V18A6,6 0 0,1 0,12C0,8.91 2.34,6.36 5.35,6.04C6.6,3.64 9.11,2 12,2C15.64,2 18.67,4.6 19.36,8.04M8,13V20H16V13H8M9,18H15V19H9V18M15,17H9V16H15V17M9,14H15V15H9V14Z" /></g><g id="cloud-sync"><path d="M12,4C15.64,4 18.67,6.59 19.35,10.04C21.95,10.22 24,12.36 24,15A5,5 0 0,1 19,20H6A6,6 0 0,1 0,14C0,10.91 2.34,8.36 5.35,8.04C6.6,5.64 9.11,4 12,4M7.5,9.69C6.06,11.5 6.2,14.06 7.82,15.68C8.66,16.5 9.81,17 11,17V18.86L13.83,16.04L11,13.21V15C10.34,15 9.7,14.74 9.23,14.27C8.39,13.43 8.26,12.11 8.92,11.12L7.5,9.69M9.17,8.97L10.62,10.42L12,11.79V10C12.66,10 13.3,10.26 13.77,10.73C14.61,11.57 14.74,12.89 14.08,13.88L15.5,15.31C16.94,13.5 16.8,10.94 15.18,9.32C14.34,8.5 13.19,8 12,8V6.14L9.17,8.97Z" /></g><g id="cloud-upload"><path d="M14,13V17H10V13H7L12,8L17,13M19.35,10.03C18.67,6.59 15.64,4 12,4C9.11,4 6.6,5.64 5.35,8.03C2.34,8.36 0,10.9 0,14A6,6 0 0,0 6,20H19A5,5 0 0,0 24,15C24,12.36 21.95,10.22 19.35,10.03Z" /></g><g id="code-array"><path d="M3,5A2,2 0 0,1 5,3H19A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5C3.89,21 3,20.1 3,19V5M6,6V18H10V16H8V8H10V6H6M16,16H14V18H18V6H14V8H16V16Z" /></g><g id="code-braces"><path d="M8,3A2,2 0 0,0 6,5V9A2,2 0 0,1 4,11H3V13H4A2,2 0 0,1 6,15V19A2,2 0 0,0 8,21H10V19H8V14A2,2 0 0,0 6,12A2,2 0 0,0 8,10V5H10V3M16,3A2,2 0 0,1 18,5V9A2,2 0 0,0 20,11H21V13H20A2,2 0 0,0 18,15V19A2,2 0 0,1 16,21H14V19H16V14A2,2 0 0,1 18,12A2,2 0 0,1 16,10V5H14V3H16Z" /></g><g id="code-brackets"><path d="M15,4V6H18V18H15V20H20V4M4,4V20H9V18H6V6H9V4H4Z" /></g><g id="code-equal"><path d="M6,13H11V15H6M13,13H18V15H13M13,9H18V11H13M6,9H11V11H6M5,3C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3H5Z" /></g><g id="code-greater-than"><path d="M10.41,7.41L15,12L10.41,16.6L9,15.18L12.18,12L9,8.82M5,3C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3H5Z" /></g><g id="code-greater-than-or-equal"><path d="M13,13H18V15H13M13,9H18V11H13M6.91,7.41L11.5,12L6.91,16.6L5.5,15.18L8.68,12L5.5,8.82M5,3C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3H5Z" /></g><g id="code-less-than"><path d="M13.59,7.41L9,12L13.59,16.6L15,15.18L11.82,12L15,8.82M19,3C20.11,3 21,3.9 21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3H19Z" /></g><g id="code-less-than-or-equal"><path d="M13,13H18V15H13M13,9H18V11H13M10.09,7.41L11.5,8.82L8.32,12L11.5,15.18L10.09,16.6L5.5,12M5,3C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3H5Z" /></g><g id="code-not-equal"><path d="M6,15H8V17H6M11,13H18V15H11M11,9H18V11H11M6,7H8V13H6M5,3C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3H5Z" /></g><g id="code-not-equal-variant"><path d="M11,6.5V9.33L8.33,12L11,14.67V17.5L5.5,12M13,6.43L18.57,12L13,17.57V14.74L15.74,12L13,9.26M5,3C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3H5Z" /></g><g id="code-parentheses"><path d="M17.62,3C19.13,5.27 20,8.55 20,12C20,15.44 19.13,18.72 17.62,21L16,19.96C17.26,18.07 18,15.13 18,12C18,8.87 17.26,5.92 16,4.03L17.62,3M6.38,3L8,4.04C6.74,5.92 6,8.87 6,12C6,15.13 6.74,18.08 8,19.96L6.38,21C4.87,18.73 4,15.45 4,12C4,8.55 4.87,5.27 6.38,3Z" /></g><g id="code-string"><path d="M3,5A2,2 0 0,1 5,3H19A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5C3.89,21 3,20.1 3,19V5M12.5,11H11.5A1.5,1.5 0 0,1 10,9.5A1.5,1.5 0 0,1 11.5,8H12.5A1.5,1.5 0 0,1 14,9.5H16A3.5,3.5 0 0,0 12.5,6H11.5A3.5,3.5 0 0,0 8,9.5A3.5,3.5 0 0,0 11.5,13H12.5A1.5,1.5 0 0,1 14,14.5A1.5,1.5 0 0,1 12.5,16H11.5A1.5,1.5 0 0,1 10,14.5H8A3.5,3.5 0 0,0 11.5,18H12.5A3.5,3.5 0 0,0 16,14.5A3.5,3.5 0 0,0 12.5,11Z" /></g><g id="code-tags"><path d="M14.6,16.6L19.2,12L14.6,7.4L16,6L22,12L16,18L14.6,16.6M9.4,16.6L4.8,12L9.4,7.4L8,6L2,12L8,18L9.4,16.6Z" /></g><g id="code-tags-check"><path d="M6.59,3.41L2,8L6.59,12.6L8,11.18L4.82,8L8,4.82L6.59,3.41M12.41,3.41L11,4.82L14.18,8L11,11.18L12.41,12.6L17,8L12.41,3.41M21.59,11.59L13.5,19.68L9.83,16L8.42,17.41L13.5,22.5L23,13L21.59,11.59Z" /></g><g id="codepen"><path d="M19.45,13.29L17.5,12L19.45,10.71M12.77,18.78V15.17L16.13,12.93L18.83,14.74M12,13.83L9.26,12L12,10.17L14.74,12M11.23,18.78L5.17,14.74L7.87,12.93L11.23,15.17M4.55,10.71L6.5,12L4.55,13.29M11.23,5.22V8.83L7.87,11.07L5.17,9.26M12.77,5.22L18.83,9.26L16.13,11.07L12.77,8.83M21,9.16C21,9.15 21,9.13 21,9.12C21,9.1 21,9.08 20.97,9.06C20.97,9.05 20.97,9.03 20.96,9C20.96,9 20.95,9 20.94,8.96C20.94,8.95 20.93,8.94 20.92,8.93C20.92,8.91 20.91,8.89 20.9,8.88C20.89,8.86 20.88,8.85 20.88,8.84C20.87,8.82 20.85,8.81 20.84,8.79C20.83,8.78 20.83,8.77 20.82,8.76A0.04,0.04 0 0,0 20.78,8.72C20.77,8.71 20.76,8.7 20.75,8.69C20.73,8.67 20.72,8.66 20.7,8.65C20.69,8.64 20.68,8.63 20.67,8.62C20.66,8.62 20.66,8.62 20.66,8.61L12.43,3.13C12.17,2.96 11.83,2.96 11.57,3.13L3.34,8.61C3.34,8.62 3.34,8.62 3.33,8.62C3.32,8.63 3.31,8.64 3.3,8.65C3.28,8.66 3.27,8.67 3.25,8.69C3.24,8.7 3.23,8.71 3.22,8.72C3.21,8.73 3.2,8.74 3.18,8.76C3.17,8.77 3.17,8.78 3.16,8.79C3.15,8.81 3.13,8.82 3.12,8.84C3.12,8.85 3.11,8.86 3.1,8.88C3.09,8.89 3.08,8.91 3.08,8.93C3.07,8.94 3.06,8.95 3.06,8.96C3.05,9 3.05,9 3.04,9C3.03,9.03 3.03,9.05 3.03,9.06C3,9.08 3,9.1 3,9.12C3,9.13 3,9.15 3,9.16C3,9.19 3,9.22 3,9.26V14.74C3,14.78 3,14.81 3,14.84C3,14.85 3,14.87 3,14.88C3,14.9 3,14.92 3.03,14.94C3.03,14.95 3.03,14.97 3.04,15C3.05,15 3.05,15 3.06,15.04C3.06,15.05 3.07,15.06 3.08,15.07C3.08,15.09 3.09,15.11 3.1,15.12C3.11,15.14 3.12,15.15 3.12,15.16C3.13,15.18 3.15,15.19 3.16,15.21C3.17,15.22 3.17,15.23 3.18,15.24C3.2,15.25 3.21,15.27 3.22,15.28C3.23,15.29 3.24,15.3 3.25,15.31C3.27,15.33 3.28,15.34 3.3,15.35C3.31,15.36 3.32,15.37 3.33,15.38C3.34,15.38 3.34,15.38 3.34,15.39L11.57,20.87C11.7,20.96 11.85,21 12,21C12.15,21 12.3,20.96 12.43,20.87L20.66,15.39C20.66,15.38 20.66,15.38 20.67,15.38C20.68,15.37 20.69,15.36 20.7,15.35C20.72,15.34 20.73,15.33 20.75,15.31C20.76,15.3 20.77,15.29 20.78,15.28C20.79,15.27 20.8,15.25 20.82,15.24C20.83,15.23 20.83,15.22 20.84,15.21C20.85,15.19 20.87,15.18 20.88,15.16C20.88,15.15 20.89,15.14 20.9,15.12C20.91,15.11 20.92,15.09 20.92,15.07C20.93,15.06 20.94,15.05 20.94,15.04C20.95,15 20.96,15 20.96,15C20.97,14.97 20.97,14.95 20.97,14.94C21,14.92 21,14.9 21,14.88C21,14.87 21,14.85 21,14.84C21,14.81 21,14.78 21,14.74V9.26C21,9.22 21,9.19 21,9.16Z" /></g><g id="coffee"><path d="M2,21H20V19H2M20,8H18V5H20M20,3H4V13A4,4 0 0,0 8,17H14A4,4 0 0,0 18,13V10H20A2,2 0 0,0 22,8V5C22,3.89 21.1,3 20,3Z" /></g><g id="coffee-to-go"><path d="M3,19V17H17L15.26,15.24L16.67,13.83L20.84,18L16.67,22.17L15.26,20.76L17,19H3M17,8V5H15V8H17M17,3C18.11,3 19,3.9 19,5V8C19,9.11 18.11,10 17,10H15V11A4,4 0 0,1 11,15H7A4,4 0 0,1 3,11V3H17Z" /></g><g id="coin"><path d="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4M11,17V16H9V14H13V13H10A1,1 0 0,1 9,12V9A1,1 0 0,1 10,8H11V7H13V8H15V10H11V11H14A1,1 0 0,1 15,12V15A1,1 0 0,1 14,16H13V17H11Z" /></g><g id="coins"><path d="M15,4A8,8 0 0,1 23,12A8,8 0 0,1 15,20A8,8 0 0,1 7,12A8,8 0 0,1 15,4M15,18A6,6 0 0,0 21,12A6,6 0 0,0 15,6A6,6 0 0,0 9,12A6,6 0 0,0 15,18M3,12C3,14.61 4.67,16.83 7,17.65V19.74C3.55,18.85 1,15.73 1,12C1,8.27 3.55,5.15 7,4.26V6.35C4.67,7.17 3,9.39 3,12Z" /></g><g id="collage"><path d="M5,3C3.89,3 3,3.89 3,5V19C3,20.11 3.89,21 5,21H11V3M13,3V11H21V5C21,3.89 20.11,3 19,3M13,13V21H19C20.11,21 21,20.11 21,19V13" /></g><g id="color-helper"><path d="M0,24H24V20H0V24Z" /></g><g id="comment"><path d="M9,22A1,1 0 0,1 8,21V18H4A2,2 0 0,1 2,16V4C2,2.89 2.9,2 4,2H20A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H13.9L10.2,21.71C10,21.9 9.75,22 9.5,22V22H9Z" /></g><g id="comment-account"><path d="M9,22A1,1 0 0,1 8,21V18H4A2,2 0 0,1 2,16V4C2,2.89 2.9,2 4,2H20A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H13.9L10.2,21.71C10,21.9 9.75,22 9.5,22V22H9M16,14V13C16,11.67 13.33,11 12,11C10.67,11 8,11.67 8,13V14H16M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6Z" /></g><g id="comment-account-outline"><path d="M9,22A1,1 0 0,1 8,21V18H4A2,2 0 0,1 2,16V4C2,2.89 2.9,2 4,2H20A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H13.9L10.2,21.71C10,21.9 9.75,22 9.5,22V22H9M10,16V19.08L13.08,16H20V4H4V16H10M16,14H8V13C8,11.67 10.67,11 12,11C13.33,11 16,11.67 16,13V14M12,6A2,2 0 0,1 14,8A2,2 0 0,1 12,10A2,2 0 0,1 10,8A2,2 0 0,1 12,6Z" /></g><g id="comment-alert"><path d="M9,22A1,1 0 0,1 8,21V18H4A2,2 0 0,1 2,16V4C2,2.89 2.9,2 4,2H20A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H13.9L10.2,21.71C10,21.9 9.75,22 9.5,22V22H9M13,10V6H11V10H13M13,14V12H11V14H13Z" /></g><g id="comment-alert-outline"><path d="M9,22A1,1 0 0,1 8,21V18H4A2,2 0 0,1 2,16V4C2,2.89 2.9,2 4,2H20A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H13.9L10.2,21.71C10,21.9 9.75,22 9.5,22V22H9M10,16V19.08L13.08,16H20V4H4V16H10M13,10H11V6H13V10M13,14H11V12H13V14Z" /></g><g id="comment-check"><path d="M9,22A1,1 0 0,1 8,21V18H4A2,2 0 0,1 2,16V4C2,2.89 2.9,2 4,2H20A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H13.9L10.2,21.71C10,21.9 9.75,22 9.5,22V22H9M10,15L18,7L16.59,5.58L10,12.17L7.41,9.59L6,11L10,15Z" /></g><g id="comment-check-outline"><path d="M9,22A1,1 0 0,1 8,21V18H4A2,2 0 0,1 2,16V4C2,2.89 2.9,2 4,2H20A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H13.9L10.2,21.71C10,21.9 9.75,22 9.5,22V22H9M10,16V19.08L13.08,16H20V4H4V16H10M16.5,8L11,13.5L7.5,10L8.91,8.59L11,10.67L15.09,6.59L16.5,8Z" /></g><g id="comment-multiple-outline"><path d="M12,23A1,1 0 0,1 11,22V19H7A2,2 0 0,1 5,17V7C5,5.89 5.9,5 7,5H21A2,2 0 0,1 23,7V17A2,2 0 0,1 21,19H16.9L13.2,22.71C13,22.9 12.75,23 12.5,23V23H12M13,17V20.08L16.08,17H21V7H7V17H13M3,15H1V3A2,2 0 0,1 3,1H19V3H3V15Z" /></g><g id="comment-outline"><path d="M9,22A1,1 0 0,1 8,21V18H4A2,2 0 0,1 2,16V4C2,2.89 2.9,2 4,2H20A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H13.9L10.2,21.71C10,21.9 9.75,22 9.5,22V22H9M10,16V19.08L13.08,16H20V4H4V16H10Z" /></g><g id="comment-plus-outline"><path d="M9,22A1,1 0 0,1 8,21V18H4A2,2 0 0,1 2,16V4C2,2.89 2.9,2 4,2H20A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H13.9L10.2,21.71C10,21.9 9.75,22 9.5,22V22H9M10,16V19.08L13.08,16H20V4H4V16H10M11,6H13V9H16V11H13V14H11V11H8V9H11V6Z" /></g><g id="comment-processing"><path d="M9,22A1,1 0 0,1 8,21V18H4A2,2 0 0,1 2,16V4C2,2.89 2.9,2 4,2H20A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H13.9L10.2,21.71C10,21.9 9.75,22 9.5,22V22H9M17,11V9H15V11H17M13,11V9H11V11H13M9,11V9H7V11H9Z" /></g><g id="comment-processing-outline"><path d="M9,22A1,1 0 0,1 8,21V18H4A2,2 0 0,1 2,16V4C2,2.89 2.9,2 4,2H20A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H13.9L10.2,21.71C10,21.9 9.75,22 9.5,22V22H9M10,16V19.08L13.08,16H20V4H4V16H10M17,11H15V9H17V11M13,11H11V9H13V11M9,11H7V9H9V11Z" /></g><g id="comment-question-outline"><path d="M4,2A2,2 0 0,0 2,4V16A2,2 0 0,0 4,18H8V21A1,1 0 0,0 9,22H9.5V22C9.75,22 10,21.9 10.2,21.71L13.9,18H20A2,2 0 0,0 22,16V4C22,2.89 21.1,2 20,2H4M4,4H20V16H13.08L10,19.08V16H4V4M12.19,5.5C11.3,5.5 10.59,5.68 10.05,6.04C9.5,6.4 9.22,7 9.27,7.69C0.21,7.69 6.57,7.69 11.24,7.69C11.24,7.41 11.34,7.2 11.5,7.06C11.7,6.92 11.92,6.85 12.19,6.85C12.5,6.85 12.77,6.93 12.95,7.11C13.13,7.28 13.22,7.5 13.22,7.8C13.22,8.08 13.14,8.33 13,8.54C12.83,8.76 12.62,8.94 12.36,9.08C11.84,9.4 11.5,9.68 11.29,9.92C11.1,10.16 11,10.5 11,11H13C13,10.72 13.05,10.5 13.14,10.32C13.23,10.15 13.4,10 13.66,9.85C14.12,9.64 14.5,9.36 14.79,9C15.08,8.63 15.23,8.24 15.23,7.8C15.23,7.1 14.96,6.54 14.42,6.12C13.88,5.71 13.13,5.5 12.19,5.5M11,12V14H13V12H11Z" /></g><g id="comment-remove-outline"><path d="M9,22A1,1 0 0,1 8,21V18H4A2,2 0 0,1 2,16V4C2,2.89 2.9,2 4,2H20A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H13.9L10.2,21.71C10,21.9 9.75,22 9.5,22V22H9M10,16V19.08L13.08,16H20V4H4V16H10M9.41,6L12,8.59L14.59,6L16,7.41L13.41,10L16,12.59L14.59,14L12,11.41L9.41,14L8,12.59L10.59,10L8,7.41L9.41,6Z" /></g><g id="comment-text"><path d="M9,22A1,1 0 0,1 8,21V18H4A2,2 0 0,1 2,16V4C2,2.89 2.9,2 4,2H20A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H13.9L10.2,21.71C10,21.9 9.75,22 9.5,22V22H9M5,5V7H19V5H5M5,9V11H13V9H5M5,13V15H15V13H5Z" /></g><g id="comment-text-outline"><path d="M9,22A1,1 0 0,1 8,21V18H4A2,2 0 0,1 2,16V4C2,2.89 2.9,2 4,2H20A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H13.9L10.2,21.71C10,21.9 9.75,22 9.5,22V22H9M10,16V19.08L13.08,16H20V4H4V16H10M6,7H18V9H6V7M6,11H15V13H6V11Z" /></g><g id="compare"><path d="M19,3H14V5H19V18L14,12V21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3M10,18H5L10,12M10,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H10V23H12V1H10V3Z" /></g><g id="compass"><path d="M14.19,14.19L6,18L9.81,9.81L18,6M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,10.9A1.1,1.1 0 0,0 10.9,12A1.1,1.1 0 0,0 12,13.1A1.1,1.1 0 0,0 13.1,12A1.1,1.1 0 0,0 12,10.9Z" /></g><g id="compass-outline"><path d="M7,17L10.2,10.2L17,7L13.8,13.8L7,17M12,11.1A0.9,0.9 0 0,0 11.1,12A0.9,0.9 0 0,0 12,12.9A0.9,0.9 0 0,0 12.9,12A0.9,0.9 0 0,0 12,11.1M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z" /></g><g id="console"><path d="M20,19V7H4V19H20M20,3A2,2 0 0,1 22,5V19A2,2 0 0,1 20,21H4A2,2 0 0,1 2,19V5C2,3.89 2.9,3 4,3H20M13,17V15H18V17H13M9.58,13L5.57,9H8.4L11.7,12.3C12.09,12.69 12.09,13.33 11.7,13.72L8.42,17H5.59L9.58,13Z" /></g><g id="contact-mail"><path d="M21,8V7L18,9L15,7V8L18,10M22,3H2A2,2 0 0,0 0,5V19A2,2 0 0,0 2,21H22A2,2 0 0,0 24,19V5A2,2 0 0,0 22,3M8,6A3,3 0 0,1 11,9A3,3 0 0,1 8,12A3,3 0 0,1 5,9A3,3 0 0,1 8,6M14,18H2V17C2,15 6,13.9 8,13.9C10,13.9 14,15 14,17M22,12H14V6H22" /></g><g id="contacts"><path d="M20,0H4V2H20V0M4,24H20V22H4V24M20,4H4A2,2 0 0,0 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V6A2,2 0 0,0 20,4M12,6.75A2.25,2.25 0 0,1 14.25,9A2.25,2.25 0 0,1 12,11.25A2.25,2.25 0 0,1 9.75,9A2.25,2.25 0 0,1 12,6.75M17,17H7V15.5C7,13.83 10.33,13 12,13C13.67,13 17,13.83 17,15.5V17Z" /></g><g id="content-copy"><path d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z" /></g><g id="content-cut"><path d="M19,3L13,9L15,11L22,4V3M12,12.5A0.5,0.5 0 0,1 11.5,12A0.5,0.5 0 0,1 12,11.5A0.5,0.5 0 0,1 12.5,12A0.5,0.5 0 0,1 12,12.5M6,20A2,2 0 0,1 4,18C4,16.89 4.9,16 6,16A2,2 0 0,1 8,18C8,19.11 7.1,20 6,20M6,8A2,2 0 0,1 4,6C4,4.89 4.9,4 6,4A2,2 0 0,1 8,6C8,7.11 7.1,8 6,8M9.64,7.64C9.87,7.14 10,6.59 10,6A4,4 0 0,0 6,2A4,4 0 0,0 2,6A4,4 0 0,0 6,10C6.59,10 7.14,9.87 7.64,9.64L10,12L7.64,14.36C7.14,14.13 6.59,14 6,14A4,4 0 0,0 2,18A4,4 0 0,0 6,22A4,4 0 0,0 10,18C10,17.41 9.87,16.86 9.64,16.36L12,14L19,21H22V20L9.64,7.64Z" /></g><g id="content-duplicate"><path d="M11,17H4A2,2 0 0,1 2,15V3A2,2 0 0,1 4,1H16V3H4V15H11V13L15,16L11,19V17M19,21V7H8V13H6V7A2,2 0 0,1 8,5H19A2,2 0 0,1 21,7V21A2,2 0 0,1 19,23H8A2,2 0 0,1 6,21V19H8V21H19Z" /></g><g id="content-paste"><path d="M19,20H5V4H7V7H17V4H19M12,2A1,1 0 0,1 13,3A1,1 0 0,1 12,4A1,1 0 0,1 11,3A1,1 0 0,1 12,2M19,2H14.82C14.4,0.84 13.3,0 12,0C10.7,0 9.6,0.84 9.18,2H5A2,2 0 0,0 3,4V20A2,2 0 0,0 5,22H19A2,2 0 0,0 21,20V4A2,2 0 0,0 19,2Z" /></g><g id="content-save"><path d="M15,9H5V5H15M12,19A3,3 0 0,1 9,16A3,3 0 0,1 12,13A3,3 0 0,1 15,16A3,3 0 0,1 12,19M17,3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V7L17,3Z" /></g><g id="content-save-all"><path d="M17,7V3H7V7H17M14,17A3,3 0 0,0 17,14A3,3 0 0,0 14,11A3,3 0 0,0 11,14A3,3 0 0,0 14,17M19,1L23,5V17A2,2 0 0,1 21,19H7C5.89,19 5,18.1 5,17V3A2,2 0 0,1 7,1H19M1,7H3V21H17V23H3A2,2 0 0,1 1,21V7Z" /></g><g id="content-save-settings"><path d="M15,8V4H5V8H15M12,18A3,3 0 0,0 15,15A3,3 0 0,0 12,12A3,3 0 0,0 9,15A3,3 0 0,0 12,18M17,2L21,6V18A2,2 0 0,1 19,20H5C3.89,20 3,19.1 3,18V4A2,2 0 0,1 5,2H17M11,22H13V24H11V22M7,22H9V24H7V22M15,22H17V24H15V22Z" /></g><g id="contrast"><path d="M4.38,20.9C3.78,20.71 3.3,20.23 3.1,19.63L19.63,3.1C20.23,3.3 20.71,3.78 20.9,4.38L4.38,20.9M20,16V18H13V16H20M3,6H6V3H8V6H11V8H8V11H6V8H3V6Z" /></g><g id="contrast-box"><path d="M17,15.5H12V17H17M19,19H5L19,5M5.5,7.5H7.5V5.5H9V7.5H11V9H9V11H7.5V9H5.5M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3Z" /></g><g id="contrast-circle"><path d="M12,20C9.79,20 7.79,19.1 6.34,17.66L17.66,6.34C19.1,7.79 20,9.79 20,12A8,8 0 0,1 12,20M6,8H8V6H9.5V8H11.5V9.5H9.5V11.5H8V9.5H6M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,16H17V14.5H12V16Z" /></g><g id="cookie"><path d="M12,3A9,9 0 0,0 3,12A9,9 0 0,0 12,21A9,9 0 0,0 21,12C21,11.5 20.96,11 20.87,10.5C20.6,10 20,10 20,10H18V9C18,8 17,8 17,8H15V7C15,6 14,6 14,6H13V4C13,3 12,3 12,3M9.5,6A1.5,1.5 0 0,1 11,7.5A1.5,1.5 0 0,1 9.5,9A1.5,1.5 0 0,1 8,7.5A1.5,1.5 0 0,1 9.5,6M6.5,10A1.5,1.5 0 0,1 8,11.5A1.5,1.5 0 0,1 6.5,13A1.5,1.5 0 0,1 5,11.5A1.5,1.5 0 0,1 6.5,10M11.5,11A1.5,1.5 0 0,1 13,12.5A1.5,1.5 0 0,1 11.5,14A1.5,1.5 0 0,1 10,12.5A1.5,1.5 0 0,1 11.5,11M16.5,13A1.5,1.5 0 0,1 18,14.5A1.5,1.5 0 0,1 16.5,16H16.5A1.5,1.5 0 0,1 15,14.5H15A1.5,1.5 0 0,1 16.5,13M11,16A1.5,1.5 0 0,1 12.5,17.5A1.5,1.5 0 0,1 11,19A1.5,1.5 0 0,1 9.5,17.5A1.5,1.5 0 0,1 11,16Z" /></g><g id="copyright"><path d="M10.08,10.86C10.13,10.53 10.24,10.24 10.38,10C10.5,9.74 10.72,9.53 10.97,9.37C11.21,9.22 11.5,9.15 11.88,9.14C12.11,9.15 12.32,9.19 12.5,9.27C12.71,9.36 12.89,9.5 13.03,9.63C13.17,9.78 13.28,9.96 13.37,10.16C13.46,10.36 13.5,10.58 13.5,10.8H15.3C15.28,10.33 15.19,9.9 15,9.5C14.85,9.12 14.62,8.78 14.32,8.5C14,8.22 13.66,8 13.24,7.84C12.82,7.68 12.36,7.61 11.85,7.61C11.2,7.61 10.63,7.72 10.15,7.95C9.67,8.18 9.27,8.5 8.95,8.87C8.63,9.26 8.39,9.71 8.24,10.23C8.09,10.75 8,11.29 8,11.87V12.14C8,12.72 8.08,13.26 8.23,13.78C8.38,14.3 8.62,14.75 8.94,15.13C9.26,15.5 9.66,15.82 10.14,16.04C10.62,16.26 11.19,16.38 11.84,16.38C12.31,16.38 12.75,16.3 13.16,16.15C13.57,16 13.93,15.79 14.24,15.5C14.55,15.25 14.8,14.94 15,14.58C15.16,14.22 15.27,13.84 15.28,13.43H13.5C13.5,13.64 13.43,13.83 13.34,14C13.25,14.19 13.13,14.34 13,14.47C12.83,14.6 12.66,14.7 12.46,14.77C12.27,14.84 12.07,14.86 11.86,14.87C11.5,14.86 11.2,14.79 10.97,14.64C10.72,14.5 10.5,14.27 10.38,14C10.24,13.77 10.13,13.47 10.08,13.14C10.03,12.81 10,12.47 10,12.14V11.87C10,11.5 10.03,11.19 10.08,10.86M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20Z" /></g><g id="counter"><path d="M4,4H20A2,2 0 0,1 22,6V18A2,2 0 0,1 20,20H4A2,2 0 0,1 2,18V6A2,2 0 0,1 4,4M4,6V18H11V6H4M20,18V6H18.76C19,6.54 18.95,7.07 18.95,7.13C18.88,7.8 18.41,8.5 18.24,8.75L15.91,11.3L19.23,11.28L19.24,12.5L14.04,12.47L14,11.47C14,11.47 17.05,8.24 17.2,7.95C17.34,7.67 17.91,6 16.5,6C15.27,6.05 15.41,7.3 15.41,7.3L13.87,7.31C13.87,7.31 13.88,6.65 14.25,6H13V18H15.58L15.57,17.14L16.54,17.13C16.54,17.13 17.45,16.97 17.46,16.08C17.5,15.08 16.65,15.08 16.5,15.08C16.37,15.08 15.43,15.13 15.43,15.95H13.91C13.91,15.95 13.95,13.89 16.5,13.89C19.1,13.89 18.96,15.91 18.96,15.91C18.96,15.91 19,17.16 17.85,17.63L18.37,18H20M8.92,16H7.42V10.2L5.62,10.76V9.53L8.76,8.41H8.92V16Z" /></g><g id="cow"><path d="M10.5,18A0.5,0.5 0 0,1 11,18.5A0.5,0.5 0 0,1 10.5,19A0.5,0.5 0 0,1 10,18.5A0.5,0.5 0 0,1 10.5,18M13.5,18A0.5,0.5 0 0,1 14,18.5A0.5,0.5 0 0,1 13.5,19A0.5,0.5 0 0,1 13,18.5A0.5,0.5 0 0,1 13.5,18M10,11A1,1 0 0,1 11,12A1,1 0 0,1 10,13A1,1 0 0,1 9,12A1,1 0 0,1 10,11M14,11A1,1 0 0,1 15,12A1,1 0 0,1 14,13A1,1 0 0,1 13,12A1,1 0 0,1 14,11M18,18C18,20.21 15.31,22 12,22C8.69,22 6,20.21 6,18C6,17.1 6.45,16.27 7.2,15.6C6.45,14.6 6,13.35 6,12L6.12,10.78C5.58,10.93 4.93,10.93 4.4,10.78C3.38,10.5 1.84,9.35 2.07,8.55C2.3,7.75 4.21,7.6 5.23,7.9C5.82,8.07 6.45,8.5 6.82,8.96L7.39,8.15C6.79,7.05 7,4 10,3L9.91,3.14V3.14C9.63,3.58 8.91,4.97 9.67,6.47C10.39,6.17 11.17,6 12,6C12.83,6 13.61,6.17 14.33,6.47C15.09,4.97 14.37,3.58 14.09,3.14L14,3C17,4 17.21,7.05 16.61,8.15L17.18,8.96C17.55,8.5 18.18,8.07 18.77,7.9C19.79,7.6 21.7,7.75 21.93,8.55C22.16,9.35 20.62,10.5 19.6,10.78C19.07,10.93 18.42,10.93 17.88,10.78L18,12C18,13.35 17.55,14.6 16.8,15.6C17.55,16.27 18,17.1 18,18M12,16C9.79,16 8,16.9 8,18C8,19.1 9.79,20 12,20C14.21,20 16,19.1 16,18C16,16.9 14.21,16 12,16M12,14C13.12,14 14.17,14.21 15.07,14.56C15.65,13.87 16,13 16,12A4,4 0 0,0 12,8A4,4 0 0,0 8,12C8,13 8.35,13.87 8.93,14.56C9.83,14.21 10.88,14 12,14M14.09,3.14V3.14Z" /></g><g id="creation"><path d="M19,1L17.74,3.75L15,5L17.74,6.26L19,9L20.25,6.26L23,5L20.25,3.75M9,4L6.5,9.5L1,12L6.5,14.5L9,20L11.5,14.5L17,12L11.5,9.5M19,15L17.74,17.74L15,19L17.74,20.25L19,23L20.25,20.25L23,19L20.25,17.74" /></g><g id="credit-card"><path d="M20,8H4V6H20M20,18H4V12H20M20,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V6C22,4.89 21.1,4 20,4Z" /></g><g id="credit-card-multiple"><path d="M21,8V6H7V8H21M21,16V11H7V16H21M21,4A2,2 0 0,1 23,6V16A2,2 0 0,1 21,18H7C5.89,18 5,17.1 5,16V6C5,4.89 5.89,4 7,4H21M3,20H18V22H3A2,2 0 0,1 1,20V9H3V20Z" /></g><g id="credit-card-off"><path d="M0.93,4.2L2.21,2.93L20,20.72L18.73,22L16.73,20H4C2.89,20 2,19.1 2,18V6C2,5.78 2.04,5.57 2.11,5.38L0.93,4.2M20,8V6H7.82L5.82,4H20A2,2 0 0,1 22,6V18C22,18.6 21.74,19.13 21.32,19.5L19.82,18H20V12H13.82L9.82,8H20M4,8H4.73L4,7.27V8M4,12V18H14.73L8.73,12H4Z" /></g><g id="credit-card-plus"><path d="M21,18H24V20H21V23H19V20H16V18H19V15H21V18M19,8V6H3V8H19M19,12H3V18H14V20H3C1.89,20 1,19.1 1,18V6C1,4.89 1.89,4 3,4H19A2,2 0 0,1 21,6V13H19V12Z" /></g><g id="credit-card-scan"><path d="M2,4H6V2H2A2,2 0 0,0 0,4V8H2V4M22,2H18V4H22V8H24V4A2,2 0 0,0 22,2M2,16H0V20A2,2 0 0,0 2,22H6V20H2V16M22,20H18V22H22A2,2 0 0,0 24,20V16H22V20M4,8V16A2,2 0 0,0 6,18H18A2,2 0 0,0 20,16V8A2,2 0 0,0 18,6H6A2,2 0 0,0 4,8M6,16V12H18V16H6M18,8V10H6V8H18Z" /></g><g id="crop"><path d="M7,17V1H5V5H1V7H5V17A2,2 0 0,0 7,19H17V23H19V19H23V17M17,15H19V7C19,5.89 18.1,5 17,5H9V7H17V15Z" /></g><g id="crop-free"><path d="M19,3H15V5H19V9H21V5C21,3.89 20.1,3 19,3M19,19H15V21H19A2,2 0 0,0 21,19V15H19M5,15H3V19A2,2 0 0,0 5,21H9V19H5M3,5V9H5V5H9V3H5A2,2 0 0,0 3,5Z" /></g><g id="crop-landscape"><path d="M19,17H5V7H19M19,5H5A2,2 0 0,0 3,7V17A2,2 0 0,0 5,19H19A2,2 0 0,0 21,17V7C21,5.89 20.1,5 19,5Z" /></g><g id="crop-portrait"><path d="M17,19H7V5H17M17,3H7A2,2 0 0,0 5,5V19A2,2 0 0,0 7,21H17A2,2 0 0,0 19,19V5C19,3.89 18.1,3 17,3Z" /></g><g id="crop-rotate"><path d="M7.47,21.5C4.2,19.93 1.86,16.76 1.5,13H0C0.5,19.16 5.66,24 11.95,24C12.18,24 12.39,24 12.61,23.97L8.8,20.15L7.47,21.5M12.05,0C11.82,0 11.61,0 11.39,0.04L15.2,3.85L16.53,2.5C19.8,4.07 22.14,7.24 22.5,11H24C23.5,4.84 18.34,0 12.05,0M16,14H18V8C18,6.89 17.1,6 16,6H10V8H16V14M8,16V4H6V6H4V8H6V16A2,2 0 0,0 8,18H16V20H18V18H20V16H8Z" /></g><g id="crop-square"><path d="M18,18H6V6H18M18,4H6A2,2 0 0,0 4,6V18A2,2 0 0,0 6,20H18A2,2 0 0,0 20,18V6C20,4.89 19.1,4 18,4Z" /></g><g id="crosshairs"><path d="M3.05,13H1V11H3.05C3.5,6.83 6.83,3.5 11,3.05V1H13V3.05C17.17,3.5 20.5,6.83 20.95,11H23V13H20.95C20.5,17.17 17.17,20.5 13,20.95V23H11V20.95C6.83,20.5 3.5,17.17 3.05,13M12,5A7,7 0 0,0 5,12A7,7 0 0,0 12,19A7,7 0 0,0 19,12A7,7 0 0,0 12,5Z" /></g><g id="crosshairs-gps"><path d="M12,8A4,4 0 0,1 16,12A4,4 0 0,1 12,16A4,4 0 0,1 8,12A4,4 0 0,1 12,8M3.05,13H1V11H3.05C3.5,6.83 6.83,3.5 11,3.05V1H13V3.05C17.17,3.5 20.5,6.83 20.95,11H23V13H20.95C20.5,17.17 17.17,20.5 13,20.95V23H11V20.95C6.83,20.5 3.5,17.17 3.05,13M12,5A7,7 0 0,0 5,12A7,7 0 0,0 12,19A7,7 0 0,0 19,12A7,7 0 0,0 12,5Z" /></g><g id="crown"><path d="M5,16L3,5L8.5,12L12,5L15.5,12L21,5L19,16H5M19,19A1,1 0 0,1 18,20H6A1,1 0 0,1 5,19V18H19V19Z" /></g><g id="cube"><path d="M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L6.04,7.5L12,10.85L17.96,7.5L12,4.15Z" /></g><g id="cube-outline"><path d="M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L6.04,7.5L12,10.85L17.96,7.5L12,4.15M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V9.21L13,12.58V19.29L19,15.91Z" /></g><g id="cube-send"><path d="M16,4L9,8.04V15.96L16,20L23,15.96V8.04M16,6.31L19.8,8.5L16,10.69L12.21,8.5M0,7V9H7V7M11,10.11L15,12.42V17.11L11,14.81M21,10.11V14.81L17,17.11V12.42M2,11V13H7V11M4,15V17H7V15" /></g><g id="cube-unfolded"><path d="M6,9V4H13V9H23V16H18V21H11V16H1V9H6M16,16H13V19H16V16M8,9H11V6H8V9M6,14V11H3V14H6M18,11V14H21V11H18M13,11V14H16V11H13M8,11V14H11V11H8Z" /></g><g id="cup"><path d="M18.32,8H5.67L5.23,4H18.77M3,2L5,20.23C5.13,21.23 5.97,22 7,22H17C18,22 18.87,21.23 19,20.23L21,2H3Z" /></g><g id="cup-off"><path d="M1,4.27L2.28,3L21,21.72L19.73,23L18.27,21.54C17.93,21.83 17.5,22 17,22H7C5.97,22 5.13,21.23 5,20.23L3.53,6.8L1,4.27M18.32,8L18.77,4H5.82L3.82,2H21L19.29,17.47L9.82,8H18.32Z" /></g><g id="cup-water"><path d="M18.32,8H5.67L5.23,4H18.77M12,19A3,3 0 0,1 9,16C9,14 12,10.6 12,10.6C12,10.6 15,14 15,16A3,3 0 0,1 12,19M3,2L5,20.23C5.13,21.23 5.97,22 7,22H17C18,22 18.87,21.23 19,20.23L21,2H3Z" /></g><g id="currency-btc"><path d="M4.5,5H8V2H10V5H11.5V2H13.5V5C19,5 19,11 16,11.25C20,11 21,19 13.5,19V22H11.5V19H10V22H8V19H4.5L5,17H6A1,1 0 0,0 7,16V8A1,1 0 0,0 6,7H4.5V5M10,7V11C10,11 14.5,11.25 14.5,9C14.5,6.75 10,7 10,7M10,12.5V17C10,17 15.5,17 15.5,14.75C15.5,12.5 10,12.5 10,12.5Z" /></g><g id="currency-eur"><path d="M7.07,11L7,12L7.07,13H17.35L16.5,15H7.67C8.8,17.36 11.21,19 14,19C16.23,19 18.22,17.96 19.5,16.33V19.12C18,20.3 16.07,21 14,21C10.08,21 6.75,18.5 5.5,15H2L3,13H5.05L5,12L5.05,11H2L3,9H5.5C6.75,5.5 10.08,3 14,3C16.5,3 18.8,4.04 20.43,5.71L19.57,7.75C18.29,6.08 16.27,5 14,5C11.21,5 8.8,6.64 7.67,9H19.04L18.19,11H7.07Z" /></g><g id="currency-gbp"><path d="M6.5,21V19.75C7.44,19.29 8.24,18.57 8.81,17.7C9.38,16.83 9.67,15.85 9.68,14.75L9.66,13.77L9.57,13H7V11H9.4C9.25,10.17 9.16,9.31 9.13,8.25C9.16,6.61 9.64,5.33 10.58,4.41C11.5,3.5 12.77,3 14.32,3C15.03,3 15.64,3.07 16.13,3.2C16.63,3.32 17,3.47 17.31,3.65L16.76,5.39C16.5,5.25 16.19,5.12 15.79,5C15.38,4.9 14.89,4.85 14.32,4.84C13.25,4.86 12.5,5.19 12,5.83C11.5,6.46 11.29,7.28 11.3,8.28L11.4,9.77L11.6,11H15.5V13H11.79C11.88,14 11.84,15 11.65,15.96C11.35,17.16 10.74,18.18 9.83,19H18V21H6.5Z" /></g><g id="currency-inr"><path d="M8,3H18L17,5H13.74C14.22,5.58 14.58,6.26 14.79,7H18L17,9H15C14.75,11.57 12.74,13.63 10.2,13.96V14H9.5L15.5,21H13L7,14V12H9.5V12C11.26,12 12.72,10.7 12.96,9H7L8,7H12.66C12.1,5.82 10.9,5 9.5,5H7L8,3Z" /></g><g id="currency-ngn"><path d="M4,9H6V3H8L11.42,9H16V3H18V9H20V11H18V13H20V15H18V21H16L12.57,15H8V21H6V15H4V13H6V11H4V9M8,9H9.13L8,7.03V9M8,11V13H11.42L10.28,11H8M16,17V15H14.85L16,17M12.56,11L13.71,13H16V11H12.56Z" /></g><g id="currency-rub"><path d="M6,10H7V3H14.5C17,3 19,5 19,7.5C19,10 17,12 14.5,12H9V14H15V16H9V21H7V16H6V14H7V12H6V10M14.5,5H9V10H14.5A2.5,2.5 0 0,0 17,7.5A2.5,2.5 0 0,0 14.5,5Z" /></g><g id="currency-try"><path d="M19,12A9,9 0 0,1 10,21H8V12.77L5,13.87V11.74L8,10.64V8.87L5,9.96V7.84L8,6.74V3H10V6L15,4.2V6.32L10,8.14V9.92L15,8.1V10.23L10,12.05V19A7,7 0 0,0 17,12H19Z" /></g><g id="currency-usd"><path d="M11.8,10.9C9.53,10.31 8.8,9.7 8.8,8.75C8.8,7.66 9.81,6.9 11.5,6.9C13.28,6.9 13.94,7.75 14,9H16.21C16.14,7.28 15.09,5.7 13,5.19V3H10V5.16C8.06,5.58 6.5,6.84 6.5,8.77C6.5,11.08 8.41,12.23 11.2,12.9C13.7,13.5 14.2,14.38 14.2,15.31C14.2,16 13.71,17.1 11.5,17.1C9.44,17.1 8.63,16.18 8.5,15H6.32C6.44,17.19 8.08,18.42 10,18.83V21H13V18.85C14.95,18.5 16.5,17.35 16.5,15.3C16.5,12.46 14.07,11.5 11.8,10.9Z" /></g><g id="currency-usd-off"><path d="M12.5,6.9C14.28,6.9 14.94,7.75 15,9H17.21C17.14,7.28 16.09,5.7 14,5.19V3H11V5.16C10.47,5.28 9.97,5.46 9.5,5.7L11,7.17C11.4,7 11.9,6.9 12.5,6.9M5.33,4.06L4.06,5.33L7.5,8.77C7.5,10.85 9.06,12 11.41,12.68L14.92,16.19C14.58,16.67 13.87,17.1 12.5,17.1C10.44,17.1 9.63,16.18 9.5,15H7.32C7.44,17.19 9.08,18.42 11,18.83V21H14V18.85C14.96,18.67 15.82,18.3 16.45,17.73L18.67,19.95L19.94,18.68L5.33,4.06Z" /></g><g id="cursor-default"><path d="M13.64,21.97C13.14,22.21 12.54,22 12.31,21.5L10.13,16.76L7.62,18.78C7.45,18.92 7.24,19 7,19A1,1 0 0,1 6,18V3A1,1 0 0,1 7,2C7.24,2 7.47,2.09 7.64,2.23L7.65,2.22L19.14,11.86C19.57,12.22 19.62,12.85 19.27,13.27C19.12,13.45 18.91,13.57 18.7,13.61L15.54,14.23L17.74,18.96C18,19.46 17.76,20.05 17.26,20.28L13.64,21.97Z" /></g><g id="cursor-default-outline"><path d="M10.07,14.27C10.57,14.03 11.16,14.25 11.4,14.75L13.7,19.74L15.5,18.89L13.19,13.91C12.95,13.41 13.17,12.81 13.67,12.58L13.95,12.5L16.25,12.05L8,5.12V15.9L9.82,14.43L10.07,14.27M13.64,21.97C13.14,22.21 12.54,22 12.31,21.5L10.13,16.76L7.62,18.78C7.45,18.92 7.24,19 7,19A1,1 0 0,1 6,18V3A1,1 0 0,1 7,2C7.24,2 7.47,2.09 7.64,2.23L7.65,2.22L19.14,11.86C19.57,12.22 19.62,12.85 19.27,13.27C19.12,13.45 18.91,13.57 18.7,13.61L15.54,14.23L17.74,18.96C18,19.46 17.76,20.05 17.26,20.28L13.64,21.97Z" /></g><g id="cursor-move"><path d="M13,6V11H18V7.75L22.25,12L18,16.25V13H13V18H16.25L12,22.25L7.75,18H11V13H6V16.25L1.75,12L6,7.75V11H11V6H7.75L12,1.75L16.25,6H13Z" /></g><g id="cursor-pointer"><path d="M10,2A2,2 0 0,1 12,4V8.5C12,8.5 14,8.25 14,9.25C14,9.25 16,9 16,10C16,10 18,9.75 18,10.75C18,10.75 20,10.5 20,11.5V15C20,16 17,21 17,22H9C9,22 7,15 4,13C4,13 3,7 8,12V4A2,2 0 0,1 10,2Z" /></g><g id="cursor-text"><path d="M13,19A1,1 0 0,0 14,20H16V22H13.5C12.95,22 12,21.55 12,21C12,21.55 11.05,22 10.5,22H8V20H10A1,1 0 0,0 11,19V5A1,1 0 0,0 10,4H8V2H10.5C11.05,2 12,2.45 12,3C12,2.45 12.95,2 13.5,2H16V4H14A1,1 0 0,0 13,5V19Z" /></g><g id="database"><path d="M12,3C7.58,3 4,4.79 4,7C4,9.21 7.58,11 12,11C16.42,11 20,9.21 20,7C20,4.79 16.42,3 12,3M4,9V12C4,14.21 7.58,16 12,16C16.42,16 20,14.21 20,12V9C20,11.21 16.42,13 12,13C7.58,13 4,11.21 4,9M4,14V17C4,19.21 7.58,21 12,21C16.42,21 20,19.21 20,17V14C20,16.21 16.42,18 12,18C7.58,18 4,16.21 4,14Z" /></g><g id="database-minus"><path d="M9,3C4.58,3 1,4.79 1,7C1,9.21 4.58,11 9,11C13.42,11 17,9.21 17,7C17,4.79 13.42,3 9,3M1,9V12C1,14.21 4.58,16 9,16C13.42,16 17,14.21 17,12V9C17,11.21 13.42,13 9,13C4.58,13 1,11.21 1,9M1,14V17C1,19.21 4.58,21 9,21C10.41,21 11.79,20.81 13,20.46V17.46C11.79,17.81 10.41,18 9,18C4.58,18 1,16.21 1,14M15,17V19H23V17" /></g><g id="database-plus"><path d="M9,3C4.58,3 1,4.79 1,7C1,9.21 4.58,11 9,11C13.42,11 17,9.21 17,7C17,4.79 13.42,3 9,3M1,9V12C1,14.21 4.58,16 9,16C13.42,16 17,14.21 17,12V9C17,11.21 13.42,13 9,13C4.58,13 1,11.21 1,9M1,14V17C1,19.21 4.58,21 9,21C10.41,21 11.79,20.81 13,20.46V17.46C11.79,17.81 10.41,18 9,18C4.58,18 1,16.21 1,14M18,14V17H15V19H18V22H20V19H23V17H20V14" /></g><g id="debug-step-into"><path d="M12,22A2,2 0 0,1 10,20A2,2 0 0,1 12,18A2,2 0 0,1 14,20A2,2 0 0,1 12,22M13,2V13L17.5,8.5L18.92,9.92L12,16.84L5.08,9.92L6.5,8.5L11,13V2H13Z" /></g><g id="debug-step-out"><path d="M12,22A2,2 0 0,1 10,20A2,2 0 0,1 12,18A2,2 0 0,1 14,20A2,2 0 0,1 12,22M13,16H11V6L6.5,10.5L5.08,9.08L12,2.16L18.92,9.08L17.5,10.5L13,6V16Z" /></g><g id="debug-step-over"><path d="M12,14A2,2 0 0,1 14,16A2,2 0 0,1 12,18A2,2 0 0,1 10,16A2,2 0 0,1 12,14M23.46,8.86L21.87,15.75L15,14.16L18.8,11.78C17.39,9.5 14.87,8 12,8C8.05,8 4.77,10.86 4.12,14.63L2.15,14.28C2.96,9.58 7.06,6 12,6C15.58,6 18.73,7.89 20.5,10.72L23.46,8.86Z" /></g><g id="decimal-decrease"><path d="M12,17L15,20V18H21V16H15V14L12,17M9,5A3,3 0 0,1 12,8V11A3,3 0 0,1 9,14A3,3 0 0,1 6,11V8A3,3 0 0,1 9,5M9,7A1,1 0 0,0 8,8V11A1,1 0 0,0 9,12A1,1 0 0,0 10,11V8A1,1 0 0,0 9,7M4,12A1,1 0 0,1 5,13A1,1 0 0,1 4,14A1,1 0 0,1 3,13A1,1 0 0,1 4,12Z" /></g><g id="decimal-increase"><path d="M22,17L19,20V18H13V16H19V14L22,17M9,5A3,3 0 0,1 12,8V11A3,3 0 0,1 9,14A3,3 0 0,1 6,11V8A3,3 0 0,1 9,5M9,7A1,1 0 0,0 8,8V11A1,1 0 0,0 9,12A1,1 0 0,0 10,11V8A1,1 0 0,0 9,7M16,5A3,3 0 0,1 19,8V11A3,3 0 0,1 16,14A3,3 0 0,1 13,11V8A3,3 0 0,1 16,5M16,7A1,1 0 0,0 15,8V11A1,1 0 0,0 16,12A1,1 0 0,0 17,11V8A1,1 0 0,0 16,7M4,12A1,1 0 0,1 5,13A1,1 0 0,1 4,14A1,1 0 0,1 3,13A1,1 0 0,1 4,12Z" /></g><g id="delete"><path d="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z" /></g><g id="delete-circle"><path d="M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M17,7H14.5L13.5,6H10.5L9.5,7H7V9H17V7M9,18H15A1,1 0 0,0 16,17V10H8V17A1,1 0 0,0 9,18Z" /></g><g id="delete-forever"><path d="M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19M8.46,11.88L9.87,10.47L12,12.59L14.12,10.47L15.53,11.88L13.41,14L15.53,16.12L14.12,17.53L12,15.41L9.88,17.53L8.47,16.12L10.59,14L8.46,11.88M15.5,4L14.5,3H9.5L8.5,4H5V6H19V4H15.5Z" /></g><g id="delete-sweep"><path d="M15,16H19V18H15V16M15,8H22V10H15V8M15,12H21V14H15V12M3,18A2,2 0 0,0 5,20H11A2,2 0 0,0 13,18V8H3V18M14,5H11L10,4H6L5,5H2V7H14V5Z" /></g><g id="delete-variant"><path d="M21.03,3L18,20.31C17.83,21.27 17,22 16,22H8C7,22 6.17,21.27 6,20.31L2.97,3H21.03M5.36,5L8,20H16L18.64,5H5.36M9,18V14H13V18H9M13,13.18L9.82,10L13,6.82L16.18,10L13,13.18Z" /></g><g id="delta"><path d="M12,7.77L18.39,18H5.61L12,7.77M12,4L2,20H22" /></g><g id="deskphone"><path d="M5,3H19A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3M15,5V19H19V5H15M5,5V9H13V5H5M5,11V13H7V11H5M8,11V13H10V11H8M11,11V13H13V11H11M5,14V16H7V14H5M8,14V16H10V14H8M11,14V16H13V14H11M11,17V19H13V17H11M8,17V19H10V17H8M5,17V19H7V17H5Z" /></g><g id="desktop-mac"><path d="M21,14H3V4H21M21,2H3C1.89,2 1,2.89 1,4V16A2,2 0 0,0 3,18H10L8,21V22H16V21L14,18H21A2,2 0 0,0 23,16V4C23,2.89 22.1,2 21,2Z" /></g><g id="desktop-tower"><path d="M8,2H16A2,2 0 0,1 18,4V20A2,2 0 0,1 16,22H8A2,2 0 0,1 6,20V4A2,2 0 0,1 8,2M8,4V6H16V4H8M16,8H8V10H16V8M16,18H14V20H16V18Z" /></g><g id="details"><path d="M6.38,6H17.63L12,16L6.38,6M3,4L12,20L21,4H3Z" /></g><g id="developer-board"><path d="M22,9V7H20V5A2,2 0 0,0 18,3H4A2,2 0 0,0 2,5V19A2,2 0 0,0 4,21H18A2,2 0 0,0 20,19V17H22V15H20V13H22V11H20V9H22M18,19H4V5H18V19M6,13H11V17H6V13M12,7H16V10H12V7M6,7H11V12H6V7M12,11H16V17H12V11Z" /></g><g id="deviantart"><path d="M6,6H12L14,2H18V6L14.5,13H18V18H12L10,22H6V18L9.5,11H6V6Z" /></g><g id="dialpad"><path d="M12,19A2,2 0 0,0 10,21A2,2 0 0,0 12,23A2,2 0 0,0 14,21A2,2 0 0,0 12,19M6,1A2,2 0 0,0 4,3A2,2 0 0,0 6,5A2,2 0 0,0 8,3A2,2 0 0,0 6,1M6,7A2,2 0 0,0 4,9A2,2 0 0,0 6,11A2,2 0 0,0 8,9A2,2 0 0,0 6,7M6,13A2,2 0 0,0 4,15A2,2 0 0,0 6,17A2,2 0 0,0 8,15A2,2 0 0,0 6,13M18,5A2,2 0 0,0 20,3A2,2 0 0,0 18,1A2,2 0 0,0 16,3A2,2 0 0,0 18,5M12,13A2,2 0 0,0 10,15A2,2 0 0,0 12,17A2,2 0 0,0 14,15A2,2 0 0,0 12,13M18,13A2,2 0 0,0 16,15A2,2 0 0,0 18,17A2,2 0 0,0 20,15A2,2 0 0,0 18,13M18,7A2,2 0 0,0 16,9A2,2 0 0,0 18,11A2,2 0 0,0 20,9A2,2 0 0,0 18,7M12,7A2,2 0 0,0 10,9A2,2 0 0,0 12,11A2,2 0 0,0 14,9A2,2 0 0,0 12,7M12,1A2,2 0 0,0 10,3A2,2 0 0,0 12,5A2,2 0 0,0 14,3A2,2 0 0,0 12,1Z" /></g><g id="diamond"><path d="M16,9H19L14,16M10,9H14L12,17M5,9H8L10,16M15,4H17L19,7H16M11,4H13L14,7H10M7,4H9L8,7H5M6,2L2,8L12,22L22,8L18,2H6Z" /></g><g id="dice-1"><path d="M5,3H19A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3M12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12A2,2 0 0,0 12,10Z" /></g><g id="dice-2"><path d="M5,3H19A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3M7,5A2,2 0 0,0 5,7A2,2 0 0,0 7,9A2,2 0 0,0 9,7A2,2 0 0,0 7,5M17,15A2,2 0 0,0 15,17A2,2 0 0,0 17,19A2,2 0 0,0 19,17A2,2 0 0,0 17,15Z" /></g><g id="dice-3"><path d="M5,3H19A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3M12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12A2,2 0 0,0 12,10M7,5A2,2 0 0,0 5,7A2,2 0 0,0 7,9A2,2 0 0,0 9,7A2,2 0 0,0 7,5M17,15A2,2 0 0,0 15,17A2,2 0 0,0 17,19A2,2 0 0,0 19,17A2,2 0 0,0 17,15Z" /></g><g id="dice-4"><path d="M5,3H19A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3M7,5A2,2 0 0,0 5,7A2,2 0 0,0 7,9A2,2 0 0,0 9,7A2,2 0 0,0 7,5M17,15A2,2 0 0,0 15,17A2,2 0 0,0 17,19A2,2 0 0,0 19,17A2,2 0 0,0 17,15M17,5A2,2 0 0,0 15,7A2,2 0 0,0 17,9A2,2 0 0,0 19,7A2,2 0 0,0 17,5M7,15A2,2 0 0,0 5,17A2,2 0 0,0 7,19A2,2 0 0,0 9,17A2,2 0 0,0 7,15Z" /></g><g id="dice-5"><path d="M5,3H19A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3M7,5A2,2 0 0,0 5,7A2,2 0 0,0 7,9A2,2 0 0,0 9,7A2,2 0 0,0 7,5M17,15A2,2 0 0,0 15,17A2,2 0 0,0 17,19A2,2 0 0,0 19,17A2,2 0 0,0 17,15M17,5A2,2 0 0,0 15,7A2,2 0 0,0 17,9A2,2 0 0,0 19,7A2,2 0 0,0 17,5M12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12A2,2 0 0,0 12,10M7,15A2,2 0 0,0 5,17A2,2 0 0,0 7,19A2,2 0 0,0 9,17A2,2 0 0,0 7,15Z" /></g><g id="dice-6"><path d="M5,3H19A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3M7,5A2,2 0 0,0 5,7A2,2 0 0,0 7,9A2,2 0 0,0 9,7A2,2 0 0,0 7,5M17,15A2,2 0 0,0 15,17A2,2 0 0,0 17,19A2,2 0 0,0 19,17A2,2 0 0,0 17,15M17,10A2,2 0 0,0 15,12A2,2 0 0,0 17,14A2,2 0 0,0 19,12A2,2 0 0,0 17,10M17,5A2,2 0 0,0 15,7A2,2 0 0,0 17,9A2,2 0 0,0 19,7A2,2 0 0,0 17,5M7,10A2,2 0 0,0 5,12A2,2 0 0,0 7,14A2,2 0 0,0 9,12A2,2 0 0,0 7,10M7,15A2,2 0 0,0 5,17A2,2 0 0,0 7,19A2,2 0 0,0 9,17A2,2 0 0,0 7,15Z" /></g><g id="dice-d20"><path d="M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L5,8.09V15.91L12,19.85L19,15.91V8.09L12,4.15M14.93,8.27A2.57,2.57 0 0,1 17.5,10.84V13.5C17.5,14.9 16.35,16.05 14.93,16.05C13.5,16.05 12.36,14.9 12.36,13.5V10.84A2.57,2.57 0 0,1 14.93,8.27M14.92,9.71C14.34,9.71 13.86,10.18 13.86,10.77V13.53C13.86,14.12 14.34,14.6 14.92,14.6C15.5,14.6 16,14.12 16,13.53V10.77C16,10.18 15.5,9.71 14.92,9.71M11.45,14.76V15.96L6.31,15.93V14.91C6.31,14.91 9.74,11.58 9.75,10.57C9.75,9.33 8.73,9.46 8.73,9.46C8.73,9.46 7.75,9.5 7.64,10.71L6.14,10.76C6.14,10.76 6.18,8.26 8.83,8.26C11.2,8.26 11.23,10.04 11.23,10.5C11.23,12.18 8.15,14.77 8.15,14.77L11.45,14.76Z" /></g><g id="dice-d4"><path d="M13.43,15.15H14.29V16.36H13.43V18H11.92V16.36H8.82L8.75,15.41L11.91,10.42H13.43V15.15M10.25,15.15H11.92V12.47L10.25,15.15M22,21H2C1.64,21 1.31,20.81 1.13,20.5C0.95,20.18 0.96,19.79 1.15,19.5L11.15,3C11.5,2.38 12.5,2.38 12.86,3L22.86,19.5C23.04,19.79 23.05,20.18 22.87,20.5C22.69,20.81 22.36,21 22,21M3.78,19H20.23L12,5.43L3.78,19Z" /></g><g id="dice-d6"><path d="M5,3H19A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3M5,5V19H19V5H5M13.39,9.53C10.89,9.5 10.86,11.53 10.86,11.53C10.86,11.53 11.41,10.87 12.53,10.87C13.19,10.87 14.5,11.45 14.55,13.41C14.61,15.47 12.77,16 12.77,16C12.77,16 9.27,16.86 9.3,12.66C9.33,7.94 13.39,8.33 13.39,8.33V9.53M11.95,12.1C11.21,12 10.83,12.78 10.83,12.78L10.85,13.5C10.85,14.27 11.39,14.83 12,14.83C12.61,14.83 13.05,14.27 13.05,13.5C13.05,12.73 12.56,12.1 11.95,12.1Z" /></g><g id="dice-d8"><path d="M12,23C11.67,23 11.37,22.84 11.18,22.57L4.18,12.57C3.94,12.23 3.94,11.77 4.18,11.43L11.18,1.43C11.55,0.89 12.45,0.89 12.82,1.43L19.82,11.43C20.06,11.77 20.06,12.23 19.82,12.57L12.82,22.57C12.63,22.84 12.33,23 12,23M6.22,12L12,20.26L17.78,12L12,3.74L6.22,12M12,8.25C13.31,8.25 14.38,9.2 14.38,10.38C14.38,11.07 14,11.68 13.44,12.07C14.14,12.46 14.6,13.13 14.6,13.9C14.6,15.12 13.44,16.1 12,16.1C10.56,16.1 9.4,15.12 9.4,13.9C9.4,13.13 9.86,12.46 10.56,12.07C10,11.68 9.63,11.07 9.63,10.38C9.63,9.2 10.69,8.25 12,8.25M12,12.65A1.1,1.1 0 0,0 10.9,13.75A1.1,1.1 0 0,0 12,14.85A1.1,1.1 0 0,0 13.1,13.75A1.1,1.1 0 0,0 12,12.65M12,9.5C11.5,9.5 11.1,9.95 11.1,10.5C11.1,11.05 11.5,11.5 12,11.5C12.5,11.5 12.9,11.05 12.9,10.5C12.9,9.95 12.5,9.5 12,9.5Z" /></g><g id="dictionary"><path d="M5.81,2C4.83,2.09 4,3 4,4V20C4,21.05 4.95,22 6,22H18C19.05,22 20,21.05 20,20V4C20,2.89 19.1,2 18,2H12V9L9.5,7.5L7,9V2H6C5.94,2 5.87,2 5.81,2M12,13H13A1,1 0 0,1 14,14V18H13V16H12V18H11V14A1,1 0 0,1 12,13M12,14V15H13V14H12M15,15H18V16L16,19H18V20H15V19L17,16H15V15Z" /></g><g id="directions"><path d="M14,14.5V12H10V15H8V11A1,1 0 0,1 9,10H14V7.5L17.5,11M21.71,11.29L12.71,2.29H12.7C12.31,1.9 11.68,1.9 11.29,2.29L2.29,11.29C1.9,11.68 1.9,12.32 2.29,12.71L11.29,21.71C11.68,22.09 12.31,22.1 12.71,21.71L21.71,12.71C22.1,12.32 22.1,11.68 21.71,11.29Z" /></g><g id="directions-fork"><path d="M3,4V12.5L6,9.5L9,13C10,14 10,15 10,15V21H14V14C14,14 14,13 13.47,12C12.94,11 12,10 12,10L9,6.58L11.5,4M18,4L13.54,8.47L14,9C14,9 14.93,10 15.47,11C15.68,11.4 15.8,11.79 15.87,12.13L21,7" /></g><g id="discord"><path d="M22,24L16.75,19L17.38,21H4.5A2.5,2.5 0 0,1 2,18.5V3.5A2.5,2.5 0 0,1 4.5,1H19.5A2.5,2.5 0 0,1 22,3.5V24M12,6.8C9.32,6.8 7.44,7.95 7.44,7.95C8.47,7.03 10.27,6.5 10.27,6.5L10.1,6.33C8.41,6.36 6.88,7.53 6.88,7.53C5.16,11.12 5.27,14.22 5.27,14.22C6.67,16.03 8.75,15.9 8.75,15.9L9.46,15C8.21,14.73 7.42,13.62 7.42,13.62C7.42,13.62 9.3,14.9 12,14.9C14.7,14.9 16.58,13.62 16.58,13.62C16.58,13.62 15.79,14.73 14.54,15L15.25,15.9C15.25,15.9 17.33,16.03 18.73,14.22C18.73,14.22 18.84,11.12 17.12,7.53C17.12,7.53 15.59,6.36 13.9,6.33L13.73,6.5C13.73,6.5 15.53,7.03 16.56,7.95C16.56,7.95 14.68,6.8 12,6.8M9.93,10.59C10.58,10.59 11.11,11.16 11.1,11.86C11.1,12.55 10.58,13.13 9.93,13.13C9.29,13.13 8.77,12.55 8.77,11.86C8.77,11.16 9.28,10.59 9.93,10.59M14.1,10.59C14.75,10.59 15.27,11.16 15.27,11.86C15.27,12.55 14.75,13.13 14.1,13.13C13.46,13.13 12.94,12.55 12.94,11.86C12.94,11.16 13.45,10.59 14.1,10.59Z" /></g><g id="disk"><path d="M12,14C10.89,14 10,13.1 10,12C10,10.89 10.89,10 12,10C13.11,10 14,10.89 14,12A2,2 0 0,1 12,14M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z" /></g><g id="disk-alert"><path d="M10,14C8.89,14 8,13.1 8,12C8,10.89 8.89,10 10,10A2,2 0 0,1 12,12A2,2 0 0,1 10,14M10,4A8,8 0 0,0 2,12A8,8 0 0,0 10,20A8,8 0 0,0 18,12A8,8 0 0,0 10,4M20,12H22V7H20M20,16H22V14H20V16Z" /></g><g id="disqus"><path d="M12.08,22C9.63,22 7.39,21.11 5.66,19.63L1.41,20.21L3.05,16.15C2.5,14.88 2.16,13.5 2.16,12C2.16,6.5 6.6,2 12.08,2C17.56,2 22,6.5 22,12C22,17.5 17.56,22 12.08,22M17.5,11.97V11.94C17.5,9.06 15.46,7 11.95,7H8.16V17H11.9C15.43,17 17.5,14.86 17.5,11.97M12,14.54H10.89V9.46H12C13.62,9.46 14.7,10.39 14.7,12V12C14.7,13.63 13.62,14.54 12,14.54Z" /></g><g id="disqus-outline"><path d="M11.9,14.5H10.8V9.5H11.9C13.5,9.5 14.6,10.4 14.6,12C14.6,13.6 13.5,14.5 11.9,14.5M11.9,7H8.1V17H11.8C15.3,17 17.4,14.9 17.4,12V12C17.4,9.1 15.4,7 11.9,7M12,20C10.1,20 8.3,19.3 6.9,18.1L6.2,17.5L4.5,17.7L5.2,16.1L4.9,15.3C4.4,14.2 4.2,13.1 4.2,11.9C4.2,7.5 7.8,3.9 12.1,3.9C16.4,3.9 19.9,7.6 19.9,12C19.9,16.4 16.3,20 12,20M12,2C6.5,2 2.1,6.5 2.1,12C2.1,13.5 2.4,14.9 3,16.2L1.4,20.3L5.7,19.7C7.4,21.2 9.7,22.1 12.1,22.1C17.6,22.1 22,17.6 22,12.1C22,6.6 17.5,2 12,2Z" /></g><g id="division"><path d="M19,13H5V11H19V13M12,5A2,2 0 0,1 14,7A2,2 0 0,1 12,9A2,2 0 0,1 10,7A2,2 0 0,1 12,5M12,15A2,2 0 0,1 14,17A2,2 0 0,1 12,19A2,2 0 0,1 10,17A2,2 0 0,1 12,15Z" /></g><g id="division-box"><path d="M17,13V11H7V13H17M19,3A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5C3.89,21 3,20.1 3,19V5C3,3.89 3.89,3 5,3H19M12,7A1,1 0 0,0 11,8A1,1 0 0,0 12,9A1,1 0 0,0 13,8A1,1 0 0,0 12,7M12,15A1,1 0 0,0 11,16A1,1 0 0,0 12,17A1,1 0 0,0 13,16A1,1 0 0,0 12,15Z" /></g><g id="dna"><path d="M4,2H6V4C6,5.44 6.68,6.61 7.88,7.78C8.74,8.61 9.89,9.41 11.09,10.2L9.26,11.39C8.27,10.72 7.31,10 6.5,9.21C5.07,7.82 4,6.1 4,4V2M18,2H20V4C20,6.1 18.93,7.82 17.5,9.21C16.09,10.59 14.29,11.73 12.54,12.84C10.79,13.96 9.09,15.05 7.88,16.22C6.68,17.39 6,18.56 6,20V22H4V20C4,17.9 5.07,16.18 6.5,14.79C7.91,13.41 9.71,12.27 11.46,11.16C13.21,10.04 14.91,8.95 16.12,7.78C17.32,6.61 18,5.44 18,4V2M14.74,12.61C15.73,13.28 16.69,14 17.5,14.79C18.93,16.18 20,17.9 20,20V22H18V20C18,18.56 17.32,17.39 16.12,16.22C15.26,15.39 14.11,14.59 12.91,13.8L14.74,12.61M7,3H17V4L16.94,4.5H7.06L7,4V3M7.68,6H16.32C16.08,6.34 15.8,6.69 15.42,7.06L14.91,7.5H9.07L8.58,7.06C8.2,6.69 7.92,6.34 7.68,6M9.09,16.5H14.93L15.42,16.94C15.8,17.31 16.08,17.66 16.32,18H7.68C7.92,17.66 8.2,17.31 8.58,16.94L9.09,16.5M7.06,19.5H16.94L17,20V21H7V20L7.06,19.5Z" /></g><g id="dns"><path d="M7,9A2,2 0 0,1 5,7A2,2 0 0,1 7,5A2,2 0 0,1 9,7A2,2 0 0,1 7,9M20,3H4A1,1 0 0,0 3,4V10A1,1 0 0,0 4,11H20A1,1 0 0,0 21,10V4A1,1 0 0,0 20,3M7,19A2,2 0 0,1 5,17A2,2 0 0,1 7,15A2,2 0 0,1 9,17A2,2 0 0,1 7,19M20,13H4A1,1 0 0,0 3,14V20A1,1 0 0,0 4,21H20A1,1 0 0,0 21,20V14A1,1 0 0,0 20,13Z" /></g><g id="do-not-disturb"><path d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M17,13H7V11H17V13Z" /></g><g id="do-not-disturb-off"><path d="M17,11V13H15.54L20.22,17.68C21.34,16.07 22,14.11 22,12A10,10 0 0,0 12,2C9.89,2 7.93,2.66 6.32,3.78L13.54,11H17M2.27,2.27L1,3.54L3.78,6.32C2.66,7.93 2,9.89 2,12A10,10 0 0,0 12,22C14.11,22 16.07,21.34 17.68,20.22L20.46,23L21.73,21.73L2.27,2.27M7,13V11H8.46L10.46,13H7Z" /></g><g id="dolby"><path d="M2,5V19H22V5H2M6,17H4V7H6C8.86,7.09 11.1,9.33 11,12C11.1,14.67 8.86,16.91 6,17M20,17H18C15.14,16.91 12.9,14.67 13,12C12.9,9.33 15.14,7.09 18,7H20V17Z" /></g><g id="domain"><path d="M18,15H16V17H18M18,11H16V13H18M20,19H12V17H14V15H12V13H14V11H12V9H20M10,7H8V5H10M10,11H8V9H10M10,15H8V13H10M10,19H8V17H10M6,7H4V5H6M6,11H4V9H6M6,15H4V13H6M6,19H4V17H6M12,7V3H2V21H22V7H12Z" /></g><g id="dots-horizontal"><path d="M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z" /></g><g id="dots-vertical"><path d="M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z" /></g><g id="douban"><path d="M20,6H4V4H20V6M20,18V20H4V18H7.33L6.26,14H5V8H19V14H17.74L16.67,18H20M7,12H17V10H7V12M9.4,18H14.6L15.67,14H8.33L9.4,18Z" /></g><g id="download"><path d="M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z" /></g><g id="drag"><path d="M7,19V17H9V19H7M11,19V17H13V19H11M15,19V17H17V19H15M7,15V13H9V15H7M11,15V13H13V15H11M15,15V13H17V15H15M7,11V9H9V11H7M11,11V9H13V11H11M15,11V9H17V11H15M7,7V5H9V7H7M11,7V5H13V7H11M15,7V5H17V7H15Z" /></g><g id="drag-horizontal"><path d="M3,15V13H5V15H3M3,11V9H5V11H3M7,15V13H9V15H7M7,11V9H9V11H7M11,15V13H13V15H11M11,11V9H13V11H11M15,15V13H17V15H15M15,11V9H17V11H15M19,15V13H21V15H19M19,11V9H21V11H19Z" /></g><g id="drag-vertical"><path d="M9,3H11V5H9V3M13,3H15V5H13V3M9,7H11V9H9V7M13,7H15V9H13V7M9,11H11V13H9V11M13,11H15V13H13V11M9,15H11V17H9V15M13,15H15V17H13V15M9,19H11V21H9V19M13,19H15V21H13V19Z" /></g><g id="drawing"><path d="M8.5,3A5.5,5.5 0 0,1 14,8.5C14,9.83 13.53,11.05 12.74,12H21V21H12V12.74C11.05,13.53 9.83,14 8.5,14A5.5,5.5 0 0,1 3,8.5A5.5,5.5 0 0,1 8.5,3Z" /></g><g id="drawing-box"><path d="M18,18H12V12.21C11.34,12.82 10.47,13.2 9.5,13.2C7.46,13.2 5.8,11.54 5.8,9.5A3.7,3.7 0 0,1 9.5,5.8C11.54,5.8 13.2,7.46 13.2,9.5C13.2,10.47 12.82,11.34 12.21,12H18M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3Z" /></g><g id="dribbble"><path d="M16.42,18.42C16,16.5 15.5,14.73 15,13.17C15.5,13.1 16,13.06 16.58,13.06H16.6V13.06H16.6C17.53,13.06 18.55,13.18 19.66,13.43C19.28,15.5 18.08,17.27 16.42,18.42M12,19.8C10.26,19.8 8.66,19.23 7.36,18.26C7.64,17.81 8.23,16.94 9.18,16.04C10.14,15.11 11.5,14.15 13.23,13.58C13.82,15.25 14.36,17.15 14.77,19.29C13.91,19.62 13,19.8 12,19.8M4.2,12C4.2,11.96 4.2,11.93 4.2,11.89C4.42,11.9 4.71,11.9 5.05,11.9H5.06C6.62,11.89 9.36,11.76 12.14,10.89C12.29,11.22 12.44,11.56 12.59,11.92C10.73,12.54 9.27,13.53 8.19,14.5C7.16,15.46 6.45,16.39 6.04,17C4.9,15.66 4.2,13.91 4.2,12M8.55,5C9.1,5.65 10.18,7.06 11.34,9.25C9,9.96 6.61,10.12 5.18,10.12C5.14,10.12 5.1,10.12 5.06,10.12H5.05C4.81,10.12 4.6,10.12 4.43,10.11C5,7.87 6.5,6 8.55,5M12,4.2C13.84,4.2 15.53,4.84 16.86,5.91C15.84,7.14 14.5,8 13.03,8.65C12,6.67 11,5.25 10.34,4.38C10.88,4.27 11.43,4.2 12,4.2M18.13,7.18C19.1,8.42 19.71,9.96 19.79,11.63C18.66,11.39 17.6,11.28 16.6,11.28V11.28H16.59C15.79,11.28 15.04,11.35 14.33,11.47C14.16,11.05 14,10.65 13.81,10.26C15.39,9.57 16.9,8.58 18.13,7.18M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" /></g><g id="dribbble-box"><path d="M5,3H19A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3M15.09,16.5C14.81,15.14 14.47,13.91 14.08,12.82L15.2,12.74H15.22V12.74C15.87,12.74 16.59,12.82 17.36,13C17.09,14.44 16.26,15.69 15.09,16.5M12,17.46C10.79,17.46 9.66,17.06 8.76,16.39C8.95,16.07 9.36,15.46 10,14.83C10.7,14.18 11.64,13.5 12.86,13.11C13.28,14.27 13.65,15.6 13.94,17.1C13.33,17.33 12.68,17.46 12,17.46M6.54,12V11.92L7.14,11.93V11.93C8.24,11.93 10.15,11.83 12.1,11.22L12.41,11.94C11.11,12.38 10.09,13.07 9.34,13.76C8.61,14.42 8.12,15.08 7.83,15.5C7.03,14.56 6.54,13.34 6.54,12M9.59,7.11C9.97,7.56 10.73,8.54 11.54,10.08C9.89,10.57 8.23,10.68 7.22,10.68H7.14V10.68H6.7C7.09,9.11 8.17,7.81 9.59,7.11M12,6.54C13.29,6.54 14.47,7 15.41,7.74C14.69,8.6 13.74,9.22 12.72,9.66C12,8.27 11.31,7.28 10.84,6.67C11.21,6.59 11.6,6.54 12,6.54M16.29,8.63C16.97,9.5 17.4,10.57 17.45,11.74C16.66,11.58 15.92,11.5 15.22,11.5V11.5C14.66,11.5 14.13,11.54 13.63,11.63L13.27,10.78C14.37,10.3 15.43,9.61 16.29,8.63M12,5A7,7 0 0,0 5,12A7,7 0 0,0 12,19A7,7 0 0,0 19,12A7,7 0 0,0 12,5Z" /></g><g id="drone"><path d="M22,11H21L20,9H13.75L16,12.5H14L10.75,9H4C3.45,9 2,8.55 2,8C2,7.45 3.5,5.5 5.5,5.5C7.5,5.5 7.67,6.5 9,7H21A1,1 0 0,1 22,8V9L22,11M10.75,6.5L14,3H16L13.75,6.5H10.75M18,11V9.5H19.75L19,11H18M3,19A1,1 0 0,1 2,18A1,1 0 0,1 3,17A4,4 0 0,1 7,21A1,1 0 0,1 6,22A1,1 0 0,1 5,21A2,2 0 0,0 3,19M11,21A1,1 0 0,1 10,22A1,1 0 0,1 9,21A6,6 0 0,0 3,15A1,1 0 0,1 2,14A1,1 0 0,1 3,13A8,8 0 0,1 11,21Z" /></g><g id="dropbox"><path d="M12,14.56L16.35,18.16L18.2,16.95V18.3L12,22L5.82,18.3V16.95L7.68,18.16L12,14.56M7.68,2.5L12,6.09L16.32,2.5L22.5,6.5L18.23,9.94L22.5,13.36L16.32,17.39L12,13.78L7.68,17.39L1.5,13.36L5.77,9.94L1.5,6.5L7.68,2.5M12,13.68L18.13,9.94L12,6.19L5.87,9.94L12,13.68Z" /></g><g id="drupal"><path d="M20.47,14.65C20.47,15.29 20.25,16.36 19.83,17.1C19.4,17.85 19.08,18.06 18.44,18.06C17.7,17.95 16.31,15.82 15.36,15.72C14.18,15.72 11.73,18.17 9.71,18.17C8.54,18.17 8.11,17.95 7.79,17.74C7.15,17.31 6.94,16.67 6.94,15.82C6.94,14.22 8.43,12.84 10.24,12.84C12.59,12.84 14.18,15.18 15.36,15.08C16.31,15.08 18.23,13.16 19.19,13.16C20.15,12.95 20.47,14 20.47,14.65M16.63,5.28C15.57,4.64 14.61,4.32 13.54,3.68C12.91,3.25 12.05,2.3 11.31,1.44C11,2.83 10.78,3.36 10.24,3.79C9.18,4.53 8.64,4.85 7.69,5.28C6.94,5.7 3,8.05 3,13.16C3,18.27 7.37,22 12.05,22C16.85,22 21,18.5 21,13.27C21.21,8.05 17.27,5.7 16.63,5.28Z" /></g><g id="duck"><path d="M8.5,5A1.5,1.5 0 0,0 7,6.5A1.5,1.5 0 0,0 8.5,8A1.5,1.5 0 0,0 10,6.5A1.5,1.5 0 0,0 8.5,5M10,2A5,5 0 0,1 15,7C15,8.7 14.15,10.2 12.86,11.1C14.44,11.25 16.22,11.61 18,12.5C21,14 22,12 22,12C22,12 21,21 15,21H9C9,21 4,21 4,16C4,13 7,12 6,10C2,10 2,6.5 2,6.5C3,7 4.24,7 5,6.65C5.19,4.05 7.36,2 10,2Z" /></g><g id="dumbbell"><path d="M4.22,14.12L3.5,13.41C2.73,12.63 2.73,11.37 3.5,10.59C4.3,9.8 5.56,9.8 6.34,10.59L8.92,13.16L13.16,8.92L10.59,6.34C9.8,5.56 9.8,4.3 10.59,3.5C11.37,2.73 12.63,2.73 13.41,3.5L14.12,4.22L19.78,9.88L20.5,10.59C21.27,11.37 21.27,12.63 20.5,13.41C19.7,14.2 18.44,14.2 17.66,13.41L15.08,10.84L10.84,15.08L13.41,17.66C14.2,18.44 14.2,19.7 13.41,20.5C12.63,21.27 11.37,21.27 10.59,20.5L9.88,19.78L4.22,14.12M3.16,19.42L4.22,18.36L2.81,16.95C2.42,16.56 2.42,15.93 2.81,15.54C3.2,15.15 3.83,15.15 4.22,15.54L8.46,19.78C8.85,20.17 8.85,20.8 8.46,21.19C8.07,21.58 7.44,21.58 7.05,21.19L5.64,19.78L4.58,20.84L3.16,19.42M19.42,3.16L20.84,4.58L19.78,5.64L21.19,7.05C21.58,7.44 21.58,8.07 21.19,8.46C20.8,8.86 20.17,8.86 19.78,8.46L15.54,4.22C15.15,3.83 15.15,3.2 15.54,2.81C15.93,2.42 16.56,2.42 16.95,2.81L18.36,4.22L19.42,3.16Z" /></g><g id="earth"><path d="M17.9,17.39C17.64,16.59 16.89,16 16,16H15V13A1,1 0 0,0 14,12H8V10H10A1,1 0 0,0 11,9V7H13A2,2 0 0,0 15,5V4.59C17.93,5.77 20,8.64 20,12C20,14.08 19.2,15.97 17.9,17.39M11,19.93C7.05,19.44 4,16.08 4,12C4,11.38 4.08,10.78 4.21,10.21L9,15V16A2,2 0 0,0 11,18M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" /></g><g id="earth-off"><path d="M22,5.27L20.5,6.75C21.46,8.28 22,10.07 22,12A10,10 0 0,1 12,22C10.08,22 8.28,21.46 6.75,20.5L5.27,22L4,20.72L20.72,4L22,5.27M17.9,17.39C19.2,15.97 20,14.08 20,12C20,10.63 19.66,9.34 19.05,8.22L14.83,12.44C14.94,12.6 15,12.79 15,13V16H16C16.89,16 17.64,16.59 17.9,17.39M11,19.93V18C10.5,18 10.07,17.83 9.73,17.54L8.22,19.05C9.07,19.5 10,19.8 11,19.93M15,4.59V5A2,2 0 0,1 13,7H11V9A1,1 0 0,1 10,10H8V12H10.18L8.09,14.09L4.21,10.21C4.08,10.78 4,11.38 4,12C4,13.74 4.56,15.36 5.5,16.67L4.08,18.1C2.77,16.41 2,14.3 2,12A10,10 0 0,1 12,2C14.3,2 16.41,2.77 18.1,4.08L16.67,5.5C16.16,5.14 15.6,4.83 15,4.59Z" /></g><g id="edge"><path d="M2.74,10.81C3.83,-1.36 22.5,-1.36 21.2,13.56H8.61C8.61,17.85 14.42,19.21 19.54,16.31V20.53C13.25,23.88 5,21.43 5,14.09C5,8.58 9.97,6.81 9.97,6.81C9.97,6.81 8.58,8.58 8.54,10.05H15.7C15.7,2.93 5.9,5.57 2.74,10.81Z" /></g><g id="eject"><path d="M12,5L5.33,15H18.67M5,17H19V19H5V17Z" /></g><g id="elevation-decline"><path d="M21,21H3V11.25L9.45,15L13.22,12.8L21,17.29V21M3,8.94V6.75L9.45,10.5L13.22,8.3L21,12.79V15L13.22,10.5L9.45,12.67L3,8.94Z" /></g><g id="elevation-rise"><path d="M3,21V17.29L10.78,12.8L14.55,15L21,11.25V21H3M21,8.94L14.55,12.67L10.78,10.5L3,15V12.79L10.78,8.3L14.55,10.5L21,6.75V8.94Z" /></g><g id="elevator"><path d="M7,2L11,6H8V10H6V6H3L7,2M17,10L13,6H16V2H18V6H21L17,10M7,12H17A2,2 0 0,1 19,14V20A2,2 0 0,1 17,22H7A2,2 0 0,1 5,20V14A2,2 0 0,1 7,12M7,14V20H17V14H7Z" /></g><g id="email"><path d="M20,8L12,13L4,8V6L12,11L20,6M20,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V6C22,4.89 21.1,4 20,4Z" /></g><g id="email-open"><path d="M4,8L12,13L20,8V8L12,3L4,8V8M22,8V18A2,2 0 0,1 20,20H4A2,2 0 0,1 2,18V8C2,7.27 2.39,6.64 2.97,6.29L12,0.64L21.03,6.29C21.61,6.64 22,7.27 22,8Z" /></g><g id="email-open-outline"><path d="M12,15.36L4,10.36V18H20V10.36L12,15.36M4,8L12,13L20,8V8L12,3L4,8V8M22,8V18A2,2 0 0,1 20,20H4A2,2 0 0,1 2,18V8C2,7.27 2.39,6.64 2.97,6.29L12,0.64L21.03,6.29C21.61,6.64 22,7.27 22,8Z" /></g><g id="email-outline"><path d="M20,4H4A2,2 0 0,0 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V6A2,2 0 0,0 20,4M20,18H4V8L12,13L20,8V18M20,6L12,11L4,6V6H20V6Z" /></g><g id="email-secure"><path d="M20.5,0A2.5,2.5 0 0,1 23,2.5V3A1,1 0 0,1 24,4V8A1,1 0 0,1 23,9H18A1,1 0 0,1 17,8V4A1,1 0 0,1 18,3V2.5A2.5,2.5 0 0,1 20.5,0M12,11L4,6V8L12,13L16.18,10.39C16.69,10.77 17.32,11 18,11H22V18A2,2 0 0,1 20,20H4A2,2 0 0,1 2,18V6A2,2 0 0,1 4,4H15V8C15,8.36 15.06,8.7 15.18,9L12,11M20.5,1A1.5,1.5 0 0,0 19,2.5V3H22V2.5A1.5,1.5 0 0,0 20.5,1Z" /></g><g id="email-variant"><path d="M12,13L2,6.76V6C2,4.89 2.89,4 4,4H20A2,2 0 0,1 22,6V6.75L12,13M22,18A2,2 0 0,1 20,20H4C2.89,20 2,19.1 2,18V9.11L4,10.36V18H20V10.36L22,9.11V18Z" /></g><g id="emby"><path d="M11,2L6,7L7,8L2,13L7,18L8,17L13,22L18,17L17,16L22,11L17,6L16,7L11,2M10,8.5L16,12L10,15.5V8.5Z" /></g><g id="emoticon"><path d="M12,17.5C14.33,17.5 16.3,16.04 17.11,14H6.89C7.69,16.04 9.67,17.5 12,17.5M8.5,11A1.5,1.5 0 0,0 10,9.5A1.5,1.5 0 0,0 8.5,8A1.5,1.5 0 0,0 7,9.5A1.5,1.5 0 0,0 8.5,11M15.5,11A1.5,1.5 0 0,0 17,9.5A1.5,1.5 0 0,0 15.5,8A1.5,1.5 0 0,0 14,9.5A1.5,1.5 0 0,0 15.5,11M12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20M12,2C6.47,2 2,6.5 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" /></g><g id="emoticon-cool"><path d="M19,10C19,11.38 16.88,12.5 15.5,12.5C14.12,12.5 12.75,11.38 12.75,10H11.25C11.25,11.38 9.88,12.5 8.5,12.5C7.12,12.5 5,11.38 5,10H4.25C4.09,10.64 4,11.31 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12C20,11.31 19.91,10.64 19.75,10H19M12,4C9.04,4 6.45,5.61 5.07,8H18.93C17.55,5.61 14.96,4 12,4M22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12M12,17.23C10.25,17.23 8.71,16.5 7.81,15.42L9.23,14C9.68,14.72 10.75,15.23 12,15.23C13.25,15.23 14.32,14.72 14.77,14L16.19,15.42C15.29,16.5 13.75,17.23 12,17.23Z" /></g><g id="emoticon-dead"><path d="M12,2C6.47,2 2,6.47 2,12C2,17.53 6.47,22 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2M12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20M16.18,7.76L15.12,8.82L14.06,7.76L13,8.82L14.06,9.88L13,10.94L14.06,12L15.12,10.94L16.18,12L17.24,10.94L16.18,9.88L17.24,8.82L16.18,7.76M7.82,12L8.88,10.94L9.94,12L11,10.94L9.94,9.88L11,8.82L9.94,7.76L8.88,8.82L7.82,7.76L6.76,8.82L7.82,9.88L6.76,10.94L7.82,12M12,14C9.67,14 7.69,15.46 6.89,17.5H17.11C16.31,15.46 14.33,14 12,14Z" /></g><g id="emoticon-devil"><path d="M1.5,2.09C2.4,3 3.87,3.73 5.69,4.25C7.41,2.84 9.61,2 12,2C14.39,2 16.59,2.84 18.31,4.25C20.13,3.73 21.6,3 22.5,2.09C22.47,3.72 21.65,5.21 20.28,6.4C21.37,8 22,9.92 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12C2,9.92 2.63,8 3.72,6.4C2.35,5.21 1.53,3.72 1.5,2.09M20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12M10.5,10C10.5,10.8 9.8,11.5 9,11.5C8.2,11.5 7.5,10.8 7.5,10V8.5L10.5,10M16.5,10C16.5,10.8 15.8,11.5 15,11.5C14.2,11.5 13.5,10.8 13.5,10L16.5,8.5V10M12,17.23C10.25,17.23 8.71,16.5 7.81,15.42L9.23,14C9.68,14.72 10.75,15.23 12,15.23C13.25,15.23 14.32,14.72 14.77,14L16.19,15.42C15.29,16.5 13.75,17.23 12,17.23Z" /></g><g id="emoticon-excited"><path d="M12,2C6.47,2 2,6.47 2,12C2,17.53 6.47,22 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2M12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20M13,9.94L14.06,11L15.12,9.94L16.18,11L17.24,9.94L15.12,7.82L13,9.94M8.88,9.94L9.94,11L11,9.94L8.88,7.82L6.76,9.94L7.82,11L8.88,9.94M12,17.5C14.33,17.5 16.31,16.04 17.11,14H6.89C7.69,16.04 9.67,17.5 12,17.5Z" /></g><g id="emoticon-happy"><path d="M20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12M22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12M10,9.5C10,10.3 9.3,11 8.5,11C7.7,11 7,10.3 7,9.5C7,8.7 7.7,8 8.5,8C9.3,8 10,8.7 10,9.5M17,9.5C17,10.3 16.3,11 15.5,11C14.7,11 14,10.3 14,9.5C14,8.7 14.7,8 15.5,8C16.3,8 17,8.7 17,9.5M12,17.23C10.25,17.23 8.71,16.5 7.81,15.42L9.23,14C9.68,14.72 10.75,15.23 12,15.23C13.25,15.23 14.32,14.72 14.77,14L16.19,15.42C15.29,16.5 13.75,17.23 12,17.23Z" /></g><g id="emoticon-neutral"><path d="M8.5,11A1.5,1.5 0 0,1 7,9.5A1.5,1.5 0 0,1 8.5,8A1.5,1.5 0 0,1 10,9.5A1.5,1.5 0 0,1 8.5,11M15.5,11A1.5,1.5 0 0,1 14,9.5A1.5,1.5 0 0,1 15.5,8A1.5,1.5 0 0,1 17,9.5A1.5,1.5 0 0,1 15.5,11M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22C6.47,22 2,17.5 2,12A10,10 0 0,1 12,2M9,14H15A1,1 0 0,1 16,15A1,1 0 0,1 15,16H9A1,1 0 0,1 8,15A1,1 0 0,1 9,14Z" /></g><g id="emoticon-poop"><path d="M9,11C9.55,11 10,11.9 10,13C10,14.1 9.55,15 9,15C8.45,15 8,14.1 8,13C8,11.9 8.45,11 9,11M15,11C15.55,11 16,11.9 16,13C16,14.1 15.55,15 15,15C14.45,15 14,14.1 14,13C14,11.9 14.45,11 15,11M9.75,1.75C9.75,1.75 16,4 15,8C15,8 19,8 17.25,11.5C17.25,11.5 21.46,11.94 20.28,15.34C19,16.53 18.7,16.88 17.5,17.75L20.31,16.14C21.35,16.65 24.37,18.47 21,21C17,24 11,21.25 9,21.25C7,21.25 5,22 4,22C3,22 2,21 2,19C2,17 4,16 5,16C5,16 2,13 7,11C7,11 5,8 9,7C9,7 8,6 9,5C10,4 9.75,2.75 9.75,1.75M8,17C9.33,18.17 10.67,19.33 12,19.33C13.33,19.33 14.67,18.17 16,17H8M9,10C7.9,10 7,11.34 7,13C7,14.66 7.9,16 9,16C10.1,16 11,14.66 11,13C11,11.34 10.1,10 9,10M15,10C13.9,10 13,11.34 13,13C13,14.66 13.9,16 15,16C16.1,16 17,14.66 17,13C17,11.34 16.1,10 15,10Z" /></g><g id="emoticon-sad"><path d="M20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12M22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12M15.5,8C16.3,8 17,8.7 17,9.5C17,10.3 16.3,11 15.5,11C14.7,11 14,10.3 14,9.5C14,8.7 14.7,8 15.5,8M10,9.5C10,10.3 9.3,11 8.5,11C7.7,11 7,10.3 7,9.5C7,8.7 7.7,8 8.5,8C9.3,8 10,8.7 10,9.5M12,14C13.75,14 15.29,14.72 16.19,15.81L14.77,17.23C14.32,16.5 13.25,16 12,16C10.75,16 9.68,16.5 9.23,17.23L7.81,15.81C8.71,14.72 10.25,14 12,14Z" /></g><g id="emoticon-tongue"><path d="M9,8A2,2 0 0,1 11,10C11,10.36 10.9,10.71 10.73,11C10.39,10.4 9.74,10 9,10C8.26,10 7.61,10.4 7.27,11C7.1,10.71 7,10.36 7,10A2,2 0 0,1 9,8M15,8A2,2 0 0,1 17,10C17,10.36 16.9,10.71 16.73,11C16.39,10.4 15.74,10 15,10C14.26,10 13.61,10.4 13.27,11C13.1,10.71 13,10.36 13,10A2,2 0 0,1 15,8M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22C6.47,22 2,17.5 2,12A10,10 0 0,1 12,2M9,13H15A1,1 0 0,1 16,14A1,1 0 0,1 15,15C15,17 14.1,18 13,18C11.9,18 11,17 11,15H9A1,1 0 0,1 8,14A1,1 0 0,1 9,13Z" /></g><g id="engine"><path d="M7,4V6H10V8H7L5,10V13H3V10H1V18H3V15H5V18H8L10,20H18V16H20V19H23V9H20V12H18V8H12V6H15V4H7Z" /></g><g id="engine-outline"><path d="M8,10H16V18H11L9,16H7V11M7,4V6H10V8H7L5,10V13H3V10H1V18H3V15H5V18H8L10,20H18V16H20V19H23V9H20V12H18V8H12V6H15V4H7Z" /></g><g id="equal"><path d="M19,10H5V8H19V10M19,16H5V14H19V16Z" /></g><g id="equal-box"><path d="M17,16V14H7V16H17M19,3A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5C3.89,21 3,20.1 3,19V5C3,3.89 3.89,3 5,3H19M17,10V8H7V10H17Z" /></g><g id="eraser"><path d="M16.24,3.56L21.19,8.5C21.97,9.29 21.97,10.55 21.19,11.34L12,20.53C10.44,22.09 7.91,22.09 6.34,20.53L2.81,17C2.03,16.21 2.03,14.95 2.81,14.16L13.41,3.56C14.2,2.78 15.46,2.78 16.24,3.56M4.22,15.58L7.76,19.11C8.54,19.9 9.8,19.9 10.59,19.11L14.12,15.58L9.17,10.63L4.22,15.58Z" /></g><g id="eraser-variant"><path d="M15.14,3C14.63,3 14.12,3.2 13.73,3.59L2.59,14.73C1.81,15.5 1.81,16.77 2.59,17.56L5.03,20H12.69L21.41,11.27C22.2,10.5 22.2,9.23 21.41,8.44L16.56,3.59C16.17,3.2 15.65,3 15.14,3M17,18L15,20H22V18" /></g><g id="escalator"><path d="M20,8H18.95L6.95,20H4A2,2 0 0,1 2,18A2,2 0 0,1 4,16H5.29L7,14.29V10A1,1 0 0,1 8,9H9A1,1 0 0,1 10,10V11.29L17.29,4H20A2,2 0 0,1 22,6A2,2 0 0,1 20,8M8.5,5A1.5,1.5 0 0,1 10,6.5A1.5,1.5 0 0,1 8.5,8A1.5,1.5 0 0,1 7,6.5A1.5,1.5 0 0,1 8.5,5Z" /></g><g id="ethernet"><path d="M7,15H9V18H11V15H13V18H15V15H17V18H19V9H15V6H9V9H5V18H7V15M4.38,3H19.63C20.94,3 22,4.06 22,5.38V19.63A2.37,2.37 0 0,1 19.63,22H4.38C3.06,22 2,20.94 2,19.63V5.38C2,4.06 3.06,3 4.38,3Z" /></g><g id="ethernet-cable"><path d="M11,3V7H13V3H11M8,4V11H16V4H14V8H10V4H8M10,12V22H14V12H10Z" /></g><g id="ethernet-cable-off"><path d="M11,3H13V7H11V3M8,4H10V8H14V4H16V11H12.82L8,6.18V4M20,20.72L18.73,22L14,17.27V22H10V13.27L2,5.27L3.28,4L20,20.72Z" /></g><g id="etsy"><path d="M6.72,20.78C8.23,20.71 10.07,20.78 11.87,20.78C13.72,20.78 15.62,20.66 17.12,20.78C17.72,20.83 18.28,21.19 18.77,20.87C19.16,20.38 18.87,19.71 18.96,19.05C19.12,17.78 20.28,16.27 18.59,15.95C17.87,16.61 18.35,17.23 17.95,18.05C17.45,19.03 15.68,19.37 14,19.5C12.54,19.62 10,19.76 9.5,18.77C9.04,17.94 9.29,16.65 9.29,15.58C9.29,14.38 9.16,13.22 9.5,12.3C11.32,12.43 13.7,11.69 15,12.5C15.87,13 15.37,14.06 16.38,14.4C17.07,14.21 16.7,13.32 16.66,12.5C16.63,11.94 16.63,11.19 16.66,10.57C16.69,9.73 17,8.76 16.1,8.74C15.39,9.3 15.93,10.23 15.18,10.75C14.95,10.92 14.43,11 14.08,11C12.7,11.17 10.54,11.05 9.38,10.84C9.23,9.16 9.24,6.87 9.38,5.19C10,4.57 11.45,4.54 12.42,4.55C14.13,4.55 16.79,4.7 17.3,5.55C17.58,6 17.36,7 17.85,7.1C18.85,7.33 18.36,5.55 18.41,4.73C18.44,4.11 18.71,3.72 18.59,3.27C18.27,2.83 17.79,3.05 17.5,3.09C14.35,3.5 9.6,3.27 6.26,3.27C5.86,3.27 5.16,3.07 4.88,3.54C4.68,4.6 6.12,4.16 6.62,4.73C6.79,4.91 7.03,5.73 7.08,6.28C7.23,7.74 7.08,9.97 7.08,12.12C7.08,14.38 7.26,16.67 7.08,18.05C7,18.53 6.73,19.3 6.62,19.41C6,20.04 4.34,19.35 4.5,20.69C5.09,21.08 5.93,20.82 6.72,20.78Z" /></g><g id="ev-station"><path d="M19.77,7.23L19.78,7.22L16.06,3.5L15,4.56L17.11,6.67C16.17,7.03 15.5,7.93 15.5,9A2.5,2.5 0 0,0 18,11.5C18.36,11.5 18.69,11.42 19,11.29V18.5A1,1 0 0,1 18,19.5A1,1 0 0,1 17,18.5V14A2,2 0 0,0 15,12H14V5A2,2 0 0,0 12,3H6A2,2 0 0,0 4,5V21H14V13.5H15.5V18.5A2.5,2.5 0 0,0 18,21A2.5,2.5 0 0,0 20.5,18.5V9C20.5,8.31 20.22,7.68 19.77,7.23M18,10A1,1 0 0,1 17,9A1,1 0 0,1 18,8A1,1 0 0,1 19,9A1,1 0 0,1 18,10M8,18V13.5H6L10,6V11H12L8,18Z" /></g><g id="evernote"><path d="M15.09,11.63C15.09,11.63 15.28,10.35 16,10.35C16.76,10.35 17.78,12.06 17.78,12.06C17.78,12.06 15.46,11.63 15.09,11.63M19,4.69C18.64,4.09 16.83,3.41 15.89,3.41C14.96,3.41 13.5,3.41 13.5,3.41C13.5,3.41 12.7,2 10.88,2C9.05,2 9.17,2.81 9.17,3.5V6.32L8.34,7.19H4.5C4.5,7.19 3.44,7.91 3.44,9.44C3.44,11 3.92,16.35 7.13,16.85C10.93,17.43 11.58,15.67 11.58,15.46C11.58,14.56 11.6,13.21 11.6,13.21C11.6,13.21 12.71,15.33 14.39,15.33C16.07,15.33 17.04,16.3 17.04,17.29C17.04,18.28 17.04,19.13 17.04,19.13C17.04,19.13 17,20.28 16,20.28C15,20.28 13.89,20.28 13.89,20.28C13.89,20.28 13.2,19.74 13.2,19C13.2,18.25 13.53,18.05 13.93,18.05C14.32,18.05 14.65,18.09 14.65,18.09V16.53C14.65,16.53 11.47,16.5 11.47,18.94C11.47,21.37 13.13,22 14.46,22C15.8,22 16.63,22 16.63,22C16.63,22 20.56,21.5 20.56,13.75C20.56,6 19.33,5.28 19,4.69M7.5,6.31H4.26L8.32,2.22V5.5L7.5,6.31Z" /></g><g id="exclamation"><path d="M11,4.5H13V15.5H11V4.5M13,17.5V19.5H11V17.5H13Z" /></g><g id="exit-to-app"><path d="M19,3H5C3.89,3 3,3.89 3,5V9H5V5H19V19H5V15H3V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3M10.08,15.58L11.5,17L16.5,12L11.5,7L10.08,8.41L12.67,11H3V13H12.67L10.08,15.58Z" /></g><g id="export"><path d="M23,12L19,8V11H10V13H19V16M1,18V6C1,4.89 1.9,4 3,4H15A2,2 0 0,1 17,6V9H15V6H3V18H15V15H17V18A2,2 0 0,1 15,20H3A2,2 0 0,1 1,18Z" /></g><g id="eye"><path d="M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9M12,17A5,5 0 0,1 7,12A5,5 0 0,1 12,7A5,5 0 0,1 17,12A5,5 0 0,1 12,17M12,4.5C7,4.5 2.73,7.61 1,12C2.73,16.39 7,19.5 12,19.5C17,19.5 21.27,16.39 23,12C21.27,7.61 17,4.5 12,4.5Z" /></g><g id="eye-off"><path d="M11.83,9L15,12.16C15,12.11 15,12.05 15,12A3,3 0 0,0 12,9C11.94,9 11.89,9 11.83,9M7.53,9.8L9.08,11.35C9.03,11.56 9,11.77 9,12A3,3 0 0,0 12,15C12.22,15 12.44,14.97 12.65,14.92L14.2,16.47C13.53,16.8 12.79,17 12,17A5,5 0 0,1 7,12C7,11.21 7.2,10.47 7.53,9.8M2,4.27L4.28,6.55L4.73,7C3.08,8.3 1.78,10 1,12C2.73,16.39 7,19.5 12,19.5C13.55,19.5 15.03,19.2 16.38,18.66L16.81,19.08L19.73,22L21,20.73L3.27,3M12,7A5,5 0 0,1 17,12C17,12.64 16.87,13.26 16.64,13.82L19.57,16.75C21.07,15.5 22.27,13.86 23,12C21.27,7.61 17,4.5 12,4.5C10.6,4.5 9.26,4.75 8,5.2L10.17,7.35C10.74,7.13 11.35,7 12,7Z" /></g><g id="eyedropper"><path d="M19.35,11.72L17.22,13.85L15.81,12.43L8.1,20.14L3.5,22L2,20.5L3.86,15.9L11.57,8.19L10.15,6.78L12.28,4.65L19.35,11.72M16.76,3C17.93,1.83 19.83,1.83 21,3C22.17,4.17 22.17,6.07 21,7.24L19.08,9.16L14.84,4.92L16.76,3M5.56,17.03L4.5,19.5L6.97,18.44L14.4,11L13,9.6L5.56,17.03Z" /></g><g id="eyedropper-variant"><path d="M6.92,19L5,17.08L13.06,9L15,10.94M20.71,5.63L18.37,3.29C18,2.9 17.35,2.9 16.96,3.29L13.84,6.41L11.91,4.5L10.5,5.91L11.92,7.33L3,16.25V21H7.75L16.67,12.08L18.09,13.5L19.5,12.09L17.58,10.17L20.7,7.05C21.1,6.65 21.1,6 20.71,5.63Z" /></g><g id="face"><path d="M9,11.75A1.25,1.25 0 0,0 7.75,13A1.25,1.25 0 0,0 9,14.25A1.25,1.25 0 0,0 10.25,13A1.25,1.25 0 0,0 9,11.75M15,11.75A1.25,1.25 0 0,0 13.75,13A1.25,1.25 0 0,0 15,14.25A1.25,1.25 0 0,0 16.25,13A1.25,1.25 0 0,0 15,11.75M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,20C7.59,20 4,16.41 4,12C4,11.71 4,11.42 4.05,11.14C6.41,10.09 8.28,8.16 9.26,5.77C11.07,8.33 14.05,10 17.42,10C18.2,10 18.95,9.91 19.67,9.74C19.88,10.45 20,11.21 20,12C20,16.41 16.41,20 12,20Z" /></g><g id="face-profile"><path d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,8.39C13.57,9.4 15.42,10 17.42,10C18.2,10 18.95,9.91 19.67,9.74C19.88,10.45 20,11.21 20,12C20,16.41 16.41,20 12,20C9,20 6.39,18.34 5,15.89L6.75,14V13A1.25,1.25 0 0,1 8,11.75A1.25,1.25 0 0,1 9.25,13V14H12M16,11.75A1.25,1.25 0 0,0 14.75,13A1.25,1.25 0 0,0 16,14.25A1.25,1.25 0 0,0 17.25,13A1.25,1.25 0 0,0 16,11.75Z" /></g><g id="facebook"><path d="M17,2V2H17V6H15C14.31,6 14,6.81 14,7.5V10H14L17,10V14H14V22H10V14H7V10H10V6A4,4 0 0,1 14,2H17Z" /></g><g id="facebook-box"><path d="M19,4V7H17A1,1 0 0,0 16,8V10H19V13H16V20H13V13H11V10H13V7.5C13,5.56 14.57,4 16.5,4M20,2H4A2,2 0 0,0 2,4V20A2,2 0 0,0 4,22H20A2,2 0 0,0 22,20V4C22,2.89 21.1,2 20,2Z" /></g><g id="facebook-messenger"><path d="M12,2C6.5,2 2,6.14 2,11.25C2,14.13 3.42,16.7 5.65,18.4L5.71,22L9.16,20.12L9.13,20.11C10.04,20.36 11,20.5 12,20.5C17.5,20.5 22,16.36 22,11.25C22,6.14 17.5,2 12,2M13.03,14.41L10.54,11.78L5.5,14.41L10.88,8.78L13.46,11.25L18.31,8.78L13.03,14.41Z" /></g><g id="factory"><path d="M4,18V20H8V18H4M4,14V16H14V14H4M10,18V20H14V18H10M16,14V16H20V14H16M16,18V20H20V18H16M2,22V8L7,12V8L12,12V8L17,12L18,2H21L22,12V22H2Z" /></g><g id="fan"><path d="M12,11A1,1 0 0,0 11,12A1,1 0 0,0 12,13A1,1 0 0,0 13,12A1,1 0 0,0 12,11M12.5,2C17,2 17.11,5.57 14.75,6.75C13.76,7.24 13.32,8.29 13.13,9.22C13.61,9.42 14.03,9.73 14.35,10.13C18.05,8.13 22.03,8.92 22.03,12.5C22.03,17 18.46,17.1 17.28,14.73C16.78,13.74 15.72,13.3 14.79,13.11C14.59,13.59 14.28,14 13.88,14.34C15.87,18.03 15.08,22 11.5,22C7,22 6.91,18.42 9.27,17.24C10.25,16.75 10.69,15.71 10.89,14.79C10.4,14.59 9.97,14.27 9.65,13.87C5.96,15.85 2,15.07 2,11.5C2,7 5.56,6.89 6.74,9.26C7.24,10.25 8.29,10.68 9.22,10.87C9.41,10.39 9.73,9.97 10.14,9.65C8.15,5.96 8.94,2 12.5,2Z" /></g><g id="fast-forward"><path d="M13,6V18L21.5,12M4,18L12.5,12L4,6V18Z" /></g><g id="fax"><path d="M6,2A1,1 0 0,0 5,3V7H6V5H8V4H6V3H8V2H6M11,2A1,1 0 0,0 10,3V7H11V5H12V7H13V3A1,1 0 0,0 12,2H11M15,2L16.42,4.5L15,7H16.13L17,5.5L17.87,7H19L17.58,4.5L19,2H17.87L17,3.5L16.13,2H15M11,3H12V4H11V3M5,9A3,3 0 0,0 2,12V18H6V22H18V18H22V12A3,3 0 0,0 19,9H5M19,11A1,1 0 0,1 20,12A1,1 0 0,1 19,13A1,1 0 0,1 18,12A1,1 0 0,1 19,11M8,15H16V20H8V15Z" /></g><g id="ferry"><path d="M6,6H18V9.96L12,8L6,9.96M3.94,19H4C5.6,19 7,18.12 8,17C9,18.12 10.4,19 12,19C13.6,19 15,18.12 16,17C17,18.12 18.4,19 20,19H20.05L21.95,12.31C22.03,12.06 22,11.78 21.89,11.54C21.76,11.3 21.55,11.12 21.29,11.04L20,10.62V6C20,4.89 19.1,4 18,4H15V1H9V4H6A2,2 0 0,0 4,6V10.62L2.71,11.04C2.45,11.12 2.24,11.3 2.11,11.54C2,11.78 1.97,12.06 2.05,12.31M20,21C18.61,21 17.22,20.53 16,19.67C13.56,21.38 10.44,21.38 8,19.67C6.78,20.53 5.39,21 4,21H2V23H4C5.37,23 6.74,22.65 8,22C10.5,23.3 13.5,23.3 16,22C17.26,22.65 18.62,23 20,23H22V21H20Z" /></g><g id="file"><path d="M13,9V3.5L18.5,9M6,2C4.89,2 4,2.89 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6Z" /></g><g id="file-chart"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M7,20H9V14H7V20M11,20H13V12H11V20M15,20H17V16H15V20Z" /></g><g id="file-check"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M10.45,18.46L15.2,13.71L14.03,12.3L10.45,15.88L8.86,14.3L7.7,15.46L10.45,18.46Z" /></g><g id="file-cloud"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M15.68,15C15.34,13.3 13.82,12 12,12C10.55,12 9.3,12.82 8.68,14C7.17,14.18 6,15.45 6,17A3,3 0 0,0 9,20H15.5A2.5,2.5 0 0,0 18,17.5C18,16.18 16.97,15.11 15.68,15Z" /></g><g id="file-delimited"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M14,15V11H10V15H12.3C12.6,17 12,18 9.7,19.38L10.85,20.2C13,19 14,16 14,15Z" /></g><g id="file-document"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M15,18V16H6V18H15M18,14V12H6V14H18Z" /></g><g id="file-document-box"><path d="M14,17H7V15H14M17,13H7V11H17M17,9H7V7H17M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3Z" /></g><g id="file-excel"><path d="M6,2H14L20,8V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M13,3.5V9H18.5L13,3.5M17,11H13V13H14L12,14.67L10,13H11V11H7V13H8L11,15.5L8,18H7V20H11V18H10L12,16.33L14,18H13V20H17V18H16L13,15.5L16,13H17V11Z" /></g><g id="file-excel-box"><path d="M16.2,17H14.2L12,13.2L9.8,17H7.8L11,12L7.8,7H9.8L12,10.8L14.2,7H16.2L13,12M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3Z" /></g><g id="file-export"><path d="M6,2C4.89,2 4,2.9 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M13,3.5L18.5,9H13M8.93,12.22H16V19.29L13.88,17.17L11.05,20L8.22,17.17L11.05,14.35" /></g><g id="file-find"><path d="M9,13A3,3 0 0,0 12,16A3,3 0 0,0 15,13A3,3 0 0,0 12,10A3,3 0 0,0 9,13M20,19.59V8L14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18C18.45,22 18.85,21.85 19.19,21.6L14.76,17.17C13.96,17.69 13,18 12,18A5,5 0 0,1 7,13A5,5 0 0,1 12,8A5,5 0 0,1 17,13C17,14 16.69,14.96 16.17,15.75L20,19.59Z" /></g><g id="file-hidden"><path d="M13,9H14V11H11V7H13V9M18.5,9L16.38,6.88L17.63,5.63L20,8V10H18V11H15V9H18.5M13,3.5V2H12V4H13V6H11V4H9V2H8V4H6V5H4V4C4,2.89 4.89,2 6,2H14L16.36,4.36L15.11,5.61L13,3.5M20,20A2,2 0 0,1 18,22H16V20H18V19H20V20M18,15H20V18H18V15M12,22V20H15V22H12M8,22V20H11V22H8M6,22C4.89,22 4,21.1 4,20V18H6V20H7V22H6M4,14H6V17H4V14M4,10H6V13H4V10M18,11H20V14H18V11M4,6H6V9H4V6Z" /></g><g id="file-image"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6,20H15L18,20V12L14,16L12,14L6,20M8,9A2,2 0 0,0 6,11A2,2 0 0,0 8,13A2,2 0 0,0 10,11A2,2 0 0,0 8,9Z" /></g><g id="file-import"><path d="M6,2C4.89,2 4,2.9 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M13,3.5L18.5,9H13M10.05,11.22L12.88,14.05L15,11.93V19H7.93L10.05,16.88L7.22,14.05" /></g><g id="file-lock"><path d="M6,2C4.89,2 4,2.9 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6M13,3.5L18.5,9H13V3.5M12,11A3,3 0 0,1 15,14V15H16V19H8V15H9V14C9,12.36 10.34,11 12,11M12,13A1,1 0 0,0 11,14V15H13V14C13,13.47 12.55,13 12,13Z" /></g><g id="file-multiple"><path d="M15,7H20.5L15,1.5V7M8,0H16L22,6V18A2,2 0 0,1 20,20H8C6.89,20 6,19.1 6,18V2A2,2 0 0,1 8,0M4,4V22H20V24H4A2,2 0 0,1 2,22V4H4Z" /></g><g id="file-music"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M9,16A2,2 0 0,0 7,18A2,2 0 0,0 9,20A2,2 0 0,0 11,18V13H14V11H10V16.27C9.71,16.1 9.36,16 9,16Z" /></g><g id="file-outline"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M11,4H6V20H11L18,20V11H11V4Z" /></g><g id="file-pdf"><path d="M14,9H19.5L14,3.5V9M7,2H15L21,8V20A2,2 0 0,1 19,22H7C5.89,22 5,21.1 5,20V4A2,2 0 0,1 7,2M11.93,12.44C12.34,13.34 12.86,14.08 13.46,14.59L13.87,14.91C13,15.07 11.8,15.35 10.53,15.84V15.84L10.42,15.88L10.92,14.84C11.37,13.97 11.7,13.18 11.93,12.44M18.41,16.25C18.59,16.07 18.68,15.84 18.69,15.59C18.72,15.39 18.67,15.2 18.57,15.04C18.28,14.57 17.53,14.35 16.29,14.35L15,14.42L14.13,13.84C13.5,13.32 12.93,12.41 12.53,11.28L12.57,11.14C12.9,9.81 13.21,8.2 12.55,7.54C12.39,7.38 12.17,7.3 11.94,7.3H11.7C11.33,7.3 11,7.69 10.91,8.07C10.54,9.4 10.76,10.13 11.13,11.34V11.35C10.88,12.23 10.56,13.25 10.05,14.28L9.09,16.08L8.2,16.57C7,17.32 6.43,18.16 6.32,18.69C6.28,18.88 6.3,19.05 6.37,19.23L6.4,19.28L6.88,19.59L7.32,19.7C8.13,19.7 9.05,18.75 10.29,16.63L10.47,16.56C11.5,16.23 12.78,16 14.5,15.81C15.53,16.32 16.74,16.55 17.5,16.55C17.94,16.55 18.24,16.44 18.41,16.25M18,15.54L18.09,15.65C18.08,15.75 18.05,15.76 18,15.78H17.96L17.77,15.8C17.31,15.8 16.6,15.61 15.87,15.29C15.96,15.19 16,15.19 16.1,15.19C17.5,15.19 17.9,15.44 18,15.54M8.83,17C8.18,18.19 7.59,18.85 7.14,19C7.19,18.62 7.64,17.96 8.35,17.31L8.83,17M11.85,10.09C11.62,9.19 11.61,8.46 11.78,8.04L11.85,7.92L12,7.97C12.17,8.21 12.19,8.53 12.09,9.07L12.06,9.23L11.9,10.05L11.85,10.09Z" /></g><g id="file-pdf-box"><path d="M11.43,10.94C11.2,11.68 10.87,12.47 10.42,13.34C10.22,13.72 10,14.08 9.92,14.38L10.03,14.34V14.34C11.3,13.85 12.5,13.57 13.37,13.41C13.22,13.31 13.08,13.2 12.96,13.09C12.36,12.58 11.84,11.84 11.43,10.94M17.91,14.75C17.74,14.94 17.44,15.05 17,15.05C16.24,15.05 15,14.82 14,14.31C12.28,14.5 11,14.73 9.97,15.06C9.92,15.08 9.86,15.1 9.79,15.13C8.55,17.25 7.63,18.2 6.82,18.2C6.66,18.2 6.5,18.16 6.38,18.09L5.9,17.78L5.87,17.73C5.8,17.55 5.78,17.38 5.82,17.19C5.93,16.66 6.5,15.82 7.7,15.07C7.89,14.93 8.19,14.77 8.59,14.58C8.89,14.06 9.21,13.45 9.55,12.78C10.06,11.75 10.38,10.73 10.63,9.85V9.84C10.26,8.63 10.04,7.9 10.41,6.57C10.5,6.19 10.83,5.8 11.2,5.8H11.44C11.67,5.8 11.89,5.88 12.05,6.04C12.71,6.7 12.4,8.31 12.07,9.64C12.05,9.7 12.04,9.75 12.03,9.78C12.43,10.91 13,11.82 13.63,12.34C13.89,12.54 14.18,12.74 14.5,12.92C14.95,12.87 15.38,12.85 15.79,12.85C17.03,12.85 17.78,13.07 18.07,13.54C18.17,13.7 18.22,13.89 18.19,14.09C18.18,14.34 18.09,14.57 17.91,14.75M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3M17.5,14.04C17.4,13.94 17,13.69 15.6,13.69C15.53,13.69 15.46,13.69 15.37,13.79C16.1,14.11 16.81,14.3 17.27,14.3C17.34,14.3 17.4,14.29 17.46,14.28H17.5C17.55,14.26 17.58,14.25 17.59,14.15C17.57,14.12 17.55,14.08 17.5,14.04M8.33,15.5C8.12,15.62 7.95,15.73 7.85,15.81C7.14,16.46 6.69,17.12 6.64,17.5C7.09,17.35 7.68,16.69 8.33,15.5M11.35,8.59L11.4,8.55C11.47,8.23 11.5,7.95 11.56,7.73L11.59,7.57C11.69,7 11.67,6.71 11.5,6.47L11.35,6.42C11.33,6.45 11.3,6.5 11.28,6.54C11.11,6.96 11.12,7.69 11.35,8.59Z" /></g><g id="file-powerpoint"><path d="M6,2H14L20,8V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M13,3.5V9H18.5L13,3.5M8,11V13H9V19H8V20H12V19H11V17H13A3,3 0 0,0 16,14A3,3 0 0,0 13,11H8M13,13A1,1 0 0,1 14,14A1,1 0 0,1 13,15H11V13H13Z" /></g><g id="file-powerpoint-box"><path d="M9.8,13.4H12.3C13.8,13.4 14.46,13.12 15.1,12.58C15.74,12.03 16,11.25 16,10.23C16,9.26 15.75,8.5 15.1,7.88C14.45,7.29 13.83,7 12.3,7H8V17H9.8V13.4M19,3A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5C3,3.89 3.9,3 5,3H19M9.8,12V8.4H12.1C12.76,8.4 13.27,8.65 13.6,9C13.93,9.35 14.1,9.72 14.1,10.24C14.1,10.8 13.92,11.19 13.6,11.5C13.28,11.81 12.9,12 12.22,12H9.8Z" /></g><g id="file-presentation-box"><path d="M19,16H5V8H19M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3Z" /></g><g id="file-restore"><path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M12,18C9.95,18 8.19,16.76 7.42,15H9.13C9.76,15.9 10.81,16.5 12,16.5A3.5,3.5 0 0,0 15.5,13A3.5,3.5 0 0,0 12,9.5C10.65,9.5 9.5,10.28 8.9,11.4L10.5,13H6.5V9L7.8,10.3C8.69,8.92 10.23,8 12,8A5,5 0 0,1 17,13A5,5 0 0,1 12,18Z" /></g><g id="file-send"><path d="M14,2H6C4.89,2 4,2.89 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M12.54,19.37V17.37H8.54V15.38H12.54V13.38L15.54,16.38L12.54,19.37M13,9V3.5L18.5,9H13Z" /></g><g id="file-tree"><path d="M3,3H9V7H3V3M15,10H21V14H15V10M15,17H21V21H15V17M13,13H7V18H13V20H7L5,20V9H7V11H13V13Z" /></g><g id="file-video"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M17,19V13L14,15.2V13H7V19H14V16.8L17,19Z" /></g><g id="file-word"><path d="M6,2H14L20,8V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M13,3.5V9H18.5L13,3.5M7,13L8.5,20H10.5L12,17L13.5,20H15.5L17,13H18V11H14V13H15L14.1,17.2L13,15V15H11V15L9.9,17.2L9,13H10V11H6V13H7Z" /></g><g id="file-word-box"><path d="M15.5,17H14L12,9.5L10,17H8.5L6.1,7H7.8L9.34,14.5L11.3,7H12.7L14.67,14.5L16.2,7H17.9M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3Z" /></g><g id="file-xml"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6.12,15.5L9.86,19.24L11.28,17.83L8.95,15.5L11.28,13.17L9.86,11.76L6.12,15.5M17.28,15.5L13.54,11.76L12.12,13.17L14.45,15.5L12.12,17.83L13.54,19.24L17.28,15.5Z" /></g><g id="film"><path d="M3.5,3H5V1.8C5,1.36 5.36,1 5.8,1H10.2C10.64,1 11,1.36 11,1.8V3H12.5A1.5,1.5 0 0,1 14,4.5V5H22V20H14V20.5A1.5,1.5 0 0,1 12.5,22H3.5A1.5,1.5 0 0,1 2,20.5V4.5A1.5,1.5 0 0,1 3.5,3M18,7V9H20V7H18M14,7V9H16V7H14M10,7V9H12V7H10M14,16V18H16V16H14M18,16V18H20V16H18M10,16V18H12V16H10Z" /></g><g id="filmstrip"><path d="M18,9H16V7H18M18,13H16V11H18M18,17H16V15H18M8,9H6V7H8M8,13H6V11H8M8,17H6V15H8M18,3V5H16V3H8V5H6V3H4V21H6V19H8V21H16V19H18V21H20V3H18Z" /></g><g id="filmstrip-off"><path d="M1,4.27L2.28,3L21,21.72L19.73,23L16,19.27V21H8V19H6V21H4V7.27L1,4.27M18,9V7H16V9H18M18,13V11H16V13H18M18,15H16.82L6.82,5H8V3H16V5H18V3H20V18.18L18,16.18V15M8,13V11.27L7.73,11H6V13H8M8,17V15H6V17H8M6,3V4.18L4.82,3H6Z" /></g><g id="filter"><path d="M3,2H21V2H21V4H20.92L14,10.92V22.91L10,18.91V10.91L3.09,4H3V2Z" /></g><g id="filter-outline"><path d="M3,2H21V2H21V4H20.92L15,9.92V22.91L9,16.91V9.91L3.09,4H3V2M11,16.08L13,18.08V9H13.09L18.09,4H5.92L10.92,9H11V16.08Z" /></g><g id="filter-remove"><path d="M14.76,20.83L17.6,18L14.76,15.17L16.17,13.76L19,16.57L21.83,13.76L23.24,15.17L20.43,18L23.24,20.83L21.83,22.24L19,19.4L16.17,22.24L14.76,20.83M2,2H20V2H20V4H19.92L13,10.92V22.91L9,18.91V10.91L2.09,4H2V2Z" /></g><g id="filter-remove-outline"><path d="M14.73,20.83L17.58,18L14.73,15.17L16.15,13.76L19,16.57L21.8,13.76L23.22,15.17L20.41,18L23.22,20.83L21.8,22.24L19,19.4L16.15,22.24L14.73,20.83M2,2H20V2H20V4H19.92L14,9.92V22.91L8,16.91V9.91L2.09,4H2V2M10,16.08L12,18.08V9H12.09L17.09,4H4.92L9.92,9H10V16.08Z" /></g><g id="filter-variant"><path d="M6,13H18V11H6M3,6V8H21V6M10,18H14V16H10V18Z" /></g><g id="fingerprint"><path d="M11.83,1.73C8.43,1.79 6.23,3.32 6.23,3.32C5.95,3.5 5.88,3.91 6.07,4.19C6.27,4.5 6.66,4.55 6.96,4.34C6.96,4.34 11.27,1.15 17.46,4.38C17.75,4.55 18.14,4.45 18.31,4.15C18.5,3.85 18.37,3.47 18.03,3.28C16.36,2.4 14.78,1.96 13.36,1.8C12.83,1.74 12.32,1.72 11.83,1.73M12.22,4.34C6.26,4.26 3.41,9.05 3.41,9.05C3.22,9.34 3.3,9.72 3.58,9.91C3.87,10.1 4.26,10 4.5,9.68C4.5,9.68 6.92,5.5 12.2,5.59C17.5,5.66 19.82,9.65 19.82,9.65C20,9.94 20.38,10.04 20.68,9.87C21,9.69 21.07,9.31 20.9,9C20.9,9 18.15,4.42 12.22,4.34M11.5,6.82C9.82,6.94 8.21,7.55 7,8.56C4.62,10.53 3.1,14.14 4.77,19C4.88,19.33 5.24,19.5 5.57,19.39C5.89,19.28 6.07,18.92 5.95,18.6V18.6C4.41,14.13 5.78,11.2 7.8,9.5C9.77,7.89 13.25,7.5 15.84,9.1C17.11,9.9 18.1,11.28 18.6,12.64C19.11,14 19.08,15.32 18.67,15.94C18.25,16.59 17.4,16.83 16.65,16.64C15.9,16.45 15.29,15.91 15.26,14.77C15.23,13.06 13.89,12 12.5,11.84C11.16,11.68 9.61,12.4 9.21,14C8.45,16.92 10.36,21.07 14.78,22.45C15.11,22.55 15.46,22.37 15.57,22.04C15.67,21.71 15.5,21.35 15.15,21.25C11.32,20.06 9.87,16.43 10.42,14.29C10.66,13.33 11.5,13 12.38,13.08C13.25,13.18 14,13.7 14,14.79C14.05,16.43 15.12,17.54 16.34,17.85C17.56,18.16 18.97,17.77 19.72,16.62C20.5,15.45 20.37,13.8 19.78,12.21C19.18,10.61 18.07,9.03 16.5,8.04C14.96,7.08 13.19,6.7 11.5,6.82M11.86,9.25V9.26C10.08,9.32 8.3,10.24 7.28,12.18C5.96,14.67 6.56,17.21 7.44,19.04C8.33,20.88 9.54,22.1 9.54,22.1C9.78,22.35 10.17,22.35 10.42,22.11C10.67,21.87 10.67,21.5 10.43,21.23C10.43,21.23 9.36,20.13 8.57,18.5C7.78,16.87 7.3,14.81 8.38,12.77C9.5,10.67 11.5,10.16 13.26,10.67C15.04,11.19 16.53,12.74 16.5,15.03C16.46,15.38 16.71,15.68 17.06,15.7C17.4,15.73 17.7,15.47 17.73,15.06C17.79,12.2 15.87,10.13 13.61,9.47C13.04,9.31 12.45,9.23 11.86,9.25M12.08,14.25C11.73,14.26 11.46,14.55 11.47,14.89C11.47,14.89 11.5,16.37 12.31,17.8C13.15,19.23 14.93,20.59 18.03,20.3C18.37,20.28 18.64,20 18.62,19.64C18.6,19.29 18.3,19.03 17.91,19.06C15.19,19.31 14.04,18.28 13.39,17.17C12.74,16.07 12.72,14.88 12.72,14.88C12.72,14.53 12.44,14.25 12.08,14.25Z" /></g><g id="fire"><path d="M11.71,19C9.93,19 8.5,17.59 8.5,15.86C8.5,14.24 9.53,13.1 11.3,12.74C13.07,12.38 14.9,11.53 15.92,10.16C16.31,11.45 16.5,12.81 16.5,14.2C16.5,16.84 14.36,19 11.71,19M13.5,0.67C13.5,0.67 14.24,3.32 14.24,5.47C14.24,7.53 12.89,9.2 10.83,9.2C8.76,9.2 7.2,7.53 7.2,5.47L7.23,5.1C5.21,7.5 4,10.61 4,14A8,8 0 0,0 12,22A8,8 0 0,0 20,14C20,8.6 17.41,3.8 13.5,0.67Z" /></g><g id="firefox"><path d="M21,11.7C21,11.3 20.9,10.7 20.8,10.3C20.5,8.6 19.6,7.1 18.5,5.9C18.3,5.6 17.9,5.3 17.5,5C16.4,4.1 15.1,3.5 13.6,3.2C10.6,2.7 7.6,3.7 5.6,5.8C5.6,5.8 5.6,5.7 5.6,5.7C5.5,5.5 5.5,5.5 5.4,5.5C5.4,5.5 5.4,5.5 5.4,5.5C5.3,5.3 5.3,5.2 5.2,5.1C5.2,5.1 5.2,5.1 5.2,5.1C5.2,4.9 5.1,4.7 5.1,4.5C4.8,4.6 4.8,4.9 4.7,5.1C4.7,5.1 4.7,5.1 4.7,5.1C4.5,5.3 4.3,5.6 4.3,5.9C4.3,5.9 4.3,5.9 4.3,5.9C4.2,6.1 4,7 4.2,7.1C4.2,7.1 4.2,7.1 4.3,7.1C4.3,7.2 4.3,7.3 4.3,7.4C4.1,7.6 4,7.7 4,7.8C3.7,8.4 3.4,8.9 3.3,9.5C3.3,9.7 3.3,9.8 3.3,9.9C3.3,9.9 3.3,9.9 3.3,9.9C3.3,9.9 3.3,9.8 3.3,9.8C3.1,10 3.1,10.3 3,10.5C3.1,10.5 3.1,10.4 3.2,10.4C2.7,13 3.4,15.7 5,17.7C7.4,20.6 11.5,21.8 15.1,20.5C18.6,19.2 21,15.8 21,12C21,11.9 21,11.8 21,11.7M13.5,4.1C15,4.4 16.4,5.1 17.5,6.1C17.6,6.2 17.7,6.4 17.7,6.4C17.4,6.1 16.7,5.6 16.3,5.8C16.4,6.1 17.6,7.6 17.7,7.7C17.7,7.7 18,9 18.1,9.1C18.1,9.1 18.1,9.1 18.1,9.1C18.1,9.1 18.1,9.1 18.1,9.1C18.1,9.3 17.4,11.9 17.4,12.3C17.4,12.4 16.5,14.2 16.6,14.2C16.3,14.9 16,14.9 15.9,15C15.8,15 15.2,15.2 14.5,15.4C13.9,15.5 13.2,15.7 12.7,15.6C12.4,15.6 12,15.6 11.7,15.4C11.6,15.3 10.8,14.9 10.6,14.8C10.3,14.7 10.1,14.5 9.9,14.3C10.2,14.3 10.8,14.3 11,14.3C11.6,14.2 14.2,13.3 14.1,12.9C14.1,12.6 13.6,12.4 13.4,12.2C13.1,12.1 11.9,12.3 11.4,12.5C11.4,12.5 9.5,12 9,11.6C9,11.5 8.9,10.9 8.9,10.8C8.8,10.7 9.2,10.4 9.2,10.4C9.2,10.4 10.2,9.4 10.2,9.3C10.4,9.3 10.6,9.1 10.7,9C10.6,9 10.8,8.9 11.1,8.7C11.1,8.7 11.1,8.7 11.1,8.7C11.4,8.5 11.6,8.5 11.6,8.2C11.6,8.2 12.1,7.3 11.5,7.4C11.5,7.4 10.6,7.3 10.3,7.3C10,7.5 9.9,7.4 9.6,7.3C9.6,7.3 9.4,7.1 9.4,7C9.5,6.8 10.2,5.3 10.5,5.2C10.3,4.8 9.3,5.1 9.1,5.4C9.1,5.4 8.3,6 7.9,6.1C7.9,6.1 7.9,6.1 7.9,6.1C7.9,6 7.4,5.9 6.9,5.9C8.7,4.4 11.1,3.7 13.5,4.1Z" /></g><g id="fish"><path d="M12,20L12.76,17C9.5,16.79 6.59,15.4 5.75,13.58C5.66,14.06 5.53,14.5 5.33,14.83C4.67,16 3.33,16 2,16C3.1,16 3.5,14.43 3.5,12.5C3.5,10.57 3.1,9 2,9C3.33,9 4.67,9 5.33,10.17C5.53,10.5 5.66,10.94 5.75,11.42C6.4,10 8.32,8.85 10.66,8.32L9,5C11,5 13,5 14.33,5.67C15.46,6.23 16.11,7.27 16.69,8.38C19.61,9.08 22,10.66 22,12.5C22,14.38 19.5,16 16.5,16.66C15.67,17.76 14.86,18.78 14.17,19.33C13.33,20 12.67,20 12,20M17,11A1,1 0 0,0 16,12A1,1 0 0,0 17,13A1,1 0 0,0 18,12A1,1 0 0,0 17,11Z" /></g><g id="flag"><path d="M14.4,6L14,4H5V21H7V14H12.6L13,16H20V6H14.4Z" /></g><g id="flag-checkered"><path d="M14.4,6H20V16H13L12.6,14H7V21H5V4H14L14.4,6M14,14H16V12H18V10H16V8H14V10L13,8V6H11V8H9V6H7V8H9V10H7V12H9V10H11V12H13V10L14,12V14M11,10V8H13V10H11M14,10H16V12H14V10Z" /></g><g id="flag-outline"><path d="M14.5,6H20V16H13L12.5,14H7V21H5V4H14L14.5,6M7,6V12H13L13.5,14H18V8H14L13.5,6H7Z" /></g><g id="flag-outline-variant"><path d="M6,3A1,1 0 0,1 7,4V4.88C8.06,4.44 9.5,4 11,4C14,4 14,6 16,6C19,6 20,4 20,4V12C20,12 19,14 16,14C13,14 13,12 11,12C8,12 7,14 7,14V21H5V4A1,1 0 0,1 6,3M7,7.25V11.5C7,11.5 9,10 11,10C13,10 14,12 16,12C18,12 18,11 18,11V7.5C18,7.5 17,8 16,8C14,8 13,6 11,6C9,6 7,7.25 7,7.25Z" /></g><g id="flag-triangle"><path d="M7,2H9V22H7V2M19,9L11,14.6V3.4L19,9Z" /></g><g id="flag-variant"><path d="M6,3A1,1 0 0,1 7,4V4.88C8.06,4.44 9.5,4 11,4C14,4 14,6 16,6C19,6 20,4 20,4V12C20,12 19,14 16,14C13,14 13,12 11,12C8,12 7,14 7,14V21H5V4A1,1 0 0,1 6,3Z" /></g><g id="flash"><path d="M7,2V13H10V22L17,10H13L17,2H7Z" /></g><g id="flash-auto"><path d="M16.85,7.65L18,4L19.15,7.65M19,2H17L13.8,11H15.7L16.4,9H19.6L20.3,11H22.2M3,2V14H6V23L13,11H9L13,2H3Z" /></g><g id="flash-off"><path d="M17,10H13L17,2H7V4.18L15.46,12.64M3.27,3L2,4.27L7,9.27V13H10V22L13.58,15.86L17.73,20L19,18.73L3.27,3Z" /></g><g id="flash-red-eye"><path d="M16,5C15.44,5 15,5.44 15,6C15,6.56 15.44,7 16,7C16.56,7 17,6.56 17,6C17,5.44 16.56,5 16,5M16,2C13.27,2 10.94,3.66 10,6C10.94,8.34 13.27,10 16,10C18.73,10 21.06,8.34 22,6C21.06,3.66 18.73,2 16,2M16,3.5A2.5,2.5 0 0,1 18.5,6A2.5,2.5 0 0,1 16,8.5A2.5,2.5 0 0,1 13.5,6A2.5,2.5 0 0,1 16,3.5M3,2V14H6V23L13,11H9L10.12,8.5C9.44,7.76 8.88,6.93 8.5,6C9.19,4.29 10.5,2.88 12.11,2H3Z" /></g><g id="flashlight"><path d="M9,10L6,5H18L15,10H9M18,4H6V2H18V4M9,22V11H15V22H9M12,13A1,1 0 0,0 11,14A1,1 0 0,0 12,15A1,1 0 0,0 13,14A1,1 0 0,0 12,13Z" /></g><g id="flashlight-off"><path d="M2,5.27L3.28,4L20,20.72L18.73,22L15,18.27V22H9V12.27L2,5.27M18,5L15,10H11.82L6.82,5H18M18,4H6V2H18V4M15,11V13.18L12.82,11H15Z" /></g><g id="flask"><path d="M6,22A3,3 0 0,1 3,19C3,18.4 3.18,17.84 3.5,17.37L9,7.81V6A1,1 0 0,1 8,5V4A2,2 0 0,1 10,2H14A2,2 0 0,1 16,4V5A1,1 0 0,1 15,6V7.81L20.5,17.37C20.82,17.84 21,18.4 21,19A3,3 0 0,1 18,22H6M5,19A1,1 0 0,0 6,20H18A1,1 0 0,0 19,19C19,18.79 18.93,18.59 18.82,18.43L16.53,14.47L14,17L8.93,11.93L5.18,18.43C5.07,18.59 5,18.79 5,19M13,10A1,1 0 0,0 12,11A1,1 0 0,0 13,12A1,1 0 0,0 14,11A1,1 0 0,0 13,10Z" /></g><g id="flask-empty"><path d="M6,22A3,3 0 0,1 3,19C3,18.4 3.18,17.84 3.5,17.37L9,7.81V6A1,1 0 0,1 8,5V4A2,2 0 0,1 10,2H14A2,2 0 0,1 16,4V5A1,1 0 0,1 15,6V7.81L20.5,17.37C20.82,17.84 21,18.4 21,19A3,3 0 0,1 18,22H6Z" /></g><g id="flask-empty-outline"><path d="M5,19A1,1 0 0,0 6,20H18A1,1 0 0,0 19,19C19,18.79 18.93,18.59 18.82,18.43L13,8.35V4H11V8.35L5.18,18.43C5.07,18.59 5,18.79 5,19M6,22A3,3 0 0,1 3,19C3,18.4 3.18,17.84 3.5,17.37L9,7.81V6A1,1 0 0,1 8,5V4A2,2 0 0,1 10,2H14A2,2 0 0,1 16,4V5A1,1 0 0,1 15,6V7.81L20.5,17.37C20.82,17.84 21,18.4 21,19A3,3 0 0,1 18,22H6Z" /></g><g id="flask-outline"><path d="M5,19A1,1 0 0,0 6,20H18A1,1 0 0,0 19,19C19,18.79 18.93,18.59 18.82,18.43L13,8.35V4H11V8.35L5.18,18.43C5.07,18.59 5,18.79 5,19M6,22A3,3 0 0,1 3,19C3,18.4 3.18,17.84 3.5,17.37L9,7.81V6A1,1 0 0,1 8,5V4A2,2 0 0,1 10,2H14A2,2 0 0,1 16,4V5A1,1 0 0,1 15,6V7.81L20.5,17.37C20.82,17.84 21,18.4 21,19A3,3 0 0,1 18,22H6M13,16L14.34,14.66L16.27,18H7.73L10.39,13.39L13,16M12.5,12A0.5,0.5 0 0,1 13,12.5A0.5,0.5 0 0,1 12.5,13A0.5,0.5 0 0,1 12,12.5A0.5,0.5 0 0,1 12.5,12Z" /></g><g id="flattr"><path d="M21,9V15A6,6 0 0,1 15,21H4.41L11.07,14.35C11.38,14.04 11.69,13.73 11.84,13.75C12,13.78 12,14.14 12,14.5V17H14A3,3 0 0,0 17,14V8.41L21,4.41V9M3,15V9A6,6 0 0,1 9,3H19.59L12.93,9.65C12.62,9.96 12.31,10.27 12.16,10.25C12,10.22 12,9.86 12,9.5V7H10A3,3 0 0,0 7,10V15.59L3,19.59V15Z" /></g><g id="flip-to-back"><path d="M15,17H17V15H15M15,5H17V3H15M5,7H3V19A2,2 0 0,0 5,21H17V19H5M19,17A2,2 0 0,0 21,15H19M19,9H21V7H19M19,13H21V11H19M9,17V15H7A2,2 0 0,0 9,17M13,3H11V5H13M19,3V5H21C21,3.89 20.1,3 19,3M13,15H11V17H13M9,3C7.89,3 7,3.89 7,5H9M9,11H7V13H9M9,7H7V9H9V7Z" /></g><g id="flip-to-front"><path d="M7,21H9V19H7M11,21H13V19H11M19,15H9V5H19M19,3H9C7.89,3 7,3.89 7,5V15A2,2 0 0,0 9,17H14L18,17H19A2,2 0 0,0 21,15V5C21,3.89 20.1,3 19,3M15,21H17V19H15M3,9H5V7H3M5,21V19H3A2,2 0 0,0 5,21M3,17H5V15H3M3,13H5V11H3V13Z" /></g><g id="floppy"><path d="M4.5,22L2,19.5V4A2,2 0 0,1 4,2H20A2,2 0 0,1 22,4V20A2,2 0 0,1 20,22H17V15A1,1 0 0,0 16,14H7A1,1 0 0,0 6,15V22H4.5M5,4V10A1,1 0 0,0 6,11H18A1,1 0 0,0 19,10V4H5M8,16H11V20H8V16M20,4V5H21V4H20Z" /></g><g id="flower"><path d="M3,13A9,9 0 0,0 12,22C12,17 7.97,13 3,13M12,5.5A2.5,2.5 0 0,1 14.5,8A2.5,2.5 0 0,1 12,10.5A2.5,2.5 0 0,1 9.5,8A2.5,2.5 0 0,1 12,5.5M5.6,10.25A2.5,2.5 0 0,0 8.1,12.75C8.63,12.75 9.12,12.58 9.5,12.31C9.5,12.37 9.5,12.43 9.5,12.5A2.5,2.5 0 0,0 12,15A2.5,2.5 0 0,0 14.5,12.5C14.5,12.43 14.5,12.37 14.5,12.31C14.88,12.58 15.37,12.75 15.9,12.75C17.28,12.75 18.4,11.63 18.4,10.25C18.4,9.25 17.81,8.4 16.97,8C17.81,7.6 18.4,6.74 18.4,5.75C18.4,4.37 17.28,3.25 15.9,3.25C15.37,3.25 14.88,3.41 14.5,3.69C14.5,3.63 14.5,3.56 14.5,3.5A2.5,2.5 0 0,0 12,1A2.5,2.5 0 0,0 9.5,3.5C9.5,3.56 9.5,3.63 9.5,3.69C9.12,3.41 8.63,3.25 8.1,3.25A2.5,2.5 0 0,0 5.6,5.75C5.6,6.74 6.19,7.6 7.03,8C6.19,8.4 5.6,9.25 5.6,10.25M12,22A9,9 0 0,0 21,13C16,13 12,17 12,22Z" /></g><g id="folder"><path d="M10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6H12L10,4Z" /></g><g id="folder-account"><path d="M19,17H11V16C11,14.67 13.67,14 15,14C16.33,14 19,14.67 19,16M15,9A2,2 0 0,1 17,11A2,2 0 0,1 15,13A2,2 0 0,1 13,11C13,9.89 13.9,9 15,9M20,6H12L10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6Z" /></g><g id="folder-download"><path d="M20,6A2,2 0 0,1 22,8V18A2,2 0 0,1 20,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4H10L12,6H20M19.25,13H16V9H14V13H10.75L15,17.25" /></g><g id="folder-google-drive"><path d="M13.75,9H16.14L19,14H16.05L13.5,9.46M18.3,17H12.75L14.15,14.5H19.27L19.53,14.96M11.5,17L10.4,14.86L13.24,9.9L14.74,12.56L12.25,17M20,6H12L10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6Z" /></g><g id="folder-image"><path d="M5,17L9.5,11L13,15.5L15.5,12.5L19,17M20,6H12L10,4H4A2,2 0 0,0 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8A2,2 0 0,0 20,6Z" /></g><g id="folder-lock"><path d="M20,6A2,2 0 0,1 22,8V18A2,2 0 0,1 20,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4H10L12,6H20M19,17V13H18V12A3,3 0 0,0 15,9A3,3 0 0,0 12,12V13H11V17H19M15,11A1,1 0 0,1 16,12V13H14V12A1,1 0 0,1 15,11Z" /></g><g id="folder-lock-open"><path d="M20,6A2,2 0 0,1 22,8V18A2,2 0 0,1 20,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4H10L12,6H20M19,17V13H18L16,13H14V11A1,1 0 0,1 15,10A1,1 0 0,1 16,11H18A3,3 0 0,0 15,8A3,3 0 0,0 12,11V13H11V17H19Z" /></g><g id="folder-move"><path d="M9,18V15H5V11H9V8L14,13M20,6H12L10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6Z" /></g><g id="folder-multiple"><path d="M22,4H14L12,2H6A2,2 0 0,0 4,4V16A2,2 0 0,0 6,18H22A2,2 0 0,0 24,16V6A2,2 0 0,0 22,4M2,6H0V11H0V20A2,2 0 0,0 2,22H20V20H2V6Z" /></g><g id="folder-multiple-image"><path d="M7,15L11.5,9L15,13.5L17.5,10.5L21,15M22,4H14L12,2H6A2,2 0 0,0 4,4V16A2,2 0 0,0 6,18H22A2,2 0 0,0 24,16V6A2,2 0 0,0 22,4M2,6H0V11H0V20A2,2 0 0,0 2,22H20V20H2V6Z" /></g><g id="folder-multiple-outline"><path d="M22,4A2,2 0 0,1 24,6V16A2,2 0 0,1 22,18H6A2,2 0 0,1 4,16V4A2,2 0 0,1 6,2H12L14,4H22M2,6V20H20V22H2A2,2 0 0,1 0,20V11H0V6H2M6,6V16H22V6H6Z" /></g><g id="folder-outline"><path d="M20,18H4V8H20M20,6H12L10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6Z" /></g><g id="folder-plus"><path d="M10,4L12,6H20A2,2 0 0,1 22,8V18A2,2 0 0,1 20,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4H10M15,9V12H12V14H15V17H17V14H20V12H17V9H15Z" /></g><g id="folder-remove"><path d="M10,4L12,6H20A2,2 0 0,1 22,8V18A2,2 0 0,1 20,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4H10M12.46,10.88L14.59,13L12.46,15.12L13.88,16.54L16,14.41L18.12,16.54L19.54,15.12L17.41,13L19.54,10.88L18.12,9.46L16,11.59L13.88,9.46L12.46,10.88Z" /></g><g id="folder-star"><path d="M20,6H12L10,4H4A2,2 0 0,0 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8A2,2 0 0,0 20,6M17.94,17L15,15.28L12.06,17L12.84,13.67L10.25,11.43L13.66,11.14L15,8L16.34,11.14L19.75,11.43L17.16,13.67L17.94,17Z" /></g><g id="folder-upload"><path d="M20,6A2,2 0 0,1 22,8V18A2,2 0 0,1 20,20H4A2,2 0 0,1 2,18V6A2,2 0 0,1 4,4H10L12,6H20M10.75,13H14V17H16V13H19.25L15,8.75" /></g><g id="food"><path d="M15.5,21L14,8H16.23L15.1,3.46L16.84,3L18.09,8H22L20.5,21H15.5M5,11H10A3,3 0 0,1 13,14H2A3,3 0 0,1 5,11M13,18A3,3 0 0,1 10,21H5A3,3 0 0,1 2,18H13M3,15H8L9.5,16.5L11,15H12A1,1 0 0,1 13,16A1,1 0 0,1 12,17H3A1,1 0 0,1 2,16A1,1 0 0,1 3,15Z" /></g><g id="food-apple"><path d="M20,10C22,13 17,22 15,22C13,22 13,21 12,21C11,21 11,22 9,22C7,22 2,13 4,10C6,7 9,7 11,8V5C5.38,8.07 4.11,3.78 4.11,3.78C4.11,3.78 6.77,0.19 11,5V3H13V8C15,7 18,7 20,10Z" /></g><g id="food-fork-drink"><path d="M3,3A1,1 0 0,0 2,4V8L2,9.5C2,11.19 3.03,12.63 4.5,13.22V19.5A1.5,1.5 0 0,0 6,21A1.5,1.5 0 0,0 7.5,19.5V13.22C8.97,12.63 10,11.19 10,9.5V8L10,4A1,1 0 0,0 9,3A1,1 0 0,0 8,4V8A0.5,0.5 0 0,1 7.5,8.5A0.5,0.5 0 0,1 7,8V4A1,1 0 0,0 6,3A1,1 0 0,0 5,4V8A0.5,0.5 0 0,1 4.5,8.5A0.5,0.5 0 0,1 4,8V4A1,1 0 0,0 3,3M19.88,3C19.75,3 19.62,3.09 19.5,3.16L16,5.25V9H12V11H13L14,21H20L21,11H22V9H18V6.34L20.5,4.84C21,4.56 21.13,4 20.84,3.5C20.63,3.14 20.26,2.95 19.88,3Z" /></g><g id="food-off"><path d="M2,5.27L3.28,4L21,21.72L19.73,23L17.73,21H15.5L15.21,18.5L12.97,16.24C12.86,16.68 12.47,17 12,17H3A1,1 0 0,1 2,16A1,1 0 0,1 3,15H8L9.5,16.5L11,15H11.73L10.73,14H2A3,3 0 0,1 5,11H7.73L2,5.27M14,8H16.23L15.1,3.46L16.84,3L18.09,8H22L20.74,18.92L14.54,12.72L14,8M13,18A3,3 0 0,1 10,21H5A3,3 0 0,1 2,18H13Z" /></g><g id="food-variant"><path d="M22,18A4,4 0 0,1 18,22H15A4,4 0 0,1 11,18V16H17.79L20.55,11.23L22.11,12.13L19.87,16H22V18M9,22H2C2,19 2,16 2.33,12.83C2.6,10.3 3.08,7.66 3.6,5H3V3H4L7,3H8V5H7.4C7.92,7.66 8.4,10.3 8.67,12.83C9,16 9,19 9,22Z" /></g><g id="football"><path d="M7.5,7.5C9.17,5.87 11.29,4.69 13.37,4.18C15.46,3.67 17.5,3.83 18.6,4C19.71,4.15 19.87,4.31 20.03,5.41C20.18,6.5 20.33,8.55 19.82,10.63C19.31,12.71 18.13,14.83 16.5,16.5C14.83,18.13 12.71,19.31 10.63,19.82C8.55,20.33 6.5,20.18 5.41,20.03C4.31,19.87 4.15,19.71 4,18.6C3.83,17.5 3.67,15.46 4.18,13.37C4.69,11.29 5.87,9.17 7.5,7.5M7.3,15.79L8.21,16.7L9.42,15.5L10.63,16.7L11.54,15.79L10.34,14.58L12,12.91L13.21,14.12L14.12,13.21L12.91,12L14.58,10.34L15.79,11.54L16.7,10.63L15.5,9.42L16.7,8.21L15.79,7.3L14.58,8.5L13.37,7.3L12.46,8.21L13.66,9.42L12,11.09L10.79,9.88L9.88,10.79L11.09,12L9.42,13.66L8.21,12.46L7.3,13.37L8.5,14.58L7.3,15.79Z" /></g><g id="football-australian"><path d="M7.5,7.5C9.17,5.87 11.29,4.69 13.37,4.18C18,3 21,6 19.82,10.63C19.31,12.71 18.13,14.83 16.5,16.5C14.83,18.13 12.71,19.31 10.63,19.82C6,21 3,18 4.18,13.37C4.69,11.29 5.87,9.17 7.5,7.5M10.62,11.26L10.26,11.62L12.38,13.74L12.74,13.38L10.62,11.26M11.62,10.26L11.26,10.62L13.38,12.74L13.74,12.38L11.62,10.26M9.62,12.26L9.26,12.62L11.38,14.74L11.74,14.38L9.62,12.26M12.63,9.28L12.28,9.63L14.4,11.75L14.75,11.4L12.63,9.28M8.63,13.28L8.28,13.63L10.4,15.75L10.75,15.4L8.63,13.28M13.63,8.28L13.28,8.63L15.4,10.75L15.75,10.4L13.63,8.28Z" /></g><g id="football-helmet"><path d="M13.5,12A1.5,1.5 0 0,0 12,13.5A1.5,1.5 0 0,0 13.5,15A1.5,1.5 0 0,0 15,13.5A1.5,1.5 0 0,0 13.5,12M13.5,3C18.19,3 22,6.58 22,11C22,12.62 22,14 21.09,16C17,16 16,20 12.5,20C10.32,20 9.27,18.28 9.05,16H9L8.24,16L6.96,20.3C6.81,20.79 6.33,21.08 5.84,21H3A1,1 0 0,1 2,20A1,1 0 0,1 3,19V16A1,1 0 0,1 2,15A1,1 0 0,1 3,14H6.75L7.23,12.39C6.72,12.14 6.13,12 5.5,12H5.07L5,11C5,6.58 8.81,3 13.5,3M5,16V19H5.26L6.15,16H5Z" /></g><g id="format-align-center"><path d="M3,3H21V5H3V3M7,7H17V9H7V7M3,11H21V13H3V11M7,15H17V17H7V15M3,19H21V21H3V19Z" /></g><g id="format-align-justify"><path d="M3,3H21V5H3V3M3,7H21V9H3V7M3,11H21V13H3V11M3,15H21V17H3V15M3,19H21V21H3V19Z" /></g><g id="format-align-left"><path d="M3,3H21V5H3V3M3,7H15V9H3V7M3,11H21V13H3V11M3,15H15V17H3V15M3,19H21V21H3V19Z" /></g><g id="format-align-right"><path d="M3,3H21V5H3V3M9,7H21V9H9V7M3,11H21V13H3V11M9,15H21V17H9V15M3,19H21V21H3V19Z" /></g><g id="format-annotation-plus"><path d="M8.5,7H10.5L16,21H13.6L12.5,18H6.3L5.2,21H3L8.5,7M7.1,16H11.9L9.5,9.7L7.1,16M22,5V7H19V10H17V7H14V5H17V2H19V5H22Z" /></g><g id="format-bold"><path d="M13.5,15.5H10V12.5H13.5A1.5,1.5 0 0,1 15,14A1.5,1.5 0 0,1 13.5,15.5M10,6.5H13A1.5,1.5 0 0,1 14.5,8A1.5,1.5 0 0,1 13,9.5H10M15.6,10.79C16.57,10.11 17.25,9 17.25,8C17.25,5.74 15.5,4 13.25,4H7V18H14.04C16.14,18 17.75,16.3 17.75,14.21C17.75,12.69 16.89,11.39 15.6,10.79Z" /></g><g id="format-clear"><path d="M6,5V5.18L8.82,8H11.22L10.5,9.68L12.6,11.78L14.21,8H20V5H6M3.27,5L2,6.27L8.97,13.24L6.5,19H9.5L11.07,15.34L16.73,21L18,19.73L3.55,5.27L3.27,5Z" /></g><g id="format-color-fill"><path d="M19,11.5C19,11.5 17,13.67 17,15A2,2 0 0,0 19,17A2,2 0 0,0 21,15C21,13.67 19,11.5 19,11.5M5.21,10L10,5.21L14.79,10M16.56,8.94L7.62,0L6.21,1.41L8.59,3.79L3.44,8.94C2.85,9.5 2.85,10.47 3.44,11.06L8.94,16.56C9.23,16.85 9.62,17 10,17C10.38,17 10.77,16.85 11.06,16.56L16.56,11.06C17.15,10.47 17.15,9.5 16.56,8.94Z" /></g><g id="format-color-text"><path d="M9.62,12L12,5.67L14.37,12M11,3L5.5,17H7.75L8.87,14H15.12L16.25,17H18.5L13,3H11Z" /></g><g id="format-float-center"><path d="M9,7H15V13H9V7M3,3H21V5H3V3M3,15H21V17H3V15M3,19H17V21H3V19Z" /></g><g id="format-float-left"><path d="M3,7H9V13H3V7M3,3H21V5H3V3M21,7V9H11V7H21M21,11V13H11V11H21M3,15H17V17H3V15M3,19H21V21H3V19Z" /></g><g id="format-float-none"><path d="M3,7H9V13H3V7M3,3H21V5H3V3M21,11V13H11V11H21M3,15H17V17H3V15M3,19H21V21H3V19Z" /></g><g id="format-float-right"><path d="M15,7H21V13H15V7M3,3H21V5H3V3M13,7V9H3V7H13M9,11V13H3V11H9M3,15H17V17H3V15M3,19H21V21H3V19Z" /></g><g id="format-header-1"><path d="M3,4H5V10H9V4H11V18H9V12H5V18H3V4M14,18V16H16V6.31L13.5,7.75V5.44L16,4H18V16H20V18H14Z" /></g><g id="format-header-2"><path d="M3,4H5V10H9V4H11V18H9V12H5V18H3V4M21,18H15A2,2 0 0,1 13,16C13,15.47 13.2,15 13.54,14.64L18.41,9.41C18.78,9.05 19,8.55 19,8A2,2 0 0,0 17,6A2,2 0 0,0 15,8H13A4,4 0 0,1 17,4A4,4 0 0,1 21,8C21,9.1 20.55,10.1 19.83,10.83L15,16H21V18Z" /></g><g id="format-header-3"><path d="M3,4H5V10H9V4H11V18H9V12H5V18H3V4M15,4H19A2,2 0 0,1 21,6V16A2,2 0 0,1 19,18H15A2,2 0 0,1 13,16V15H15V16H19V12H15V10H19V6H15V7H13V6A2,2 0 0,1 15,4Z" /></g><g id="format-header-4"><path d="M3,4H5V10H9V4H11V18H9V12H5V18H3V4M18,18V13H13V11L18,4H20V11H21V13H20V18H18M18,11V7.42L15.45,11H18Z" /></g><g id="format-header-5"><path d="M3,4H5V10H9V4H11V18H9V12H5V18H3V4M15,4H20V6H15V10H17A4,4 0 0,1 21,14A4,4 0 0,1 17,18H15A2,2 0 0,1 13,16V15H15V16H17A2,2 0 0,0 19,14A2,2 0 0,0 17,12H15A2,2 0 0,1 13,10V6A2,2 0 0,1 15,4Z" /></g><g id="format-header-6"><path d="M3,4H5V10H9V4H11V18H9V12H5V18H3V4M15,4H19A2,2 0 0,1 21,6V7H19V6H15V10H19A2,2 0 0,1 21,12V16A2,2 0 0,1 19,18H15A2,2 0 0,1 13,16V6A2,2 0 0,1 15,4M15,12V16H19V12H15Z" /></g><g id="format-header-decrease"><path d="M4,4H6V10H10V4H12V18H10V12H6V18H4V4M20.42,7.41L16.83,11L20.42,14.59L19,16L14,11L19,6L20.42,7.41Z" /></g><g id="format-header-equal"><path d="M4,4H6V10H10V4H12V18H10V12H6V18H4V4M14,10V8H21V10H14M14,12H21V14H14V12Z" /></g><g id="format-header-increase"><path d="M4,4H6V10H10V4H12V18H10V12H6V18H4V4M14.59,7.41L18.17,11L14.59,14.59L16,16L21,11L16,6L14.59,7.41Z" /></g><g id="format-header-pound"><path d="M3,4H5V10H9V4H11V18H9V12H5V18H3V4M13,8H15.31L15.63,5H17.63L17.31,8H19.31L19.63,5H21.63L21.31,8H23V10H21.1L20.9,12H23V14H20.69L20.37,17H18.37L18.69,14H16.69L16.37,17H14.37L14.69,14H13V12H14.9L15.1,10H13V8M17.1,10L16.9,12H18.9L19.1,10H17.1Z" /></g><g id="format-horizontal-align-center"><path d="M19,16V13H23V11H19V8L15,12L19,16M5,8V11H1V13H5V16L9,12L5,8M11,20H13V4H11V20Z" /></g><g id="format-horizontal-align-left"><path d="M11,16V13H21V11H11V8L7,12L11,16M3,20H5V4H3V20Z" /></g><g id="format-horizontal-align-right"><path d="M13,8V11H3V13H13V16L17,12L13,8M19,20H21V4H19V20Z" /></g><g id="format-indent-decrease"><path d="M11,13H21V11H11M11,9H21V7H11M3,3V5H21V3M3,21H21V19H3M3,12L7,16V8M11,17H21V15H11V17Z" /></g><g id="format-indent-increase"><path d="M11,13H21V11H11M11,9H21V7H11M3,3V5H21V3M11,17H21V15H11M3,8V16L7,12M3,21H21V19H3V21Z" /></g><g id="format-italic"><path d="M10,4V7H12.21L8.79,15H6V18H14V15H11.79L15.21,7H18V4H10Z" /></g><g id="format-line-spacing"><path d="M10,13H22V11H10M10,19H22V17H10M10,7H22V5H10M6,7H8.5L5,3.5L1.5,7H4V17H1.5L5,20.5L8.5,17H6V7Z" /></g><g id="format-line-style"><path d="M3,16H8V14H3V16M9.5,16H14.5V14H9.5V16M16,16H21V14H16V16M3,20H5V18H3V20M7,20H9V18H7V20M11,20H13V18H11V20M15,20H17V18H15V20M19,20H21V18H19V20M3,12H11V10H3V12M13,12H21V10H13V12M3,4V8H21V4H3Z" /></g><g id="format-line-weight"><path d="M3,17H21V15H3V17M3,20H21V19H3V20M3,13H21V10H3V13M3,4V8H21V4H3Z" /></g><g id="format-list-bulleted"><path d="M7,5H21V7H7V5M7,13V11H21V13H7M4,4.5A1.5,1.5 0 0,1 5.5,6A1.5,1.5 0 0,1 4,7.5A1.5,1.5 0 0,1 2.5,6A1.5,1.5 0 0,1 4,4.5M4,10.5A1.5,1.5 0 0,1 5.5,12A1.5,1.5 0 0,1 4,13.5A1.5,1.5 0 0,1 2.5,12A1.5,1.5 0 0,1 4,10.5M7,19V17H21V19H7M4,16.5A1.5,1.5 0 0,1 5.5,18A1.5,1.5 0 0,1 4,19.5A1.5,1.5 0 0,1 2.5,18A1.5,1.5 0 0,1 4,16.5Z" /></g><g id="format-list-bulleted-type"><path d="M5,9.5L7.5,14H2.5L5,9.5M3,4H7V8H3V4M5,20A2,2 0 0,0 7,18A2,2 0 0,0 5,16A2,2 0 0,0 3,18A2,2 0 0,0 5,20M9,5V7H21V5H9M9,19H21V17H9V19M9,13H21V11H9V13Z" /></g><g id="format-list-numbers"><path d="M7,13H21V11H7M7,19H21V17H7M7,7H21V5H7M2,11H3.8L2,13.1V14H5V13H3.2L5,10.9V10H2M3,8H4V4H2V5H3M2,17H4V17.5H3V18.5H4V19H2V20H5V16H2V17Z" /></g><g id="format-paint"><path d="M18,4V3A1,1 0 0,0 17,2H5A1,1 0 0,0 4,3V7A1,1 0 0,0 5,8H17A1,1 0 0,0 18,7V6H19V10H9V21A1,1 0 0,0 10,22H12A1,1 0 0,0 13,21V12H21V4H18Z" /></g><g id="format-paragraph"><path d="M13,4A4,4 0 0,1 17,8A4,4 0 0,1 13,12H11V18H9V4H13M13,10A2,2 0 0,0 15,8A2,2 0 0,0 13,6H11V10H13Z" /></g><g id="format-quote"><path d="M14,17H17L19,13V7H13V13H16M6,17H9L11,13V7H5V13H8L6,17Z" /></g><g id="format-section"><path d="M15.67,4.42C14.7,3.84 13.58,3.54 12.45,3.56C10.87,3.56 9.66,4.34 9.66,5.56C9.66,6.96 11,7.47 13,8.14C15.5,8.95 17.4,9.97 17.4,12.38C17.36,13.69 16.69,14.89 15.6,15.61C16.25,16.22 16.61,17.08 16.6,17.97C16.6,20.79 14,21.97 11.5,21.97C10.04,22.03 8.59,21.64 7.35,20.87L8,19.34C9.04,20.05 10.27,20.43 11.53,20.44C13.25,20.44 14.53,19.66 14.53,18.24C14.53,17 13.75,16.31 11.25,15.45C8.5,14.5 6.6,13.5 6.6,11.21C6.67,9.89 7.43,8.69 8.6,8.07C7.97,7.5 7.61,6.67 7.6,5.81C7.6,3.45 9.77,2 12.53,2C13.82,2 15.09,2.29 16.23,2.89L15.67,4.42M11.35,13.42C12.41,13.75 13.44,14.18 14.41,14.71C15.06,14.22 15.43,13.45 15.41,12.64C15.41,11.64 14.77,10.76 13,10.14C11.89,9.77 10.78,9.31 9.72,8.77C8.97,9.22 8.5,10.03 8.5,10.91C8.5,11.88 9.23,12.68 11.35,13.42Z" /></g><g id="format-size"><path d="M3,12H6V19H9V12H12V9H3M9,4V7H14V19H17V7H22V4H9Z" /></g><g id="format-strikethrough"><path d="M3,14H21V12H3M5,4V7H10V10H14V7H19V4M10,19H14V16H10V19Z" /></g><g id="format-strikethrough-variant"><path d="M23,12V14H18.61C19.61,16.14 19.56,22 12.38,22C4.05,22.05 4.37,15.5 4.37,15.5L8.34,15.55C8.37,18.92 11.5,18.92 12.12,18.88C12.76,18.83 15.15,18.84 15.34,16.5C15.42,15.41 14.32,14.58 13.12,14H1V12H23M19.41,7.89L15.43,7.86C15.43,7.86 15.6,5.09 12.15,5.08C8.7,5.06 9,7.28 9,7.56C9.04,7.84 9.34,9.22 12,9.88H5.71C5.71,9.88 2.22,3.15 10.74,2C19.45,0.8 19.43,7.91 19.41,7.89Z" /></g><g id="format-subscript"><path d="M16,7.41L11.41,12L16,16.59L14.59,18L10,13.41L5.41,18L4,16.59L8.59,12L4,7.41L5.41,6L10,10.59L14.59,6L16,7.41M21.85,21.03H16.97V20.03L17.86,19.23C18.62,18.58 19.18,18.04 19.56,17.6C19.93,17.16 20.12,16.75 20.13,16.36C20.14,16.08 20.05,15.85 19.86,15.66C19.68,15.5 19.39,15.38 19,15.38C18.69,15.38 18.42,15.44 18.16,15.56L17.5,15.94L17.05,14.77C17.32,14.56 17.64,14.38 18.03,14.24C18.42,14.1 18.85,14 19.32,14C20.1,14.04 20.7,14.25 21.1,14.66C21.5,15.07 21.72,15.59 21.72,16.23C21.71,16.79 21.53,17.31 21.18,17.78C20.84,18.25 20.42,18.7 19.91,19.14L19.27,19.66V19.68H21.85V21.03Z" /></g><g id="format-superscript"><path d="M16,7.41L11.41,12L16,16.59L14.59,18L10,13.41L5.41,18L4,16.59L8.59,12L4,7.41L5.41,6L10,10.59L14.59,6L16,7.41M21.85,9H16.97V8L17.86,7.18C18.62,6.54 19.18,6 19.56,5.55C19.93,5.11 20.12,4.7 20.13,4.32C20.14,4.04 20.05,3.8 19.86,3.62C19.68,3.43 19.39,3.34 19,3.33C18.69,3.34 18.42,3.4 18.16,3.5L17.5,3.89L17.05,2.72C17.32,2.5 17.64,2.33 18.03,2.19C18.42,2.05 18.85,2 19.32,2C20.1,2 20.7,2.2 21.1,2.61C21.5,3 21.72,3.54 21.72,4.18C21.71,4.74 21.53,5.26 21.18,5.73C20.84,6.21 20.42,6.66 19.91,7.09L19.27,7.61V7.63H21.85V9Z" /></g><g id="format-text"><path d="M18.5,4L19.66,8.35L18.7,8.61C18.25,7.74 17.79,6.87 17.26,6.43C16.73,6 16.11,6 15.5,6H13V16.5C13,17 13,17.5 13.33,17.75C13.67,18 14.33,18 15,18V19H9V18C9.67,18 10.33,18 10.67,17.75C11,17.5 11,17 11,16.5V6H8.5C7.89,6 7.27,6 6.74,6.43C6.21,6.87 5.75,7.74 5.3,8.61L4.34,8.35L5.5,4H18.5Z" /></g><g id="format-textdirection-l-to-r"><path d="M21,18L17,14V17H5V19H17V22M9,10V15H11V4H13V15H15V4H17V2H9A4,4 0 0,0 5,6A4,4 0 0,0 9,10Z" /></g><g id="format-textdirection-r-to-l"><path d="M8,17V14L4,18L8,22V19H20V17M10,10V15H12V4H14V15H16V4H18V2H10A4,4 0 0,0 6,6A4,4 0 0,0 10,10Z" /></g><g id="format-title"><path d="M5,4V7H10.5V19H13.5V7H19V4H5Z" /></g><g id="format-underline"><path d="M5,21H19V19H5V21M12,17A6,6 0 0,0 18,11V3H15.5V11A3.5,3.5 0 0,1 12,14.5A3.5,3.5 0 0,1 8.5,11V3H6V11A6,6 0 0,0 12,17Z" /></g><g id="format-vertical-align-bottom"><path d="M16,13H13V3H11V13H8L12,17L16,13M4,19V21H20V19H4Z" /></g><g id="format-vertical-align-center"><path d="M8,19H11V23H13V19H16L12,15L8,19M16,5H13V1H11V5H8L12,9L16,5M4,11V13H20V11H4Z" /></g><g id="format-vertical-align-top"><path d="M8,11H11V21H13V11H16L12,7L8,11M4,3V5H20V3H4Z" /></g><g id="format-wrap-inline"><path d="M8,7L13,17H3L8,7M3,3H21V5H3V3M21,15V17H14V15H21M3,19H21V21H3V19Z" /></g><g id="format-wrap-square"><path d="M12,7L17,17H7L12,7M3,3H21V5H3V3M3,7H6V9H3V7M21,7V9H18V7H21M3,11H6V13H3V11M21,11V13H18V11H21M3,15H6V17H3V15M21,15V17H18V15H21M3,19H21V21H3V19Z" /></g><g id="format-wrap-tight"><path d="M12,7L17,17H7L12,7M3,3H21V5H3V3M3,7H9V9H3V7M21,7V9H15V7H21M3,11H7V13H3V11M21,11V13H17V11H21M3,15H6V17H3V15M21,15V17H18V15H21M3,19H21V21H3V19Z" /></g><g id="format-wrap-top-bottom"><path d="M12,7L17,17H7L12,7M3,3H21V5H3V3M3,19H21V21H3V19Z" /></g><g id="forum"><path d="M17,12V3A1,1 0 0,0 16,2H3A1,1 0 0,0 2,3V17L6,13H16A1,1 0 0,0 17,12M21,6H19V15H6V17A1,1 0 0,0 7,18H18L22,22V7A1,1 0 0,0 21,6Z" /></g><g id="forward"><path d="M12,8V4L20,12L12,20V16H4V8H12Z" /></g><g id="foursquare"><path d="M17,5L16.57,7.5C16.5,7.73 16.2,8 15.91,8C15.61,8 12,8 12,8C11.53,8 10.95,8.32 10.95,8.79V9.2C10.95,9.67 11.53,10 12,10C12,10 14.95,10 15.28,10C15.61,10 15.93,10.36 15.86,10.71C15.79,11.07 14.94,13.28 14.9,13.5C14.86,13.67 14.64,14 14.25,14C13.92,14 11.37,14 11.37,14C10.85,14 10.69,14.07 10.34,14.5C10,14.94 7.27,18.1 7.27,18.1C7.24,18.13 7,18.04 7,18V5C7,4.7 7.61,4 8,4C8,4 16.17,4 16.5,4C16.82,4 17.08,4.61 17,5M17,14.45C17.11,13.97 18.78,6.72 19.22,4.55M17.58,2C17.58,2 8.38,2 6.91,2C5.43,2 5,3.11 5,3.8C5,4.5 5,20.76 5,20.76C5,21.54 5.42,21.84 5.66,21.93C5.9,22.03 6.55,22.11 6.94,21.66C6.94,21.66 11.65,16.22 11.74,16.13C11.87,16 11.87,16 12,16C12.26,16 14.2,16 15.26,16C16.63,16 16.85,15 17,14.45C17.11,13.97 18.78,6.72 19.22,4.55C19.56,2.89 19.14,2 17.58,2Z" /></g><g id="fridge"><path d="M9,21V22H7V21A2,2 0 0,1 5,19V4A2,2 0 0,1 7,2H17A2,2 0 0,1 19,4V19A2,2 0 0,1 17,21V22H15V21H9M7,4V9H17V4H7M7,19H17V11H7V19M8,12H10V15H8V12M8,6H10V8H8V6Z" /></g><g id="fridge-filled"><path d="M7,2H17A2,2 0 0,1 19,4V9H5V4A2,2 0 0,1 7,2M19,19A2,2 0 0,1 17,21V22H15V21H9V22H7V21A2,2 0 0,1 5,19V10H19V19M8,5V7H10V5H8M8,12V15H10V12H8Z" /></g><g id="fridge-filled-bottom"><path d="M8,8V6H10V8H8M7,2H17A2,2 0 0,1 19,4V19A2,2 0 0,1 17,21V22H15V21H9V22H7V21A2,2 0 0,1 5,19V4A2,2 0 0,1 7,2M7,4V9H17V4H7M8,12V15H10V12H8Z" /></g><g id="fridge-filled-top"><path d="M7,2A2,2 0 0,0 5,4V19A2,2 0 0,0 7,21V22H9V21H15V22H17V21A2,2 0 0,0 19,19V4A2,2 0 0,0 17,2H7M8,6H10V8H8V6M7,11H17V19H7V11M8,12V15H10V12H8Z" /></g><g id="fullscreen"><path d="M5,5H10V7H7V10H5V5M14,5H19V10H17V7H14V5M17,14H19V19H14V17H17V14M10,17V19H5V14H7V17H10Z" /></g><g id="fullscreen-exit"><path d="M14,14H19V16H16V19H14V14M5,14H10V19H8V16H5V14M8,5H10V10H5V8H8V5M19,8V10H14V5H16V8H19Z" /></g><g id="function"><path d="M15.6,5.29C14.5,5.19 13.53,6 13.43,7.11L13.18,10H16V12H13L12.56,17.07C12.37,19.27 10.43,20.9 8.23,20.7C6.92,20.59 5.82,19.86 5.17,18.83L6.67,17.33C6.91,18.07 7.57,18.64 8.4,18.71C9.5,18.81 10.47,18 10.57,16.89L11,12H8V10H11.17L11.44,6.93C11.63,4.73 13.57,3.1 15.77,3.3C17.08,3.41 18.18,4.14 18.83,5.17L17.33,6.67C17.09,5.93 16.43,5.36 15.6,5.29Z" /></g><g id="gamepad"><path d="M16.5,9L13.5,12L16.5,15H22V9M9,16.5V22H15V16.5L12,13.5M7.5,9H2V15H7.5L10.5,12M15,7.5V2H9V7.5L12,10.5L15,7.5Z" /></g><g id="gamepad-variant"><path d="M7,6H17A6,6 0 0,1 23,12A6,6 0 0,1 17,18C15.22,18 13.63,17.23 12.53,16H11.47C10.37,17.23 8.78,18 7,18A6,6 0 0,1 1,12A6,6 0 0,1 7,6M6,9V11H4V13H6V15H8V13H10V11H8V9H6M15.5,12A1.5,1.5 0 0,0 14,13.5A1.5,1.5 0 0,0 15.5,15A1.5,1.5 0 0,0 17,13.5A1.5,1.5 0 0,0 15.5,12M18.5,9A1.5,1.5 0 0,0 17,10.5A1.5,1.5 0 0,0 18.5,12A1.5,1.5 0 0,0 20,10.5A1.5,1.5 0 0,0 18.5,9Z" /></g><g id="gas-cylinder"><path d="M16,9V14L16,20A2,2 0 0,1 14,22H10A2,2 0 0,1 8,20V14L8,9C8,7.14 9.27,5.57 11,5.13V4H9V2H15V4H13V5.13C14.73,5.57 16,7.14 16,9Z" /></g><g id="gas-station"><path d="M18,10A1,1 0 0,1 17,9A1,1 0 0,1 18,8A1,1 0 0,1 19,9A1,1 0 0,1 18,10M12,10H6V5H12M19.77,7.23L19.78,7.22L16.06,3.5L15,4.56L17.11,6.67C16.17,7 15.5,7.93 15.5,9A2.5,2.5 0 0,0 18,11.5C18.36,11.5 18.69,11.42 19,11.29V18.5A1,1 0 0,1 18,19.5A1,1 0 0,1 17,18.5V14C17,12.89 16.1,12 15,12H14V5C14,3.89 13.1,3 12,3H6C4.89,3 4,3.89 4,5V21H14V13.5H15.5V18.5A2.5,2.5 0 0,0 18,21A2.5,2.5 0 0,0 20.5,18.5V9C20.5,8.31 20.22,7.68 19.77,7.23Z" /></g><g id="gate"><path d="M9,5V10H7V6H5V10H3V8H1V20H3V18H5V20H7V18H9V20H11V18H13V20H15V18H17V20H19V18H21V20H23V8H21V10H19V6H17V10H15V5H13V10H11V5H9M3,12H5V16H3V12M7,12H9V16H7V12M11,12H13V16H11V12M15,12H17V16H15V12M19,12H21V16H19V12Z" /></g><g id="gauge"><path d="M17.3,18C19,16.5 20,14.4 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12C4,14.4 5,16.5 6.7,18C8.2,16.7 10,16 12,16C14,16 15.9,16.7 17.3,18M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M7,9A1,1 0 0,1 8,10A1,1 0 0,1 7,11A1,1 0 0,1 6,10A1,1 0 0,1 7,9M10,6A1,1 0 0,1 11,7A1,1 0 0,1 10,8A1,1 0 0,1 9,7A1,1 0 0,1 10,6M17,9A1,1 0 0,1 18,10A1,1 0 0,1 17,11A1,1 0 0,1 16,10A1,1 0 0,1 17,9M14.4,6.1C14.9,6.3 15.1,6.9 15,7.4L13.6,10.8C13.8,11.1 14,11.5 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12C10,11 10.7,10.1 11.7,10L13.1,6.7C13.3,6.1 13.9,5.9 14.4,6.1Z" /></g><g id="gavel"><path d="M2.3,20.28L11.9,10.68L10.5,9.26L9.78,9.97C9.39,10.36 8.76,10.36 8.37,9.97L7.66,9.26C7.27,8.87 7.27,8.24 7.66,7.85L13.32,2.19C13.71,1.8 14.34,1.8 14.73,2.19L15.44,2.9C15.83,3.29 15.83,3.92 15.44,4.31L14.73,5L16.15,6.43C16.54,6.04 17.17,6.04 17.56,6.43C17.95,6.82 17.95,7.46 17.56,7.85L18.97,9.26L19.68,8.55C20.07,8.16 20.71,8.16 21.1,8.55L21.8,9.26C22.19,9.65 22.19,10.29 21.8,10.68L16.15,16.33C15.76,16.72 15.12,16.72 14.73,16.33L14.03,15.63C13.63,15.24 13.63,14.6 14.03,14.21L14.73,13.5L13.32,12.09L3.71,21.7C3.32,22.09 2.69,22.09 2.3,21.7C1.91,21.31 1.91,20.67 2.3,20.28M20,19A2,2 0 0,1 22,21V22H12V21A2,2 0 0,1 14,19H20Z" /></g><g id="gender-female"><path d="M12,4A6,6 0 0,1 18,10C18,12.97 15.84,15.44 13,15.92V18H15V20H13V22H11V20H9V18H11V15.92C8.16,15.44 6,12.97 6,10A6,6 0 0,1 12,4M12,6A4,4 0 0,0 8,10A4,4 0 0,0 12,14A4,4 0 0,0 16,10A4,4 0 0,0 12,6Z" /></g><g id="gender-male"><path d="M9,9C10.29,9 11.5,9.41 12.47,10.11L17.58,5H13V3H21V11H19V6.41L13.89,11.5C14.59,12.5 15,13.7 15,15A6,6 0 0,1 9,21A6,6 0 0,1 3,15A6,6 0 0,1 9,9M9,11A4,4 0 0,0 5,15A4,4 0 0,0 9,19A4,4 0 0,0 13,15A4,4 0 0,0 9,11Z" /></g><g id="gender-male-female"><path d="M17.58,4H14V2H21V9H19V5.41L15.17,9.24C15.69,10.03 16,11 16,12C16,14.42 14.28,16.44 12,16.9V19H14V21H12V23H10V21H8V19H10V16.9C7.72,16.44 6,14.42 6,12A5,5 0 0,1 11,7C12,7 12.96,7.3 13.75,7.83L17.58,4M11,9A3,3 0 0,0 8,12A3,3 0 0,0 11,15A3,3 0 0,0 14,12A3,3 0 0,0 11,9Z" /></g><g id="gender-transgender"><path d="M19.58,3H15V1H23V9H21V4.41L16.17,9.24C16.69,10.03 17,11 17,12C17,14.42 15.28,16.44 13,16.9V19H15V21H13V23H11V21H9V19H11V16.9C8.72,16.44 7,14.42 7,12C7,11 7.3,10.04 7.82,9.26L6.64,8.07L5.24,9.46L3.83,8.04L5.23,6.65L3,4.42V8H1V1H8V3H4.41L6.64,5.24L8.08,3.81L9.5,5.23L8.06,6.66L9.23,7.84C10,7.31 11,7 12,7C13,7 13.96,7.3 14.75,7.83L19.58,3M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9Z" /></g><g id="ghost"><path d="M12,2A9,9 0 0,0 3,11V22L6,19L9,22L12,19L15,22L18,19L21,22V11A9,9 0 0,0 12,2M9,8A2,2 0 0,1 11,10A2,2 0 0,1 9,12A2,2 0 0,1 7,10A2,2 0 0,1 9,8M15,8A2,2 0 0,1 17,10A2,2 0 0,1 15,12A2,2 0 0,1 13,10A2,2 0 0,1 15,8Z" /></g><g id="gift"><path d="M22,12V20A2,2 0 0,1 20,22H4A2,2 0 0,1 2,20V12A1,1 0 0,1 1,11V8A2,2 0 0,1 3,6H6.17C6.06,5.69 6,5.35 6,5A3,3 0 0,1 9,2C10,2 10.88,2.5 11.43,3.24V3.23L12,4L12.57,3.23V3.24C13.12,2.5 14,2 15,2A3,3 0 0,1 18,5C18,5.35 17.94,5.69 17.83,6H21A2,2 0 0,1 23,8V11A1,1 0 0,1 22,12M4,20H11V12H4V20M20,20V12H13V20H20M9,4A1,1 0 0,0 8,5A1,1 0 0,0 9,6A1,1 0 0,0 10,5A1,1 0 0,0 9,4M15,4A1,1 0 0,0 14,5A1,1 0 0,0 15,6A1,1 0 0,0 16,5A1,1 0 0,0 15,4M3,8V10H11V8H3M13,8V10H21V8H13Z" /></g><g id="git"><path d="M2.6,10.59L8.38,4.8L10.07,6.5C9.83,7.35 10.22,8.28 11,8.73V14.27C10.4,14.61 10,15.26 10,16A2,2 0 0,0 12,18A2,2 0 0,0 14,16C14,15.26 13.6,14.61 13,14.27V9.41L15.07,11.5C15,11.65 15,11.82 15,12A2,2 0 0,0 17,14A2,2 0 0,0 19,12A2,2 0 0,0 17,10C16.82,10 16.65,10 16.5,10.07L13.93,7.5C14.19,6.57 13.71,5.55 12.78,5.16C12.35,5 11.9,4.96 11.5,5.07L9.8,3.38L10.59,2.6C11.37,1.81 12.63,1.81 13.41,2.6L21.4,10.59C22.19,11.37 22.19,12.63 21.4,13.41L13.41,21.4C12.63,22.19 11.37,22.19 10.59,21.4L2.6,13.41C1.81,12.63 1.81,11.37 2.6,10.59Z" /></g><g id="github-box"><path d="M4,2H20A2,2 0 0,1 22,4V20A2,2 0 0,1 20,22H14.85C14.5,21.92 14.5,21.24 14.5,21V18.26C14.5,17.33 14.17,16.72 13.81,16.41C16.04,16.16 18.38,15.32 18.38,11.5C18.38,10.39 18,9.5 17.35,8.79C17.45,8.54 17.8,7.5 17.25,6.15C17.25,6.15 16.41,5.88 14.5,7.17C13.71,6.95 12.85,6.84 12,6.84C11.15,6.84 10.29,6.95 9.5,7.17C7.59,5.88 6.75,6.15 6.75,6.15C6.2,7.5 6.55,8.54 6.65,8.79C6,9.5 5.62,10.39 5.62,11.5C5.62,15.31 7.95,16.17 10.17,16.42C9.89,16.67 9.63,17.11 9.54,17.76C8.97,18 7.5,18.45 6.63,16.93C6.63,16.93 6.1,15.97 5.1,15.9C5.1,15.9 4.12,15.88 5,16.5C5,16.5 5.68,16.81 6.14,17.97C6.14,17.97 6.73,19.91 9.5,19.31V21C9.5,21.24 9.5,21.92 9.14,22H4A2,2 0 0,1 2,20V4A2,2 0 0,1 4,2Z" /></g><g id="github-circle"><path d="M12,2A10,10 0 0,0 2,12C2,16.42 4.87,20.17 8.84,21.5C9.34,21.58 9.5,21.27 9.5,21C9.5,20.77 9.5,20.14 9.5,19.31C6.73,19.91 6.14,17.97 6.14,17.97C5.68,16.81 5.03,16.5 5.03,16.5C4.12,15.88 5.1,15.9 5.1,15.9C6.1,15.97 6.63,16.93 6.63,16.93C7.5,18.45 8.97,18 9.54,17.76C9.63,17.11 9.89,16.67 10.17,16.42C7.95,16.17 5.62,15.31 5.62,11.5C5.62,10.39 6,9.5 6.65,8.79C6.55,8.54 6.2,7.5 6.75,6.15C6.75,6.15 7.59,5.88 9.5,7.17C10.29,6.95 11.15,6.84 12,6.84C12.85,6.84 13.71,6.95 14.5,7.17C16.41,5.88 17.25,6.15 17.25,6.15C17.8,7.5 17.45,8.54 17.35,8.79C18,9.5 18.38,10.39 18.38,11.5C18.38,15.32 16.04,16.16 13.81,16.41C14.17,16.72 14.5,17.33 14.5,18.26C14.5,19.6 14.5,20.68 14.5,21C14.5,21.27 14.66,21.59 15.17,21.5C19.14,20.16 22,16.42 22,12A10,10 0 0,0 12,2Z" /></g><g id="glass-flute"><path d="M8,2H16C15.67,5 15.33,8 14.75,9.83C14.17,11.67 13.33,12.33 12.92,14.08C12.5,15.83 12.5,18.67 13.08,20C13.67,21.33 14.83,21.17 15.42,21.25C16,21.33 16,21.67 16,22H8C8,21.67 8,21.33 8.58,21.25C9.17,21.17 10.33,21.33 10.92,20C11.5,18.67 11.5,15.83 11.08,14.08C10.67,12.33 9.83,11.67 9.25,9.83C8.67,8 8.33,5 8,2M10,4C10.07,5.03 10.15,6.07 10.24,7H13.76C13.85,6.07 13.93,5.03 14,4H10Z" /></g><g id="glass-mug"><path d="M10,4V7H18V4H10M8,2H20L21,2V3L20,4V20L21,21V22H20L8,22H7V21L8,20V18.6L4.2,16.83C3.5,16.5 3,15.82 3,15V8A2,2 0 0,1 5,6H8V4L7,3V2H8M5,15L8,16.39V8H5V15Z" /></g><g id="glass-stange"><path d="M8,2H16V22H8V2M10,4V7H14V4H10Z" /></g><g id="glass-tulip"><path d="M8,2H16C15.67,2.67 15.33,3.33 15.58,5C15.83,6.67 16.67,9.33 16.25,10.74C15.83,12.14 14.17,12.28 13.33,13.86C12.5,15.44 12.5,18.47 13.08,19.9C13.67,21.33 14.83,21.17 15.42,21.25C16,21.33 16,21.67 16,22H8C8,21.67 8,21.33 8.58,21.25C9.17,21.17 10.33,21.33 10.92,19.9C11.5,18.47 11.5,15.44 10.67,13.86C9.83,12.28 8.17,12.14 7.75,10.74C7.33,9.33 8.17,6.67 8.42,5C8.67,3.33 8.33,2.67 8,2M10,4C10,5.19 9.83,6.17 9.64,7H14.27C14.13,6.17 14,5.19 14,4H10Z" /></g><g id="glassdoor"><path d="M18,6H16V15C16,16 15.82,16.64 15,16.95L9.5,19V6C9.5,5.3 9.74,4.1 11,4.24L18,5V3.79L9,2.11C8.64,2.04 8.36,2 8,2C6.72,2 6,2.78 6,4V20.37C6,21.95 7.37,22.26 8,22L17,18.32C18,17.91 18,17 18,16V6Z" /></g><g id="glasses"><path d="M3,10C2.76,10 2.55,10.09 2.41,10.25C2.27,10.4 2.21,10.62 2.24,10.86L2.74,13.85C2.82,14.5 3.4,15 4,15H7C7.64,15 8.36,14.44 8.5,13.82L9.56,10.63C9.6,10.5 9.57,10.31 9.5,10.19C9.39,10.07 9.22,10 9,10H3M7,17H4C2.38,17 0.96,15.74 0.76,14.14L0.26,11.15C0.15,10.3 0.39,9.5 0.91,8.92C1.43,8.34 2.19,8 3,8H9C9.83,8 10.58,8.35 11.06,8.96C11.17,9.11 11.27,9.27 11.35,9.45C11.78,9.36 12.22,9.36 12.64,9.45C12.72,9.27 12.82,9.11 12.94,8.96C13.41,8.35 14.16,8 15,8H21C21.81,8 22.57,8.34 23.09,8.92C23.6,9.5 23.84,10.3 23.74,11.11L23.23,14.18C23.04,15.74 21.61,17 20,17H17C15.44,17 13.92,15.81 13.54,14.3L12.64,11.59C12.26,11.31 11.73,11.31 11.35,11.59L10.43,14.37C10.07,15.82 8.56,17 7,17M15,10C14.78,10 14.61,10.07 14.5,10.19C14.42,10.31 14.4,10.5 14.45,10.7L15.46,13.75C15.64,14.44 16.36,15 17,15H20C20.59,15 21.18,14.5 21.25,13.89L21.76,10.82C21.79,10.62 21.73,10.4 21.59,10.25C21.45,10.09 21.24,10 21,10H15Z" /></g><g id="gmail"><path d="M20,18H18V9.25L12,13L6,9.25V18H4V6H5.2L12,10.25L18.8,6H20M20,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V6C22,4.89 21.1,4 20,4Z" /></g><g id="gnome"><path d="M18.42,2C14.26,2 13.5,7.93 15.82,7.93C18.16,7.93 22.58,2 18.42,2M12,2.73C11.92,2.73 11.85,2.73 11.78,2.74C9.44,3.04 10.26,7.12 11.5,7.19C12.72,7.27 14.04,2.73 12,2.73M7.93,4.34C7.81,4.34 7.67,4.37 7.53,4.43C5.65,5.21 7.24,8.41 8.3,8.2C9.27,8 9.39,4.3 7.93,4.34M4.93,6.85C4.77,6.84 4.59,6.9 4.41,7.03C2.9,8.07 4.91,10.58 5.8,10.19C6.57,9.85 6.08,6.89 4.93,6.85M13.29,8.77C10.1,8.8 6.03,10.42 5.32,13.59C4.53,17.11 8.56,22 12.76,22C14.83,22 17.21,20.13 17.66,17.77C18,15.97 13.65,16.69 13.81,17.88C14,19.31 12.76,20 11.55,19.1C7.69,16.16 17.93,14.7 17.25,10.69C17.03,9.39 15.34,8.76 13.29,8.77Z" /></g><g id="gondola"><path d="M18,10H13V7.59L22.12,6.07L21.88,4.59L16.41,5.5C16.46,5.35 16.5,5.18 16.5,5A1.5,1.5 0 0,0 15,3.5A1.5,1.5 0 0,0 13.5,5C13.5,5.35 13.63,5.68 13.84,5.93L13,6.07V5H11V6.41L10.41,6.5C10.46,6.35 10.5,6.18 10.5,6A1.5,1.5 0 0,0 9,4.5A1.5,1.5 0 0,0 7.5,6C7.5,6.36 7.63,6.68 7.83,6.93L1.88,7.93L2.12,9.41L11,7.93V10H6C4.89,10 4,10.9 4,12V18A2,2 0 0,0 6,20H18A2,2 0 0,0 20,18V12A2,2 0 0,0 18,10M6,12H8.25V16H6V12M9.75,16V12H14.25V16H9.75M18,16H15.75V12H18V16Z" /></g><g id="google"><path d="M21.35,11.1H12.18V13.83H18.69C18.36,17.64 15.19,19.27 12.19,19.27C8.36,19.27 5,16.25 5,12C5,7.9 8.2,4.73 12.2,4.73C15.29,4.73 17.1,6.7 17.1,6.7L19,4.72C19,4.72 16.56,2 12.1,2C6.42,2 2.03,6.8 2.03,12C2.03,17.05 6.16,22 12.25,22C17.6,22 21.5,18.33 21.5,12.91C21.5,11.76 21.35,11.1 21.35,11.1V11.1Z" /></g><g id="google-cardboard"><path d="M20.74,6H3.2C2.55,6 2,6.57 2,7.27V17.73C2,18.43 2.55,19 3.23,19H8C8.54,19 9,18.68 9.16,18.21L10.55,14.74C10.79,14.16 11.35,13.75 12,13.75C12.65,13.75 13.21,14.16 13.45,14.74L14.84,18.21C15.03,18.68 15.46,19 15.95,19H20.74C21.45,19 22,18.43 22,17.73V7.27C22,6.57 21.45,6 20.74,6M7.22,14.58C6,14.58 5,13.55 5,12.29C5,11 6,10 7.22,10C8.44,10 9.43,11 9.43,12.29C9.43,13.55 8.44,14.58 7.22,14.58M16.78,14.58C15.56,14.58 14.57,13.55 14.57,12.29C14.57,11.03 15.56,10 16.78,10C18,10 19,11.03 19,12.29C19,13.55 18,14.58 16.78,14.58Z" /></g><g id="google-chrome"><path d="M12,20L15.46,14H15.45C15.79,13.4 16,12.73 16,12C16,10.8 15.46,9.73 14.62,9H19.41C19.79,9.93 20,10.94 20,12A8,8 0 0,1 12,20M4,12C4,10.54 4.39,9.18 5.07,8L8.54,14H8.55C9.24,15.19 10.5,16 12,16C12.45,16 12.88,15.91 13.29,15.77L10.89,19.91C7,19.37 4,16.04 4,12M15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9A3,3 0 0,1 15,12M12,4C14.96,4 17.54,5.61 18.92,8H12C10.06,8 8.45,9.38 8.08,11.21L5.7,7.08C7.16,5.21 9.44,4 12,4M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" /></g><g id="google-circles"><path d="M16.66,15H17C18,15 19,14.8 19.87,14.46C19.17,18.73 15.47,22 11,22C6,22 2,17.97 2,13C2,8.53 5.27,4.83 9.54,4.13C9.2,5 9,6 9,7V7.34C6.68,8.16 5,10.38 5,13A6,6 0 0,0 11,19C13.62,19 15.84,17.32 16.66,15M17,10A3,3 0 0,0 20,7A3,3 0 0,0 17,4A3,3 0 0,0 14,7A3,3 0 0,0 17,10M17,1A6,6 0 0,1 23,7A6,6 0 0,1 17,13A6,6 0 0,1 11,7C11,3.68 13.69,1 17,1Z" /></g><g id="google-circles-communities"><path d="M15,12C13.89,12 13,12.89 13,14A2,2 0 0,0 15,16A2,2 0 0,0 17,14C17,12.89 16.1,12 15,12M12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M14,9C14,7.89 13.1,7 12,7C10.89,7 10,7.89 10,9A2,2 0 0,0 12,11A2,2 0 0,0 14,9M9,12A2,2 0 0,0 7,14A2,2 0 0,0 9,16A2,2 0 0,0 11,14C11,12.89 10.1,12 9,12Z" /></g><g id="google-circles-extended"><path d="M18,19C16.89,19 16,18.1 16,17C16,15.89 16.89,15 18,15A2,2 0 0,1 20,17A2,2 0 0,1 18,19M18,13A4,4 0 0,0 14,17A4,4 0 0,0 18,21A4,4 0 0,0 22,17A4,4 0 0,0 18,13M12,11.1A1.9,1.9 0 0,0 10.1,13A1.9,1.9 0 0,0 12,14.9A1.9,1.9 0 0,0 13.9,13A1.9,1.9 0 0,0 12,11.1M6,19C4.89,19 4,18.1 4,17C4,15.89 4.89,15 6,15A2,2 0 0,1 8,17A2,2 0 0,1 6,19M6,13A4,4 0 0,0 2,17A4,4 0 0,0 6,21A4,4 0 0,0 10,17A4,4 0 0,0 6,13M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8C10.89,8 10,7.1 10,6C10,4.89 10.89,4 12,4M12,10A4,4 0 0,0 16,6A4,4 0 0,0 12,2A4,4 0 0,0 8,6A4,4 0 0,0 12,10Z" /></g><g id="google-circles-group"><path d="M5,10A2,2 0 0,0 3,12C3,13.11 3.9,14 5,14C6.11,14 7,13.11 7,12A2,2 0 0,0 5,10M5,16A4,4 0 0,1 1,12A4,4 0 0,1 5,8A4,4 0 0,1 9,12A4,4 0 0,1 5,16M10.5,11H14V8L18,12L14,16V13H10.5V11M5,6C4.55,6 4.11,6.05 3.69,6.14C5.63,3.05 9.08,1 13,1C19.08,1 24,5.92 24,12C24,18.08 19.08,23 13,23C9.08,23 5.63,20.95 3.69,17.86C4.11,17.95 4.55,18 5,18C5.8,18 6.56,17.84 7.25,17.56C8.71,19.07 10.74,20 13,20A8,8 0 0,0 21,12A8,8 0 0,0 13,4C10.74,4 8.71,4.93 7.25,6.44C6.56,6.16 5.8,6 5,6Z" /></g><g id="google-controller"><path d="M7.97,16L5,19C4.67,19.3 4.23,19.5 3.75,19.5A1.75,1.75 0 0,1 2,17.75V17.5L3,10.12C3.21,7.81 5.14,6 7.5,6H16.5C18.86,6 20.79,7.81 21,10.12L22,17.5V17.75A1.75,1.75 0 0,1 20.25,19.5C19.77,19.5 19.33,19.3 19,19L16.03,16H7.97M7,8V10H5V11H7V13H8V11H10V10H8V8H7M16.5,8A0.75,0.75 0 0,0 15.75,8.75A0.75,0.75 0 0,0 16.5,9.5A0.75,0.75 0 0,0 17.25,8.75A0.75,0.75 0 0,0 16.5,8M14.75,9.75A0.75,0.75 0 0,0 14,10.5A0.75,0.75 0 0,0 14.75,11.25A0.75,0.75 0 0,0 15.5,10.5A0.75,0.75 0 0,0 14.75,9.75M18.25,9.75A0.75,0.75 0 0,0 17.5,10.5A0.75,0.75 0 0,0 18.25,11.25A0.75,0.75 0 0,0 19,10.5A0.75,0.75 0 0,0 18.25,9.75M16.5,11.5A0.75,0.75 0 0,0 15.75,12.25A0.75,0.75 0 0,0 16.5,13A0.75,0.75 0 0,0 17.25,12.25A0.75,0.75 0 0,0 16.5,11.5Z" /></g><g id="google-controller-off"><path d="M2,5.27L3.28,4L20,20.72L18.73,22L12.73,16H7.97L5,19C4.67,19.3 4.23,19.5 3.75,19.5A1.75,1.75 0 0,1 2,17.75V17.5L3,10.12C3.1,9.09 3.53,8.17 4.19,7.46L2,5.27M5,10V11H7V13H8V11.27L6.73,10H5M16.5,6C18.86,6 20.79,7.81 21,10.12L22,17.5V17.75C22,18.41 21.64,19 21.1,19.28L7.82,6H16.5M16.5,8A0.75,0.75 0 0,0 15.75,8.75A0.75,0.75 0 0,0 16.5,9.5A0.75,0.75 0 0,0 17.25,8.75A0.75,0.75 0 0,0 16.5,8M14.75,9.75A0.75,0.75 0 0,0 14,10.5A0.75,0.75 0 0,0 14.75,11.25A0.75,0.75 0 0,0 15.5,10.5A0.75,0.75 0 0,0 14.75,9.75M18.25,9.75A0.75,0.75 0 0,0 17.5,10.5A0.75,0.75 0 0,0 18.25,11.25A0.75,0.75 0 0,0 19,10.5A0.75,0.75 0 0,0 18.25,9.75M16.5,11.5A0.75,0.75 0 0,0 15.75,12.25A0.75,0.75 0 0,0 16.5,13A0.75,0.75 0 0,0 17.25,12.25A0.75,0.75 0 0,0 16.5,11.5Z" /></g><g id="google-drive"><path d="M7.71,3.5L1.15,15L4.58,21L11.13,9.5M9.73,15L6.3,21H19.42L22.85,15M22.28,14L15.42,2H8.58L8.57,2L15.43,14H22.28Z" /></g><g id="google-earth"><path d="M12.4,7.56C9.6,4.91 7.3,5.65 6.31,6.1C7.06,5.38 7.94,4.8 8.92,4.4C11.7,4.3 14.83,4.84 16.56,7.31C16.56,7.31 19,11.5 19.86,9.65C20.08,10.4 20.2,11.18 20.2,12C20.2,12.3 20.18,12.59 20.15,12.88C18.12,12.65 15.33,10.32 12.4,7.56M19.1,16.1C18.16,16.47 17,17.1 15.14,17.1C13.26,17.1 11.61,16.35 9.56,15.7C7.7,15.11 7,14.2 5.72,14.2C5.06,14.2 4.73,14.86 4.55,15.41C4.07,14.37 3.8,13.22 3.8,12C3.8,11.19 3.92,10.42 4.14,9.68C5.4,8.1 7.33,7.12 10.09,9.26C10.09,9.26 16.32,13.92 19.88,14.23C19.7,14.89 19.43,15.5 19.1,16.1M12,20.2C10.88,20.2 9.81,19.97 8.83,19.56C8.21,18.08 8.22,16.92 9.95,17.5C9.95,17.5 13.87,19 18,17.58C16.5,19.19 14.37,20.2 12,20.2M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z" /></g><g id="google-glass"><path d="M13,11V13.5H18.87C18.26,17 15.5,19.5 12,19.5A7.5,7.5 0 0,1 4.5,12A7.5,7.5 0 0,1 12,4.5C14.09,4.5 15.9,5.39 17.16,6.84L18.93,5.06C17.24,3.18 14.83,2 12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22C17.5,22 21.5,17.5 21.5,12V11H13Z" /></g><g id="google-maps"><path d="M5,4A2,2 0 0,0 3,6V16.29L11.18,8.11C11.06,7.59 11,7.07 11,6.53C11,5.62 11.2,4.76 11.59,4H5M18,21A2,2 0 0,0 20,19V11.86C19.24,13 18.31,14.21 17.29,15.5L16.5,16.5L15.72,15.5C14.39,13.85 13.22,12.32 12.39,10.91C12.05,10.33 11.76,9.76 11.53,9.18L7.46,13.25L15.21,21H18M3,19A2,2 0 0,0 5,21H13.79L6.75,13.96L3,17.71V19M16.5,15C19.11,11.63 21,9.1 21,6.57C21,4.05 19,2 16.5,2C14,2 12,4.05 12,6.57C12,9.1 13.87,11.63 16.5,15M18.5,6.5A2,2 0 0,1 16.5,8.5A2,2 0 0,1 14.5,6.5A2,2 0 0,1 16.5,4.5A2,2 0 0,1 18.5,6.5Z" /></g><g id="google-nearby"><path d="M4.2,3C3.57,3 3.05,3.5 3,4.11C3,8.66 3,13.24 3,17.8C3,18.46 3.54,19 4.2,19C4.31,19 4.42,19 4.53,18.95C8.5,16.84 12.56,14.38 16.5,12.08C16.94,11.89 17.21,11.46 17.21,11C17.21,10.57 17,10.17 16.6,9.96C12.5,7.56 8.21,5.07 4.53,3.05C4.42,3 4.31,3 4.2,3M19.87,6C19.76,6 19.65,6 19.54,6.05C18.6,6.57 17.53,7.18 16.5,7.75C16.85,7.95 17.19,8.14 17.5,8.33C18.5,8.88 19.07,9.9 19.07,11V11C19.07,12.18 18.38,13.27 17.32,13.77C15.92,14.59 12.92,16.36 11.32,17.29C14.07,18.89 16.82,20.5 19.54,21.95C19.65,22 19.76,22 19.87,22C20.54,22 21.07,21.46 21.07,20.8C21.07,16.24 21.08,11.66 21.07,7.11C21,6.5 20.5,6 19.87,6Z" /></g><g id="google-pages"><path d="M19,3H13V8L17,7L16,11H21V5C21,3.89 20.1,3 19,3M17,17L13,16V21H19A2,2 0 0,0 21,19V13H16M8,13H3V19A2,2 0 0,0 5,21H11V16L7,17M3,5V11H8L7,7L11,8V3H5C3.89,3 3,3.89 3,5Z" /></g><g id="google-physical-web"><path d="M12,1.5A9,9 0 0,1 21,10.5C21,13.11 19.89,15.47 18.11,17.11L17.05,16.05C18.55,14.68 19.5,12.7 19.5,10.5A7.5,7.5 0 0,0 12,3A7.5,7.5 0 0,0 4.5,10.5C4.5,12.7 5.45,14.68 6.95,16.05L5.89,17.11C4.11,15.47 3,13.11 3,10.5A9,9 0 0,1 12,1.5M12,4.5A6,6 0 0,1 18,10.5C18,12.28 17.22,13.89 16,15L14.92,13.92C15.89,13.1 16.5,11.87 16.5,10.5C16.5,8 14.5,6 12,6C9.5,6 7.5,8 7.5,10.5C7.5,11.87 8.11,13.1 9.08,13.92L8,15C6.78,13.89 6,12.28 6,10.5A6,6 0 0,1 12,4.5M8.11,17.65L11.29,14.46C11.68,14.07 12.32,14.07 12.71,14.46L15.89,17.65C16.28,18.04 16.28,18.67 15.89,19.06L12.71,22.24C12.32,22.63 11.68,22.63 11.29,22.24L8.11,19.06C7.72,18.67 7.72,18.04 8.11,17.65Z" /></g><g id="google-play"><path d="M3,20.5V3.5C3,2.91 3.34,2.39 3.84,2.15L13.69,12L3.84,21.85C3.34,21.6 3,21.09 3,20.5M16.81,15.12L6.05,21.34L14.54,12.85L16.81,15.12M20.16,10.81C20.5,11.08 20.75,11.5 20.75,12C20.75,12.5 20.53,12.9 20.18,13.18L17.89,14.5L15.39,12L17.89,9.5L20.16,10.81M6.05,2.66L16.81,8.88L14.54,11.15L6.05,2.66Z" /></g><g id="google-plus"><path d="M23,11H21V9H19V11H17V13H19V15H21V13H23M8,11V13.4H12C11.8,14.4 10.8,16.4 8,16.4C5.6,16.4 3.7,14.4 3.7,12C3.7,9.6 5.6,7.6 8,7.6C9.4,7.6 10.3,8.2 10.8,8.7L12.7,6.9C11.5,5.7 9.9,5 8,5C4.1,5 1,8.1 1,12C1,15.9 4.1,19 8,19C12,19 14.7,16.2 14.7,12.2C14.7,11.7 14.7,11.4 14.6,11H8Z" /></g><g id="google-plus-box"><path d="M20,2A2,2 0 0,1 22,4V20A2,2 0 0,1 20,22H4A2,2 0 0,1 2,20V4C2,2.89 2.9,2 4,2H20M20,12H18V10H17V12H15V13H17V15H18V13H20V12M9,11.29V13H11.86C11.71,13.71 11,15.14 9,15.14C7.29,15.14 5.93,13.71 5.93,12C5.93,10.29 7.29,8.86 9,8.86C10,8.86 10.64,9.29 11,9.64L12.36,8.36C11.5,7.5 10.36,7 9,7C6.21,7 4,9.21 4,12C4,14.79 6.21,17 9,17C11.86,17 13.79,15 13.79,12.14C13.79,11.79 13.79,11.57 13.71,11.29H9Z" /></g><g id="google-translate"><path d="M3,1C1.89,1 1,1.89 1,3V17C1,18.11 1.89,19 3,19H15L9,1H3M12.34,5L13,7H21V21H12.38L13.03,23H21C22.11,23 23,22.11 23,21V7C23,5.89 22.11,5 21,5H12.34M7.06,5.91C8.16,5.91 9.09,6.31 9.78,7L8.66,8.03C8.37,7.74 7.87,7.41 7.06,7.41C5.67,7.41 4.56,8.55 4.56,9.94C4.56,11.33 5.67,12.5 7.06,12.5C8.68,12.5 9.26,11.33 9.38,10.75H7.06V9.38H10.88C10.93,9.61 10.94,9.77 10.94,10.06C10.94,12.38 9.38,14 7.06,14C4.81,14 3,12.19 3,9.94C3,7.68 4.81,5.91 7.06,5.91M16,10V11H14.34L14.66,12H18C17.73,12.61 17.63,13.17 16.81,14.13C16.41,13.66 16.09,13.25 16,13H15C15.12,13.43 15.62,14.1 16.22,14.78C16.09,14.91 15.91,15.08 15.75,15.22L16.03,16.06C16.28,15.84 16.53,15.61 16.78,15.38C17.8,16.45 18.88,17.44 18.88,17.44L19.44,16.84C19.44,16.84 18.37,15.79 17.41,14.75C18.04,14.05 18.6,13.2 19,12H20V11H17V10H16Z" /></g><g id="google-wallet"><path d="M9.89,11.08C9.76,9.91 9.39,8.77 8.77,7.77C8.5,7.29 8.46,6.7 8.63,6.25C8.71,6 8.83,5.8 9.03,5.59C9.24,5.38 9.46,5.26 9.67,5.18C9.88,5.09 10,5.06 10.31,5.06C10.66,5.06 11,5.17 11.28,5.35L11.72,5.76L11.83,5.92C12.94,7.76 13.53,9.86 13.53,12L13.5,12.79C13.38,14.68 12.8,16.5 11.82,18.13C11.5,18.67 10.92,19 10.29,19L9.78,18.91L9.37,18.73C8.86,18.43 8.57,17.91 8.5,17.37C8.5,17.05 8.54,16.72 8.69,16.41L8.77,16.28C9.54,15 9.95,13.53 9.95,12L9.89,11.08M20.38,7.88C20.68,9.22 20.84,10.62 20.84,12C20.84,13.43 20.68,14.82 20.38,16.16L20.11,17.21C19.78,18.4 19.4,19.32 19,20C18.7,20.62 18.06,21 17.38,21C17.1,21 16.83,20.94 16.58,20.82C16,20.55 15.67,20.07 15.55,19.54L15.5,19.11C15.5,18.7 15.67,18.35 15.68,18.32C16.62,16.34 17.09,14.23 17.09,12C17.09,9.82 16.62,7.69 15.67,5.68C15.22,4.75 15.62,3.63 16.55,3.18C16.81,3.06 17.08,3 17.36,3C18.08,3 18.75,3.42 19.05,4.07C19.63,5.29 20.08,6.57 20.38,7.88M16.12,9.5C16.26,10.32 16.34,11.16 16.34,12C16.34,14 15.95,15.92 15.2,17.72C15.11,16.21 14.75,14.76 14.16,13.44L14.22,12.73L14.25,11.96C14.25,9.88 13.71,7.85 12.67,6.07C14,7.03 15.18,8.21 16.12,9.5M4,10.5C3.15,10.03 2.84,9 3.28,8.18C3.58,7.63 4.15,7.28 4.78,7.28C5.06,7.28 5.33,7.35 5.58,7.5C6.87,8.17 8.03,9.1 8.97,10.16L9.12,11.05L9.18,12C9.18,13.43 8.81,14.84 8.1,16.07C7.6,13.66 6.12,11.62 4,10.5Z" /></g><g id="gradient"><path d="M11,9H13V11H11V9M9,11H11V13H9V11M13,11H15V13H13V11M15,9H17V11H15V9M7,9H9V11H7V9M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3M9,18H7V16H9V18M13,18H11V16H13V18M17,18H15V16H17V18M19,11H17V13H19V15H17V13H15V15H13V13H11V15H9V13H7V15H5V13H7V11H5V5H19V11Z" /></g><g id="grease-pencil"><path d="M18.62,1.5C18.11,1.5 17.6,1.69 17.21,2.09L10.75,8.55L14.95,12.74L21.41,6.29C22.2,5.5 22.2,4.24 21.41,3.46L20.04,2.09C19.65,1.69 19.14,1.5 18.62,1.5M9.8,9.5L3.23,16.07L3.93,16.77C3.4,17.24 2.89,17.78 2.38,18.29C1.6,19.08 1.6,20.34 2.38,21.12C3.16,21.9 4.42,21.9 5.21,21.12C5.72,20.63 6.25,20.08 6.73,19.58L7.43,20.27L14,13.7" /></g><g id="grid"><path d="M10,4V8H14V4H10M16,4V8H20V4H16M16,10V14H20V10H16M16,16V20H20V16H16M14,20V16H10V20H14M8,20V16H4V20H8M8,14V10H4V14H8M8,8V4H4V8H8M10,14H14V10H10V14M4,2H20A2,2 0 0,1 22,4V20A2,2 0 0,1 20,22H4C2.92,22 2,21.1 2,20V4A2,2 0 0,1 4,2Z" /></g><g id="grid-off"><path d="M0,2.77L1.28,1.5L22.5,22.72L21.23,24L19.23,22H4C2.92,22 2,21.1 2,20V4.77L0,2.77M10,4V7.68L8,5.68V4H6.32L4.32,2H20A2,2 0 0,1 22,4V19.7L20,17.7V16H18.32L16.32,14H20V10H16V13.68L14,11.68V10H12.32L10.32,8H14V4H10M16,4V8H20V4H16M16,20H17.23L16,18.77V20M4,8H5.23L4,6.77V8M10,14H11.23L10,12.77V14M14,20V16.77L13.23,16H10V20H14M8,20V16H4V20H8M8,14V10.77L7.23,10H4V14H8Z" /></g><g id="group"><path d="M8,8V12H13V8H8M1,1H5V2H19V1H23V5H22V19H23V23H19V22H5V23H1V19H2V5H1V1M5,19V20H19V19H20V5H19V4H5V5H4V19H5M6,6H15V10H18V18H8V14H6V6M15,14H10V16H16V12H15V14Z" /></g><g id="guitar-electric"><path d="M20.5,2L18.65,4.08L18.83,4.26L10.45,12.16C10.23,12.38 9.45,12.75 9.26,12.28C8.81,11.12 10.23,11 10,10.8C8.94,10.28 7.73,11.18 7.67,11.23C6.94,11.78 6.5,12.43 6.26,13.13C5.96,14.04 5.17,14.15 4.73,14.17C3.64,14.24 3,14.53 2.5,15.23C2.27,15.54 1.9,16 2,16.96C2.16,18 2.95,19.33 3.56,20C4.21,20.69 5.05,21.38 5.81,21.75C6.35,22 6.68,22.08 7.47,21.88C8.17,21.7 8.86,21.14 9.15,20.4C9.39,19.76 9.42,19.3 9.53,18.78C9.67,18.11 9.76,18 10.47,17.68C11.14,17.39 11.5,17.35 12.05,16.78C12.44,16.37 12.64,15.93 12.76,15.46C12.86,15.06 12.93,14.56 12.74,14.5C12.57,14.35 12.27,15.31 11.56,14.86C11.05,14.54 11.11,13.74 11.55,13.29C14.41,10.38 16.75,8 19.63,5.09L19.86,5.32L22,3.5Z" /></g><g id="guitar-pick"><path d="M19,4.1C18.1,3.3 17,2.8 15.8,2.5C15.5,2.4 13.6,2 12.2,2C12.2,2 12.1,2 12,2C12,2 11.9,2 11.8,2C10.4,2 8.4,2.4 8.1,2.5C7,2.8 5.9,3.3 5,4.1C3,5.9 3,8.7 4,11C5,13.5 6.1,15.7 7.6,17.9C8.8,19.6 10.1,22 12,22C13.9,22 15.2,19.6 16.5,17.9C18,15.8 19.1,13.5 20.1,11C21,8.7 21,5.9 19,4.1Z" /></g><g id="guitar-pick-outline"><path d="M19,4.1C18.1,3.3 17,2.8 15.8,2.5C15.5,2.4 13.6,2 12.2,2C12.2,2 12.1,2 12,2C12,2 11.9,2 11.8,2C10.4,2 8.4,2.4 8.1,2.5C7,2.8 5.9,3.3 5,4.1C3,5.9 3,8.7 4,11C5,13.5 6.1,15.7 7.6,17.9C8.8,19.6 10.1,22 12,22C13.9,22 15.2,19.6 16.5,17.9C18,15.8 19.1,13.5 20.1,11C21,8.7 21,5.9 19,4.1M18.2,10.2C17.1,12.9 16.1,14.9 14.8,16.7C14.6,16.9 14.5,17.2 14.3,17.4C13.8,18.2 12.6,20 12,20C12,20 12,20 12,20C11.3,20 10.2,18.3 9.6,17.4C9.4,17.2 9.3,16.9 9.1,16.7C7.9,14.9 6.8,12.9 5.7,10.2C5.5,9.5 4.7,7 6.3,5.5C6.8,5 7.6,4.7 8.6,4.4C9,4.4 10.7,4 11.8,4C11.8,4 12.1,4 12.1,4C13.2,4 14.9,4.3 15.3,4.4C16.3,4.7 17.1,5 17.6,5.5C19.3,7 18.5,9.5 18.2,10.2Z" /></g><g id="hackernews"><path d="M2,2H22V22H2V2M11.25,17.5H12.75V13.06L16,7H14.5L12,11.66L9.5,7H8L11.25,13.06V17.5Z" /></g><g id="hamburger"><path d="M2,16H22V18C22,19.11 21.11,20 20,20H4C2.89,20 2,19.11 2,18V16M6,4H18C20.22,4 22,5.78 22,8V10H2V8C2,5.78 3.78,4 6,4M4,11H15L17,13L19,11H20C21.11,11 22,11.89 22,13C22,14.11 21.11,15 20,15H4C2.89,15 2,14.11 2,13C2,11.89 2.89,11 4,11Z" /></g><g id="hand-pointing-right"><path d="M21,9A1,1 0 0,1 22,10A1,1 0 0,1 21,11H16.53L16.4,12.21L14.2,17.15C14,17.65 13.47,18 12.86,18H8.5C7.7,18 7,17.27 7,16.5V10C7,9.61 7.16,9.26 7.43,9L11.63,4.1L12.4,4.84C12.6,5.03 12.72,5.29 12.72,5.58L12.69,5.8L11,9H21M2,18V10H5V18H2Z" /></g><g id="hanger"><path d="M20.76,16.34H20.75C21.5,16.77 22,17.58 22,18.5A2.5,2.5 0 0,1 19.5,21H4.5A2.5,2.5 0 0,1 2,18.5C2,17.58 2.5,16.77 3.25,16.34H3.24L11,11.86C11,11.86 11,11 12,10C13,10 14,9.1 14,8A2,2 0 0,0 12,6A2,2 0 0,0 10,8H8A4,4 0 0,1 12,4A4,4 0 0,1 16,8C16,9.86 14.73,11.42 13,11.87L20.76,16.34M4.5,19V19H19.5V19C19.67,19 19.84,18.91 19.93,18.75C20.07,18.5 20,18.21 19.75,18.07L12,13.59L4.25,18.07C4,18.21 3.93,18.5 4.07,18.75C4.16,18.91 4.33,19 4.5,19Z" /></g><g id="hangouts"><path d="M15,11L14,13H12.5L13.5,11H12V8H15M11,11L10,13H8.5L9.5,11H8V8H11M11.5,2A8.5,8.5 0 0,0 3,10.5A8.5,8.5 0 0,0 11.5,19H12V22.5C16.86,20.15 20,15 20,10.5C20,5.8 16.19,2 11.5,2Z" /></g><g id="harddisk"><path d="M6,2H18A2,2 0 0,1 20,4V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M12,4A6,6 0 0,0 6,10C6,13.31 8.69,16 12.1,16L11.22,13.77C10.95,13.29 11.11,12.68 11.59,12.4L12.45,11.9C12.93,11.63 13.54,11.79 13.82,12.27L15.74,14.69C17.12,13.59 18,11.9 18,10A6,6 0 0,0 12,4M12,9A1,1 0 0,1 13,10A1,1 0 0,1 12,11A1,1 0 0,1 11,10A1,1 0 0,1 12,9M7,18A1,1 0 0,0 6,19A1,1 0 0,0 7,20A1,1 0 0,0 8,19A1,1 0 0,0 7,18M12.09,13.27L14.58,19.58L17.17,18.08L12.95,12.77L12.09,13.27Z" /></g><g id="headphones"><path d="M12,1C7,1 3,5 3,10V17A3,3 0 0,0 6,20H9V12H5V10A7,7 0 0,1 12,3A7,7 0 0,1 19,10V12H15V20H18A3,3 0 0,0 21,17V10C21,5 16.97,1 12,1Z" /></g><g id="headphones-box"><path d="M7.2,18C6.54,18 6,17.46 6,16.8V13.2L6,12A6,6 0 0,1 12,6A6,6 0 0,1 18,12V13.2L18,16.8A1.2,1.2 0 0,1 16.8,18H14V14H16V12A4,4 0 0,0 12,8A4,4 0 0,0 8,12V14H10V18M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3Z" /></g><g id="headphones-settings"><path d="M12,1A9,9 0 0,1 21,10V17A3,3 0 0,1 18,20H15V12H19V10A7,7 0 0,0 12,3A7,7 0 0,0 5,10V12H9V20H6A3,3 0 0,1 3,17V10A9,9 0 0,1 12,1M15,24V22H17V24H15M11,24V22H13V24H11M7,24V22H9V24H7Z" /></g><g id="headset"><path d="M12,1C7,1 3,5 3,10V17A3,3 0 0,0 6,20H9V12H5V10A7,7 0 0,1 12,3A7,7 0 0,1 19,10V12H15V20H19V21H12V23H18A3,3 0 0,0 21,20V10C21,5 16.97,1 12,1Z" /></g><g id="headset-dock"><path d="M2,18H9V6.13C7.27,6.57 6,8.14 6,10V11H8V17H6A2,2 0 0,1 4,15V10A6,6 0 0,1 10,4H11A6,6 0 0,1 17,10V12H18V9H20V12A2,2 0 0,1 18,14H17V15A2,2 0 0,1 15,17H13V11H15V10C15,8.14 13.73,6.57 12,6.13V18H22V20H2V18Z" /></g><g id="headset-off"><path d="M22.5,4.77L20.43,6.84C20.8,7.82 21,8.89 21,10V20A3,3 0 0,1 18,23H12V21H19V20H15V12.27L9,18.27V20H7.27L4.77,22.5L3.5,21.22L21.22,3.5L22.5,4.77M12,1C14.53,1 16.82,2.04 18.45,3.72L17.04,5.14C15.77,3.82 14,3 12,3A7,7 0 0,0 5,10V12H9V13.18L3.5,18.67C3.19,18.19 3,17.62 3,17V10A9,9 0 0,1 12,1M19,12V10C19,9.46 18.94,8.94 18.83,8.44L15.27,12H19Z" /></g><g id="heart"><path d="M12,21.35L10.55,20.03C5.4,15.36 2,12.27 2,8.5C2,5.41 4.42,3 7.5,3C9.24,3 10.91,3.81 12,5.08C13.09,3.81 14.76,3 16.5,3C19.58,3 22,5.41 22,8.5C22,12.27 18.6,15.36 13.45,20.03L12,21.35Z" /></g><g id="heart-box"><path d="M5,3H19A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3M12,17L12.72,16.34C15.3,14 17,12.46 17,10.57C17,9.03 15.79,7.82 14.25,7.82C13.38,7.82 12.55,8.23 12,8.87C11.45,8.23 10.62,7.82 9.75,7.82C8.21,7.82 7,9.03 7,10.57C7,12.46 8.7,14 11.28,16.34L12,17Z" /></g><g id="heart-box-outline"><path d="M12,17L11.28,16.34C8.7,14 7,12.46 7,10.57C7,9.03 8.21,7.82 9.75,7.82C10.62,7.82 11.45,8.23 12,8.87C12.55,8.23 13.38,7.82 14.25,7.82C15.79,7.82 17,9.03 17,10.57C17,12.46 15.3,14 12.72,16.34L12,17M5,3H19A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3M5,5V19H19V5H5Z" /></g><g id="heart-broken"><path d="M12,21.35L10.55,20.03C5.4,15.36 2,12.27 2,8.5C2,5.41 4.42,3 7.5,3C8.17,3 8.82,3.12 9.44,3.33L13,9.35L9,14.35L12,21.35V21.35M16.5,3C19.58,3 22,5.41 22,8.5C22,12.27 18.6,15.36 13.45,20.03L12,21.35L11,14.35L15.5,9.35L12.85,4.27C13.87,3.47 15.17,3 16.5,3Z" /></g><g id="heart-outline"><path d="M12.1,18.55L12,18.65L11.89,18.55C7.14,14.24 4,11.39 4,8.5C4,6.5 5.5,5 7.5,5C9.04,5 10.54,6 11.07,7.36H12.93C13.46,6 14.96,5 16.5,5C18.5,5 20,6.5 20,8.5C20,11.39 16.86,14.24 12.1,18.55M16.5,3C14.76,3 13.09,3.81 12,5.08C10.91,3.81 9.24,3 7.5,3C4.42,3 2,5.41 2,8.5C2,12.27 5.4,15.36 10.55,20.03L12,21.35L13.45,20.03C18.6,15.36 22,12.27 22,8.5C22,5.41 19.58,3 16.5,3Z" /></g><g id="heart-pulse"><path d="M7.5,4A5.5,5.5 0 0,0 2,9.5C2,10 2.09,10.5 2.22,11H6.3L7.57,7.63C7.87,6.83 9.05,6.75 9.43,7.63L11.5,13L12.09,11.58C12.22,11.25 12.57,11 13,11H21.78C21.91,10.5 22,10 22,9.5A5.5,5.5 0 0,0 16.5,4C14.64,4 13,4.93 12,6.34C11,4.93 9.36,4 7.5,4V4M3,12.5A1,1 0 0,0 2,13.5A1,1 0 0,0 3,14.5H5.44L11,20C12,20.9 12,20.9 13,20L18.56,14.5H21A1,1 0 0,0 22,13.5A1,1 0 0,0 21,12.5H13.4L12.47,14.8C12.07,15.81 10.92,15.67 10.55,14.83L8.5,9.5L7.54,11.83C7.39,12.21 7.05,12.5 6.6,12.5H3Z" /></g><g id="help"><path d="M10,19H13V22H10V19M12,2C17.35,2.22 19.68,7.62 16.5,11.67C15.67,12.67 14.33,13.33 13.67,14.17C13,15 13,16 13,17H10C10,15.33 10,13.92 10.67,12.92C11.33,11.92 12.67,11.33 13.5,10.67C15.92,8.43 15.32,5.26 12,5A3,3 0 0,0 9,8H6A6,6 0 0,1 12,2Z" /></g><g id="help-circle"><path d="M15.07,11.25L14.17,12.17C13.45,12.89 13,13.5 13,15H11V14.5C11,13.39 11.45,12.39 12.17,11.67L13.41,10.41C13.78,10.05 14,9.55 14,9C14,7.89 13.1,7 12,7A2,2 0 0,0 10,9H8A4,4 0 0,1 12,5A4,4 0 0,1 16,9C16,9.88 15.64,10.67 15.07,11.25M13,19H11V17H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z" /></g><g id="help-circle-outline"><path d="M11,18H13V16H11V18M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,6A4,4 0 0,0 8,10H10A2,2 0 0,1 12,8A2,2 0 0,1 14,10C14,12 11,11.75 11,15H13C13,12.75 16,12.5 16,10A4,4 0 0,0 12,6Z" /></g><g id="hexagon"><path d="M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5Z" /></g><g id="hexagon-outline"><path d="M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L5,8.09V15.91L12,19.85L19,15.91V8.09L12,4.15Z" /></g><g id="highway"><path d="M10,2L8,8H11V2H10M13,2V8H16L14,2H13M2,9V10H4V11H6V10H18L18.06,11H20V10H22V9H2M7,11L3.34,22H11V11H7M13,11V22H20.66L17,11H13Z" /></g><g id="history"><path d="M11,7V12.11L15.71,14.9L16.5,13.62L12.5,11.25V7M12.5,2C8.97,2 5.91,3.92 4.27,6.77L2,4.5V11H8.5L5.75,8.25C6.96,5.73 9.5,4 12.5,4A7.5,7.5 0 0,1 20,11.5A7.5,7.5 0 0,1 12.5,19C9.23,19 6.47,16.91 5.44,14H3.34C4.44,18.03 8.11,21 12.5,21C17.74,21 22,16.75 22,11.5A9.5,9.5 0 0,0 12.5,2Z" /></g><g id="hololens"><path d="M12,8C12,8 22,8 22,11C22,11 22.09,14.36 21.75,14.25C21,11 12,11 12,11C12,11 3,11 2.25,14.25C1.91,14.36 2,11 2,11C2,8 12,8 12,8M12,12C20,12 20.75,14.25 20.75,14.25C19.75,17.25 19,18 15,18C12,18 13,16.5 12,16.5C11,16.5 12,18 9,18C5,18 4.25,17.25 3.25,14.25C3.25,14.25 4,12 12,12Z" /></g><g id="home"><path d="M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z" /></g><g id="home-map-marker"><path d="M12,3L2,12H5V20H19V12H22L12,3M12,7.7C14.1,7.7 15.8,9.4 15.8,11.5C15.8,14.5 12,18 12,18C12,18 8.2,14.5 8.2,11.5C8.2,9.4 9.9,7.7 12,7.7M12,10A1.5,1.5 0 0,0 10.5,11.5A1.5,1.5 0 0,0 12,13A1.5,1.5 0 0,0 13.5,11.5A1.5,1.5 0 0,0 12,10Z" /></g><g id="home-modern"><path d="M6,21V8A2,2 0 0,1 8,6L16,3V6A2,2 0 0,1 18,8V21H12V16H8V21H6M14,19H16V16H14V19M8,13H10V9H8V13M12,13H16V9H12V13Z" /></g><g id="home-outline"><path d="M9,19V13H11L13,13H15V19H18V10.91L12,4.91L6,10.91V19H9M12,2.09L21.91,12H20V21H13V15H11V21H4V12H2.09L12,2.09Z" /></g><g id="home-variant"><path d="M8,20H5V12H2L12,3L22,12H19V20H12V14H8V20M14,14V17H17V14H14Z" /></g><g id="hops"><path d="M21,12C21,12 12.5,10 12.5,2C12.5,2 21,2 21,12M3,12C3,2 11.5,2 11.5,2C11.5,10 3,12 3,12M12,6.5C12,6.5 13,8.66 15,10.5C14.76,14.16 12,16 12,16C12,16 9.24,14.16 9,10.5C11,8.66 12,6.5 12,6.5M20.75,13.25C20.75,13.25 20,17 18,19C18,19 15.53,17.36 14.33,14.81C15.05,13.58 15.5,12.12 15.75,11.13C17.13,12.18 18.75,13 20.75,13.25M15.5,18.25C14.5,20.25 12,21.75 12,21.75C12,21.75 9.5,20.25 8.5,18.25C8.5,18.25 9.59,17.34 10.35,15.8C10.82,16.35 11.36,16.79 12,17C12.64,16.79 13.18,16.35 13.65,15.8C14.41,17.34 15.5,18.25 15.5,18.25M3.25,13.25C5.25,13 6.87,12.18 8.25,11.13C8.5,12.12 8.95,13.58 9.67,14.81C8.47,17.36 6,19 6,19C4,17 3.25,13.25 3.25,13.25Z" /></g><g id="hospital"><path d="M18,14H14V18H10V14H6V10H10V6H14V10H18M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3Z" /></g><g id="hospital-building"><path d="M2,22V7A1,1 0 0,1 3,6H7V2H17V6H21A1,1 0 0,1 22,7V22H14V17H10V22H2M9,4V10H11V8H13V10H15V4H13V6H11V4H9M4,20H8V17H4V20M4,15H8V12H4V15M16,20H20V17H16V20M16,15H20V12H16V15M10,15H14V12H10V15Z" /></g><g id="hospital-marker"><path d="M12,2C15.86,2 19,5.13 19,9C19,14.25 12,22 12,22C12,22 5,14.25 5,9A7,7 0 0,1 12,2M9,6V12H11V10H13V12H15V6H13V8H11V6H9Z" /></g><g id="hotel"><path d="M19,7H11V14H3V5H1V20H3V17H21V20H23V11A4,4 0 0,0 19,7M7,13A3,3 0 0,0 10,10A3,3 0 0,0 7,7A3,3 0 0,0 4,10A3,3 0 0,0 7,13Z" /></g><g id="houzz"><path d="M12,24V16L5.1,20V12H5.1V4L12,0V8L5.1,12L12,16V8L18.9,4V12H18.9V20L12,24Z" /></g><g id="houzz-box"><path d="M12,4L7.41,6.69V12L12,9.3V4M12,9.3V14.7L12,20L16.59,17.31V12L16.59,6.6L12,9.3M12,14.7L7.41,12V17.4L12,14.7M5,3H19A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3Z" /></g><g id="human"><path d="M21,9H15V22H13V16H11V22H9V9H3V7H21M12,2A2,2 0 0,1 14,4A2,2 0 0,1 12,6C10.89,6 10,5.1 10,4C10,2.89 10.89,2 12,2Z" /></g><g id="human-child"><path d="M12,2A3,3 0 0,1 15,5A3,3 0 0,1 12,8A3,3 0 0,1 9,5A3,3 0 0,1 12,2M11,22H8V16H6V9H18V16H16V22H13V18H11V22Z" /></g><g id="human-female"><path d="M12,2A2,2 0 0,1 14,4A2,2 0 0,1 12,6A2,2 0 0,1 10,4A2,2 0 0,1 12,2M10.5,22V16H7.5L10.09,8.41C10.34,7.59 11.1,7 12,7C12.9,7 13.66,7.59 13.91,8.41L16.5,16H13.5V22H10.5Z" /></g><g id="human-greeting"><path d="M1.5,4V5.5C1.5,9.65 3.71,13.28 7,15.3V20H22V18C22,15.34 16.67,14 14,14C14,14 13.83,14 13.75,14C9,14 5,10 5,5.5V4M14,4A4,4 0 0,0 10,8A4,4 0 0,0 14,12A4,4 0 0,0 18,8A4,4 0 0,0 14,4Z" /></g><g id="human-handsdown"><path d="M12,1C10.89,1 10,1.9 10,3C10,4.11 10.89,5 12,5C13.11,5 14,4.11 14,3A2,2 0 0,0 12,1M10,6C9.73,6 9.5,6.11 9.31,6.28H9.3L4,11.59L5.42,13L9,9.41V22H11V15H13V22H15V9.41L18.58,13L20,11.59L14.7,6.28C14.5,6.11 14.27,6 14,6" /></g><g id="human-handsup"><path d="M5,1C5,3.7 6.56,6.16 9,7.32V22H11V15H13V22H15V7.31C17.44,6.16 19,3.7 19,1H17A5,5 0 0,1 12,6A5,5 0 0,1 7,1M12,1C10.89,1 10,1.89 10,3C10,4.11 10.89,5 12,5C13.11,5 14,4.11 14,3C14,1.89 13.11,1 12,1Z" /></g><g id="human-male"><path d="M12,2A2,2 0 0,1 14,4A2,2 0 0,1 12,6A2,2 0 0,1 10,4A2,2 0 0,1 12,2M10.5,7H13.5A2,2 0 0,1 15.5,9V14.5H14V22H10V14.5H8.5V9A2,2 0 0,1 10.5,7Z" /></g><g id="human-male-female"><path d="M7.5,2A2,2 0 0,1 9.5,4A2,2 0 0,1 7.5,6A2,2 0 0,1 5.5,4A2,2 0 0,1 7.5,2M6,7H9A2,2 0 0,1 11,9V14.5H9.5V22H5.5V14.5H4V9A2,2 0 0,1 6,7M16.5,2A2,2 0 0,1 18.5,4A2,2 0 0,1 16.5,6A2,2 0 0,1 14.5,4A2,2 0 0,1 16.5,2M15,22V16H12L14.59,8.41C14.84,7.59 15.6,7 16.5,7C17.4,7 18.16,7.59 18.41,8.41L21,16H18V22H15Z" /></g><g id="human-pregnant"><path d="M9,4C9,2.89 9.89,2 11,2C12.11,2 13,2.89 13,4C13,5.11 12.11,6 11,6C9.89,6 9,5.11 9,4M16,13C16,11.66 15.17,10.5 14,10A3,3 0 0,0 11,7A3,3 0 0,0 8,10V17H10V22H13V17H16V13Z" /></g><g id="image"><path d="M8.5,13.5L11,16.5L14.5,12L19,18H5M21,19V5C21,3.89 20.1,3 19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19Z" /></g><g id="image-album"><path d="M6,19L9,15.14L11.14,17.72L14.14,13.86L18,19H6M6,4H11V12L8.5,10.5L6,12M18,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V4A2,2 0 0,0 18,2Z" /></g><g id="image-area"><path d="M20,5A2,2 0 0,1 22,7V17A2,2 0 0,1 20,19H4C2.89,19 2,18.1 2,17V7C2,5.89 2.89,5 4,5H20M5,16H19L14.5,10L11,14.5L8.5,11.5L5,16Z" /></g><g id="image-area-close"><path d="M12,23L8,19H16L12,23M20,3A2,2 0 0,1 22,5V15A2,2 0 0,1 20,17H4A2,2 0 0,1 2,15V5A2,2 0 0,1 4,3H20M5,14H19L14.5,8L11,12.5L8.5,9.5L5,14Z" /></g><g id="image-broken"><path d="M19,3A2,2 0 0,1 21,5V11H19V13H19L17,13V15H15V17H13V19H11V21H5C3.89,21 3,20.1 3,19V5A2,2 0 0,1 5,3H19M21,15V19A2,2 0 0,1 19,21H19L15,21V19H17V17H19V15H21M19,8.5A0.5,0.5 0 0,0 18.5,8H5.5A0.5,0.5 0 0,0 5,8.5V15.5A0.5,0.5 0 0,0 5.5,16H11V15H13V13H15V11H17V9H19V8.5Z" /></g><g id="image-broken-variant"><path d="M21,5V11.59L18,8.58L14,12.59L10,8.59L6,12.59L3,9.58V5A2,2 0 0,1 5,3H19A2,2 0 0,1 21,5M18,11.42L21,14.43V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V12.42L6,15.41L10,11.41L14,15.41" /></g><g id="image-filter"><path d="M21,17H7V3H21M21,1H7A2,2 0 0,0 5,3V17A2,2 0 0,0 7,19H21A2,2 0 0,0 23,17V3A2,2 0 0,0 21,1M3,5H1V21A2,2 0 0,0 3,23H19V21H3M15.96,10.29L13.21,13.83L11.25,11.47L8.5,15H19.5L15.96,10.29Z" /></g><g id="image-filter-black-white"><path d="M19,19L12,11V19H5L12,11V5H19M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3Z" /></g><g id="image-filter-center-focus"><path d="M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9M19,19H15V21H19A2,2 0 0,0 21,19V15H19M19,3H15V5H19V9H21V5A2,2 0 0,0 19,3M5,5H9V3H5A2,2 0 0,0 3,5V9H5M5,15H3V19A2,2 0 0,0 5,21H9V19H5V15Z" /></g><g id="image-filter-center-focus-weak"><path d="M5,15H3V19A2,2 0 0,0 5,21H9V19H5M5,5H9V3H5A2,2 0 0,0 3,5V9H5M19,3H15V5H19V9H21V5A2,2 0 0,0 19,3M19,19H15V21H19A2,2 0 0,0 21,19V15H19M12,8A4,4 0 0,0 8,12A4,4 0 0,0 12,16A4,4 0 0,0 16,12A4,4 0 0,0 12,8M12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14Z" /></g><g id="image-filter-drama"><path d="M19,18H6A4,4 0 0,1 2,14A4,4 0 0,1 6,10A4,4 0 0,1 10,14H12C12,11.24 10.14,8.92 7.6,8.22C8.61,6.88 10.2,6 12,6C15.03,6 17.5,8.47 17.5,11.5V12H19A3,3 0 0,1 22,15A3,3 0 0,1 19,18M19.35,10.04C18.67,6.59 15.64,4 12,4C9.11,4 6.61,5.64 5.36,8.04C2.35,8.36 0,10.9 0,14A6,6 0 0,0 6,20H19A5,5 0 0,0 24,15C24,12.36 21.95,10.22 19.35,10.04Z" /></g><g id="image-filter-frames"><path d="M18,8H6V18H18M20,20H4V6H8.5L12.04,2.5L15.5,6H20M20,4H16L12,0L8,4H4A2,2 0 0,0 2,6V20A2,2 0 0,0 4,22H20A2,2 0 0,0 22,20V6A2,2 0 0,0 20,4Z" /></g><g id="image-filter-hdr"><path d="M14,6L10.25,11L13.1,14.8L11.5,16C9.81,13.75 7,10 7,10L1,18H23L14,6Z" /></g><g id="image-filter-none"><path d="M21,17H7V3H21M21,1H7A2,2 0 0,0 5,3V17A2,2 0 0,0 7,19H21A2,2 0 0,0 23,17V3A2,2 0 0,0 21,1M3,5H1V21A2,2 0 0,0 3,23H19V21H3V5Z" /></g><g id="image-filter-tilt-shift"><path d="M5.68,19.74C7.16,20.95 9,21.75 11,21.95V19.93C9.54,19.75 8.21,19.17 7.1,18.31M13,19.93V21.95C15,21.75 16.84,20.95 18.32,19.74L16.89,18.31C15.79,19.17 14.46,19.75 13,19.93M18.31,16.9L19.74,18.33C20.95,16.85 21.75,15 21.95,13H19.93C19.75,14.46 19.17,15.79 18.31,16.9M15,12A3,3 0 0,0 12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12M4.07,13H2.05C2.25,15 3.05,16.84 4.26,18.32L5.69,16.89C4.83,15.79 4.25,14.46 4.07,13M5.69,7.1L4.26,5.68C3.05,7.16 2.25,9 2.05,11H4.07C4.25,9.54 4.83,8.21 5.69,7.1M19.93,11H21.95C21.75,9 20.95,7.16 19.74,5.68L18.31,7.1C19.17,8.21 19.75,9.54 19.93,11M18.32,4.26C16.84,3.05 15,2.25 13,2.05V4.07C14.46,4.25 15.79,4.83 16.9,5.69M11,4.07V2.05C9,2.25 7.16,3.05 5.68,4.26L7.1,5.69C8.21,4.83 9.54,4.25 11,4.07Z" /></g><g id="image-filter-vintage"><path d="M12,16A4,4 0 0,1 8,12A4,4 0 0,1 12,8A4,4 0 0,1 16,12A4,4 0 0,1 12,16M18.7,12.4C18.42,12.24 18.13,12.11 17.84,12C18.13,11.89 18.42,11.76 18.7,11.6C20.62,10.5 21.69,8.5 21.7,6.41C19.91,5.38 17.63,5.3 15.7,6.41C15.42,6.57 15.16,6.76 14.92,6.95C14.97,6.64 15,6.32 15,6C15,3.78 13.79,1.85 12,0.81C10.21,1.85 9,3.78 9,6C9,6.32 9.03,6.64 9.08,6.95C8.84,6.75 8.58,6.56 8.3,6.4C6.38,5.29 4.1,5.37 2.3,6.4C2.3,8.47 3.37,10.5 5.3,11.59C5.58,11.75 5.87,11.88 6.16,12C5.87,12.1 5.58,12.23 5.3,12.39C3.38,13.5 2.31,15.5 2.3,17.58C4.09,18.61 6.37,18.69 8.3,17.58C8.58,17.42 8.84,17.23 9.08,17.04C9.03,17.36 9,17.68 9,18C9,20.22 10.21,22.15 12,23.19C13.79,22.15 15,20.22 15,18C15,17.68 14.97,17.36 14.92,17.05C15.16,17.25 15.42,17.43 15.7,17.59C17.62,18.7 19.9,18.62 21.7,17.59C21.69,15.5 20.62,13.5 18.7,12.4Z" /></g><g id="image-multiple"><path d="M22,16V4A2,2 0 0,0 20,2H8A2,2 0 0,0 6,4V16A2,2 0 0,0 8,18H20A2,2 0 0,0 22,16M11,12L13.03,14.71L16,11L20,16H8M2,6V20A2,2 0 0,0 4,22H18V20H4V6" /></g><g id="import"><path d="M14,12L10,8V11H2V13H10V16M20,18V6C20,4.89 19.1,4 18,4H6A2,2 0 0,0 4,6V9H6V6H18V18H6V15H4V18A2,2 0 0,0 6,20H18A2,2 0 0,0 20,18Z" /></g><g id="inbox"><path d="M19,15H15A3,3 0 0,1 12,18A3,3 0 0,1 9,15H5V5H19M19,3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3Z" /></g><g id="inbox-arrow-down"><path d="M16,10H14V7H10V10H8L12,14M19,15H15A3,3 0 0,1 12,18A3,3 0 0,1 9,15H5V5H19M19,3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3Z" /></g><g id="inbox-arrow-up"><path d="M14,14H10V11H8L12,7L16,11H14V14M16,11M5,15V5H19V15H15A3,3 0 0,1 12,18A3,3 0 0,1 9,15H5M19,3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3" /></g><g id="incognito"><path d="M12,3C9.31,3 7.41,4.22 7.41,4.22L6,9H18L16.59,4.22C16.59,4.22 14.69,3 12,3M12,11C9.27,11 5.39,11.54 5.13,11.59C4.09,11.87 3.25,12.15 2.59,12.41C1.58,12.75 1,13 1,13H23C23,13 22.42,12.75 21.41,12.41C20.75,12.15 19.89,11.87 18.84,11.59C18.84,11.59 14.82,11 12,11M7.5,14A3.5,3.5 0 0,0 4,17.5A3.5,3.5 0 0,0 7.5,21A3.5,3.5 0 0,0 11,17.5C11,17.34 11,17.18 10.97,17.03C11.29,16.96 11.63,16.9 12,16.91C12.37,16.91 12.71,16.96 13.03,17.03C13,17.18 13,17.34 13,17.5A3.5,3.5 0 0,0 16.5,21A3.5,3.5 0 0,0 20,17.5A3.5,3.5 0 0,0 16.5,14C15.03,14 13.77,14.9 13.25,16.19C12.93,16.09 12.55,16 12,16C11.45,16 11.07,16.09 10.75,16.19C10.23,14.9 8.97,14 7.5,14M7.5,15A2.5,2.5 0 0,1 10,17.5A2.5,2.5 0 0,1 7.5,20A2.5,2.5 0 0,1 5,17.5A2.5,2.5 0 0,1 7.5,15M16.5,15A2.5,2.5 0 0,1 19,17.5A2.5,2.5 0 0,1 16.5,20A2.5,2.5 0 0,1 14,17.5A2.5,2.5 0 0,1 16.5,15Z" /></g><g id="infinity"><path d="M18.6,6.62C21.58,6.62 24,9 24,12C24,14.96 21.58,17.37 18.6,17.37C17.15,17.37 15.8,16.81 14.78,15.8L12,13.34L9.17,15.85C8.2,16.82 6.84,17.38 5.4,17.38C2.42,17.38 0,14.96 0,12C0,9.04 2.42,6.62 5.4,6.62C6.84,6.62 8.2,7.18 9.22,8.2L12,10.66L14.83,8.15C15.8,7.18 17.16,6.62 18.6,6.62M7.8,14.39L10.5,12L7.84,9.65C7.16,8.97 6.31,8.62 5.4,8.62C3.53,8.62 2,10.13 2,12C2,13.87 3.53,15.38 5.4,15.38C6.31,15.38 7.16,15.03 7.8,14.39M16.2,9.61L13.5,12L16.16,14.35C16.84,15.03 17.7,15.38 18.6,15.38C20.47,15.38 22,13.87 22,12C22,10.13 20.47,8.62 18.6,8.62C17.69,8.62 16.84,8.97 16.2,9.61Z" /></g><g id="information"><path d="M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" /></g><g id="information-outline"><path d="M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z" /></g><g id="information-variant"><path d="M13.5,4A1.5,1.5 0 0,0 12,5.5A1.5,1.5 0 0,0 13.5,7A1.5,1.5 0 0,0 15,5.5A1.5,1.5 0 0,0 13.5,4M13.14,8.77C11.95,8.87 8.7,11.46 8.7,11.46C8.5,11.61 8.56,11.6 8.72,11.88C8.88,12.15 8.86,12.17 9.05,12.04C9.25,11.91 9.58,11.7 10.13,11.36C12.25,10 10.47,13.14 9.56,18.43C9.2,21.05 11.56,19.7 12.17,19.3C12.77,18.91 14.38,17.8 14.54,17.69C14.76,17.54 14.6,17.42 14.43,17.17C14.31,17 14.19,17.12 14.19,17.12C13.54,17.55 12.35,18.45 12.19,17.88C12,17.31 13.22,13.4 13.89,10.71C14,10.07 14.3,8.67 13.14,8.77Z" /></g><g id="instagram"><path d="M7.8,2H16.2C19.4,2 22,4.6 22,7.8V16.2A5.8,5.8 0 0,1 16.2,22H7.8C4.6,22 2,19.4 2,16.2V7.8A5.8,5.8 0 0,1 7.8,2M7.6,4A3.6,3.6 0 0,0 4,7.6V16.4C4,18.39 5.61,20 7.6,20H16.4A3.6,3.6 0 0,0 20,16.4V7.6C20,5.61 18.39,4 16.4,4H7.6M17.25,5.5A1.25,1.25 0 0,1 18.5,6.75A1.25,1.25 0 0,1 17.25,8A1.25,1.25 0 0,1 16,6.75A1.25,1.25 0 0,1 17.25,5.5M12,7A5,5 0 0,1 17,12A5,5 0 0,1 12,17A5,5 0 0,1 7,12A5,5 0 0,1 12,7M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9Z" /></g><g id="instapaper"><path d="M10,5A1,1 0 0,0 9,4H8V2H16V4H15A1,1 0 0,0 14,5V19A1,1 0 0,0 15,20H16V22H8V20H9A1,1 0 0,0 10,19V5Z" /></g><g id="internet-explorer"><path d="M13,3L14,3.06C16.8,1.79 19.23,1.64 20.5,2.92C21.5,3.93 21.58,5.67 20.92,7.72C21.61,9 22,10.45 22,12L21.95,13H9.08C9.45,15.28 11.06,17 13,17C14.31,17 15.47,16.21 16.2,15H21.5C20.25,18.5 16.92,21 13,21C11.72,21 10.5,20.73 9.41,20.25C6.5,21.68 3.89,21.9 2.57,20.56C1,18.96 1.68,15.57 4,12C4.93,10.54 6.14,9.06 7.57,7.65L8.38,6.88C7.21,7.57 5.71,8.62 4.19,10.17C5.03,6.08 8.66,3 13,3M13,7C11.21,7 9.69,8.47 9.18,10.5H16.82C16.31,8.47 14.79,7 13,7M20.06,4.06C19.4,3.39 18.22,3.35 16.74,3.81C18.22,4.5 19.5,5.56 20.41,6.89C20.73,5.65 20.64,4.65 20.06,4.06M3.89,20C4.72,20.84 6.4,20.69 8.44,19.76C6.59,18.67 5.17,16.94 4.47,14.88C3.27,17.15 3,19.07 3.89,20Z" /></g><g id="invert-colors"><path d="M12,19.58V19.58C10.4,19.58 8.89,18.96 7.76,17.83C6.62,16.69 6,15.19 6,13.58C6,12 6.62,10.47 7.76,9.34L12,5.1M17.66,7.93L12,2.27V2.27L6.34,7.93C3.22,11.05 3.22,16.12 6.34,19.24C7.9,20.8 9.95,21.58 12,21.58C14.05,21.58 16.1,20.8 17.66,19.24C20.78,16.12 20.78,11.05 17.66,7.93Z" /></g><g id="itunes"><path d="M7.85,17.07C7.03,17.17 3.5,17.67 4.06,20.26C4.69,23.3 9.87,22.59 9.83,19C9.81,16.57 9.83,9.2 9.83,9.2C9.83,9.2 9.76,8.53 10.43,8.39L18.19,6.79C18.19,6.79 18.83,6.65 18.83,7.29C18.83,7.89 18.83,14.2 18.83,14.2C18.83,14.2 18.9,14.83 18.12,15C17.34,15.12 13.91,15.4 14.19,18C14.5,21.07 20,20.65 20,17.07V2.61C20,2.61 20.04,1.62 18.9,1.87L9.5,3.78C9.5,3.78 8.66,3.96 8.66,4.77C8.66,5.5 8.66,16.11 8.66,16.11C8.66,16.11 8.66,16.96 7.85,17.07Z" /></g><g id="jeepney"><path d="M19,13V7H20V4H4V7H5V13H2C2,13.93 2.5,14.71 3.5,14.93V20A1,1 0 0,0 4.5,21H5.5A1,1 0 0,0 6.5,20V19H17.5V20A1,1 0 0,0 18.5,21H19.5A1,1 0 0,0 20.5,20V14.93C21.5,14.7 22,13.93 22,13H19M8,15A1.5,1.5 0 0,1 6.5,13.5A1.5,1.5 0 0,1 8,12A1.5,1.5 0 0,1 9.5,13.5A1.5,1.5 0 0,1 8,15M16,15A1.5,1.5 0 0,1 14.5,13.5A1.5,1.5 0 0,1 16,12A1.5,1.5 0 0,1 17.5,13.5A1.5,1.5 0 0,1 16,15M17.5,10.5C15.92,10.18 14.03,10 12,10C9.97,10 8,10.18 6.5,10.5V7H17.5V10.5Z" /></g><g id="jira"><path d="M12,2A1.58,1.58 0 0,1 13.58,3.58A1.58,1.58 0 0,1 12,5.16A1.58,1.58 0 0,1 10.42,3.58A1.58,1.58 0 0,1 12,2M7.79,3.05C8.66,3.05 9.37,3.76 9.37,4.63C9.37,5.5 8.66,6.21 7.79,6.21A1.58,1.58 0 0,1 6.21,4.63A1.58,1.58 0 0,1 7.79,3.05M16.21,3.05C17.08,3.05 17.79,3.76 17.79,4.63C17.79,5.5 17.08,6.21 16.21,6.21A1.58,1.58 0 0,1 14.63,4.63A1.58,1.58 0 0,1 16.21,3.05M11.8,10.95C9.7,8.84 10.22,7.79 10.22,7.79H13.91C13.91,9.37 11.8,10.95 11.8,10.95M13.91,21.47C13.91,21.47 13.91,19.37 9.7,15.16C5.5,10.95 4.96,9.89 4.43,6.74C4.43,6.74 4.83,6.21 5.36,6.74C5.88,7.26 7.07,7.66 8.12,7.66C8.12,7.66 9.17,10.95 12.07,13.05C12.07,13.05 15.88,9.11 15.88,7.53C15.88,7.53 17.07,7.79 18.5,6.74C18.5,6.74 19.5,6.21 19.57,6.74C19.7,7.79 18.64,11.47 14.3,15.16C14.3,15.16 17.07,18.32 16.8,21.47H13.91M9.17,16.21L11.41,18.71C10.36,19.76 10.22,22 10.22,22H7.07C7.59,17.79 9.17,16.21 9.17,16.21Z" /></g><g id="jsfiddle"><path d="M20.33,10.79C21.9,11.44 23,12.96 23,14.73C23,17.09 21.06,19 18.67,19H5.4C3,18.96 1,17 1,14.62C1,13.03 1.87,11.63 3.17,10.87C3.08,10.59 3.04,10.29 3.04,10C3.04,8.34 4.39,7 6.06,7C6.75,7 7.39,7.25 7.9,7.64C8.96,5.47 11.2,3.96 13.81,3.96C17.42,3.96 20.35,6.85 20.35,10.41C20.35,10.54 20.34,10.67 20.33,10.79M9.22,10.85C7.45,10.85 6,12.12 6,13.67C6,15.23 7.45,16.5 9.22,16.5C10.25,16.5 11.17,16.06 11.76,15.39L10.75,14.25C10.42,14.68 9.77,15 9.22,15C8.43,15 7.79,14.4 7.79,13.67C7.79,12.95 8.43,12.36 9.22,12.36C9.69,12.36 10.12,12.59 10.56,12.88C11,13.16 11.73,14.17 12.31,14.82C13.77,16.29 14.53,16.42 15.4,16.42C17.17,16.42 18.6,15.15 18.6,13.6C18.6,12.04 17.17,10.78 15.4,10.78C14.36,10.78 13.44,11.21 12.85,11.88L13.86,13C14.19,12.59 14.84,12.28 15.4,12.28C16.19,12.28 16.83,12.87 16.83,13.6C16.83,14.32 16.19,14.91 15.4,14.91C14.93,14.91 14.5,14.68 14.05,14.39C13.61,14.11 12.88,13.1 12.31,12.45C10.84,11 10.08,10.85 9.22,10.85Z" /></g><g id="json"><path d="M5,3H7V5H5V10A2,2 0 0,1 3,12A2,2 0 0,1 5,14V19H7V21H5C3.93,20.73 3,20.1 3,19V15A2,2 0 0,0 1,13H0V11H1A2,2 0 0,0 3,9V5A2,2 0 0,1 5,3M19,3A2,2 0 0,1 21,5V9A2,2 0 0,0 23,11H24V13H23A2,2 0 0,0 21,15V19A2,2 0 0,1 19,21H17V19H19V14A2,2 0 0,1 21,12A2,2 0 0,1 19,10V5H17V3H19M12,15A1,1 0 0,1 13,16A1,1 0 0,1 12,17A1,1 0 0,1 11,16A1,1 0 0,1 12,15M8,15A1,1 0 0,1 9,16A1,1 0 0,1 8,17A1,1 0 0,1 7,16A1,1 0 0,1 8,15M16,15A1,1 0 0,1 17,16A1,1 0 0,1 16,17A1,1 0 0,1 15,16A1,1 0 0,1 16,15Z" /></g><g id="keg"><path d="M5,22V20H6V16H5V14H6V11H5V7H11V3H10V2H11L13,2H14V3H13V7H19V11H18V14H19V16H18V20H19V22H5M17,9A1,1 0 0,0 16,8H14A1,1 0 0,0 13,9A1,1 0 0,0 14,10H16A1,1 0 0,0 17,9Z" /></g><g id="kettle"><path d="M12.5,3C7.81,3 4,5.69 4,9V9C4,10.19 4.5,11.34 5.44,12.33C4.53,13.5 4,14.96 4,16.5C4,17.64 4,18.83 4,20C4,21.11 4.89,22 6,22H19C20.11,22 21,21.11 21,20C21,18.85 21,17.61 21,16.5C21,15.28 20.66,14.07 20,13L22,11L19,8L16.9,10.1C15.58,9.38 14.05,9 12.5,9C10.65,9 8.95,9.53 7.55,10.41C7.19,9.97 7,9.5 7,9C7,7.21 9.46,5.75 12.5,5.75V5.75C13.93,5.75 15.3,6.08 16.33,6.67L18.35,4.65C16.77,3.59 14.68,3 12.5,3M12.5,11C12.84,11 13.17,11.04 13.5,11.09C10.39,11.57 8,14.25 8,17.5V20H6V17.5A6.5,6.5 0 0,1 12.5,11Z" /></g><g id="key"><path d="M7,14A2,2 0 0,1 5,12A2,2 0 0,1 7,10A2,2 0 0,1 9,12A2,2 0 0,1 7,14M12.65,10C11.83,7.67 9.61,6 7,6A6,6 0 0,0 1,12A6,6 0 0,0 7,18C9.61,18 11.83,16.33 12.65,14H17V18H21V14H23V10H12.65Z" /></g><g id="key-change"><path d="M6.5,2C8.46,2 10.13,3.25 10.74,5H22V8H18V11H15V8H10.74C10.13,9.75 8.46,11 6.5,11C4,11 2,9 2,6.5C2,4 4,2 6.5,2M6.5,5A1.5,1.5 0 0,0 5,6.5A1.5,1.5 0 0,0 6.5,8A1.5,1.5 0 0,0 8,6.5A1.5,1.5 0 0,0 6.5,5M6.5,13C8.46,13 10.13,14.25 10.74,16H22V19H20V22H18V19H16V22H13V19H10.74C10.13,20.75 8.46,22 6.5,22C4,22 2,20 2,17.5C2,15 4,13 6.5,13M6.5,16A1.5,1.5 0 0,0 5,17.5A1.5,1.5 0 0,0 6.5,19A1.5,1.5 0 0,0 8,17.5A1.5,1.5 0 0,0 6.5,16Z" /></g><g id="key-minus"><path d="M6.5,3C8.46,3 10.13,4.25 10.74,6H22V9H18V12H15V9H10.74C10.13,10.75 8.46,12 6.5,12C4,12 2,10 2,7.5C2,5 4,3 6.5,3M6.5,6A1.5,1.5 0 0,0 5,7.5A1.5,1.5 0 0,0 6.5,9A1.5,1.5 0 0,0 8,7.5A1.5,1.5 0 0,0 6.5,6M8,17H16V19H8V17Z" /></g><g id="key-plus"><path d="M6.5,3C8.46,3 10.13,4.25 10.74,6H22V9H18V12H15V9H10.74C10.13,10.75 8.46,12 6.5,12C4,12 2,10 2,7.5C2,5 4,3 6.5,3M6.5,6A1.5,1.5 0 0,0 5,7.5A1.5,1.5 0 0,0 6.5,9A1.5,1.5 0 0,0 8,7.5A1.5,1.5 0 0,0 6.5,6M8,17H11V14H13V17H16V19H13V22H11V19H8V17Z" /></g><g id="key-remove"><path d="M6.5,3C8.46,3 10.13,4.25 10.74,6H22V9H18V12H15V9H10.74C10.13,10.75 8.46,12 6.5,12C4,12 2,10 2,7.5C2,5 4,3 6.5,3M6.5,6A1.5,1.5 0 0,0 5,7.5A1.5,1.5 0 0,0 6.5,9A1.5,1.5 0 0,0 8,7.5A1.5,1.5 0 0,0 6.5,6M14.59,14L16,15.41L13.41,18L16,20.59L14.59,22L12,19.41L9.41,22L8,20.59L10.59,18L8,15.41L9.41,14L12,16.59L14.59,14Z" /></g><g id="key-variant"><path d="M22,18V22H18V19H15V16H12L9.74,13.74C9.19,13.91 8.61,14 8,14A6,6 0 0,1 2,8A6,6 0 0,1 8,2A6,6 0 0,1 14,8C14,8.61 13.91,9.19 13.74,9.74L22,18M7,5A2,2 0 0,0 5,7A2,2 0 0,0 7,9A2,2 0 0,0 9,7A2,2 0 0,0 7,5Z" /></g><g id="keyboard"><path d="M19,10H17V8H19M19,13H17V11H19M16,10H14V8H16M16,13H14V11H16M16,17H8V15H16M7,10H5V8H7M7,13H5V11H7M8,11H10V13H8M8,8H10V10H8M11,11H13V13H11M11,8H13V10H11M20,5H4C2.89,5 2,5.89 2,7V17A2,2 0 0,0 4,19H20A2,2 0 0,0 22,17V7C22,5.89 21.1,5 20,5Z" /></g><g id="keyboard-backspace"><path d="M21,11H6.83L10.41,7.41L9,6L3,12L9,18L10.41,16.58L6.83,13H21V11Z" /></g><g id="keyboard-caps"><path d="M6,18H18V16H6M12,8.41L16.59,13L18,11.58L12,5.58L6,11.58L7.41,13L12,8.41Z" /></g><g id="keyboard-close"><path d="M12,23L16,19H8M19,8H17V6H19M19,11H17V9H19M16,8H14V6H16M16,11H14V9H16M16,15H8V13H16M7,8H5V6H7M7,11H5V9H7M8,9H10V11H8M8,6H10V8H8M11,9H13V11H11M11,6H13V8H11M20,3H4C2.89,3 2,3.89 2,5V15A2,2 0 0,0 4,17H20A2,2 0 0,0 22,15V5C22,3.89 21.1,3 20,3Z" /></g><g id="keyboard-off"><path d="M1,4.27L2.28,3L20,20.72L18.73,22L15.73,19H4C2.89,19 2,18.1 2,17V7C2,6.5 2.18,6.07 2.46,5.73L1,4.27M19,10V8H17V10H19M19,13V11H17V13H19M16,10V8H14V10H16M16,13V11H14V12.18L11.82,10H13V8H11V9.18L9.82,8L6.82,5H20A2,2 0 0,1 22,7V17C22,17.86 21.46,18.59 20.7,18.87L14.82,13H16M8,15V17H13.73L11.73,15H8M5,10H6.73L5,8.27V10M7,13V11H5V13H7M8,13H9.73L8,11.27V13Z" /></g><g id="keyboard-return"><path d="M19,7V11H5.83L9.41,7.41L8,6L2,12L8,18L9.41,16.58L5.83,13H21V7H19Z" /></g><g id="keyboard-tab"><path d="M20,18H22V6H20M11.59,7.41L15.17,11H1V13H15.17L11.59,16.58L13,18L19,12L13,6L11.59,7.41Z" /></g><g id="keyboard-variant"><path d="M6,16H18V18H6V16M6,13V15H2V13H6M7,15V13H10V15H7M11,15V13H13V15H11M14,15V13H17V15H14M18,15V13H22V15H18M2,10H5V12H2V10M19,12V10H22V12H19M18,12H16V10H18V12M8,12H6V10H8V12M12,12H9V10H12V12M15,12H13V10H15V12M2,9V7H4V9H2M5,9V7H7V9H5M8,9V7H10V9H8M11,9V7H13V9H11M14,9V7H16V9H14M17,9V7H22V9H17Z" /></g><g id="kodi"><path d="M12.03,1C11.82,1 11.6,1.11 11.41,1.31C10.56,2.16 9.72,3 8.88,3.84C8.66,4.06 8.6,4.18 8.38,4.38C8.09,4.62 7.96,4.91 7.97,5.28C8,6.57 8,7.84 8,9.13C8,10.46 8,11.82 8,13.16C8,13.26 8,13.34 8.03,13.44C8.11,13.75 8.31,13.82 8.53,13.59C9.73,12.39 10.8,11.3 12,10.09C13.36,8.73 14.73,7.37 16.09,6C16.5,5.6 16.5,5.15 16.09,4.75C14.94,3.6 13.77,2.47 12.63,1.31C12.43,1.11 12.24,1 12.03,1M18.66,7.66C18.45,7.66 18.25,7.75 18.06,7.94C16.91,9.1 15.75,10.24 14.59,11.41C14.2,11.8 14.2,12.23 14.59,12.63C15.74,13.78 16.88,14.94 18.03,16.09C18.43,16.5 18.85,16.5 19.25,16.09C20.36,15 21.5,13.87 22.59,12.75C22.76,12.58 22.93,12.42 23,12.19V11.88C22.93,11.64 22.76,11.5 22.59,11.31C21.47,10.19 20.37,9.06 19.25,7.94C19.06,7.75 18.86,7.66 18.66,7.66M4.78,8.09C4.65,8.04 4.58,8.14 4.5,8.22C3.35,9.39 2.34,10.43 1.19,11.59C0.93,11.86 0.93,12.24 1.19,12.5C1.81,13.13 2.44,13.75 3.06,14.38C3.6,14.92 4,15.33 4.56,15.88C4.72,16.03 4.86,16 4.94,15.81C5,15.71 5,15.58 5,15.47C5,14.29 5,13.37 5,12.19C5,11 5,9.81 5,8.63C5,8.55 5,8.45 4.97,8.38C4.95,8.25 4.9,8.14 4.78,8.09M12.09,14.25C11.89,14.25 11.66,14.34 11.47,14.53C10.32,15.69 9.18,16.87 8.03,18.03C7.63,18.43 7.63,18.85 8.03,19.25C9.14,20.37 10.26,21.47 11.38,22.59C11.54,22.76 11.71,22.93 11.94,23H12.22C12.44,22.94 12.62,22.79 12.78,22.63C13.9,21.5 15.03,20.38 16.16,19.25C16.55,18.85 16.5,18.4 16.13,18C14.97,16.84 13.84,15.69 12.69,14.53C12.5,14.34 12.3,14.25 12.09,14.25Z" /></g><g id="label"><path d="M17.63,5.84C17.27,5.33 16.67,5 16,5H5A2,2 0 0,0 3,7V17A2,2 0 0,0 5,19H16C16.67,19 17.27,18.66 17.63,18.15L22,12L17.63,5.84Z" /></g><g id="label-outline"><path d="M16,17H5V7H16L19.55,12M17.63,5.84C17.27,5.33 16.67,5 16,5H5A2,2 0 0,0 3,7V17A2,2 0 0,0 5,19H16C16.67,19 17.27,18.66 17.63,18.15L22,12L17.63,5.84Z" /></g><g id="lambda"><path d="M6,20L10.16,7.91L9.34,6H8V4H10C10.42,4 10.78,4.26 10.93,4.63L16.66,18H18V20H16C15.57,20 15.21,19.73 15.07,19.36L11.33,10.65L8.12,20H6Z" /></g><g id="lamp"><path d="M8,2H16L20,14H4L8,2M11,15H13V20H18V22H6V20H11V15Z" /></g><g id="lan"><path d="M10,2C8.89,2 8,2.89 8,4V7C8,8.11 8.89,9 10,9H11V11H2V13H6V15H5C3.89,15 3,15.89 3,17V20C3,21.11 3.89,22 5,22H9C10.11,22 11,21.11 11,20V17C11,15.89 10.11,15 9,15H8V13H16V15H15C13.89,15 13,15.89 13,17V20C13,21.11 13.89,22 15,22H19C20.11,22 21,21.11 21,20V17C21,15.89 20.11,15 19,15H18V13H22V11H13V9H14C15.11,9 16,8.11 16,7V4C16,2.89 15.11,2 14,2H10M10,4H14V7H10V4M5,17H9V20H5V17M15,17H19V20H15V17Z" /></g><g id="lan-connect"><path d="M4,1C2.89,1 2,1.89 2,3V7C2,8.11 2.89,9 4,9H1V11H13V9H10C11.11,9 12,8.11 12,7V3C12,1.89 11.11,1 10,1H4M4,3H10V7H4V3M3,13V18L3,20H10V18H5V13H3M14,13C12.89,13 12,13.89 12,15V19C12,20.11 12.89,21 14,21H11V23H23V21H20C21.11,21 22,20.11 22,19V15C22,13.89 21.11,13 20,13H14M14,15H20V19H14V15Z" /></g><g id="lan-disconnect"><path d="M4,1C2.89,1 2,1.89 2,3V7C2,8.11 2.89,9 4,9H1V11H13V9H10C11.11,9 12,8.11 12,7V3C12,1.89 11.11,1 10,1H4M4,3H10V7H4V3M14,13C12.89,13 12,13.89 12,15V19C12,20.11 12.89,21 14,21H11V23H23V21H20C21.11,21 22,20.11 22,19V15C22,13.89 21.11,13 20,13H14M3.88,13.46L2.46,14.88L4.59,17L2.46,19.12L3.88,20.54L6,18.41L8.12,20.54L9.54,19.12L7.41,17L9.54,14.88L8.12,13.46L6,15.59L3.88,13.46M14,15H20V19H14V15Z" /></g><g id="lan-pending"><path d="M4,1C2.89,1 2,1.89 2,3V7C2,8.11 2.89,9 4,9H1V11H13V9H10C11.11,9 12,8.11 12,7V3C12,1.89 11.11,1 10,1H4M4,3H10V7H4V3M3,12V14H5V12H3M14,13C12.89,13 12,13.89 12,15V19C12,20.11 12.89,21 14,21H11V23H23V21H20C21.11,21 22,20.11 22,19V15C22,13.89 21.11,13 20,13H14M3,15V17H5V15H3M14,15H20V19H14V15M3,18V20H5V18H3M6,18V20H8V18H6M9,18V20H11V18H9Z" /></g><g id="language-c"><path d="M15.45,15.97L15.87,18.41C15.61,18.55 15.19,18.68 14.63,18.8C14.06,18.93 13.39,19 12.62,19C10.41,18.96 8.75,18.3 7.64,17.04C6.5,15.77 5.96,14.16 5.96,12.21C6,9.9 6.68,8.13 8,6.89C9.28,5.64 10.92,5 12.9,5C13.65,5 14.3,5.07 14.84,5.19C15.38,5.31 15.78,5.44 16.04,5.59L15.44,8.08L14.4,7.74C14,7.64 13.53,7.59 13,7.59C11.85,7.58 10.89,7.95 10.14,8.69C9.38,9.42 9,10.54 8.96,12.03C8.97,13.39 9.33,14.45 10.04,15.23C10.75,16 11.74,16.4 13.03,16.41L14.36,16.29C14.79,16.21 15.15,16.1 15.45,15.97Z" /></g><g id="language-cpp"><path d="M10.5,15.97L10.91,18.41C10.65,18.55 10.23,18.68 9.67,18.8C9.1,18.93 8.43,19 7.66,19C5.45,18.96 3.79,18.3 2.68,17.04C1.56,15.77 1,14.16 1,12.21C1.05,9.9 1.72,8.13 3,6.89C4.32,5.64 5.96,5 7.94,5C8.69,5 9.34,5.07 9.88,5.19C10.42,5.31 10.82,5.44 11.08,5.59L10.5,8.08L9.44,7.74C9.04,7.64 8.58,7.59 8.05,7.59C6.89,7.58 5.93,7.95 5.18,8.69C4.42,9.42 4.03,10.54 4,12.03C4,13.39 4.37,14.45 5.08,15.23C5.79,16 6.79,16.4 8.07,16.41L9.4,16.29C9.83,16.21 10.19,16.1 10.5,15.97M11,11H13V9H15V11H17V13H15V15H13V13H11V11M18,11H20V9H22V11H24V13H22V15H20V13H18V11Z" /></g><g id="language-csharp"><path d="M11.5,15.97L11.91,18.41C11.65,18.55 11.23,18.68 10.67,18.8C10.1,18.93 9.43,19 8.66,19C6.45,18.96 4.79,18.3 3.68,17.04C2.56,15.77 2,14.16 2,12.21C2.05,9.9 2.72,8.13 4,6.89C5.32,5.64 6.96,5 8.94,5C9.69,5 10.34,5.07 10.88,5.19C11.42,5.31 11.82,5.44 12.08,5.59L11.5,8.08L10.44,7.74C10.04,7.64 9.58,7.59 9.05,7.59C7.89,7.58 6.93,7.95 6.18,8.69C5.42,9.42 5.03,10.54 5,12.03C5,13.39 5.37,14.45 6.08,15.23C6.79,16 7.79,16.4 9.07,16.41L10.4,16.29C10.83,16.21 11.19,16.1 11.5,15.97M13.89,19L14.5,15H13L13.34,13H14.84L15.16,11H13.66L14,9H15.5L16.11,5H18.11L17.5,9H18.5L19.11,5H21.11L20.5,9H22L21.66,11H20.16L19.84,13H21.34L21,15H19.5L18.89,19H16.89L17.5,15H16.5L15.89,19H13.89M16.84,13H17.84L18.16,11H17.16L16.84,13Z" /></g><g id="language-css3"><path d="M5,3L4.35,6.34H17.94L17.5,8.5H3.92L3.26,11.83H16.85L16.09,15.64L10.61,17.45L5.86,15.64L6.19,14H2.85L2.06,18L9.91,21L18.96,18L20.16,11.97L20.4,10.76L21.94,3H5Z" /></g><g id="language-html5"><path d="M12,17.56L16.07,16.43L16.62,10.33H9.38L9.2,8.3H16.8L17,6.31H7L7.56,12.32H14.45L14.22,14.9L12,15.5L9.78,14.9L9.64,13.24H7.64L7.93,16.43L12,17.56M4.07,3H19.93L18.5,19.2L12,21L5.5,19.2L4.07,3Z" /></g><g id="language-javascript"><path d="M3,3H21V21H3V3M7.73,18.04C8.13,18.89 8.92,19.59 10.27,19.59C11.77,19.59 12.8,18.79 12.8,17.04V11.26H11.1V17C11.1,17.86 10.75,18.08 10.2,18.08C9.62,18.08 9.38,17.68 9.11,17.21L7.73,18.04M13.71,17.86C14.21,18.84 15.22,19.59 16.8,19.59C18.4,19.59 19.6,18.76 19.6,17.23C19.6,15.82 18.79,15.19 17.35,14.57L16.93,14.39C16.2,14.08 15.89,13.87 15.89,13.37C15.89,12.96 16.2,12.64 16.7,12.64C17.18,12.64 17.5,12.85 17.79,13.37L19.1,12.5C18.55,11.54 17.77,11.17 16.7,11.17C15.19,11.17 14.22,12.13 14.22,13.4C14.22,14.78 15.03,15.43 16.25,15.95L16.67,16.13C17.45,16.47 17.91,16.68 17.91,17.26C17.91,17.74 17.46,18.09 16.76,18.09C15.93,18.09 15.45,17.66 15.09,17.06L13.71,17.86Z" /></g><g id="language-php"><path d="M12,18.08C5.37,18.08 0,15.36 0,12C0,8.64 5.37,5.92 12,5.92C18.63,5.92 24,8.64 24,12C24,15.36 18.63,18.08 12,18.08M6.81,10.13C7.35,10.13 7.72,10.23 7.9,10.44C8.08,10.64 8.12,11 8.03,11.47C7.93,12 7.74,12.34 7.45,12.56C7.17,12.78 6.74,12.89 6.16,12.89H5.29L5.82,10.13H6.81M3.31,15.68H4.75L5.09,13.93H6.32C6.86,13.93 7.3,13.87 7.65,13.76C8,13.64 8.32,13.45 8.61,13.18C8.85,12.96 9.04,12.72 9.19,12.45C9.34,12.19 9.45,11.89 9.5,11.57C9.66,10.79 9.55,10.18 9.17,9.75C8.78,9.31 8.18,9.1 7.35,9.1H4.59L3.31,15.68M10.56,7.35L9.28,13.93H10.7L11.44,10.16H12.58C12.94,10.16 13.18,10.22 13.29,10.34C13.4,10.46 13.42,10.68 13.36,11L12.79,13.93H14.24L14.83,10.86C14.96,10.24 14.86,9.79 14.56,9.5C14.26,9.23 13.71,9.1 12.91,9.1H11.64L12,7.35H10.56M18,10.13C18.55,10.13 18.91,10.23 19.09,10.44C19.27,10.64 19.31,11 19.22,11.47C19.12,12 18.93,12.34 18.65,12.56C18.36,12.78 17.93,12.89 17.35,12.89H16.5L17,10.13H18M14.5,15.68H15.94L16.28,13.93H17.5C18.05,13.93 18.5,13.87 18.85,13.76C19.2,13.64 19.5,13.45 19.8,13.18C20.04,12.96 20.24,12.72 20.38,12.45C20.53,12.19 20.64,11.89 20.7,11.57C20.85,10.79 20.74,10.18 20.36,9.75C20,9.31 19.37,9.1 18.54,9.1H15.79L14.5,15.68Z" /></g><g id="language-python"><path d="M19.14,7.5A2.86,2.86 0 0,1 22,10.36V14.14A2.86,2.86 0 0,1 19.14,17H12C12,17.39 12.32,17.96 12.71,17.96H17V19.64A2.86,2.86 0 0,1 14.14,22.5H9.86A2.86,2.86 0 0,1 7,19.64V15.89C7,14.31 8.28,13.04 9.86,13.04H15.11C16.69,13.04 17.96,11.76 17.96,10.18V7.5H19.14M14.86,19.29C14.46,19.29 14.14,19.59 14.14,20.18C14.14,20.77 14.46,20.89 14.86,20.89A0.71,0.71 0 0,0 15.57,20.18C15.57,19.59 15.25,19.29 14.86,19.29M4.86,17.5C3.28,17.5 2,16.22 2,14.64V10.86C2,9.28 3.28,8 4.86,8H12C12,7.61 11.68,7.04 11.29,7.04H7V5.36C7,3.78 8.28,2.5 9.86,2.5H14.14C15.72,2.5 17,3.78 17,5.36V9.11C17,10.69 15.72,11.96 14.14,11.96H8.89C7.31,11.96 6.04,13.24 6.04,14.82V17.5H4.86M9.14,5.71C9.54,5.71 9.86,5.41 9.86,4.82C9.86,4.23 9.54,4.11 9.14,4.11C8.75,4.11 8.43,4.23 8.43,4.82C8.43,5.41 8.75,5.71 9.14,5.71Z" /></g><g id="language-python-text"><path d="M2,5.69C8.92,1.07 11.1,7 11.28,10.27C11.46,13.53 8.29,17.64 4.31,14.92V20.3L2,18.77V5.69M4.22,7.4V12.78C7.84,14.95 9.08,13.17 9.08,10.09C9.08,5.74 6.57,5.59 4.22,7.4M15.08,4.15C15.08,4.15 14.9,7.64 15.08,11.07C15.44,14.5 19.69,11.84 19.69,11.84V4.92L22,5.2V14.44C22,20.6 15.85,20.3 15.85,20.3L15.08,18C20.46,18 19.78,14.43 19.78,14.43C13.27,16.97 12.77,12.61 12.77,12.61V5.69L15.08,4.15Z" /></g><g id="language-swift"><path d="M17.09,19.72C14.73,21.08 11.5,21.22 8.23,19.82C5.59,18.7 3.4,16.74 2,14.5C2.67,15.05 3.46,15.5 4.3,15.9C7.67,17.47 11.03,17.36 13.4,15.9C10.03,13.31 7.16,9.94 5.03,7.19C4.58,6.74 4.25,6.18 3.91,5.68C12.19,11.73 11.83,13.27 6.32,4.67C11.21,9.61 15.75,12.41 15.75,12.41C15.91,12.5 16,12.57 16.11,12.63C16.21,12.38 16.3,12.12 16.37,11.85C17.16,9 16.26,5.73 14.29,3.04C18.84,5.79 21.54,10.95 20.41,15.28C20.38,15.39 20.35,15.5 20.36,15.67C22.6,18.5 22,21.45 21.71,20.89C20.5,18.5 18.23,19.24 17.09,19.72V19.72Z" /></g><g id="laptop"><path d="M4,6H20V16H4M20,18A2,2 0 0,0 22,16V6C22,4.89 21.1,4 20,4H4C2.89,4 2,4.89 2,6V16A2,2 0 0,0 4,18H0V20H24V18H20Z" /></g><g id="laptop-chromebook"><path d="M20,15H4V5H20M14,18H10V17H14M22,18V3H2V18H0V20H24V18H22Z" /></g><g id="laptop-mac"><path d="M12,19A1,1 0 0,1 11,18A1,1 0 0,1 12,17A1,1 0 0,1 13,18A1,1 0 0,1 12,19M4,5H20V16H4M20,18A2,2 0 0,0 22,16V5C22,3.89 21.1,3 20,3H4C2.89,3 2,3.89 2,5V16A2,2 0 0,0 4,18H0A2,2 0 0,0 2,20H22A2,2 0 0,0 24,18H20Z" /></g><g id="laptop-windows"><path d="M3,4H21A1,1 0 0,1 22,5V16A1,1 0 0,1 21,17H22L24,20V21H0V20L2,17H3A1,1 0 0,1 2,16V5A1,1 0 0,1 3,4M4,6V15H20V6H4Z" /></g><g id="lastfm"><path d="M18,17.93C15.92,17.92 14.81,16.9 14.04,15.09L13.82,14.6L11.92,10.23C11.29,8.69 9.72,7.64 7.96,7.64C5.57,7.64 3.63,9.59 3.63,12C3.63,14.41 5.57,16.36 7.96,16.36C9.62,16.36 11.08,15.41 11.8,14L12.57,15.81C11.5,17.15 9.82,18 7.96,18C4.67,18 2,15.32 2,12C2,8.69 4.67,6 7.96,6C10.44,6 12.45,7.34 13.47,9.7C13.54,9.89 14.54,12.24 15.42,14.24C15.96,15.5 16.42,16.31 17.91,16.36C19.38,16.41 20.39,15.5 20.39,14.37C20.39,13.26 19.62,13 18.32,12.56C16,11.79 14.79,11 14.79,9.15C14.79,7.33 16,6.12 18,6.12C19.31,6.12 20.24,6.7 20.89,7.86L19.62,8.5C19.14,7.84 18.61,7.57 17.94,7.57C17,7.57 16.33,8.23 16.33,9.1C16.33,10.34 17.43,10.53 18.97,11.03C21.04,11.71 22,12.5 22,14.42C22,16.45 20.27,17.93 18,17.93Z" /></g><g id="launch"><path d="M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z" /></g><g id="layers"><path d="M12,16L19.36,10.27L21,9L12,2L3,9L4.63,10.27M12,18.54L4.62,12.81L3,14.07L12,21.07L21,14.07L19.37,12.8L12,18.54Z" /></g><g id="layers-off"><path d="M3.27,1L2,2.27L6.22,6.5L3,9L4.63,10.27L12,16L14.1,14.37L15.53,15.8L12,18.54L4.63,12.81L3,14.07L12,21.07L16.95,17.22L20.73,21L22,19.73L3.27,1M19.36,10.27L21,9L12,2L9.09,4.27L16.96,12.15L19.36,10.27M19.81,15L21,14.07L19.57,12.64L18.38,13.56L19.81,15Z" /></g><g id="lead-pencil"><path d="M16.84,2.73C16.45,2.73 16.07,2.88 15.77,3.17L13.65,5.29L18.95,10.6L21.07,8.5C21.67,7.89 21.67,6.94 21.07,6.36L17.9,3.17C17.6,2.88 17.22,2.73 16.84,2.73M12.94,6L4.84,14.11L7.4,14.39L7.58,16.68L9.86,16.85L10.15,19.41L18.25,11.3M4.25,15.04L2.5,21.73L9.2,19.94L8.96,17.78L6.65,17.61L6.47,15.29" /></g><g id="leaf"><path d="M17,8C8,10 5.9,16.17 3.82,21.34L5.71,22L6.66,19.7C7.14,19.87 7.64,20 8,20C19,20 22,3 22,3C21,5 14,5.25 9,6.25C4,7.25 2,11.5 2,13.5C2,15.5 3.75,17.25 3.75,17.25C7,8 17,8 17,8Z" /></g><g id="led-off"><path d="M12,6A4,4 0 0,0 8,10V16H6V18H9V23H11V18H13V23H15V18H18V16H16V10A4,4 0 0,0 12,6Z" /></g><g id="led-on"><path d="M11,0V4H13V0H11M18.3,2.29L15.24,5.29L16.64,6.71L19.7,3.71L18.3,2.29M5.71,2.29L4.29,3.71L7.29,6.71L8.71,5.29L5.71,2.29M12,6A4,4 0 0,0 8,10V16H6V18H9V23H11V18H13V23H15V18H18V16H16V10A4,4 0 0,0 12,6M2,9V11H6V9H2M18,9V11H22V9H18Z" /></g><g id="led-outline"><path d="M12,6A4,4 0 0,0 8,10V16H6V18H9V23H11V18H13V23H15V18H18V16H16V10A4,4 0 0,0 12,6M12,8A2,2 0 0,1 14,10V15H10V10A2,2 0 0,1 12,8Z" /></g><g id="led-variant-off"><path d="M12,3C10.05,3 8.43,4.4 8.08,6.25L16.82,15H18V13H16V7A4,4 0 0,0 12,3M3.28,4L2,5.27L8,11.27V13H6V15H9V21H11V15H11.73L13,16.27V21H15V18.27L18.73,22L20,20.72L15,15.72L8,8.72L3.28,4Z" /></g><g id="led-variant-on"><path d="M12,3A4,4 0 0,0 8,7V13H6V15H9V21H11V15H13V21H15V15H18V13H16V7A4,4 0 0,0 12,3Z" /></g><g id="led-variant-outline"><path d="M12,3A4,4 0 0,0 8,7V13H6V15H9V21H11V15H13V21H15V15H18V13H16V7A4,4 0 0,0 12,3M12,5A2,2 0 0,1 14,7V12H10V7A2,2 0 0,1 12,5Z" /></g><g id="library"><path d="M12,8A3,3 0 0,0 15,5A3,3 0 0,0 12,2A3,3 0 0,0 9,5A3,3 0 0,0 12,8M12,11.54C9.64,9.35 6.5,8 3,8V19C6.5,19 9.64,20.35 12,22.54C14.36,20.35 17.5,19 21,19V8C17.5,8 14.36,9.35 12,11.54Z" /></g><g id="library-books"><path d="M19,7H9V5H19M15,15H9V13H15M19,11H9V9H19M20,2H8A2,2 0 0,0 6,4V16A2,2 0 0,0 8,18H20A2,2 0 0,0 22,16V4A2,2 0 0,0 20,2M4,6H2V20A2,2 0 0,0 4,22H18V20H4V6Z" /></g><g id="library-music"><path d="M4,6H2V20A2,2 0 0,0 4,22H18V20H4M18,7H15V12.5A2.5,2.5 0 0,1 12.5,15A2.5,2.5 0 0,1 10,12.5A2.5,2.5 0 0,1 12.5,10C13.07,10 13.58,10.19 14,10.5V5H18M20,2H8A2,2 0 0,0 6,4V16A2,2 0 0,0 8,18H20A2,2 0 0,0 22,16V4A2,2 0 0,0 20,2Z" /></g><g id="library-plus"><path d="M19,11H15V15H13V11H9V9H13V5H15V9H19M20,2H8A2,2 0 0,0 6,4V16A2,2 0 0,0 8,18H20A2,2 0 0,0 22,16V4A2,2 0 0,0 20,2M4,6H2V20A2,2 0 0,0 4,22H18V20H4V6Z" /></g><g id="lightbulb"><path d="M12,2A7,7 0 0,0 5,9C5,11.38 6.19,13.47 8,14.74V17A1,1 0 0,0 9,18H15A1,1 0 0,0 16,17V14.74C17.81,13.47 19,11.38 19,9A7,7 0 0,0 12,2M9,21A1,1 0 0,0 10,22H14A1,1 0 0,0 15,21V20H9V21Z" /></g><g id="lightbulb-outline"><path d="M12,2A7,7 0 0,1 19,9C19,11.38 17.81,13.47 16,14.74V17A1,1 0 0,1 15,18H9A1,1 0 0,1 8,17V14.74C6.19,13.47 5,11.38 5,9A7,7 0 0,1 12,2M9,21V20H15V21A1,1 0 0,1 14,22H10A1,1 0 0,1 9,21M12,4A5,5 0 0,0 7,9C7,11.05 8.23,12.81 10,13.58V16H14V13.58C15.77,12.81 17,11.05 17,9A5,5 0 0,0 12,4Z" /></g><g id="link"><path d="M16,6H13V7.9H16C18.26,7.9 20.1,9.73 20.1,12A4.1,4.1 0 0,1 16,16.1H13V18H16A6,6 0 0,0 22,12C22,8.68 19.31,6 16,6M3.9,12C3.9,9.73 5.74,7.9 8,7.9H11V6H8A6,6 0 0,0 2,12A6,6 0 0,0 8,18H11V16.1H8C5.74,16.1 3.9,14.26 3.9,12M8,13H16V11H8V13Z" /></g><g id="link-off"><path d="M2,5.27L3.28,4L20,20.72L18.73,22L14.73,18H13V16.27L9.73,13H8V11.27L5.5,8.76C4.5,9.5 3.9,10.68 3.9,12C3.9,14.26 5.74,16.1 8,16.1H11V18H8A6,6 0 0,1 2,12C2,10.16 2.83,8.5 4.14,7.41L2,5.27M16,6A6,6 0 0,1 22,12C22,14.21 20.8,16.15 19,17.19L17.6,15.77C19.07,15.15 20.1,13.7 20.1,12C20.1,9.73 18.26,7.9 16,7.9H13V6H16M8,6H11V7.9H9.72L7.82,6H8M16,11V13H14.82L12.82,11H16Z" /></g><g id="link-variant"><path d="M10.59,13.41C11,13.8 11,14.44 10.59,14.83C10.2,15.22 9.56,15.22 9.17,14.83C7.22,12.88 7.22,9.71 9.17,7.76V7.76L12.71,4.22C14.66,2.27 17.83,2.27 19.78,4.22C21.73,6.17 21.73,9.34 19.78,11.29L18.29,12.78C18.3,11.96 18.17,11.14 17.89,10.36L18.36,9.88C19.54,8.71 19.54,6.81 18.36,5.64C17.19,4.46 15.29,4.46 14.12,5.64L10.59,9.17C9.41,10.34 9.41,12.24 10.59,13.41M13.41,9.17C13.8,8.78 14.44,8.78 14.83,9.17C16.78,11.12 16.78,14.29 14.83,16.24V16.24L11.29,19.78C9.34,21.73 6.17,21.73 4.22,19.78C2.27,17.83 2.27,14.66 4.22,12.71L5.71,11.22C5.7,12.04 5.83,12.86 6.11,13.65L5.64,14.12C4.46,15.29 4.46,17.19 5.64,18.36C6.81,19.54 8.71,19.54 9.88,18.36L13.41,14.83C14.59,13.66 14.59,11.76 13.41,10.59C13,10.2 13,9.56 13.41,9.17Z" /></g><g id="link-variant-off"><path d="M2,5.27L3.28,4L20,20.72L18.73,22L13.9,17.17L11.29,19.78C9.34,21.73 6.17,21.73 4.22,19.78C2.27,17.83 2.27,14.66 4.22,12.71L5.71,11.22C5.7,12.04 5.83,12.86 6.11,13.65L5.64,14.12C4.46,15.29 4.46,17.19 5.64,18.36C6.81,19.54 8.71,19.54 9.88,18.36L12.5,15.76L10.88,14.15C10.87,14.39 10.77,14.64 10.59,14.83C10.2,15.22 9.56,15.22 9.17,14.83C8.12,13.77 7.63,12.37 7.72,11L2,5.27M12.71,4.22C14.66,2.27 17.83,2.27 19.78,4.22C21.73,6.17 21.73,9.34 19.78,11.29L18.29,12.78C18.3,11.96 18.17,11.14 17.89,10.36L18.36,9.88C19.54,8.71 19.54,6.81 18.36,5.64C17.19,4.46 15.29,4.46 14.12,5.64L10.79,8.97L9.38,7.55L12.71,4.22M13.41,9.17C13.8,8.78 14.44,8.78 14.83,9.17C16.2,10.54 16.61,12.5 16.06,14.23L14.28,12.46C14.23,11.78 13.94,11.11 13.41,10.59C13,10.2 13,9.56 13.41,9.17Z" /></g><g id="linkedin"><path d="M21,21H17V14.25C17,13.19 15.81,12.31 14.75,12.31C13.69,12.31 13,13.19 13,14.25V21H9V9H13V11C13.66,9.93 15.36,9.24 16.5,9.24C19,9.24 21,11.28 21,13.75V21M7,21H3V9H7V21M5,3A2,2 0 0,1 7,5A2,2 0 0,1 5,7A2,2 0 0,1 3,5A2,2 0 0,1 5,3Z" /></g><g id="linkedin-box"><path d="M19,19H16V13.7A1.5,1.5 0 0,0 14.5,12.2A1.5,1.5 0 0,0 13,13.7V19H10V10H13V11.2C13.5,10.36 14.59,9.8 15.5,9.8A3.5,3.5 0 0,1 19,13.3M6.5,8.31C5.5,8.31 4.69,7.5 4.69,6.5A1.81,1.81 0 0,1 6.5,4.69C7.5,4.69 8.31,5.5 8.31,6.5A1.81,1.81 0 0,1 6.5,8.31M8,19H5V10H8M20,2H4C2.89,2 2,2.89 2,4V20A2,2 0 0,0 4,22H20A2,2 0 0,0 22,20V4C22,2.89 21.1,2 20,2Z" /></g><g id="linux"><path d="M13.18,14.5C12.53,15.26 11.47,15.26 10.82,14.5L7.44,10.5C7.16,11.28 7,12.12 7,13C7,14.67 7.57,16.18 8.5,17.27C10,17.37 11.29,17.96 11.78,19C11.85,19 11.93,19 12.22,19C12.71,18 13.95,17.44 15.46,17.33C16.41,16.24 17,14.7 17,13C17,12.12 16.84,11.28 16.56,10.5L13.18,14.5M20,20.75C20,21.3 19.3,22 18.75,22H13.25C12.7,22 12,21.3 12,20.75C12,21.3 11.3,22 10.75,22H5.25C4.7,22 4,21.3 4,20.75C4,19.45 4.94,18.31 6.3,17.65C5.5,16.34 5,14.73 5,13C4,15 2.7,15.56 2.09,15C1.5,14.44 1.79,12.83 3.1,11.41C3.84,10.6 5,9.62 5.81,9.25C6.13,8.56 6.54,7.93 7,7.38V7A5,5 0 0,1 12,2A5,5 0 0,1 17,7V7.38C17.46,7.93 17.87,8.56 18.19,9.25C19,9.62 20.16,10.6 20.9,11.41C22.21,12.83 22.5,14.44 21.91,15C21.3,15.56 20,15 19,13C19,14.75 18.5,16.37 17.67,17.69C19.05,18.33 20,19.44 20,20.75M9.88,9C9.46,9.5 9.46,10.27 9.88,10.75L11.13,12.25C11.54,12.73 12.21,12.73 12.63,12.25L13.88,10.75C14.29,10.27 14.29,9.5 13.88,9H9.88M10,5.25C9.45,5.25 9,5.9 9,7C9,8.1 9.45,8.75 10,8.75C10.55,8.75 11,8.1 11,7C11,5.9 10.55,5.25 10,5.25M14,5.25C13.45,5.25 13,5.9 13,7C13,8.1 13.45,8.75 14,8.75C14.55,8.75 15,8.1 15,7C15,5.9 14.55,5.25 14,5.25Z" /></g><g id="lock"><path d="M12,17A2,2 0 0,0 14,15C14,13.89 13.1,13 12,13A2,2 0 0,0 10,15A2,2 0 0,0 12,17M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V10C4,8.89 4.9,8 6,8H7V6A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,3A3,3 0 0,0 9,6V8H15V6A3,3 0 0,0 12,3Z" /></g><g id="lock-open"><path d="M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V10A2,2 0 0,1 6,8H15V6A3,3 0 0,0 12,3A3,3 0 0,0 9,6H7A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,17A2,2 0 0,0 14,15A2,2 0 0,0 12,13A2,2 0 0,0 10,15A2,2 0 0,0 12,17Z" /></g><g id="lock-open-outline"><path d="M18,20V10H6V20H18M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V10A2,2 0 0,1 6,8H15V6A3,3 0 0,0 12,3A3,3 0 0,0 9,6H7A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,17A2,2 0 0,1 10,15A2,2 0 0,1 12,13A2,2 0 0,1 14,15A2,2 0 0,1 12,17Z" /></g><g id="lock-outline"><path d="M12,17C10.89,17 10,16.1 10,15C10,13.89 10.89,13 12,13A2,2 0 0,1 14,15A2,2 0 0,1 12,17M18,20V10H6V20H18M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V10C4,8.89 4.89,8 6,8H7V6A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,3A3,3 0 0,0 9,6V8H15V6A3,3 0 0,0 12,3Z" /></g><g id="lock-plus"><path d="M18,8H17V6A5,5 0 0,0 12,1A5,5 0 0,0 7,6V8H6A2,2 0 0,0 4,10V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V10A2,2 0 0,0 18,8M8.9,6C8.9,4.29 10.29,2.9 12,2.9C13.71,2.9 15.1,4.29 15.1,6V8H8.9V6M16,16H13V19H11V16H8V14H11V11H13V14H16V16Z" /></g><g id="login"><path d="M10,17.25V14H3V10H10V6.75L15.25,12L10,17.25M8,2H17A2,2 0 0,1 19,4V20A2,2 0 0,1 17,22H8A2,2 0 0,1 6,20V16H8V20H17V4H8V8H6V4A2,2 0 0,1 8,2Z" /></g><g id="login-variant"><path d="M19,3H5C3.89,3 3,3.89 3,5V9H5V5H19V19H5V15H3V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3M10.08,15.58L11.5,17L16.5,12L11.5,7L10.08,8.41L12.67,11H3V13H12.67L10.08,15.58Z" /></g><g id="logout"><path d="M17,17.25V14H10V10H17V6.75L22.25,12L17,17.25M13,2A2,2 0 0,1 15,4V8H13V4H4V20H13V16H15V20A2,2 0 0,1 13,22H4A2,2 0 0,1 2,20V4A2,2 0 0,1 4,2H13Z" /></g><g id="logout-variant"><path d="M14.08,15.59L16.67,13H7V11H16.67L14.08,8.41L15.5,7L20.5,12L15.5,17L14.08,15.59M19,3A2,2 0 0,1 21,5V9.67L19,7.67V5H5V19H19V16.33L21,14.33V19A2,2 0 0,1 19,21H5C3.89,21 3,20.1 3,19V5C3,3.89 3.89,3 5,3H19Z" /></g><g id="looks"><path d="M12,6A11,11 0 0,0 1,17H3C3,12.04 7.04,8 12,8C16.96,8 21,12.04 21,17H23A11,11 0 0,0 12,6M12,10C8.14,10 5,13.14 5,17H7A5,5 0 0,1 12,12A5,5 0 0,1 17,17H19C19,13.14 15.86,10 12,10Z" /></g><g id="loupe"><path d="M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22H20A2,2 0 0,0 22,20V12A10,10 0 0,0 12,2M13,7H11V11H7V13H11V17H13V13H17V11H13V7Z" /></g><g id="lumx"><path d="M12.35,1.75L20.13,9.53L13.77,15.89L12.35,14.47L17.3,9.53L10.94,3.16L12.35,1.75M15.89,9.53L14.47,10.94L10.23,6.7L5.28,11.65L3.87,10.23L10.23,3.87L15.89,9.53M10.23,8.11L11.65,9.53L6.7,14.47L13.06,20.84L11.65,22.25L3.87,14.47L10.23,8.11M8.11,14.47L9.53,13.06L13.77,17.3L18.72,12.35L20.13,13.77L13.77,20.13L8.11,14.47Z" /></g><g id="magnet"><path d="M3,7V13A9,9 0 0,0 12,22A9,9 0 0,0 21,13V7H17V13A5,5 0 0,1 12,18A5,5 0 0,1 7,13V7M17,5H21V2H17M3,5H7V2H3" /></g><g id="magnet-on"><path d="M3,7V13A9,9 0 0,0 12,22A9,9 0 0,0 21,13V7H17V13A5,5 0 0,1 12,18A5,5 0 0,1 7,13V7M17,5H21V2H17M3,5H7V2H3M13,1.5L9,9H11V14.5L15,7H13V1.5Z" /></g><g id="magnify"><path d="M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z" /></g><g id="magnify-minus"><path d="M9,2A7,7 0 0,1 16,9C16,10.57 15.5,12 14.61,13.19L15.41,14H16L22,20L20,22L14,16V15.41L13.19,14.61C12,15.5 10.57,16 9,16A7,7 0 0,1 2,9A7,7 0 0,1 9,2M5,8V10H13V8H5Z" /></g><g id="magnify-plus"><path d="M9,2A7,7 0 0,1 16,9C16,10.57 15.5,12 14.61,13.19L15.41,14H16L22,20L20,22L14,16V15.41L13.19,14.61C12,15.5 10.57,16 9,16A7,7 0 0,1 2,9A7,7 0 0,1 9,2M8,5V8H5V10H8V13H10V10H13V8H10V5H8Z" /></g><g id="mail-ru"><path d="M15.45,11.91C15.34,9.7 13.7,8.37 11.72,8.37H11.64C9.35,8.37 8.09,10.17 8.09,12.21C8.09,14.5 9.62,15.95 11.63,15.95C13.88,15.95 15.35,14.3 15.46,12.36M11.65,6.39C13.18,6.39 14.62,7.07 15.67,8.13V8.13C15.67,7.62 16,7.24 16.5,7.24H16.61C17.35,7.24 17.5,7.94 17.5,8.16V16.06C17.46,16.58 18.04,16.84 18.37,16.5C19.64,15.21 21.15,9.81 17.58,6.69C14.25,3.77 9.78,4.25 7.4,5.89C4.88,7.63 3.26,11.5 4.83,15.11C6.54,19.06 11.44,20.24 14.35,19.06C15.83,18.47 16.5,20.46 15,21.11C12.66,22.1 6.23,22 3.22,16.79C1.19,13.27 1.29,7.08 6.68,3.87C10.81,1.42 16.24,2.1 19.5,5.5C22.95,9.1 22.75,15.8 19.4,18.41C17.89,19.59 15.64,18.44 15.66,16.71L15.64,16.15C14.59,17.2 13.18,17.81 11.65,17.81C8.63,17.81 6,15.15 6,12.13C6,9.08 8.63,6.39 11.65,6.39Z" /></g><g id="map"><path d="M15,19L9,16.89V5L15,7.11M20.5,3C20.44,3 20.39,3 20.34,3L15,5.1L9,3L3.36,4.9C3.15,4.97 3,5.15 3,5.38V20.5A0.5,0.5 0 0,0 3.5,21C3.55,21 3.61,21 3.66,20.97L9,18.9L15,21L20.64,19.1C20.85,19 21,18.85 21,18.62V3.5A0.5,0.5 0 0,0 20.5,3Z" /></g><g id="map-marker"><path d="M12,11.5A2.5,2.5 0 0,1 9.5,9A2.5,2.5 0 0,1 12,6.5A2.5,2.5 0 0,1 14.5,9A2.5,2.5 0 0,1 12,11.5M12,2A7,7 0 0,0 5,9C5,14.25 12,22 12,22C12,22 19,14.25 19,9A7,7 0 0,0 12,2Z" /></g><g id="map-marker-circle"><path d="M12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,12.5A1.5,1.5 0 0,1 10.5,11A1.5,1.5 0 0,1 12,9.5A1.5,1.5 0 0,1 13.5,11A1.5,1.5 0 0,1 12,12.5M12,7.2C9.9,7.2 8.2,8.9 8.2,11C8.2,14 12,17.5 12,17.5C12,17.5 15.8,14 15.8,11C15.8,8.9 14.1,7.2 12,7.2Z" /></g><g id="map-marker-minus"><path d="M9,11.5A2.5,2.5 0 0,0 11.5,9A2.5,2.5 0 0,0 9,6.5A2.5,2.5 0 0,0 6.5,9A2.5,2.5 0 0,0 9,11.5M9,2C12.86,2 16,5.13 16,9C16,14.25 9,22 9,22C9,22 2,14.25 2,9A7,7 0 0,1 9,2M15,17H23V19H15V17Z" /></g><g id="map-marker-multiple"><path d="M14,11.5A2.5,2.5 0 0,0 16.5,9A2.5,2.5 0 0,0 14,6.5A2.5,2.5 0 0,0 11.5,9A2.5,2.5 0 0,0 14,11.5M14,2C17.86,2 21,5.13 21,9C21,14.25 14,22 14,22C14,22 7,14.25 7,9A7,7 0 0,1 14,2M5,9C5,13.5 10.08,19.66 11,20.81L10,22C10,22 3,14.25 3,9C3,5.83 5.11,3.15 8,2.29C6.16,3.94 5,6.33 5,9Z" /></g><g id="map-marker-off"><path d="M16.37,16.1L11.75,11.47L11.64,11.36L3.27,3L2,4.27L5.18,7.45C5.06,7.95 5,8.46 5,9C5,14.25 12,22 12,22C12,22 13.67,20.15 15.37,17.65L18.73,21L20,19.72M12,6.5A2.5,2.5 0 0,1 14.5,9C14.5,9.73 14.17,10.39 13.67,10.85L17.3,14.5C18.28,12.62 19,10.68 19,9A7,7 0 0,0 12,2C10,2 8.24,2.82 6.96,4.14L10.15,7.33C10.61,6.82 11.26,6.5 12,6.5Z" /></g><g id="map-marker-plus"><path d="M9,11.5A2.5,2.5 0 0,0 11.5,9A2.5,2.5 0 0,0 9,6.5A2.5,2.5 0 0,0 6.5,9A2.5,2.5 0 0,0 9,11.5M9,2C12.86,2 16,5.13 16,9C16,14.25 9,22 9,22C9,22 2,14.25 2,9A7,7 0 0,1 9,2M15,17H18V14H20V17H23V19H20V22H18V19H15V17Z" /></g><g id="map-marker-radius"><path d="M12,2C15.31,2 18,4.66 18,7.95C18,12.41 12,19 12,19C12,19 6,12.41 6,7.95C6,4.66 8.69,2 12,2M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M20,19C20,21.21 16.42,23 12,23C7.58,23 4,21.21 4,19C4,17.71 5.22,16.56 7.11,15.83L7.75,16.74C6.67,17.19 6,17.81 6,18.5C6,19.88 8.69,21 12,21C15.31,21 18,19.88 18,18.5C18,17.81 17.33,17.19 16.25,16.74L16.89,15.83C18.78,16.56 20,17.71 20,19Z" /></g><g id="margin"><path d="M14.63,6.78L12.9,5.78L18.5,2.08L18.1,8.78L16.37,7.78L8.73,21H6.42L14.63,6.78M17.5,12C19.43,12 21,13.74 21,16.5C21,19.26 19.43,21 17.5,21C15.57,21 14,19.26 14,16.5C14,13.74 15.57,12 17.5,12M17.5,14C16.67,14 16,14.84 16,16.5C16,18.16 16.67,19 17.5,19C18.33,19 19,18.16 19,16.5C19,14.84 18.33,14 17.5,14M7.5,5C9.43,5 11,6.74 11,9.5C11,12.26 9.43,14 7.5,14C5.57,14 4,12.26 4,9.5C4,6.74 5.57,5 7.5,5M7.5,7C6.67,7 6,7.84 6,9.5C6,11.16 6.67,12 7.5,12C8.33,12 9,11.16 9,9.5C9,7.84 8.33,7 7.5,7Z" /></g><g id="markdown"><path d="M2,16V8H4L7,11L10,8H12V16H10V10.83L7,13.83L4,10.83V16H2M16,8H19V12H21.5L17.5,16.5L13.5,12H16V8Z" /></g><g id="marker"><path d="M18.5,1.15C17.97,1.15 17.46,1.34 17.07,1.73L11.26,7.55L16.91,13.2L22.73,7.39C23.5,6.61 23.5,5.35 22.73,4.56L19.89,1.73C19.5,1.34 19,1.15 18.5,1.15M10.3,8.5L4.34,14.46C3.56,15.24 3.56,16.5 4.36,17.31C3.14,18.54 1.9,19.77 0.67,21H6.33L7.19,20.14C7.97,20.9 9.22,20.89 10,20.12L15.95,14.16" /></g><g id="marker-check"><path d="M10,16L5,11L6.41,9.58L10,13.17L17.59,5.58L19,7M19,1H5C3.89,1 3,1.89 3,3V15.93C3,16.62 3.35,17.23 3.88,17.59L12,23L20.11,17.59C20.64,17.23 21,16.62 21,15.93V3C21,1.89 20.1,1 19,1Z" /></g><g id="martini"><path d="M7.5,7L5.5,5H18.5L16.5,7M11,13V19H6V21H18V19H13V13L21,5V3H3V5L11,13Z" /></g><g id="material-ui"><path d="M8,16.61V15.37L14,11.91V7.23L9,10.12L4,7.23V13L3,13.58L2,13V5L3.07,4.38L9,7.81L12.93,5.54L14.93,4.38L16,5V13.06L10.92,16L14.97,18.33L20,15.43V11L21,10.42L22,11V16.58L14.97,20.64L8,16.61M22,9.75L21,10.33L20,9.75V8.58L21,8L22,8.58V9.75Z" /></g><g id="math-compass"><path d="M13,4.2V3C13,2.4 12.6,2 12,2V4.2C9.8,4.6 9,5.7 9,7C9,7.8 9.3,8.5 9.8,9L4,19.9V22L6.2,20L11.6,10C11.7,10 11.9,10 12,10C13.7,10 15,8.7 15,7C15,5.7 14.2,4.6 13,4.2M12.9,7.5C12.7,7.8 12.4,8 12,8C11.4,8 11,7.6 11,7C11,6.8 11.1,6.7 11.1,6.5C11.3,6.2 11.6,6 12,6C12.6,6 13,6.4 13,7C13,7.2 12.9,7.3 12.9,7.5M20,19.9V22H20L17.8,20L13.4,11.8C14.1,11.6 14.7,11.3 15.2,10.9L20,19.9Z" /></g><g id="matrix"><path d="M2,2H6V4H4V20H6V22H2V2M20,4H18V2H22V22H18V20H20V4M9,5H10V10H11V11H8V10H9V6L8,6.5V5.5L9,5M15,13H16V18H17V19H14V18H15V14L14,14.5V13.5L15,13M9,13C10.1,13 11,14.34 11,16C11,17.66 10.1,19 9,19C7.9,19 7,17.66 7,16C7,14.34 7.9,13 9,13M9,14C8.45,14 8,14.9 8,16C8,17.1 8.45,18 9,18C9.55,18 10,17.1 10,16C10,14.9 9.55,14 9,14M15,5C16.1,5 17,6.34 17,8C17,9.66 16.1,11 15,11C13.9,11 13,9.66 13,8C13,6.34 13.9,5 15,5M15,6C14.45,6 14,6.9 14,8C14,9.1 14.45,10 15,10C15.55,10 16,9.1 16,8C16,6.9 15.55,6 15,6Z" /></g><g id="maxcdn"><path d="M20.6,6.69C19.73,5.61 18.38,5 16.9,5H2.95L4.62,8.57L2.39,19H6.05L8.28,8.57H11.4L9.17,19H12.83L15.06,8.57H16.9C17.3,8.57 17.63,8.7 17.82,8.94C18,9.17 18.07,9.5 18,9.9L16.04,19H19.69L21.5,10.65C21.78,9.21 21.46,7.76 20.6,6.69Z" /></g><g id="medium"><path d="M21.93,6.62L15.89,16.47L11.57,9.43L15,3.84C15.17,3.58 15.5,3.47 15.78,3.55L21.93,6.62M22,19.78C22,20.35 21.5,20.57 20.89,20.26L16.18,17.91L22,8.41V19.78M9,19.94C9,20.5 8.57,20.76 8.07,20.5L2.55,17.76C2.25,17.6 2,17.2 2,16.86V4.14C2,3.69 2.33,3.5 2.74,3.68L8.7,6.66L9,7.12V19.94M15.29,17.46L10,14.81V8.81L15.29,17.46Z" /></g><g id="memory"><path d="M17,17H7V7H17M21,11V9H19V7C19,5.89 18.1,5 17,5H15V3H13V5H11V3H9V5H7C5.89,5 5,5.89 5,7V9H3V11H5V13H3V15H5V17A2,2 0 0,0 7,19H9V21H11V19H13V21H15V19H17A2,2 0 0,0 19,17V15H21V13H19V11M13,13H11V11H13M15,9H9V15H15V9Z" /></g><g id="menu"><path d="M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z" /></g><g id="menu-down"><path d="M7,10L12,15L17,10H7Z" /></g><g id="menu-down-outline"><path d="M18,9V10.5L12,16.5L6,10.5V9H18M12,13.67L14.67,11H9.33L12,13.67Z" /></g><g id="menu-left"><path d="M14,7L9,12L14,17V7Z" /></g><g id="menu-right"><path d="M10,17L15,12L10,7V17Z" /></g><g id="menu-up"><path d="M7,15L12,10L17,15H7Z" /></g><g id="menu-up-outline"><path d="M18,16V14.5L12,8.5L6,14.5V16H18M12,11.33L14.67,14H9.33L12,11.33Z" /></g><g id="message"><path d="M20,2H4A2,2 0 0,0 2,4V22L6,18H20A2,2 0 0,0 22,16V4C22,2.89 21.1,2 20,2Z" /></g><g id="message-alert"><path d="M13,10H11V6H13M13,14H11V12H13M20,2H4A2,2 0 0,0 2,4V22L6,18H20A2,2 0 0,0 22,16V4C22,2.89 21.1,2 20,2Z" /></g><g id="message-bulleted"><path d="M20,2H4A2,2 0 0,0 2,4V22L6,18H20A2,2 0 0,0 22,16V4A2,2 0 0,0 20,2M8,14H6V12H8V14M8,11H6V9H8V11M8,8H6V6H8V8M15,14H10V12H15V14M18,11H10V9H18V11M18,8H10V6H18V8Z" /></g><g id="message-bulleted-off"><path d="M1.27,1.73L0,3L2,5V22L6,18H15L20.73,23.73L22,22.46L1.27,1.73M8,14H6V12H8V14M6,11V9L8,11H6M20,2H4.08L10,7.92V6H18V8H10.08L11.08,9H18V11H13.08L20.07,18C21.14,17.95 22,17.08 22,16V4A2,2 0 0,0 20,2Z" /></g><g id="message-draw"><path d="M18,14H10.5L12.5,12H18M6,14V11.5L12.88,4.64C13.07,4.45 13.39,4.45 13.59,4.64L15.35,6.41C15.55,6.61 15.55,6.92 15.35,7.12L8.47,14M20,2H4A2,2 0 0,0 2,4V22L6,18H20A2,2 0 0,0 22,16V4C22,2.89 21.1,2 20,2Z" /></g><g id="message-image"><path d="M5,14L8.5,9.5L11,12.5L14.5,8L19,14M20,2H4A2,2 0 0,0 2,4V22L6,18H20A2,2 0 0,0 22,16V4C22,2.89 21.1,2 20,2Z" /></g><g id="message-outline"><path d="M20,2H4A2,2 0 0,0 2,4V22L6,18H20A2,2 0 0,0 22,16V4A2,2 0 0,0 20,2M20,16H6L4,18V4H20" /></g><g id="message-plus"><path d="M20,2A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H6L2,22V4C2,2.89 2.9,2 4,2H20M11,6V9H8V11H11V14H13V11H16V9H13V6H11Z" /></g><g id="message-processing"><path d="M17,11H15V9H17M13,11H11V9H13M9,11H7V9H9M20,2H4A2,2 0 0,0 2,4V22L6,18H20A2,2 0 0,0 22,16V4C22,2.89 21.1,2 20,2Z" /></g><g id="message-reply"><path d="M22,4C22,2.89 21.1,2 20,2H4A2,2 0 0,0 2,4V16A2,2 0 0,0 4,18H18L22,22V4Z" /></g><g id="message-reply-text"><path d="M18,8H6V6H18V8M18,11H6V9H18V11M18,14H6V12H18V14M22,4A2,2 0 0,0 20,2H4A2,2 0 0,0 2,4V16A2,2 0 0,0 4,18H18L22,22V4Z" /></g><g id="message-text"><path d="M20,2H4A2,2 0 0,0 2,4V22L6,18H20A2,2 0 0,0 22,16V4A2,2 0 0,0 20,2M6,9H18V11H6M14,14H6V12H14M18,8H6V6H18" /></g><g id="message-text-outline"><path d="M20,2A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H6L2,22V4C2,2.89 2.9,2 4,2H20M4,4V17.17L5.17,16H20V4H4M6,7H18V9H6V7M6,11H15V13H6V11Z" /></g><g id="message-video"><path d="M18,14L14,10.8V14H6V6H14V9.2L18,6M20,2H4A2,2 0 0,0 2,4V22L6,18H20A2,2 0 0,0 22,16V4C22,2.89 21.1,2 20,2Z" /></g><g id="meteor"><path d="M2.8,3L19.67,18.82C19.67,18.82 20,19.27 19.58,19.71C19.17,20.15 18.63,19.77 18.63,19.77L2.8,3M7.81,4.59L20.91,16.64C20.91,16.64 21.23,17.08 20.82,17.5C20.4,17.97 19.86,17.59 19.86,17.59L7.81,4.59M4.29,8L17.39,20.03C17.39,20.03 17.71,20.47 17.3,20.91C16.88,21.36 16.34,21 16.34,21L4.29,8M12.05,5.96L21.2,14.37C21.2,14.37 21.42,14.68 21.13,15C20.85,15.3 20.47,15.03 20.47,15.03L12.05,5.96M5.45,11.91L14.6,20.33C14.6,20.33 14.82,20.64 14.54,20.95C14.25,21.26 13.87,21 13.87,21L5.45,11.91M16.38,7.92L20.55,11.74C20.55,11.74 20.66,11.88 20.5,12.03C20.38,12.17 20.19,12.05 20.19,12.05L16.38,7.92M7.56,16.1L11.74,19.91C11.74,19.91 11.85,20.06 11.7,20.2C11.56,20.35 11.37,20.22 11.37,20.22L7.56,16.1Z" /></g><g id="microphone"><path d="M12,2A3,3 0 0,1 15,5V11A3,3 0 0,1 12,14A3,3 0 0,1 9,11V5A3,3 0 0,1 12,2M19,11C19,14.53 16.39,17.44 13,17.93V21H11V17.93C7.61,17.44 5,14.53 5,11H7A5,5 0 0,0 12,16A5,5 0 0,0 17,11H19Z" /></g><g id="microphone-off"><path d="M19,11C19,12.19 18.66,13.3 18.1,14.28L16.87,13.05C17.14,12.43 17.3,11.74 17.3,11H19M15,11.16L9,5.18V5A3,3 0 0,1 12,2A3,3 0 0,1 15,5V11L15,11.16M4.27,3L21,19.73L19.73,21L15.54,16.81C14.77,17.27 13.91,17.58 13,17.72V21H11V17.72C7.72,17.23 5,14.41 5,11H6.7C6.7,14 9.24,16.1 12,16.1C12.81,16.1 13.6,15.91 14.31,15.58L12.65,13.92L12,14A3,3 0 0,1 9,11V10.28L3,4.27L4.27,3Z" /></g><g id="microphone-outline"><path d="M17.3,11C17.3,14 14.76,16.1 12,16.1C9.24,16.1 6.7,14 6.7,11H5C5,14.41 7.72,17.23 11,17.72V21H13V17.72C16.28,17.23 19,14.41 19,11M10.8,4.9C10.8,4.24 11.34,3.7 12,3.7C12.66,3.7 13.2,4.24 13.2,4.9L13.19,11.1C13.19,11.76 12.66,12.3 12,12.3C11.34,12.3 10.8,11.76 10.8,11.1M12,14A3,3 0 0,0 15,11V5A3,3 0 0,0 12,2A3,3 0 0,0 9,5V11A3,3 0 0,0 12,14Z" /></g><g id="microphone-settings"><path d="M19,10H17.3C17.3,13 14.76,15.1 12,15.1C9.24,15.1 6.7,13 6.7,10H5C5,13.41 7.72,16.23 11,16.72V20H13V16.72C16.28,16.23 19,13.41 19,10M15,24H17V22H15M11,24H13V22H11M12,13A3,3 0 0,0 15,10V4A3,3 0 0,0 12,1A3,3 0 0,0 9,4V10A3,3 0 0,0 12,13M7,24H9V22H7V24Z" /></g><g id="microphone-variant"><path d="M9,3A4,4 0 0,1 13,7H5A4,4 0 0,1 9,3M11.84,9.82L11,18H10V19A2,2 0 0,0 12,21A2,2 0 0,0 14,19V14A4,4 0 0,1 18,10H20L19,11L20,12H18A2,2 0 0,0 16,14V19A4,4 0 0,1 12,23A4,4 0 0,1 8,19V18H7L6.16,9.82C5.67,9.32 5.31,8.7 5.13,8H12.87C12.69,8.7 12.33,9.32 11.84,9.82M9,11A1,1 0 0,0 8,12A1,1 0 0,0 9,13A1,1 0 0,0 10,12A1,1 0 0,0 9,11Z" /></g><g id="microphone-variant-off"><path d="M2,5.27L3.28,4L20,20.72L18.73,22L16,19.26C15.86,21.35 14.12,23 12,23A4,4 0 0,1 8,19V18H7L6.16,9.82C5.82,9.47 5.53,9.06 5.33,8.6L2,5.27M9,3A4,4 0 0,1 13,7H8.82L6.08,4.26C6.81,3.5 7.85,3 9,3M11.84,9.82L11.82,10L9.82,8H12.87C12.69,8.7 12.33,9.32 11.84,9.82M11,18H10V19A2,2 0 0,0 12,21A2,2 0 0,0 14,19V17.27L11.35,14.62L11,18M18,10H20L19,11L20,12H18A2,2 0 0,0 16,14V14.18L14.3,12.5C14.9,11 16.33,10 18,10M8,12A1,1 0 0,0 9,13C9.21,13 9.4,12.94 9.56,12.83L8.17,11.44C8.06,11.6 8,11.79 8,12Z" /></g><g id="microscope"><path d="M9.46,6.28L11.05,9C8.47,9.26 6.5,11.41 6.5,14A5,5 0 0,0 11.5,19C13.55,19 15.31,17.77 16.08,16H13.5V14H21.5V16H19.25C18.84,17.57 17.97,18.96 16.79,20H19.5V22H3.5V20H6.21C4.55,18.53 3.5,16.39 3.5,14C3.5,10.37 5.96,7.2 9.46,6.28M12.74,2.07L13.5,3.37L14.36,2.87L17.86,8.93L14.39,10.93L10.89,4.87L11.76,4.37L11,3.07L12.74,2.07Z" /></g><g id="microsoft"><path d="M2,3H11V12H2V3M11,22H2V13H11V22M21,3V12H12V3H21M21,22H12V13H21V22Z" /></g><g id="minecraft"><path d="M4,2H20A2,2 0 0,1 22,4V20A2,2 0 0,1 20,22H4A2,2 0 0,1 2,20V4A2,2 0 0,1 4,2M6,6V10H10V12H8V18H10V16H14V18H16V12H14V10H18V6H14V10H10V6H6Z" /></g><g id="minus"><path d="M19,13H5V11H19V13Z" /></g><g id="minus-box"><path d="M17,13H7V11H17M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3Z" /></g><g id="minus-circle"><path d="M17,13H7V11H17M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" /></g><g id="minus-circle-outline"><path d="M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M7,13H17V11H7" /></g><g id="minus-network"><path d="M16,11V9H8V11H16M17,3A2,2 0 0,1 19,5V15A2,2 0 0,1 17,17H13V19H14A1,1 0 0,1 15,20H22V22H15A1,1 0 0,1 14,23H10A1,1 0 0,1 9,22H2V20H9A1,1 0 0,1 10,19H11V17H7C5.89,17 5,16.1 5,15V5A2,2 0 0,1 7,3H17Z" /></g><g id="mixcloud"><path d="M21.11,18.5C20.97,18.5 20.83,18.44 20.71,18.36C20.37,18.13 20.28,17.68 20.5,17.34C21.18,16.34 21.54,15.16 21.54,13.93C21.54,12.71 21.18,11.53 20.5,10.5C20.28,10.18 20.37,9.73 20.71,9.5C21.04,9.28 21.5,9.37 21.72,9.7C22.56,10.95 23,12.41 23,13.93C23,15.45 22.56,16.91 21.72,18.16C21.58,18.37 21.35,18.5 21.11,18.5M19,17.29C18.88,17.29 18.74,17.25 18.61,17.17C18.28,16.94 18.19,16.5 18.42,16.15C18.86,15.5 19.1,14.73 19.1,13.93C19.1,13.14 18.86,12.37 18.42,11.71C18.19,11.37 18.28,10.92 18.61,10.69C18.95,10.47 19.4,10.55 19.63,10.89C20.24,11.79 20.56,12.84 20.56,13.93C20.56,15 20.24,16.07 19.63,16.97C19.5,17.18 19.25,17.29 19,17.29M14.9,15.73C15.89,15.73 16.7,14.92 16.7,13.93C16.7,13.17 16.22,12.5 15.55,12.25C15.5,12.55 15.43,12.85 15.34,13.14C15.23,13.44 14.95,13.64 14.64,13.64C14.57,13.64 14.5,13.62 14.41,13.6C14.03,13.47 13.82,13.06 13.95,12.67C14.09,12.24 14.17,11.78 14.17,11.32C14.17,8.93 12.22,7 9.82,7C8.1,7 6.56,8 5.87,9.5C6.54,9.7 7.16,10.04 7.66,10.54C7.95,10.83 7.95,11.29 7.66,11.58C7.38,11.86 6.91,11.86 6.63,11.58C6.17,11.12 5.56,10.86 4.9,10.86C3.56,10.86 2.46,11.96 2.46,13.3C2.46,14.64 3.56,15.73 4.9,15.73H14.9M15.6,10.75C17.06,11.07 18.17,12.37 18.17,13.93C18.17,15.73 16.7,17.19 14.9,17.19H4.9C2.75,17.19 1,15.45 1,13.3C1,11.34 2.45,9.73 4.33,9.45C5.12,7.12 7.33,5.5 9.82,5.5C12.83,5.5 15.31,7.82 15.6,10.75Z" /></g><g id="monitor"><path d="M21,16H3V4H21M21,2H3C1.89,2 1,2.89 1,4V16A2,2 0 0,0 3,18H10V20H8V22H16V20H14V18H21A2,2 0 0,0 23,16V4C23,2.89 22.1,2 21,2Z" /></g><g id="monitor-multiple"><path d="M22,17V7H6V17H22M22,5A2,2 0 0,1 24,7V17C24,18.11 23.1,19 22,19H16V21H18V23H10V21H12V19H6C4.89,19 4,18.11 4,17V7A2,2 0 0,1 6,5H22M2,3V15H0V3A2,2 0 0,1 2,1H20V3H2Z" /></g><g id="more"><path d="M19,13.5A1.5,1.5 0 0,1 17.5,12A1.5,1.5 0 0,1 19,10.5A1.5,1.5 0 0,1 20.5,12A1.5,1.5 0 0,1 19,13.5M14,13.5A1.5,1.5 0 0,1 12.5,12A1.5,1.5 0 0,1 14,10.5A1.5,1.5 0 0,1 15.5,12A1.5,1.5 0 0,1 14,13.5M9,13.5A1.5,1.5 0 0,1 7.5,12A1.5,1.5 0 0,1 9,10.5A1.5,1.5 0 0,1 10.5,12A1.5,1.5 0 0,1 9,13.5M22,3H7C6.31,3 5.77,3.35 5.41,3.88L0,12L5.41,20.11C5.77,20.64 6.37,21 7.06,21H22A2,2 0 0,0 24,19V5C24,3.89 23.1,3 22,3Z" /></g><g id="motorbike"><path d="M16.36,4.27H18.55V2.13H16.36V1.07H18.22C17.89,0.43 17.13,0 16.36,0C15.16,0 14.18,0.96 14.18,2.13C14.18,3.31 15.16,4.27 16.36,4.27M10.04,9.39L13,6.93L17.45,9.6H10.25M19.53,12.05L21.05,10.56C21.93,9.71 21.93,8.43 21.05,7.57L19.2,9.39L13.96,4.27C13.64,3.73 13,3.41 12.33,3.41C11.78,3.41 11.35,3.63 11,3.95L7,7.89C6.65,8.21 6.44,8.64 6.44,9.17V9.71H5.13C4.04,9.71 3.16,10.67 3.16,11.84V12.27C3.5,12.16 3.93,12.16 4.25,12.16C7.09,12.16 9.5,14.4 9.5,17.28C9.5,17.6 9.5,18.03 9.38,18.35H14.5C14.4,18.03 14.4,17.6 14.4,17.28C14.4,14.29 16.69,12.05 19.53,12.05M4.36,19.73C2.84,19.73 1.64,18.56 1.64,17.07C1.64,15.57 2.84,14.4 4.36,14.4C5.89,14.4 7.09,15.57 7.09,17.07C7.09,18.56 5.89,19.73 4.36,19.73M4.36,12.8C1.96,12.8 0,14.72 0,17.07C0,19.41 1.96,21.33 4.36,21.33C6.76,21.33 8.73,19.41 8.73,17.07C8.73,14.72 6.76,12.8 4.36,12.8M19.64,19.73C18.11,19.73 16.91,18.56 16.91,17.07C16.91,15.57 18.11,14.4 19.64,14.4C21.16,14.4 22.36,15.57 22.36,17.07C22.36,18.56 21.16,19.73 19.64,19.73M19.64,12.8C17.24,12.8 15.27,14.72 15.27,17.07C15.27,19.41 17.24,21.33 19.64,21.33C22.04,21.33 24,19.41 24,17.07C24,14.72 22.04,12.8 19.64,12.8Z" /></g><g id="mouse"><path d="M11,1.07C7.05,1.56 4,4.92 4,9H11M4,15A8,8 0 0,0 12,23A8,8 0 0,0 20,15V11H4M13,1.07V9H20C20,4.92 16.94,1.56 13,1.07Z" /></g><g id="mouse-off"><path d="M2,5.27L3.28,4L20,20.72L18.73,22L17.5,20.79C16.08,22.16 14.14,23 12,23A8,8 0 0,1 4,15V11H7.73L5.73,9H4C4,8.46 4.05,7.93 4.15,7.42L2,5.27M11,1.07V9H10.82L5.79,3.96C7.05,2.4 8.9,1.33 11,1.07M20,11V15C20,15.95 19.83,16.86 19.53,17.71L12.82,11H20M13,1.07C16.94,1.56 20,4.92 20,9H13V1.07Z" /></g><g id="mouse-variant"><path d="M14,7H10V2.1C12.28,2.56 14,4.58 14,7M4,7C4,4.58 5.72,2.56 8,2.1V7H4M14,12C14,14.42 12.28,16.44 10,16.9V18A3,3 0 0,0 13,21A3,3 0 0,0 16,18V13A4,4 0 0,1 20,9H22L21,10L22,11H20A2,2 0 0,0 18,13H18V18A5,5 0 0,1 13,23A5,5 0 0,1 8,18V16.9C5.72,16.44 4,14.42 4,12V9H14V12Z" /></g><g id="mouse-variant-off"><path d="M2,5.27L3.28,4L20,20.72L18.73,22L17.29,20.56C16.42,22 14.82,23 13,23A5,5 0 0,1 8,18V16.9C5.72,16.44 4,14.42 4,12V9H5.73L2,5.27M14,7H10V2.1C12.28,2.56 14,4.58 14,7M8,2.1V6.18L5.38,3.55C6.07,2.83 7,2.31 8,2.1M14,12V12.17L10.82,9H14V12M10,16.9V18A3,3 0 0,0 13,21C14.28,21 15.37,20.2 15.8,19.07L12.4,15.67C11.74,16.28 10.92,16.71 10,16.9M16,13A4,4 0 0,1 20,9H22L21,10L22,11H20A2,2 0 0,0 18,13V16.18L16,14.18V13Z" /></g><g id="move-resize"><path d="M9,1V2H10V5H9V6H12V5H11V2H12V1M9,7C7.89,7 7,7.89 7,9V21C7,22.11 7.89,23 9,23H21C22.11,23 23,22.11 23,21V9C23,7.89 22.11,7 21,7M1,9V12H2V11H5V12H6V9H5V10H2V9M9,9H21V21H9M14,10V11H15V16H11V15H10V18H11V17H15V19H14V20H17V19H16V17H19V18H20V15H19V16H16V11H17V10" /></g><g id="move-resize-variant"><path d="M1.88,0.46L0.46,1.88L5.59,7H2V9H9V2H7V5.59M11,7V9H21V15H23V9A2,2 0 0,0 21,7M7,11V21A2,2 0 0,0 9,23H15V21H9V11M15.88,14.46L14.46,15.88L19.6,21H17V23H23V17H21V19.59" /></g><g id="movie"><path d="M18,4L20,8H17L15,4H13L15,8H12L10,4H8L10,8H7L5,4H4A2,2 0 0,0 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V4H18Z" /></g><g id="multiplication"><path d="M11,3H13V10.27L19.29,6.64L20.29,8.37L14,12L20.3,15.64L19.3,17.37L13,13.72V21H11V13.73L4.69,17.36L3.69,15.63L10,12L3.72,8.36L4.72,6.63L11,10.26V3Z" /></g><g id="multiplication-box"><path d="M19,3A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5C3.89,21 3,20.1 3,19V5C3,3.89 3.89,3 5,3H19M11,17H13V13.73L15.83,15.36L16.83,13.63L14,12L16.83,10.36L15.83,8.63L13,10.27V7H11V10.27L8.17,8.63L7.17,10.36L10,12L7.17,13.63L8.17,15.36L11,13.73V17Z" /></g><g id="music-box"><path d="M16,9H13V14.5A2.5,2.5 0 0,1 10.5,17A2.5,2.5 0 0,1 8,14.5A2.5,2.5 0 0,1 10.5,12C11.07,12 11.58,12.19 12,12.5V7H16M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3Z" /></g><g id="music-box-outline"><path d="M16,9H13V14.5A2.5,2.5 0 0,1 10.5,17A2.5,2.5 0 0,1 8,14.5A2.5,2.5 0 0,1 10.5,12C11.07,12 11.58,12.19 12,12.5V7H16V9M19,3A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3H19M5,5V19H19V5H5Z" /></g><g id="music-circle"><path d="M16,9V7H12V12.5C11.58,12.19 11.07,12 10.5,12A2.5,2.5 0 0,0 8,14.5A2.5,2.5 0 0,0 10.5,17A2.5,2.5 0 0,0 13,14.5V9H16M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z" /></g><g id="music-note"><path d="M12,3V12.26C11.5,12.09 11,12 10.5,12C8,12 6,14 6,16.5C6,19 8,21 10.5,21C13,21 15,19 15,16.5V6H19V3H12Z" /></g><g id="music-note-bluetooth"><path d="M10,3V12.26C9.5,12.09 9,12 8.5,12C6,12 4,14 4,16.5C4,19 6,21 8.5,21C11,21 13,19 13,16.5V6H17V3H10M20,7V10.79L17.71,8.5L17,9.21L19.79,12L17,14.79L17.71,15.5L20,13.21V17H20.5L23.35,14.15L21.21,12L23.36,9.85L20.5,7H20M21,8.91L21.94,9.85L21,10.79V8.91M21,13.21L21.94,14.15L21,15.09V13.21Z" /></g><g id="music-note-bluetooth-off"><path d="M10,3V8.68L13,11.68V6H17V3H10M3.28,4.5L2,5.77L8.26,12.03C5.89,12.15 4,14.1 4,16.5C4,19 6,21 8.5,21C10.9,21 12.85,19.11 12.97,16.74L17.68,21.45L18.96,20.18L13,14.22L10,11.22L3.28,4.5M20,7V10.79L17.71,8.5L17,9.21L19.79,12L17,14.79L17.71,15.5L20,13.21V17H20.5L23.35,14.15L21.21,12L23.36,9.85L20.5,7H20M21,8.91L21.94,9.85L21,10.79V8.91M21,13.21L21.94,14.15L21,15.09V13.21Z" /></g><g id="music-note-eighth"><path d="M12,3V12.26C11.5,12.09 11,12 10.5,12C8.54,12 6.9,13.26 6.28,15H3V18H6.28C6.9,19.74 8.54,21 10.5,21C12.46,21 14.1,19.74 14.72,18H19V15H15V6H19V3H12Z" /></g><g id="music-note-half"><path d="M12,3V12.26C11.5,12.09 11,12 10.5,12C8.54,12 6.9,13.26 6.28,15H3V18H6.28C6.9,19.74 8.54,21 10.5,21C12.46,21 14.1,19.74 14.72,18H19V15H15V9L15,6V3H12M10.5,14.5A2,2 0 0,1 12.5,16.5A2,2 0 0,1 10.5,18.5A2,2 0 0,1 8.5,16.5A2,2 0 0,1 10.5,14.5Z" /></g><g id="music-note-off"><path d="M12,3V8.68L15,11.68V6H19V3H12M5.28,4.5L4,5.77L10.26,12.03C7.89,12.15 6,14.1 6,16.5C6,19 8,21 10.5,21C12.9,21 14.85,19.11 14.97,16.74L19.68,21.45L20.96,20.18L15,14.22L12,11.22L5.28,4.5Z" /></g><g id="music-note-quarter"><path d="M12,3H15V15H19V18H14.72C14.1,19.74 12.46,21 10.5,21C8.54,21 6.9,19.74 6.28,18H3V15H6.28C6.9,13.26 8.54,12 10.5,12C11,12 11.5,12.09 12,12.26V3Z" /></g><g id="music-note-sixteenth"><path d="M12,3V12.26C11.5,12.09 11,12 10.5,12C8.54,12 6.9,13.26 6.28,15H3V18H6.28C6.9,19.74 8.54,21 10.5,21C12.46,21 14.1,19.74 14.72,18H19V15H15V10H19V7H15V6H19V3H12Z" /></g><g id="music-note-whole"><path d="M10.5,12C8.6,12 6.9,13.2 6.26,15H3V18H6.26C6.9,19.8 8.6,21 10.5,21C12.4,21 14.1,19.8 14.74,18H19V15H14.74C14.1,13.2 12.4,12 10.5,12M10.5,14.5A2,2 0 0,1 12.5,16.5A2,2 0 0,1 10.5,18.5A2,2 0 0,1 8.5,16.5A2,2 0 0,1 10.5,14.5Z" /></g><g id="nature"><path d="M13,16.12C16.47,15.71 19.17,12.76 19.17,9.17C19.17,5.3 16.04,2.17 12.17,2.17A7,7 0 0,0 5.17,9.17C5.17,12.64 7.69,15.5 11,16.06V20H5V22H19V20H13V16.12Z" /></g><g id="nature-people"><path d="M4.5,11A1.5,1.5 0 0,0 6,9.5A1.5,1.5 0 0,0 4.5,8A1.5,1.5 0 0,0 3,9.5A1.5,1.5 0 0,0 4.5,11M22.17,9.17C22.17,5.3 19.04,2.17 15.17,2.17A7,7 0 0,0 8.17,9.17C8.17,12.64 10.69,15.5 14,16.06V20H6V17H7V13A1,1 0 0,0 6,12H3A1,1 0 0,0 2,13V17H3V22H19V20H16V16.12C19.47,15.71 22.17,12.76 22.17,9.17Z" /></g><g id="navigation"><path d="M12,2L4.5,20.29L5.21,21L12,18L18.79,21L19.5,20.29L12,2Z" /></g><g id="near-me"><path d="M21,3L3,10.53V11.5L9.84,14.16L12.5,21H13.46L21,3Z" /></g><g id="needle"><path d="M11.15,15.18L9.73,13.77L11.15,12.35L12.56,13.77L13.97,12.35L12.56,10.94L13.97,9.53L15.39,10.94L16.8,9.53L13.97,6.7L6.9,13.77L9.73,16.6L11.15,15.18M3.08,19L6.2,15.89L4.08,13.77L13.97,3.87L16.1,6L17.5,4.58L16.1,3.16L17.5,1.75L21.75,6L20.34,7.4L18.92,6L17.5,7.4L19.63,9.53L9.73,19.42L7.61,17.3L3.08,21.84V19Z" /></g><g id="nest-protect"><path d="M12,18A6,6 0 0,0 18,12C18,8.68 15.31,6 12,6C8.68,6 6,8.68 6,12A6,6 0 0,0 12,18M19,3A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5C3.89,21 3,20.1 3,19V5C3,3.89 3.89,3 5,3H19M8,12A4,4 0 0,1 12,8A4,4 0 0,1 16,12A4,4 0 0,1 12,16A4,4 0 0,1 8,12Z" /></g><g id="nest-thermostat"><path d="M16.95,16.95L14.83,14.83C15.55,14.1 16,13.1 16,12C16,11.26 15.79,10.57 15.43,10L17.6,7.81C18.5,9 19,10.43 19,12C19,13.93 18.22,15.68 16.95,16.95M12,5C13.57,5 15,5.5 16.19,6.4L14,8.56C13.43,8.21 12.74,8 12,8A4,4 0 0,0 8,12C8,13.1 8.45,14.1 9.17,14.83L7.05,16.95C5.78,15.68 5,13.93 5,12A7,7 0 0,1 12,5M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z" /></g><g id="new-box"><path d="M20,4C21.11,4 22,4.89 22,6V18C22,19.11 21.11,20 20,20H4C2.89,20 2,19.11 2,18V6C2,4.89 2.89,4 4,4H20M8.5,15V9H7.25V12.5L4.75,9H3.5V15H4.75V11.5L7.3,15H8.5M13.5,10.26V9H9.5V15H13.5V13.75H11V12.64H13.5V11.38H11V10.26H13.5M20.5,14V9H19.25V13.5H18.13V10H16.88V13.5H15.75V9H14.5V14A1,1 0 0,0 15.5,15H19.5A1,1 0 0,0 20.5,14Z" /></g><g id="newspaper"><path d="M20,11H4V8H20M20,15H13V13H20M20,19H13V17H20M11,19H4V13H11M20.33,4.67L18.67,3L17,4.67L15.33,3L13.67,4.67L12,3L10.33,4.67L8.67,3L7,4.67L5.33,3L3.67,4.67L2,3V19A2,2 0 0,0 4,21H20A2,2 0 0,0 22,19V3L20.33,4.67Z" /></g><g id="nfc"><path d="M10.59,7.66C10.59,7.66 11.19,7.39 11.57,7.82C11.95,8.26 12.92,9.94 12.92,11.62C12.92,13.3 12.5,15.09 12.05,15.68C11.62,16.28 11.19,16.28 10.86,16.06C10.54,15.85 5.5,12 5.23,11.89C4.95,11.78 4.85,12.05 5.12,13.5C5.39,15 4.95,15.41 4.57,15.47C4.2,15.5 3.06,15.2 3,12.16C2.95,9.13 3.76,8.64 4.14,8.64C4.85,8.64 10.27,13.5 10.64,13.46C10.97,13.41 11.13,11.35 10.5,9.72C9.78,7.96 10.59,7.66 10.59,7.66M19.3,4.63C21.12,8.24 21,11.66 21,12C21,12.34 21.12,15.76 19.3,19.37C19.3,19.37 18.83,19.92 18.12,19.59C17.42,19.26 17.66,18.4 17.66,18.4C17.66,18.4 19.14,15.55 19.1,12.05V12C19.14,8.5 17.66,5.6 17.66,5.6C17.66,5.6 17.42,4.74 18.12,4.41C18.83,4.08 19.3,4.63 19.3,4.63M15.77,6.25C17.26,8.96 17.16,11.66 17.14,12C17.16,12.34 17.26,14.92 15.77,17.85C15.77,17.85 15.3,18.4 14.59,18.07C13.89,17.74 14.13,16.88 14.13,16.88C14.13,16.88 15.09,15.5 15.24,12.05V12C15.14,8.53 14.13,7.23 14.13,7.23C14.13,7.23 13.89,6.36 14.59,6.04C15.3,5.71 15.77,6.25 15.77,6.25Z" /></g><g id="nfc-tap"><path d="M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M4,4H11A2,2 0 0,1 13,6V9H11V6H4V11H6V9L9,12L6,15V13H4A2,2 0 0,1 2,11V6A2,2 0 0,1 4,4M20,20H13A2,2 0 0,1 11,18V15H13V18H20V13H18V15L15,12L18,9V11H20A2,2 0 0,1 22,13V18A2,2 0 0,1 20,20Z" /></g><g id="nfc-variant"><path d="M18,6H13A2,2 0 0,0 11,8V10.28C10.41,10.62 10,11.26 10,12A2,2 0 0,0 12,14C13.11,14 14,13.1 14,12C14,11.26 13.6,10.62 13,10.28V8H16V16H8V8H10V6H8L6,6V18H18M20,20H4V4H20M20,2H4A2,2 0 0,0 2,4V20A2,2 0 0,0 4,22H20C21.11,22 22,21.1 22,20V4C22,2.89 21.11,2 20,2Z" /></g><g id="nodejs"><path d="M12,1.85C11.73,1.85 11.45,1.92 11.22,2.05L3.78,6.35C3.3,6.63 3,7.15 3,7.71V16.29C3,16.85 3.3,17.37 3.78,17.65L5.73,18.77C6.68,19.23 7,19.24 7.44,19.24C8.84,19.24 9.65,18.39 9.65,16.91V8.44C9.65,8.32 9.55,8.22 9.43,8.22H8.5C8.37,8.22 8.27,8.32 8.27,8.44V16.91C8.27,17.57 7.59,18.22 6.5,17.67L4.45,16.5C4.38,16.45 4.34,16.37 4.34,16.29V7.71C4.34,7.62 4.38,7.54 4.45,7.5L11.89,3.21C11.95,3.17 12.05,3.17 12.11,3.21L19.55,7.5C19.62,7.54 19.66,7.62 19.66,7.71V16.29C19.66,16.37 19.62,16.45 19.55,16.5L12.11,20.79C12.05,20.83 11.95,20.83 11.88,20.79L10,19.65C9.92,19.62 9.84,19.61 9.79,19.64C9.26,19.94 9.16,20 8.67,20.15C8.55,20.19 8.36,20.26 8.74,20.47L11.22,21.94C11.46,22.08 11.72,22.15 12,22.15C12.28,22.15 12.54,22.08 12.78,21.94L20.22,17.65C20.7,17.37 21,16.85 21,16.29V7.71C21,7.15 20.7,6.63 20.22,6.35L12.78,2.05C12.55,1.92 12.28,1.85 12,1.85M14,8C11.88,8 10.61,8.89 10.61,10.39C10.61,12 11.87,12.47 13.91,12.67C16.34,12.91 16.53,13.27 16.53,13.75C16.53,14.58 15.86,14.93 14.3,14.93C12.32,14.93 11.9,14.44 11.75,13.46C11.73,13.36 11.64,13.28 11.53,13.28H10.57C10.45,13.28 10.36,13.37 10.36,13.5C10.36,14.74 11.04,16.24 14.3,16.24C16.65,16.24 18,15.31 18,13.69C18,12.08 16.92,11.66 14.63,11.35C12.32,11.05 12.09,10.89 12.09,10.35C12.09,9.9 12.29,9.3 14,9.3C15.5,9.3 16.09,9.63 16.32,10.66C16.34,10.76 16.43,10.83 16.53,10.83H17.5C17.55,10.83 17.61,10.81 17.65,10.76C17.69,10.72 17.72,10.66 17.7,10.6C17.56,8.82 16.38,8 14,8Z" /></g><g id="note"><path d="M14,10V4.5L19.5,10M5,3C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V9L15,3H5Z" /></g><g id="note-multiple"><path d="M16,9H21.5L16,3.5V9M7,2H17L23,8V18A2,2 0 0,1 21,20H7C5.89,20 5,19.1 5,18V4A2,2 0 0,1 7,2M3,6V22H21V24H3A2,2 0 0,1 1,22V6H3Z" /></g><g id="note-multiple-outline"><path d="M3,6V22H21V24H3A2,2 0 0,1 1,22V6H3M16,9H21.5L16,3.5V9M7,2H17L23,8V18A2,2 0 0,1 21,20H7C5.89,20 5,19.1 5,18V4A2,2 0 0,1 7,2M7,4V18H21V11H14V4H7Z" /></g><g id="note-outline"><path d="M14,10H19.5L14,4.5V10M5,3H15L21,9V19A2,2 0 0,1 19,21H5C3.89,21 3,20.1 3,19V5C3,3.89 3.89,3 5,3M5,5V19H19V12H12V5H5Z" /></g><g id="note-plus"><path d="M14,10H19.5L14,4.5V10M5,3H15L21,9V19A2,2 0 0,1 19,21H5C3.89,21 3,20.1 3,19V5C3,3.89 3.89,3 5,3M9,18H11V15H14V13H11V10H9V13H6V15H9V18Z" /></g><g id="note-plus-outline"><path d="M15,10H20.5L15,4.5V10M4,3H16L22,9V19A2,2 0 0,1 20,21H4C2.89,21 2,20.1 2,19V5C2,3.89 2.89,3 4,3M4,5V19H20V12H13V5H4M8,17V15H6V13H8V11H10V13H12V15H10V17H8Z" /></g><g id="note-text"><path d="M14,10H19.5L14,4.5V10M5,3H15L21,9V19A2,2 0 0,1 19,21H5C3.89,21 3,20.1 3,19V5C3,3.89 3.89,3 5,3M5,12V14H19V12H5M5,16V18H14V16H5Z" /></g><g id="notification-clear-all"><path d="M5,13H19V11H5M3,17H17V15H3M7,7V9H21V7" /></g><g id="nuke"><path d="M14.04,12H10V11H5.5A3.5,3.5 0 0,1 2,7.5A3.5,3.5 0 0,1 5.5,4C6.53,4 7.45,4.44 8.09,5.15C8.5,3.35 10.08,2 12,2C13.92,2 15.5,3.35 15.91,5.15C16.55,4.44 17.47,4 18.5,4A3.5,3.5 0 0,1 22,7.5A3.5,3.5 0 0,1 18.5,11H14.04V12M10,16.9V15.76H5V13.76H19V15.76H14.04V16.92L20,19.08C20.58,19.29 21,19.84 21,20.5A1.5,1.5 0 0,1 19.5,22H4.5A1.5,1.5 0 0,1 3,20.5C3,19.84 3.42,19.29 4,19.08L10,16.9Z" /></g><g id="numeric"><path d="M4,17V9H2V7H6V17H4M22,15C22,16.11 21.1,17 20,17H16V15H20V13H18V11H20V9H16V7H20A2,2 0 0,1 22,9V10.5A1.5,1.5 0 0,1 20.5,12A1.5,1.5 0 0,1 22,13.5V15M14,15V17H8V13C8,11.89 8.9,11 10,11H12V9H8V7H12A2,2 0 0,1 14,9V11C14,12.11 13.1,13 12,13H10V15H14Z" /></g><g id="numeric-0-box"><path d="M19,3A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3H19M11,7A2,2 0 0,0 9,9V15A2,2 0 0,0 11,17H13A2,2 0 0,0 15,15V9A2,2 0 0,0 13,7H11M11,9H13V15H11V9Z" /></g><g id="numeric-0-box-multiple-outline"><path d="M21,17V3H7V17H21M21,1A2,2 0 0,1 23,3V17A2,2 0 0,1 21,19H7A2,2 0 0,1 5,17V3A2,2 0 0,1 7,1H21M3,5V21H19V23H3A2,2 0 0,1 1,21V5H3M13,5H15A2,2 0 0,1 17,7V13A2,2 0 0,1 15,15H13A2,2 0 0,1 11,13V7A2,2 0 0,1 13,5M13,7V13H15V7H13Z" /></g><g id="numeric-0-box-outline"><path d="M19,19V5H5V19H19M19,3A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3H19M11,7H13A2,2 0 0,1 15,9V15A2,2 0 0,1 13,17H11A2,2 0 0,1 9,15V9A2,2 0 0,1 11,7M11,9V15H13V9H11Z" /></g><g id="numeric-1-box"><path d="M14,17H12V9H10V7H14M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3Z" /></g><g id="numeric-1-box-multiple-outline"><path d="M21,17H7V3H21M21,1H7A2,2 0 0,0 5,3V17A2,2 0 0,0 7,19H21A2,2 0 0,0 23,17V3A2,2 0 0,0 21,1M14,15H16V5H12V7H14M3,5H1V21A2,2 0 0,0 3,23H19V21H3V5Z" /></g><g id="numeric-1-box-outline"><path d="M19,19H5V5H19M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3M12,17H14V7H10V9H12" /></g><g id="numeric-2-box"><path d="M15,11C15,12.11 14.1,13 13,13H11V15H15V17H9V13C9,11.89 9.9,11 11,11H13V9H9V7H13A2,2 0 0,1 15,9M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3Z" /></g><g id="numeric-2-box-multiple-outline"><path d="M17,13H13V11H15A2,2 0 0,0 17,9V7C17,5.89 16.1,5 15,5H11V7H15V9H13A2,2 0 0,0 11,11V15H17M21,17H7V3H21M21,1H7A2,2 0 0,0 5,3V17A2,2 0 0,0 7,19H21A2,2 0 0,0 23,17V3A2,2 0 0,0 21,1M3,5H1V21A2,2 0 0,0 3,23H19V21H3V5Z" /></g><g id="numeric-2-box-outline"><path d="M15,15H11V13H13A2,2 0 0,0 15,11V9C15,7.89 14.1,7 13,7H9V9H13V11H11A2,2 0 0,0 9,13V17H15M19,19H5V5H19M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3Z" /></g><g id="numeric-3-box"><path d="M15,10.5A1.5,1.5 0 0,1 13.5,12C14.34,12 15,12.67 15,13.5V15C15,16.11 14.11,17 13,17H9V15H13V13H11V11H13V9H9V7H13C14.11,7 15,7.89 15,9M19,3H5C3.91,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19C20.11,21 21,20.1 21,19V5A2,2 0 0,0 19,3Z" /></g><g id="numeric-3-box-multiple-outline"><path d="M17,13V11.5A1.5,1.5 0 0,0 15.5,10A1.5,1.5 0 0,0 17,8.5V7C17,5.89 16.1,5 15,5H11V7H15V9H13V11H15V13H11V15H15A2,2 0 0,0 17,13M3,5H1V21A2,2 0 0,0 3,23H19V21H3M21,17H7V3H21M21,1H7A2,2 0 0,0 5,3V17A2,2 0 0,0 7,19H21A2,2 0 0,0 23,17V3A2,2 0 0,0 21,1Z" /></g><g id="numeric-3-box-outline"><path d="M15,15V13.5A1.5,1.5 0 0,0 13.5,12A1.5,1.5 0 0,0 15,10.5V9C15,7.89 14.1,7 13,7H9V9H13V11H11V13H13V15H9V17H13A2,2 0 0,0 15,15M19,19H5V5H19M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3Z" /></g><g id="numeric-4-box"><path d="M15,17H13V13H9V7H11V11H13V7H15M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3Z" /></g><g id="numeric-4-box-multiple-outline"><path d="M21,17H7V3H21M21,1H7A2,2 0 0,0 5,3V17A2,2 0 0,0 7,19H21A2,2 0 0,0 23,17V3A2,2 0 0,0 21,1M15,15H17V5H15V9H13V5H11V11H15M3,5H1V21A2,2 0 0,0 3,23H19V21H3V5Z" /></g><g id="numeric-4-box-outline"><path d="M19,19H5V5H19M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3M13,17H15V7H13V11H11V7H9V13H13" /></g><g id="numeric-5-box"><path d="M15,9H11V11H13A2,2 0 0,1 15,13V15C15,16.11 14.1,17 13,17H9V15H13V13H9V7H15M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3Z" /></g><g id="numeric-5-box-multiple-outline"><path d="M17,13V11C17,9.89 16.1,9 15,9H13V7H17V5H11V11H15V13H11V15H15A2,2 0 0,0 17,13M3,5H1V21A2,2 0 0,0 3,23H19V21H3M21,17H7V3H21M21,1H7A2,2 0 0,0 5,3V17A2,2 0 0,0 7,19H21A2,2 0 0,0 23,17V3A2,2 0 0,0 21,1Z" /></g><g id="numeric-5-box-outline"><path d="M15,15V13C15,11.89 14.1,11 13,11H11V9H15V7H9V13H13V15H9V17H13A2,2 0 0,0 15,15M19,19H5V5H19M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3Z" /></g><g id="numeric-6-box"><path d="M15,9H11V11H13A2,2 0 0,1 15,13V15C15,16.11 14.1,17 13,17H11A2,2 0 0,1 9,15V9C9,7.89 9.9,7 11,7H15M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3M11,15H13V13H11V15Z" /></g><g id="numeric-6-box-multiple-outline"><path d="M13,11H15V13H13M13,15H15A2,2 0 0,0 17,13V11C17,9.89 16.1,9 15,9H13V7H17V5H13A2,2 0 0,0 11,7V13C11,14.11 11.9,15 13,15M21,17H7V3H21M21,1H7A2,2 0 0,0 5,3V17A2,2 0 0,0 7,19H21A2,2 0 0,0 23,17V3A2,2 0 0,0 21,1M3,5H1V21A2,2 0 0,0 3,23H19V21H3V5Z" /></g><g id="numeric-6-box-outline"><path d="M11,13H13V15H11M11,17H13A2,2 0 0,0 15,15V13C15,11.89 14.1,11 13,11H11V9H15V7H11A2,2 0 0,0 9,9V15C9,16.11 9.9,17 11,17M19,19H5V5H19M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3Z" /></g><g id="numeric-7-box"><path d="M19,3A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3H19M11,17L15,9V7H9V9H13L9,17H11Z" /></g><g id="numeric-7-box-multiple-outline"><path d="M13,15L17,7V5H11V7H15L11,15M21,17H7V3H21M21,1H7A2,2 0 0,0 5,3V17A2,2 0 0,0 7,19H21A2,2 0 0,0 23,17V3A2,2 0 0,0 21,1M3,5H1V21A2,2 0 0,0 3,23H19V21H3V5Z" /></g><g id="numeric-7-box-outline"><path d="M11,17L15,9V7H9V9H13L9,17M19,19H5V5H19M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3Z" /></g><g id="numeric-8-box"><path d="M19,3A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3H19M11,17H13A2,2 0 0,0 15,15V13.5A1.5,1.5 0 0,0 13.5,12A1.5,1.5 0 0,0 15,10.5V9C15,7.89 14.1,7 13,7H11A2,2 0 0,0 9,9V10.5A1.5,1.5 0 0,0 10.5,12A1.5,1.5 0 0,0 9,13.5V15C9,16.11 9.9,17 11,17M11,13H13V15H11V13M11,9H13V11H11V9Z" /></g><g id="numeric-8-box-multiple-outline"><path d="M13,11H15V13H13M13,7H15V9H13M13,15H15A2,2 0 0,0 17,13V11.5A1.5,1.5 0 0,0 15.5,10A1.5,1.5 0 0,0 17,8.5V7C17,5.89 16.1,5 15,5H13A2,2 0 0,0 11,7V8.5A1.5,1.5 0 0,0 12.5,10A1.5,1.5 0 0,0 11,11.5V13C11,14.11 11.9,15 13,15M21,17H7V3H21M21,1H7A2,2 0 0,0 5,3V17A2,2 0 0,0 7,19H21A2,2 0 0,0 23,17V3A2,2 0 0,0 21,1M3,5H1V21A2,2 0 0,0 3,23H19V21H3V5Z" /></g><g id="numeric-8-box-outline"><path d="M11,13H13V15H11M11,9H13V11H11M11,17H13A2,2 0 0,0 15,15V13.5A1.5,1.5 0 0,0 13.5,12A1.5,1.5 0 0,0 15,10.5V9C15,7.89 14.1,7 13,7H11A2,2 0 0,0 9,9V10.5A1.5,1.5 0 0,0 10.5,12A1.5,1.5 0 0,0 9,13.5V15C9,16.11 9.9,17 11,17M19,19H5V5H19M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3Z" /></g><g id="numeric-9-box"><path d="M19,3A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3H19M13,11H11V9H13V11M13,7H11A2,2 0 0,0 9,9V11C9,12.11 9.9,13 11,13H13V15H9V17H13A2,2 0 0,0 15,15V9C15,7.89 14.1,7 13,7Z" /></g><g id="numeric-9-box-multiple-outline"><path d="M15,9H13V7H15M15,5H13A2,2 0 0,0 11,7V9C11,10.11 11.9,11 13,11H15V13H11V15H15A2,2 0 0,0 17,13V7C17,5.89 16.1,5 15,5M21,17H7V3H21M21,1H7A2,2 0 0,0 5,3V17A2,2 0 0,0 7,19H21A2,2 0 0,0 23,17V3A2,2 0 0,0 21,1M3,5H1V21A2,2 0 0,0 3,23H19V21H3V5Z" /></g><g id="numeric-9-box-outline"><path d="M13,11H11V9H13M13,7H11A2,2 0 0,0 9,9V11C9,12.11 9.9,13 11,13H13V15H9V17H13A2,2 0 0,0 15,15V9C15,7.89 14.1,7 13,7M19,19H5V5H19M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3Z" /></g><g id="numeric-9-plus-box"><path d="M21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3H19A2,2 0 0,1 21,5M19,11H17V9H15V11H13V13H15V15H17V13H19V11M10,7H8A2,2 0 0,0 6,9V11C6,12.11 6.9,13 8,13H10V15H6V17H10A2,2 0 0,0 12,15V9C12,7.89 11.1,7 10,7M8,9H10V11H8V9Z" /></g><g id="numeric-9-plus-box-multiple-outline"><path d="M21,9H19V7H17V9H15V11H17V13H19V11H21V17H7V3H21M21,1H7A2,2 0 0,0 5,3V17A2,2 0 0,0 7,19H21A2,2 0 0,0 23,17V3A2,2 0 0,0 21,1M11,9V8H12V9M14,12V8C14,6.89 13.1,6 12,6H11A2,2 0 0,0 9,8V9C9,10.11 9.9,11 11,11H12V12H9V14H12A2,2 0 0,0 14,12M3,5H1V21A2,2 0 0,0 3,23H19V21H3V5Z" /></g><g id="numeric-9-plus-box-outline"><path d="M19,11H17V9H15V11H13V13H15V15H17V13H19V19H5V5H19M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3M9,11V10H10V11M12,14V10C12,8.89 11.1,8 10,8H9A2,2 0 0,0 7,10V11C7,12.11 7.9,13 9,13H10V14H7V16H10A2,2 0 0,0 12,14Z" /></g><g id="nutrition"><path d="M22,18A4,4 0 0,1 18,22H14A4,4 0 0,1 10,18V16H22V18M4,3H14A2,2 0 0,1 16,5V14H8V19H4A2,2 0 0,1 2,17V5A2,2 0 0,1 4,3M4,6V8H6V6H4M14,8V6H8V8H14M4,10V12H6V10H4M8,10V12H14V10H8M4,14V16H6V14H4Z" /></g><g id="oar"><path d="M20.23,15.21C18.77,13.75 14.97,10.2 12.77,11.27L4.5,3L3,4.5L11.28,12.79C10.3,15 13.88,18.62 15.35,20.08C17.11,21.84 18.26,20.92 19.61,19.57C21.1,18.08 21.61,16.61 20.23,15.21Z" /></g><g id="octagon"><path d="M15.73,3H8.27L3,8.27V15.73L8.27,21H15.73L21,15.73V8.27" /></g><g id="octagon-outline"><path d="M8.27,3L3,8.27V15.73L8.27,21H15.73C17.5,19.24 21,15.73 21,15.73V8.27L15.73,3M9.1,5H14.9L19,9.1V14.9L14.9,19H9.1L5,14.9V9.1" /></g><g id="odnoklassniki"><path d="M17.83,12.74C17.55,12.17 16.76,11.69 15.71,12.5C14.28,13.64 12,13.64 12,13.64C12,13.64 9.72,13.64 8.29,12.5C7.24,11.69 6.45,12.17 6.17,12.74C5.67,13.74 6.23,14.23 7.5,15.04C8.59,15.74 10.08,16 11.04,16.1L10.24,16.9C9.1,18.03 8,19.12 7.25,19.88C6.8,20.34 6.8,21.07 7.25,21.5L7.39,21.66C7.84,22.11 8.58,22.11 9.03,21.66L12,18.68C13.15,19.81 14.24,20.9 15,21.66C15.45,22.11 16.18,22.11 16.64,21.66L16.77,21.5C17.23,21.07 17.23,20.34 16.77,19.88L13.79,16.9L13,16.09C13.95,16 15.42,15.73 16.5,15.04C17.77,14.23 18.33,13.74 17.83,12.74M12,4.57C13.38,4.57 14.5,5.69 14.5,7.06C14.5,8.44 13.38,9.55 12,9.55C10.62,9.55 9.5,8.44 9.5,7.06C9.5,5.69 10.62,4.57 12,4.57M12,12.12C14.8,12.12 17.06,9.86 17.06,7.06C17.06,4.27 14.8,2 12,2C9.2,2 6.94,4.27 6.94,7.06C6.94,9.86 9.2,12.12 12,12.12Z" /></g><g id="office"><path d="M3,18L7,16.75V7L14,5V19.5L3.5,18.25L14,22L20,20.75V3.5L13.95,2L3,5.75V18Z" /></g><g id="oil"><path d="M22,12.5C22,12.5 24,14.67 24,16A2,2 0 0,1 22,18A2,2 0 0,1 20,16C20,14.67 22,12.5 22,12.5M6,6H10A1,1 0 0,1 11,7A1,1 0 0,1 10,8H9V10H11C11.74,10 12.39,10.4 12.73,11L19.24,7.24L22.5,9.13C23,9.4 23.14,10 22.87,10.5C22.59,10.97 22,11.14 21.5,10.86L19.4,9.65L15.75,15.97C15.41,16.58 14.75,17 14,17H5A2,2 0 0,1 3,15V12A2,2 0 0,1 5,10H7V8H6A1,1 0 0,1 5,7A1,1 0 0,1 6,6M5,12V15H14L16.06,11.43L12.6,13.43L11.69,12H5M0.38,9.21L2.09,7.5C2.5,7.11 3.11,7.11 3.5,7.5C3.89,7.89 3.89,8.5 3.5,8.91L1.79,10.62C1.4,11 0.77,11 0.38,10.62C0,10.23 0,9.6 0.38,9.21Z" /></g><g id="oil-temperature"><path d="M11.5,1A1.5,1.5 0 0,0 10,2.5V14.5C9.37,14.97 9,15.71 9,16.5A2.5,2.5 0 0,0 11.5,19A2.5,2.5 0 0,0 14,16.5C14,15.71 13.63,15 13,14.5V13H17V11H13V9H17V7H13V5H17V3H13V2.5A1.5,1.5 0 0,0 11.5,1M0,15V17C0.67,17 0.79,17.21 1.29,17.71C1.79,18.21 2.67,19 4,19C5.33,19 6.21,18.21 6.71,17.71C6.82,17.59 6.91,17.5 7,17.41V15.16C6.21,15.42 5.65,15.93 5.29,16.29C4.79,16.79 4.67,17 4,17C3.33,17 3.21,16.79 2.71,16.29C2.21,15.79 1.33,15 0,15M16,15V17C16.67,17 16.79,17.21 17.29,17.71C17.79,18.21 18.67,19 20,19C21.33,19 22.21,18.21 22.71,17.71C23.21,17.21 23.33,17 24,17V15C22.67,15 21.79,15.79 21.29,16.29C20.79,16.79 20.67,17 20,17C19.33,17 19.21,16.79 18.71,16.29C18.21,15.79 17.33,15 16,15M8,20C6.67,20 5.79,20.79 5.29,21.29C4.79,21.79 4.67,22 4,22C3.33,22 3.21,21.79 2.71,21.29C2.35,20.93 1.79,20.42 1,20.16V22.41C1.09,22.5 1.18,22.59 1.29,22.71C1.79,23.21 2.67,24 4,24C5.33,24 6.21,23.21 6.71,22.71C7.21,22.21 7.33,22 8,22C8.67,22 8.79,22.21 9.29,22.71C9.73,23.14 10.44,23.8 11.5,23.96C11.66,24 11.83,24 12,24C13.33,24 14.21,23.21 14.71,22.71C15.21,22.21 15.33,22 16,22C16.67,22 16.79,22.21 17.29,22.71C17.79,23.21 18.67,24 20,24C21.33,24 22.21,23.21 22.71,22.71C22.82,22.59 22.91,22.5 23,22.41V20.16C22.21,20.42 21.65,20.93 21.29,21.29C20.79,21.79 20.67,22 20,22C19.33,22 19.21,21.79 18.71,21.29C18.21,20.79 17.33,20 16,20C14.67,20 13.79,20.79 13.29,21.29C12.79,21.79 12.67,22 12,22C11.78,22 11.63,21.97 11.5,21.92C11.22,21.82 11.05,21.63 10.71,21.29C10.21,20.79 9.33,20 8,20Z" /></g><g id="omega"><path d="M19.15,19H13.39V16.87C15.5,15.25 16.59,13.24 16.59,10.84C16.59,9.34 16.16,8.16 15.32,7.29C14.47,6.42 13.37,6 12.03,6C10.68,6 9.57,6.42 8.71,7.3C7.84,8.17 7.41,9.37 7.41,10.88C7.41,13.26 8.5,15.26 10.61,16.87V19H4.85V16.87H8.41C6.04,15.32 4.85,13.23 4.85,10.6C4.85,8.5 5.5,6.86 6.81,5.66C8.12,4.45 9.84,3.85 11.97,3.85C14.15,3.85 15.89,4.45 17.19,5.64C18.5,6.83 19.15,8.5 19.15,10.58C19.15,13.21 17.95,15.31 15.55,16.87H19.15V19Z" /></g><g id="onedrive"><path d="M20.08,13.64C21.17,13.81 22,14.75 22,15.89C22,16.78 21.5,17.55 20.75,17.92L20.58,18H9.18L9.16,18V18C7.71,18 6.54,16.81 6.54,15.36C6.54,13.9 7.72,12.72 9.18,12.72L9.4,12.73L9.39,12.53A3.3,3.3 0 0,1 12.69,9.23C13.97,9.23 15.08,9.96 15.63,11C16.08,10.73 16.62,10.55 17.21,10.55A2.88,2.88 0 0,1 20.09,13.43L20.08,13.64M8.82,12.16C7.21,12.34 5.96,13.7 5.96,15.36C5.96,16.04 6.17,16.66 6.5,17.18H4.73A2.73,2.73 0 0,1 2,14.45C2,13 3.12,11.83 4.53,11.73L4.46,11.06C4.46,9.36 5.84,8 7.54,8C8.17,8 8.77,8.18 9.26,8.5C9.95,7.11 11.4,6.15 13.07,6.15C15.27,6.15 17.08,7.83 17.3,9.97H17.21C16.73,9.97 16.27,10.07 15.84,10.25C15.12,9.25 13.96,8.64 12.69,8.64C10.67,8.64 9,10.19 8.82,12.16Z" /></g><g id="opacity"><path d="M17.66,8L12,2.35L6.34,8C4.78,9.56 4,11.64 4,13.64C4,15.64 4.78,17.75 6.34,19.31C7.9,20.87 9.95,21.66 12,21.66C14.05,21.66 16.1,20.87 17.66,19.31C19.22,17.75 20,15.64 20,13.64C20,11.64 19.22,9.56 17.66,8M6,14C6,12 6.62,10.73 7.76,9.6L12,5.27L16.24,9.65C17.38,10.77 18,12 18,14H6Z" /></g><g id="open-in-app"><path d="M12,10L8,14H11V20H13V14H16M19,4H5C3.89,4 3,4.9 3,6V18A2,2 0 0,0 5,20H9V18H5V8H19V18H15V20H19A2,2 0 0,0 21,18V6A2,2 0 0,0 19,4Z" /></g><g id="open-in-new"><path d="M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z" /></g><g id="openid"><path d="M14,2L11,3.5V19.94C7,19.5 4,17.46 4,15C4,12.75 6.5,10.85 10,10.22V8.19C4.86,8.88 1,11.66 1,15C1,18.56 5.36,21.5 11,21.94C11.03,21.94 11.06,21.94 11.09,21.94L14,20.5V2M15,8.19V10.22C16.15,10.43 17.18,10.77 18.06,11.22L16.5,12L23,13.5L22.5,9L20.5,10C19,9.12 17.12,8.47 15,8.19Z" /></g><g id="opera"><path d="M17.33,3.57C15.86,2.56 14.05,2 12,2C10.13,2 8.46,2.47 7.06,3.32C4.38,4.95 2.72,8 2.72,11.9C2.72,17.19 6.43,22 12,22C17.57,22 21.28,17.19 21.28,11.9C21.28,8.19 19.78,5.25 17.33,3.57M12,3.77C15,3.77 15.6,7.93 15.6,11.72C15.6,15.22 15.26,19.91 12.04,19.91C8.82,19.91 8.4,15.17 8.4,11.67C8.4,7.89 9,3.77 12,3.77Z" /></g><g id="ornament"><path d="M12,1A3,3 0 0,1 15,4V5A1,1 0 0,1 16,6V7.07C18.39,8.45 20,11.04 20,14A8,8 0 0,1 12,22A8,8 0 0,1 4,14C4,11.04 5.61,8.45 8,7.07V6A1,1 0 0,1 9,5V4A3,3 0 0,1 12,1M12,3A1,1 0 0,0 11,4V5H13V4A1,1 0 0,0 12,3M12,8C10.22,8 8.63,8.77 7.53,10H16.47C15.37,8.77 13.78,8 12,8M6.34,16H7.59L6,14.43C6.05,15 6.17,15.5 6.34,16M12.59,16L8.59,12H6.41L10.41,16H12.59M17.66,12H16.41L18,13.57C17.95,13 17.83,12.5 17.66,12M11.41,12L15.41,16H17.59L13.59,12H11.41M12,20C13.78,20 15.37,19.23 16.47,18H7.53C8.63,19.23 10.22,20 12,20Z" /></g><g id="ornament-variant"><path d="M12,1A3,3 0 0,1 15,4V5A1,1 0 0,1 16,6V7.07C18.39,8.45 20,11.04 20,14A8,8 0 0,1 12,22A8,8 0 0,1 4,14C4,11.04 5.61,8.45 8,7.07V6A1,1 0 0,1 9,5V4A3,3 0 0,1 12,1M12,3A1,1 0 0,0 11,4V5H13V4A1,1 0 0,0 12,3M12,8C10.22,8 8.63,8.77 7.53,10H16.47C15.37,8.77 13.78,8 12,8M12,20C13.78,20 15.37,19.23 16.47,18H7.53C8.63,19.23 10.22,20 12,20M12,12A2,2 0 0,0 10,14A2,2 0 0,0 12,16A2,2 0 0,0 14,14A2,2 0 0,0 12,12M18,14C18,13.31 17.88,12.65 17.67,12C16.72,12.19 16,13 16,14C16,15 16.72,15.81 17.67,15.97C17.88,15.35 18,14.69 18,14M6,14C6,14.69 6.12,15.35 6.33,15.97C7.28,15.81 8,15 8,14C8,13 7.28,12.19 6.33,12C6.12,12.65 6,13.31 6,14Z" /></g><g id="owl"><path d="M12,16C12.56,16.84 13.31,17.53 14.2,18L12,20.2L9.8,18C10.69,17.53 11.45,16.84 12,16M17,11.2A2,2 0 0,0 15,13.2A2,2 0 0,0 17,15.2A2,2 0 0,0 19,13.2C19,12.09 18.1,11.2 17,11.2M7,11.2A2,2 0 0,0 5,13.2A2,2 0 0,0 7,15.2A2,2 0 0,0 9,13.2C9,12.09 8.1,11.2 7,11.2M17,8.7A4,4 0 0,1 21,12.7A4,4 0 0,1 17,16.7A4,4 0 0,1 13,12.7A4,4 0 0,1 17,8.7M7,8.7A4,4 0 0,1 11,12.7A4,4 0 0,1 7,16.7A4,4 0 0,1 3,12.7A4,4 0 0,1 7,8.7M2.24,1C4,4.7 2.73,7.46 1.55,10.2C1.19,11 1,11.83 1,12.7A6,6 0 0,0 7,18.7C7.21,18.69 7.42,18.68 7.63,18.65L10.59,21.61L12,23L13.41,21.61L16.37,18.65C16.58,18.68 16.79,18.69 17,18.7A6,6 0 0,0 23,12.7C23,11.83 22.81,11 22.45,10.2C21.27,7.46 20,4.7 21.76,1C19.12,3.06 15.36,4.69 12,4.7C8.64,4.69 4.88,3.06 2.24,1Z" /></g><g id="package"><path d="M5.12,5H18.87L17.93,4H5.93L5.12,5M20.54,5.23C20.83,5.57 21,6 21,6.5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V6.5C3,6 3.17,5.57 3.46,5.23L4.84,3.55C5.12,3.21 5.53,3 6,3H18C18.47,3 18.88,3.21 19.15,3.55L20.54,5.23M6,18H12V15H6V18Z" /></g><g id="package-down"><path d="M5.12,5L5.93,4H17.93L18.87,5M12,17.5L6.5,12H10V10H14V12H17.5L12,17.5M20.54,5.23L19.15,3.55C18.88,3.21 18.47,3 18,3H6C5.53,3 5.12,3.21 4.84,3.55L3.46,5.23C3.17,5.57 3,6 3,6.5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V6.5C21,6 20.83,5.57 20.54,5.23Z" /></g><g id="package-up"><path d="M20.54,5.23C20.83,5.57 21,6 21,6.5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V6.5C3,6 3.17,5.57 3.46,5.23L4.84,3.55C5.12,3.21 5.53,3 6,3H18C18.47,3 18.88,3.21 19.15,3.55L20.54,5.23M5.12,5H18.87L17.93,4H5.93L5.12,5M12,9.5L6.5,15H10V17H14V15H17.5L12,9.5Z" /></g><g id="package-variant"><path d="M2,10.96C1.5,10.68 1.35,10.07 1.63,9.59L3.13,7C3.24,6.8 3.41,6.66 3.6,6.58L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.66,6.72 20.82,6.88 20.91,7.08L22.36,9.6C22.64,10.08 22.47,10.69 22,10.96L21,11.54V16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V10.96C2.7,11.13 2.32,11.14 2,10.96M12,4.15V4.15L12,10.85V10.85L17.96,7.5L12,4.15M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V12.69L14,15.59C13.67,15.77 13.3,15.76 13,15.6V19.29L19,15.91M13.85,13.36L20.13,9.73L19.55,8.72L13.27,12.35L13.85,13.36Z" /></g><g id="package-variant-closed"><path d="M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L10.11,5.22L16,8.61L17.96,7.5L12,4.15M6.04,7.5L12,10.85L13.96,9.75L8.08,6.35L6.04,7.5M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V9.21L13,12.58V19.29L19,15.91Z" /></g><g id="page-first"><path d="M18.41,16.59L13.82,12L18.41,7.41L17,6L11,12L17,18L18.41,16.59M6,6H8V18H6V6Z" /></g><g id="page-last"><path d="M5.59,7.41L10.18,12L5.59,16.59L7,18L13,12L7,6L5.59,7.41M16,6H18V18H16V6Z" /></g><g id="palette"><path d="M17.5,12A1.5,1.5 0 0,1 16,10.5A1.5,1.5 0 0,1 17.5,9A1.5,1.5 0 0,1 19,10.5A1.5,1.5 0 0,1 17.5,12M14.5,8A1.5,1.5 0 0,1 13,6.5A1.5,1.5 0 0,1 14.5,5A1.5,1.5 0 0,1 16,6.5A1.5,1.5 0 0,1 14.5,8M9.5,8A1.5,1.5 0 0,1 8,6.5A1.5,1.5 0 0,1 9.5,5A1.5,1.5 0 0,1 11,6.5A1.5,1.5 0 0,1 9.5,8M6.5,12A1.5,1.5 0 0,1 5,10.5A1.5,1.5 0 0,1 6.5,9A1.5,1.5 0 0,1 8,10.5A1.5,1.5 0 0,1 6.5,12M12,3A9,9 0 0,0 3,12A9,9 0 0,0 12,21A1.5,1.5 0 0,0 13.5,19.5C13.5,19.11 13.35,18.76 13.11,18.5C12.88,18.23 12.73,17.88 12.73,17.5A1.5,1.5 0 0,1 14.23,16H16A5,5 0 0,0 21,11C21,6.58 16.97,3 12,3Z" /></g><g id="palette-advanced"><path d="M22,22H10V20H22V22M2,22V20H9V22H2M18,18V10H22V18H18M18,3H22V9H18V3M2,18V3H16V18H2M9,14.56A3,3 0 0,0 12,11.56C12,9.56 9,6.19 9,6.19C9,6.19 6,9.56 6,11.56A3,3 0 0,0 9,14.56Z" /></g><g id="panda"><path d="M12,3C13.74,3 15.36,3.5 16.74,4.35C17.38,3.53 18.38,3 19.5,3A3.5,3.5 0 0,1 23,6.5C23,8 22.05,9.28 20.72,9.78C20.9,10.5 21,11.23 21,12A9,9 0 0,1 12,21A9,9 0 0,1 3,12C3,11.23 3.1,10.5 3.28,9.78C1.95,9.28 1,8 1,6.5A3.5,3.5 0 0,1 4.5,3C5.62,3 6.62,3.53 7.26,4.35C8.64,3.5 10.26,3 12,3M12,5A7,7 0 0,0 5,12A7,7 0 0,0 12,19A7,7 0 0,0 19,12A7,7 0 0,0 12,5M16.19,10.3C16.55,11.63 16.08,12.91 15.15,13.16C14.21,13.42 13.17,12.54 12.81,11.2C12.45,9.87 12.92,8.59 13.85,8.34C14.79,8.09 15.83,8.96 16.19,10.3M7.81,10.3C8.17,8.96 9.21,8.09 10.15,8.34C11.08,8.59 11.55,9.87 11.19,11.2C10.83,12.54 9.79,13.42 8.85,13.16C7.92,12.91 7.45,11.63 7.81,10.3M12,14C12.6,14 13.13,14.19 13.5,14.5L12.5,15.5C12.5,15.92 12.84,16.25 13.25,16.25A0.75,0.75 0 0,0 14,15.5A0.5,0.5 0 0,1 14.5,15A0.5,0.5 0 0,1 15,15.5A1.75,1.75 0 0,1 13.25,17.25C12.76,17.25 12.32,17.05 12,16.72C11.68,17.05 11.24,17.25 10.75,17.25A1.75,1.75 0 0,1 9,15.5A0.5,0.5 0 0,1 9.5,15A0.5,0.5 0 0,1 10,15.5A0.75,0.75 0 0,0 10.75,16.25A0.75,0.75 0 0,0 11.5,15.5L10.5,14.5C10.87,14.19 11.4,14 12,14Z" /></g><g id="pandora"><path d="M16.87,7.73C16.87,9.9 15.67,11.7 13.09,11.7H10.45V3.66H13.09C15.67,3.66 16.87,5.5 16.87,7.73M10.45,15.67V13.41H13.09C17.84,13.41 20.5,10.91 20.5,7.73C20.5,4.45 17.84,2 13.09,2H3.5V2.92C6.62,2.92 7.17,3.66 7.17,8.28V15.67C7.17,20.29 6.62,21.08 3.5,21.08V22H14.1V21.08C11,21.08 10.45,20.29 10.45,15.67Z" /></g><g id="panorama"><path d="M8.5,12.5L11,15.5L14.5,11L19,17H5M23,18V6A2,2 0 0,0 21,4H3A2,2 0 0,0 1,6V18A2,2 0 0,0 3,20H21A2,2 0 0,0 23,18Z" /></g><g id="panorama-fisheye"><path d="M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2C6.47,2 2,6.47 2,12C2,17.53 6.47,22 12,22C17.53,22 22,17.53 22,12C22,6.47 17.53,2 12,2Z" /></g><g id="panorama-horizontal"><path d="M21.43,4C21.33,4 21.23,4 21.12,4.06C18.18,5.16 15.09,5.7 12,5.7C8.91,5.7 5.82,5.15 2.88,4.06C2.77,4 2.66,4 2.57,4C2.23,4 2,4.23 2,4.63V19.38C2,19.77 2.23,20 2.57,20C2.67,20 2.77,20 2.88,19.94C5.82,18.84 8.91,18.3 12,18.3C15.09,18.3 18.18,18.85 21.12,19.94C21.23,20 21.33,20 21.43,20C21.76,20 22,19.77 22,19.37V4.63C22,4.23 21.76,4 21.43,4M20,6.54V17.45C17.4,16.68 14.72,16.29 12,16.29C9.28,16.29 6.6,16.68 4,17.45V6.54C6.6,7.31 9.28,7.7 12,7.7C14.72,7.71 17.4,7.32 20,6.54Z" /></g><g id="panorama-vertical"><path d="M6.54,20C7.31,17.4 7.7,14.72 7.7,12C7.7,9.28 7.31,6.6 6.54,4H17.45C16.68,6.6 16.29,9.28 16.29,12C16.29,14.72 16.68,17.4 17.45,20M19.94,21.12C18.84,18.18 18.3,15.09 18.3,12C18.3,8.91 18.85,5.82 19.94,2.88C20,2.77 20,2.66 20,2.57C20,2.23 19.77,2 19.37,2H4.63C4.23,2 4,2.23 4,2.57C4,2.67 4,2.77 4.06,2.88C5.16,5.82 5.71,8.91 5.71,12C5.71,15.09 5.16,18.18 4.07,21.12C4,21.23 4,21.34 4,21.43C4,21.76 4.23,22 4.63,22H19.38C19.77,22 20,21.76 20,21.43C20,21.33 20,21.23 19.94,21.12Z" /></g><g id="panorama-wide-angle"><path d="M12,4C9.27,4 6.78,4.24 4.05,4.72L3.12,4.88L2.87,5.78C2.29,7.85 2,9.93 2,12C2,14.07 2.29,16.15 2.87,18.22L3.12,19.11L4.05,19.27C6.78,19.76 9.27,20 12,20C14.73,20 17.22,19.76 19.95,19.28L20.88,19.12L21.13,18.23C21.71,16.15 22,14.07 22,12C22,9.93 21.71,7.85 21.13,5.78L20.88,4.89L19.95,4.73C17.22,4.24 14.73,4 12,4M12,6C14.45,6 16.71,6.2 19.29,6.64C19.76,8.42 20,10.22 20,12C20,13.78 19.76,15.58 19.29,17.36C16.71,17.8 14.45,18 12,18C9.55,18 7.29,17.8 4.71,17.36C4.24,15.58 4,13.78 4,12C4,10.22 4.24,8.42 4.71,6.64C7.29,6.2 9.55,6 12,6Z" /></g><g id="paper-cut-vertical"><path d="M11.43,3.23L12,4L12.57,3.23V3.24C13.12,2.5 14,2 15,2A3,3 0 0,1 18,5C18,5.35 17.94,5.69 17.83,6H20A2,2 0 0,1 22,8V20A2,2 0 0,1 20,22H4A2,2 0 0,1 2,20V8A2,2 0 0,1 4,6H6.17C6.06,5.69 6,5.35 6,5A3,3 0 0,1 9,2C10,2 10.88,2.5 11.43,3.24V3.23M4,8V20H11A1,1 0 0,1 12,19A1,1 0 0,1 13,20H20V8H15L14.9,8L17,10.92L15.4,12.1L12.42,8H11.58L8.6,12.1L7,10.92L9.1,8H9L4,8M9,4A1,1 0 0,0 8,5A1,1 0 0,0 9,6A1,1 0 0,0 10,5A1,1 0 0,0 9,4M15,4A1,1 0 0,0 14,5A1,1 0 0,0 15,6A1,1 0 0,0 16,5A1,1 0 0,0 15,4M12,16A1,1 0 0,1 13,17A1,1 0 0,1 12,18A1,1 0 0,1 11,17A1,1 0 0,1 12,16M12,13A1,1 0 0,1 13,14A1,1 0 0,1 12,15A1,1 0 0,1 11,14A1,1 0 0,1 12,13M12,10A1,1 0 0,1 13,11A1,1 0 0,1 12,12A1,1 0 0,1 11,11A1,1 0 0,1 12,10Z" /></g><g id="paperclip"><path d="M16.5,6V17.5A4,4 0 0,1 12.5,21.5A4,4 0 0,1 8.5,17.5V5A2.5,2.5 0 0,1 11,2.5A2.5,2.5 0 0,1 13.5,5V15.5A1,1 0 0,1 12.5,16.5A1,1 0 0,1 11.5,15.5V6H10V15.5A2.5,2.5 0 0,0 12.5,18A2.5,2.5 0 0,0 15,15.5V5A4,4 0 0,0 11,1A4,4 0 0,0 7,5V17.5A5.5,5.5 0 0,0 12.5,23A5.5,5.5 0 0,0 18,17.5V6H16.5Z" /></g><g id="parking"><path d="M13.2,11H10V7H13.2A2,2 0 0,1 15.2,9A2,2 0 0,1 13.2,11M13,3H6V21H10V15H13A6,6 0 0,0 19,9C19,5.68 16.31,3 13,3Z" /></g><g id="pause"><path d="M14,19H18V5H14M6,19H10V5H6V19Z" /></g><g id="pause-circle"><path d="M15,16H13V8H15M11,16H9V8H11M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" /></g><g id="pause-circle-outline"><path d="M13,16V8H15V16H13M9,16V8H11V16H9M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z" /></g><g id="pause-octagon"><path d="M15.73,3L21,8.27V15.73L15.73,21H8.27L3,15.73V8.27L8.27,3H15.73M15,16V8H13V16H15M11,16V8H9V16H11Z" /></g><g id="pause-octagon-outline"><path d="M15,16H13V8H15V16M11,16H9V8H11V16M15.73,3L21,8.27V15.73L15.73,21H8.27L3,15.73V8.27L8.27,3H15.73M14.9,5H9.1L5,9.1V14.9L9.1,19H14.9L19,14.9V9.1L14.9,5Z" /></g><g id="paw"><path d="M8.35,3C9.53,2.83 10.78,4.12 11.14,5.9C11.5,7.67 10.85,9.25 9.67,9.43C8.5,9.61 7.24,8.32 6.87,6.54C6.5,4.77 7.17,3.19 8.35,3M15.5,3C16.69,3.19 17.35,4.77 17,6.54C16.62,8.32 15.37,9.61 14.19,9.43C13,9.25 12.35,7.67 12.72,5.9C13.08,4.12 14.33,2.83 15.5,3M3,7.6C4.14,7.11 5.69,8 6.5,9.55C7.26,11.13 7,12.79 5.87,13.28C4.74,13.77 3.2,12.89 2.41,11.32C1.62,9.75 1.9,8.08 3,7.6M21,7.6C22.1,8.08 22.38,9.75 21.59,11.32C20.8,12.89 19.26,13.77 18.13,13.28C17,12.79 16.74,11.13 17.5,9.55C18.31,8 19.86,7.11 21,7.6M19.33,18.38C19.37,19.32 18.65,20.36 17.79,20.75C16,21.57 13.88,19.87 11.89,19.87C9.9,19.87 7.76,21.64 6,20.75C5,20.26 4.31,18.96 4.44,17.88C4.62,16.39 6.41,15.59 7.47,14.5C8.88,13.09 9.88,10.44 11.89,10.44C13.89,10.44 14.95,13.05 16.3,14.5C17.41,15.72 19.26,16.75 19.33,18.38Z" /></g><g id="paw-off"><path d="M2,4.27L3.28,3L21.5,21.22L20.23,22.5L18.23,20.5C18.09,20.6 17.94,20.68 17.79,20.75C16,21.57 13.88,19.87 11.89,19.87C9.9,19.87 7.76,21.64 6,20.75C5,20.26 4.31,18.96 4.44,17.88C4.62,16.39 6.41,15.59 7.47,14.5C8.21,13.77 8.84,12.69 9.55,11.82L2,4.27M8.35,3C9.53,2.83 10.78,4.12 11.14,5.9C11.32,6.75 11.26,7.56 11,8.19L7.03,4.2C7.29,3.55 7.75,3.1 8.35,3M15.5,3C16.69,3.19 17.35,4.77 17,6.54C16.62,8.32 15.37,9.61 14.19,9.43C13,9.25 12.35,7.67 12.72,5.9C13.08,4.12 14.33,2.83 15.5,3M3,7.6C4.14,7.11 5.69,8 6.5,9.55C7.26,11.13 7,12.79 5.87,13.28C4.74,13.77 3.2,12.89 2.41,11.32C1.62,9.75 1.9,8.08 3,7.6M21,7.6C22.1,8.08 22.38,9.75 21.59,11.32C20.8,12.89 19.26,13.77 18.13,13.28C17,12.79 16.74,11.13 17.5,9.55C18.31,8 19.86,7.11 21,7.6Z" /></g><g id="pen"><path d="M20.71,7.04C20.37,7.38 20.04,7.71 20.03,8.04C20,8.36 20.34,8.69 20.66,9C21.14,9.5 21.61,9.95 21.59,10.44C21.57,10.93 21.06,11.44 20.55,11.94L16.42,16.08L15,14.66L19.25,10.42L18.29,9.46L16.87,10.87L13.12,7.12L16.96,3.29C17.35,2.9 18,2.9 18.37,3.29L20.71,5.63C21.1,6 21.1,6.65 20.71,7.04M3,17.25L12.56,7.68L16.31,11.43L6.75,21H3V17.25Z" /></g><g id="pencil"><path d="M20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18,2.9 17.35,2.9 16.96,3.29L15.12,5.12L18.87,8.87M3,17.25V21H6.75L17.81,9.93L14.06,6.18L3,17.25Z" /></g><g id="pencil-box"><path d="M19,3A2,2 0 0,1 21,5V19C21,20.11 20.1,21 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3H19M16.7,9.35C16.92,9.14 16.92,8.79 16.7,8.58L15.42,7.3C15.21,7.08 14.86,7.08 14.65,7.3L13.65,8.3L15.7,10.35L16.7,9.35M7,14.94V17H9.06L15.12,10.94L13.06,8.88L7,14.94Z" /></g><g id="pencil-box-outline"><path d="M19,19V5H5V19H19M19,3A2,2 0 0,1 21,5V19C21,20.11 20.1,21 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3H19M16.7,9.35L15.7,10.35L13.65,8.3L14.65,7.3C14.86,7.08 15.21,7.08 15.42,7.3L16.7,8.58C16.92,8.79 16.92,9.14 16.7,9.35M7,14.94L13.06,8.88L15.12,10.94L9.06,17H7V14.94Z" /></g><g id="pencil-lock"><path d="M5.5,2A2.5,2.5 0 0,0 3,4.5V5A1,1 0 0,0 2,6V10A1,1 0 0,0 3,11H8A1,1 0 0,0 9,10V6A1,1 0 0,0 8,5V4.5A2.5,2.5 0 0,0 5.5,2M5.5,3A1.5,1.5 0 0,1 7,4.5V5H4V4.5A1.5,1.5 0 0,1 5.5,3M19.66,3C19.4,3 19.16,3.09 18.97,3.28L17.13,5.13L20.88,8.88L22.72,7.03C23.11,6.64 23.11,6 22.72,5.63L20.38,3.28C20.18,3.09 19.91,3 19.66,3M16.06,6.19L5,17.25V21H8.75L19.81,9.94L16.06,6.19Z" /></g><g id="pencil-off"><path d="M18.66,2C18.4,2 18.16,2.09 17.97,2.28L16.13,4.13L19.88,7.88L21.72,6.03C22.11,5.64 22.11,5 21.72,4.63L19.38,2.28C19.18,2.09 18.91,2 18.66,2M3.28,4L2,5.28L8.5,11.75L4,16.25V20H7.75L12.25,15.5L18.72,22L20,20.72L13.5,14.25L9.75,10.5L3.28,4M15.06,5.19L11.03,9.22L14.78,12.97L18.81,8.94L15.06,5.19Z" /></g><g id="percent"><path d="M7,4A3,3 0 0,1 10,7A3,3 0 0,1 7,10A3,3 0 0,1 4,7A3,3 0 0,1 7,4M17,14A3,3 0 0,1 20,17A3,3 0 0,1 17,20A3,3 0 0,1 14,17A3,3 0 0,1 17,14M20,5.41L5.41,20L4,18.59L18.59,4L20,5.41Z" /></g><g id="pharmacy"><path d="M16,14H13V17H11V14H8V12H11V9H13V12H16M21,5H18.35L19.5,1.85L17.15,1L15.69,5H3V7L5,13L3,19V21H21V19L19,13L21,7V5Z" /></g><g id="phone"><path d="M6.62,10.79C8.06,13.62 10.38,15.94 13.21,17.38L15.41,15.18C15.69,14.9 16.08,14.82 16.43,14.93C17.55,15.3 18.75,15.5 20,15.5A1,1 0 0,1 21,16.5V20A1,1 0 0,1 20,21A17,17 0 0,1 3,4A1,1 0 0,1 4,3H7.5A1,1 0 0,1 8.5,4C8.5,5.25 8.7,6.45 9.07,7.57C9.18,7.92 9.1,8.31 8.82,8.59L6.62,10.79Z" /></g><g id="phone-bluetooth"><path d="M20,15.5C18.75,15.5 17.55,15.3 16.43,14.93C16.08,14.82 15.69,14.9 15.41,15.18L13.21,17.38C10.38,15.94 8.06,13.62 6.62,10.79L8.82,8.59C9.1,8.31 9.18,7.92 9.07,7.57C8.7,6.45 8.5,5.25 8.5,4A1,1 0 0,0 7.5,3H4A1,1 0 0,0 3,4A17,17 0 0,0 20,21A1,1 0 0,0 21,20V16.5A1,1 0 0,0 20,15.5M18,7.21L18.94,8.14L18,9.08M18,2.91L18.94,3.85L18,4.79M14.71,9.5L17,7.21V11H17.5L20.35,8.14L18.21,6L20.35,3.85L17.5,1H17V4.79L14.71,2.5L14,3.21L16.79,6L14,8.79L14.71,9.5Z" /></g><g id="phone-classic"><path d="M12,3C7.46,3 3.34,4.78 0.29,7.67C0.11,7.85 0,8.1 0,8.38C0,8.66 0.11,8.91 0.29,9.09L2.77,11.57C2.95,11.75 3.2,11.86 3.5,11.86C3.75,11.86 4,11.75 4.18,11.58C4.97,10.84 5.87,10.22 6.84,9.73C7.17,9.57 7.4,9.23 7.4,8.83V5.73C8.85,5.25 10.39,5 12,5C13.59,5 15.14,5.25 16.59,5.72V8.82C16.59,9.21 16.82,9.56 17.15,9.72C18.13,10.21 19,10.84 19.82,11.57C20,11.75 20.25,11.85 20.5,11.85C20.8,11.85 21.05,11.74 21.23,11.56L23.71,9.08C23.89,8.9 24,8.65 24,8.37C24,8.09 23.88,7.85 23.7,7.67C20.65,4.78 16.53,3 12,3M9,7V10C9,10 3,15 3,18V22H21V18C21,15 15,10 15,10V7H13V9H11V7H9M12,12A4,4 0 0,1 16,16A4,4 0 0,1 12,20A4,4 0 0,1 8,16A4,4 0 0,1 12,12M12,13.5A2.5,2.5 0 0,0 9.5,16A2.5,2.5 0 0,0 12,18.5A2.5,2.5 0 0,0 14.5,16A2.5,2.5 0 0,0 12,13.5Z" /></g><g id="phone-forward"><path d="M20,15.5C18.75,15.5 17.55,15.3 16.43,14.93C16.08,14.82 15.69,14.9 15.41,15.18L13.21,17.38C10.38,15.94 8.06,13.62 6.62,10.79L8.82,8.59C9.1,8.31 9.18,7.92 9.07,7.57C8.7,6.45 8.5,5.25 8.5,4A1,1 0 0,0 7.5,3H4A1,1 0 0,0 3,4A17,17 0 0,0 20,21A1,1 0 0,0 21,20V16.5A1,1 0 0,0 20,15.5M18,11L23,6L18,1V4H14V8H18V11Z" /></g><g id="phone-hangup"><path d="M12,9C10.4,9 8.85,9.25 7.4,9.72V12.82C7.4,13.22 7.17,13.56 6.84,13.72C5.86,14.21 4.97,14.84 4.17,15.57C4,15.75 3.75,15.86 3.5,15.86C3.2,15.86 2.95,15.74 2.77,15.56L0.29,13.08C0.11,12.9 0,12.65 0,12.38C0,12.1 0.11,11.85 0.29,11.67C3.34,8.77 7.46,7 12,7C16.54,7 20.66,8.77 23.71,11.67C23.89,11.85 24,12.1 24,12.38C24,12.65 23.89,12.9 23.71,13.08L21.23,15.56C21.05,15.74 20.8,15.86 20.5,15.86C20.25,15.86 20,15.75 19.82,15.57C19.03,14.84 18.14,14.21 17.16,13.72C16.83,13.56 16.6,13.22 16.6,12.82V9.72C15.15,9.25 13.6,9 12,9Z" /></g><g id="phone-in-talk"><path d="M15,12H17A5,5 0 0,0 12,7V9A3,3 0 0,1 15,12M19,12H21C21,7 16.97,3 12,3V5C15.86,5 19,8.13 19,12M20,15.5C18.75,15.5 17.55,15.3 16.43,14.93C16.08,14.82 15.69,14.9 15.41,15.18L13.21,17.38C10.38,15.94 8.06,13.62 6.62,10.79L8.82,8.59C9.1,8.31 9.18,7.92 9.07,7.57C8.7,6.45 8.5,5.25 8.5,4A1,1 0 0,0 7.5,3H4A1,1 0 0,0 3,4A17,17 0 0,0 20,21A1,1 0 0,0 21,20V16.5A1,1 0 0,0 20,15.5Z" /></g><g id="phone-incoming"><path d="M4,3A1,1 0 0,0 3,4A17,17 0 0,0 20,21A1,1 0 0,0 21,20V16.5A1,1 0 0,0 20,15.5C18.75,15.5 17.55,15.3 16.43,14.93C16.08,14.82 15.69,14.9 15.41,15.17L13.21,17.37C10.38,15.93 8.06,13.62 6.62,10.78L8.82,8.57C9.1,8.31 9.18,7.92 9.07,7.57C8.7,6.45 8.5,5.25 8.5,4A1,1 0 0,0 7.5,3H4M19,11V9.5H15.5L21,4L20,3L14.5,8.5V5H13V11H19Z" /></g><g id="phone-locked"><path d="M19.2,4H15.8V3.5C15.8,2.56 16.56,1.8 17.5,1.8C18.44,1.8 19.2,2.56 19.2,3.5M20,4V3.5A2.5,2.5 0 0,0 17.5,1A2.5,2.5 0 0,0 15,3.5V4A1,1 0 0,0 14,5V9A1,1 0 0,0 15,10H20A1,1 0 0,0 21,9V5A1,1 0 0,0 20,4M20,15.5C18.75,15.5 17.55,15.3 16.43,14.93C16.08,14.82 15.69,14.9 15.41,15.18L13.21,17.38C10.38,15.94 8.06,13.62 6.62,10.79L8.82,8.59C9.1,8.31 9.18,7.92 9.07,7.57C8.7,6.45 8.5,5.25 8.5,4A1,1 0 0,0 7.5,3H4A1,1 0 0,0 3,4A17,17 0 0,0 20,21A1,1 0 0,0 21,20V16.5A1,1 0 0,0 20,15.5Z" /></g><g id="phone-log"><path d="M20,15.5A1,1 0 0,1 21,16.5V20A1,1 0 0,1 20,21A17,17 0 0,1 3,4A1,1 0 0,1 4,3H7.5A1,1 0 0,1 8.5,4C8.5,5.24 8.7,6.45 9.07,7.57C9.18,7.92 9.1,8.31 8.82,8.58L6.62,10.79C8.06,13.62 10.38,15.94 13.21,17.38L15.41,15.18C15.69,14.9 16.08,14.82 16.43,14.93C17.55,15.3 18.75,15.5 20,15.5M12,3H14V5H12M15,3H21V5H15M12,6H14V8H12M15,6H21V8H15M12,9H14V11H12M15,9H21V11H15" /></g><g id="phone-minus"><path d="M4,3A1,1 0 0,0 3,4A17,17 0 0,0 20,21A1,1 0 0,0 21,20V16.5A1,1 0 0,0 20,15.5C18.76,15.5 17.55,15.3 16.43,14.93C16.08,14.82 15.69,14.9 15.41,15.18L13.21,17.38C10.38,15.94 8.07,13.62 6.62,10.79L8.82,8.58C9.1,8.31 9.18,7.92 9.07,7.57C8.7,6.45 8.5,5.24 8.5,4A1,1 0 0,0 7.5,3M13,6V8H21V6" /></g><g id="phone-missed"><path d="M23.71,16.67C20.66,13.77 16.54,12 12,12C7.46,12 3.34,13.77 0.29,16.67C0.11,16.85 0,17.1 0,17.38C0,17.65 0.11,17.9 0.29,18.08L2.77,20.56C2.95,20.74 3.2,20.86 3.5,20.86C3.75,20.86 4,20.75 4.18,20.57C4.97,19.83 5.86,19.21 6.84,18.72C7.17,18.56 7.4,18.22 7.4,17.82V14.72C8.85,14.25 10.39,14 12,14C13.6,14 15.15,14.25 16.6,14.72V17.82C16.6,18.22 16.83,18.56 17.16,18.72C18.14,19.21 19.03,19.83 19.82,20.57C20,20.75 20.25,20.86 20.5,20.86C20.8,20.86 21.05,20.74 21.23,20.56L23.71,18.08C23.89,17.9 24,17.65 24,17.38C24,17.1 23.89,16.85 23.71,16.67M6.5,5.5L12,11L19,4L18,3L12,9L7.5,4.5H11V3H5V9H6.5V5.5Z" /></g><g id="phone-outgoing"><path d="M4,3A1,1 0 0,0 3,4A17,17 0 0,0 20,21A1,1 0 0,0 21,20V16.5A1,1 0 0,0 20,15.5C18.75,15.5 17.55,15.3 16.43,14.93C16.08,14.82 15.69,14.9 15.41,15.17L13.21,17.37C10.38,15.93 8.06,13.62 6.62,10.78L8.82,8.57C9.1,8.31 9.18,7.92 9.07,7.57C8.7,6.45 8.5,5.25 8.5,4A1,1 0 0,0 7.5,3H4M15,3V4.5H18.5L13,10L14,11L19.5,5.5V9H21V3H15Z" /></g><g id="phone-paused"><path d="M19,10H21V3H19M20,15.5C18.75,15.5 17.55,15.3 16.43,14.93C16.08,14.82 15.69,14.9 15.41,15.18L13.21,17.38C10.38,15.94 8.06,13.62 6.62,10.79L8.82,8.59C9.1,8.31 9.18,7.92 9.07,7.57C8.7,6.45 8.5,5.25 8.5,4A1,1 0 0,0 7.5,3H4A1,1 0 0,0 3,4A17,17 0 0,0 20,21A1,1 0 0,0 21,20V16.5A1,1 0 0,0 20,15.5M17,3H15V10H17V3Z" /></g><g id="phone-plus"><path d="M4,3A1,1 0 0,0 3,4A17,17 0 0,0 20,21A1,1 0 0,0 21,20V16.5A1,1 0 0,0 20,15.5C18.76,15.5 17.55,15.3 16.43,14.93C16.08,14.82 15.69,14.9 15.41,15.18L13.21,17.38C10.38,15.94 8.07,13.62 6.62,10.79L8.82,8.58C9.1,8.31 9.18,7.92 9.07,7.57C8.7,6.45 8.5,5.24 8.5,4A1,1 0 0,0 7.5,3M16,3V6H13V8H16V11H18V8H21V6H18V3" /></g><g id="phone-settings"><path d="M19,11H21V9H19M20,15.5C18.75,15.5 17.55,15.3 16.43,14.93C16.08,14.82 15.69,14.9 15.41,15.18L13.21,17.38C10.38,15.94 8.06,13.62 6.62,10.79L8.82,8.59C9.1,8.31 9.18,7.92 9.07,7.57C8.7,6.45 8.5,5.25 8.5,4A1,1 0 0,0 7.5,3H4A1,1 0 0,0 3,4A17,17 0 0,0 20,21A1,1 0 0,0 21,20V16.5A1,1 0 0,0 20,15.5M17,9H15V11H17M13,9H11V11H13V9Z" /></g><g id="phone-voip"><path d="M13,17V19H14A1,1 0 0,1 15,20H22V22H15A1,1 0 0,1 14,23H10A1,1 0 0,1 9,22H2V20H9A1,1 0 0,1 10,19H11V17H13M23.7,7.67C23.88,7.85 24,8.09 24,8.37C24,8.65 23.89,8.9 23.71,9.08L21.23,11.56C21.05,11.74 20.8,11.85 20.5,11.85C20.25,11.85 20,11.75 19.82,11.57C19,10.84 18.13,10.21 17.15,9.72C16.82,9.56 16.59,9.21 16.59,8.82V5.72C15.14,5.25 13.59,5 12,5C10.4,5 8.85,5.25 7.4,5.73V8.83C7.4,9.23 7.17,9.57 6.84,9.73C5.87,10.22 4.97,10.84 4.18,11.58C4,11.75 3.75,11.86 3.5,11.86C3.2,11.86 2.95,11.75 2.77,11.57L0.29,9.09C0.11,8.91 0,8.66 0,8.38C0,8.1 0.11,7.85 0.29,7.67C3.34,4.78 7.46,3 12,3C16.53,3 20.65,4.78 23.7,7.67M11,10V15H10V10H11M12,10H15V13H13V15H12V10M14,12V11H13V12H14Z" /></g><g id="pi"><path d="M4,5V7H6V19H8V7H14V16A3,3 0 0,0 17,19A3,3 0 0,0 20,16H18A1,1 0 0,1 17,17A1,1 0 0,1 16,16V7H18V5" /></g><g id="pi-box"><path d="M5,3C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3M6,7H17V9H15V14A1,1 0 0,0 16,15A1,1 0 0,0 17,14H19A3,3 0 0,1 16,17A3,3 0 0,1 13,14V9H10V17H8V9H6" /></g><g id="piano"><path d="M4,3H20A2,2 0 0,1 22,5V19A2,2 0 0,1 20,21H4A2,2 0 0,1 2,19V5A2,2 0 0,1 4,3M4,5V19H8V13H6.75V5H4M9,19H15V13H13.75V5H10.25V13H9V19M16,19H20V5H17.25V13H16V19Z" /></g><g id="pig"><path d="M9.5,9A1.5,1.5 0 0,0 8,10.5A1.5,1.5 0 0,0 9.5,12A1.5,1.5 0 0,0 11,10.5A1.5,1.5 0 0,0 9.5,9M14.5,9A1.5,1.5 0 0,0 13,10.5A1.5,1.5 0 0,0 14.5,12A1.5,1.5 0 0,0 16,10.5A1.5,1.5 0 0,0 14.5,9M12,4L12.68,4.03C13.62,3.24 14.82,2.59 15.72,2.35C17.59,1.85 20.88,2.23 21.31,3.83C21.62,5 20.6,6.45 19.03,7.38C20.26,8.92 21,10.87 21,13A9,9 0 0,1 12,22A9,9 0 0,1 3,13C3,10.87 3.74,8.92 4.97,7.38C3.4,6.45 2.38,5 2.69,3.83C3.12,2.23 6.41,1.85 8.28,2.35C9.18,2.59 10.38,3.24 11.32,4.03L12,4M10,16A1,1 0 0,1 11,17A1,1 0 0,1 10,18A1,1 0 0,1 9,17A1,1 0 0,1 10,16M14,16A1,1 0 0,1 15,17A1,1 0 0,1 14,18A1,1 0 0,1 13,17A1,1 0 0,1 14,16M12,13C9.24,13 7,15.34 7,17C7,18.66 9.24,20 12,20C14.76,20 17,18.66 17,17C17,15.34 14.76,13 12,13M7.76,4.28C7.31,4.16 4.59,4.35 4.59,4.35C4.59,4.35 6.8,6.1 7.24,6.22C7.69,6.34 9.77,6.43 9.91,5.9C10.06,5.36 8.2,4.4 7.76,4.28M16.24,4.28C15.8,4.4 13.94,5.36 14.09,5.9C14.23,6.43 16.31,6.34 16.76,6.22C17.2,6.1 19.41,4.35 19.41,4.35C19.41,4.35 16.69,4.16 16.24,4.28Z" /></g><g id="pill"><path d="M4.22,11.29L11.29,4.22C13.64,1.88 17.43,1.88 19.78,4.22C22.12,6.56 22.12,10.36 19.78,12.71L12.71,19.78C10.36,22.12 6.56,22.12 4.22,19.78C1.88,17.43 1.88,13.64 4.22,11.29M5.64,12.71C4.59,13.75 4.24,15.24 4.6,16.57L10.59,10.59L14.83,14.83L18.36,11.29C19.93,9.73 19.93,7.2 18.36,5.64C16.8,4.07 14.27,4.07 12.71,5.64L5.64,12.71Z" /></g><g id="pin"><path d="M16,12V4H17V2H7V4H8V12L6,14V16H11.2V22H12.8V16H18V14L16,12Z" /></g><g id="pin-off"><path d="M2,5.27L3.28,4L20,20.72L18.73,22L12.8,16.07V22H11.2V16H6V14L8,12V11.27L2,5.27M16,12L18,14V16H17.82L8,6.18V4H7V2H17V4H16V12Z" /></g><g id="pine-tree"><path d="M10,21V18H3L8,13H5L10,8H7L12,3L17,8H14L19,13H16L21,18H14V21H10Z" /></g><g id="pine-tree-box"><path d="M4,2H20A2,2 0 0,1 22,4V20A2,2 0 0,1 20,22H4A2,2 0 0,1 2,20V4A2,2 0 0,1 4,2M11,19H13V17H18L14,13H17L13,9H16L12,5L8,9H11L7,13H10L6,17H11V19Z" /></g><g id="pinterest"><path d="M13.25,17.25C12.25,17.25 11.29,16.82 10.6,16.1L9.41,20.1L9.33,20.36L9.29,20.34C9.04,20.75 8.61,21 8.12,21C7.37,21 6.75,20.38 6.75,19.62C6.75,19.56 6.76,19.5 6.77,19.44L6.75,19.43L6.81,19.21L9.12,12.26C9.12,12.26 8.87,11.5 8.87,10.42C8.87,8.27 10.03,7.62 10.95,7.62C11.88,7.62 12.73,7.95 12.73,9.26C12.73,10.94 11.61,11.8 11.61,13C11.61,13.94 12.37,14.69 13.29,14.69C16.21,14.69 17.25,12.5 17.25,10.44C17.25,7.71 14.89,5.5 12,5.5C9.1,5.5 6.75,7.71 6.75,10.44C6.75,11.28 7,12.12 7.43,12.85C7.54,13.05 7.6,13.27 7.6,13.5A1.25,1.25 0 0,1 6.35,14.75C5.91,14.75 5.5,14.5 5.27,14.13C4.6,13 4.25,11.73 4.25,10.44C4.25,6.33 7.73,3 12,3C16.27,3 19.75,6.33 19.75,10.44C19.75,13.72 17.71,17.25 13.25,17.25Z" /></g><g id="pinterest-box"><path d="M13,16.2C12.2,16.2 11.43,15.86 10.88,15.28L9.93,18.5L9.86,18.69L9.83,18.67C9.64,19 9.29,19.2 8.9,19.2C8.29,19.2 7.8,18.71 7.8,18.1C7.8,18.05 7.81,18 7.81,17.95H7.8L7.85,17.77L9.7,12.21C9.7,12.21 9.5,11.59 9.5,10.73C9.5,9 10.42,8.5 11.16,8.5C11.91,8.5 12.58,8.76 12.58,9.81C12.58,11.15 11.69,11.84 11.69,12.81C11.69,13.55 12.29,14.16 13.03,14.16C15.37,14.16 16.2,12.4 16.2,10.75C16.2,8.57 14.32,6.8 12,6.8C9.68,6.8 7.8,8.57 7.8,10.75C7.8,11.42 8,12.09 8.34,12.68C8.43,12.84 8.5,13 8.5,13.2A1,1 0 0,1 7.5,14.2C7.13,14.2 6.79,14 6.62,13.7C6.08,12.81 5.8,11.79 5.8,10.75C5.8,7.47 8.58,4.8 12,4.8C15.42,4.8 18.2,7.47 18.2,10.75C18.2,13.37 16.57,16.2 13,16.2M20,2H4C2.89,2 2,2.89 2,4V20A2,2 0 0,0 4,22H20A2,2 0 0,0 22,20V4C22,2.89 21.1,2 20,2Z" /></g><g id="pizza"><path d="M12,15A2,2 0 0,1 10,13C10,11.89 10.9,11 12,11A2,2 0 0,1 14,13A2,2 0 0,1 12,15M7,7C7,5.89 7.89,5 9,5A2,2 0 0,1 11,7A2,2 0 0,1 9,9C7.89,9 7,8.1 7,7M12,2C8.43,2 5.23,3.54 3,6L12,22L21,6C18.78,3.54 15.57,2 12,2Z" /></g><g id="plane-shield"><path d="M12,1L3,5V11C3,16.55 6.84,21.74 12,23C17.16,21.74 21,16.55 21,11V5L12,1M12,5.68C12.5,5.68 12.95,6.11 12.95,6.63V10.11L18,13.26V14.53L12.95,12.95V16.42L14.21,17.37V18.32L12,17.68L9.79,18.32V17.37L11.05,16.42V12.95L6,14.53V13.26L11.05,10.11V6.63C11.05,6.11 11.5,5.68 12,5.68Z" /></g><g id="play"><path d="M8,5.14V19.14L19,12.14L8,5.14Z" /></g><g id="play-box-outline"><path d="M19,19H5V5H19M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3M10,8V16L15,12L10,8Z" /></g><g id="play-circle"><path d="M10,16.5V7.5L16,12M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" /></g><g id="play-circle-outline"><path d="M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M10,16.5L16,12L10,7.5V16.5Z" /></g><g id="play-pause"><path d="M3,5V19L11,12M13,19H16V5H13M18,5V19H21V5" /></g><g id="play-protected-content"><path d="M2,5V18H11V16H4V7H17V11H19V5H2M9,9V14L12.5,11.5L9,9M21.04,11.67L16.09,16.62L13.96,14.5L12.55,15.91L16.09,19.45L22.45,13.09L21.04,11.67Z" /></g><g id="playlist-check"><path d="M14,10H2V12H14V10M14,6H2V8H14V6M2,16H10V14H2V16M21.5,11.5L23,13L16,20L11.5,15.5L13,14L16,17L21.5,11.5Z" /></g><g id="playlist-minus"><path d="M2,16H10V14H2M12,14V16H22V14M14,6H2V8H14M14,10H2V12H14V10Z" /></g><g id="playlist-play"><path d="M19,9H2V11H19V9M19,5H2V7H19V5M2,15H15V13H2V15M17,13V19L22,16L17,13Z" /></g><g id="playlist-plus"><path d="M2,16H10V14H2M18,14V10H16V14H12V16H16V20H18V16H22V14M14,6H2V8H14M14,10H2V12H14V10Z" /></g><g id="playlist-remove"><path d="M2,6V8H14V6H2M2,10V12H10V10H2M14.17,10.76L12.76,12.17L15.59,15L12.76,17.83L14.17,19.24L17,16.41L19.83,19.24L21.24,17.83L18.41,15L21.24,12.17L19.83,10.76L17,13.59L14.17,10.76M2,14V16H10V14H2Z" /></g><g id="playstation"><path d="M9.5,4.27C10.88,4.53 12.9,5.14 14,5.5C16.75,6.45 17.69,7.63 17.69,10.29C17.69,12.89 16.09,13.87 14.05,12.89V8.05C14.05,7.5 13.95,6.97 13.41,6.82C13,6.69 12.76,7.07 12.76,7.63V19.73L9.5,18.69V4.27M13.37,17.62L18.62,15.75C19.22,15.54 19.31,15.24 18.83,15.08C18.34,14.92 17.47,14.97 16.87,15.18L13.37,16.41V14.45L13.58,14.38C13.58,14.38 14.59,14 16,13.87C17.43,13.71 19.17,13.89 20.53,14.4C22.07,14.89 22.25,15.61 21.86,16.1C21.46,16.6 20.5,16.95 20.5,16.95L13.37,19.5V17.62M3.5,17.42C1.93,17 1.66,16.05 2.38,15.5C3.05,15 4.18,14.65 4.18,14.65L8.86,13V14.88L5.5,16.09C4.9,16.3 4.81,16.6 5.29,16.76C5.77,16.92 6.65,16.88 7.24,16.66L8.86,16.08V17.77L8.54,17.83C6.92,18.09 5.2,18 3.5,17.42Z" /></g><g id="plex"><path d="M4,2C2.89,2 2,2.89 2,4V20C2,21.11 2.89,22 4,22H20C21.11,22 22,21.11 22,20V4C22,2.89 21.11,2 20,2H4M8.56,6H12.06L15.5,12L12.06,18H8.56L12,12L8.56,6Z" /></g><g id="plus"><path d="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" /></g><g id="plus-box"><path d="M17,13H13V17H11V13H7V11H11V7H13V11H17M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3Z" /></g><g id="plus-circle"><path d="M17,13H13V17H11V13H7V11H11V7H13V11H17M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" /></g><g id="plus-circle-multiple-outline"><path d="M16,8H14V11H11V13H14V16H16V13H19V11H16M2,12C2,9.21 3.64,6.8 6,5.68V3.5C2.5,4.76 0,8.09 0,12C0,15.91 2.5,19.24 6,20.5V18.32C3.64,17.2 2,14.79 2,12M15,3C10.04,3 6,7.04 6,12C6,16.96 10.04,21 15,21C19.96,21 24,16.96 24,12C24,7.04 19.96,3 15,3M15,19C11.14,19 8,15.86 8,12C8,8.14 11.14,5 15,5C18.86,5 22,8.14 22,12C22,15.86 18.86,19 15,19Z" /></g><g id="plus-circle-outline"><path d="M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M13,7H11V11H7V13H11V17H13V13H17V11H13V7Z" /></g><g id="plus-network"><path d="M16,11V9H13V6H11V9H8V11H11V14H13V11H16M17,3A2,2 0 0,1 19,5V15A2,2 0 0,1 17,17H13V19H14A1,1 0 0,1 15,20H22V22H15A1,1 0 0,1 14,23H10A1,1 0 0,1 9,22H2V20H9A1,1 0 0,1 10,19H11V17H7C5.89,17 5,16.1 5,15V5A2,2 0 0,1 7,3H17Z" /></g><g id="plus-one"><path d="M10,8V12H14V14H10V18H8V14H4V12H8V8H10M14.5,6.08L19,5V18H17V7.4L14.5,7.9V6.08Z" /></g><g id="pocket"><path d="M22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12V4.5A2.5,2.5 0 0,1 4.5,2H19.5A2.5,2.5 0 0,1 22,4.5V12M15.88,8.25L12,12.13L8.12,8.24C7.53,7.65 6.58,7.65 6,8.24C5.41,8.82 5.41,9.77 6,10.36L10.93,15.32C11.5,15.9 12.47,15.9 13.06,15.32L18,10.37C18.59,9.78 18.59,8.83 18,8.25C17.42,7.66 16.47,7.66 15.88,8.25Z" /></g><g id="pokeball"><path d="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4C7.92,4 4.55,7.05 4.06,11H8.13C8.57,9.27 10.14,8 12,8C13.86,8 15.43,9.27 15.87,11H19.94C19.45,7.05 16.08,4 12,4M12,20C16.08,20 19.45,16.95 19.94,13H15.87C15.43,14.73 13.86,16 12,16C10.14,16 8.57,14.73 8.13,13H4.06C4.55,16.95 7.92,20 12,20M12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12A2,2 0 0,0 12,10Z" /></g><g id="polaroid"><path d="M6,3H18A2,2 0 0,1 20,5V19A2,2 0 0,1 18,21H6A2,2 0 0,1 4,19V5A2,2 0 0,1 6,3M6,5V17H18V5H6Z" /></g><g id="poll"><path d="M3,22V8H7V22H3M10,22V2H14V22H10M17,22V14H21V22H17Z" /></g><g id="poll-box"><path d="M17,17H15V13H17M13,17H11V7H13M9,17H7V10H9M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3Z" /></g><g id="polymer"><path d="M19,4H15L7.1,16.63L4.5,12L9,4H5L0.5,12L5,20H9L16.89,7.37L19.5,12L15,20H19L23.5,12L19,4Z" /></g><g id="pool"><path d="M2,15C3.67,14.25 5.33,13.5 7,13.17V5A3,3 0 0,1 10,2C11.31,2 12.42,2.83 12.83,4H10A1,1 0 0,0 9,5V6H14V5A3,3 0 0,1 17,2C18.31,2 19.42,2.83 19.83,4H17A1,1 0 0,0 16,5V14.94C18,14.62 20,13 22,13V15C19.78,15 17.56,17 15.33,17C13.11,17 10.89,15 8.67,15C6.44,15 4.22,16 2,17V15M14,8H9V10H14V8M14,12H9V13C10.67,13.16 12.33,14.31 14,14.79V12M2,19C4.22,18 6.44,17 8.67,17C10.89,17 13.11,19 15.33,19C17.56,19 19.78,17 22,17V19C19.78,19 17.56,21 15.33,21C13.11,21 10.89,19 8.67,19C6.44,19 4.22,20 2,21V19Z" /></g><g id="popcorn"><path d="M7,22H4.75C4.75,22 4,22 3.81,20.65L2.04,3.81L2,3.5C2,2.67 2.9,2 4,2C5.1,2 6,2.67 6,3.5C6,2.67 6.9,2 8,2C9.1,2 10,2.67 10,3.5C10,2.67 10.9,2 12,2C13.09,2 14,2.66 14,3.5V3.5C14,2.67 14.9,2 16,2C17.1,2 18,2.67 18,3.5C18,2.67 18.9,2 20,2C21.1,2 22,2.67 22,3.5L21.96,3.81L20.19,20.65C20,22 19.25,22 19.25,22H17L16.5,22H13.75L10.25,22H7.5L7,22M17.85,4.93C17.55,4.39 16.84,4 16,4C15.19,4 14.36,4.36 14,4.87L13.78,20H16.66L17.85,4.93M10,4.87C9.64,4.36 8.81,4 8,4C7.16,4 6.45,4.39 6.15,4.93L7.34,20H10.22L10,4.87Z" /></g><g id="pot"><path d="M19,19A2,2 0 0,1 17,21H7A2,2 0 0,1 5,19V13H3V10H21V13H19V19M6,6H8V8H6V6M11,6H13V8H11V6M16,6H18V8H16V6M18,3H20V5H18V3M13,3H15V5H13V3M8,3H10V5H8V3Z" /></g><g id="pot-mix"><path d="M19,19A2,2 0 0,1 17,21H7A2,2 0 0,1 5,19V13H3V10H14L18,3.07L19.73,4.07L16.31,10H21V13H19V19Z" /></g><g id="pound"><path d="M5.41,21L6.12,17H2.12L2.47,15H6.47L7.53,9H3.53L3.88,7H7.88L8.59,3H10.59L9.88,7H15.88L16.59,3H18.59L17.88,7H21.88L21.53,9H17.53L16.47,15H20.47L20.12,17H16.12L15.41,21H13.41L14.12,17H8.12L7.41,21H5.41M9.53,9L8.47,15H14.47L15.53,9H9.53Z" /></g><g id="pound-box"><path d="M3,5A2,2 0 0,1 5,3H19A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5C3.89,21 3,20.1 3,19V5M7,18H9L9.35,16H13.35L13,18H15L15.35,16H17.35L17.71,14H15.71L16.41,10H18.41L18.76,8H16.76L17.12,6H15.12L14.76,8H10.76L11.12,6H9.12L8.76,8H6.76L6.41,10H8.41L7.71,14H5.71L5.35,16H7.35L7,18M10.41,10H14.41L13.71,14H9.71L10.41,10Z" /></g><g id="power"><path d="M16.56,5.44L15.11,6.89C16.84,7.94 18,9.83 18,12A6,6 0 0,1 12,18A6,6 0 0,1 6,12C6,9.83 7.16,7.94 8.88,6.88L7.44,5.44C5.36,6.88 4,9.28 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12C20,9.28 18.64,6.88 16.56,5.44M13,3H11V13H13" /></g><g id="power-plug"><path d="M16,7V3H14V7H10V3H8V7H8C7,7 6,8 6,9V14.5L9.5,18V21H14.5V18L18,14.5V9C18,8 17,7 16,7Z" /></g><g id="power-plug-off"><path d="M8,3V6.18C11.1,9.23 14.1,12.3 17.2,15.3C17.4,15 17.8,14.8 18,14.4V8.8C18,7.68 16.7,7.16 16,6.84V3H14V7H10V3H8M3.28,4C2.85,4.42 2.43,4.85 2,5.27L6,9.27V14.5C7.17,15.65 8.33,16.83 9.5,18V21H14.5V18C14.72,17.73 14.95,18.33 15.17,18.44C16.37,19.64 17.47,20.84 18.67,22.04C19.17,21.64 19.57,21.14 19.97,20.74C14.37,15.14 8.77,9.64 3.27,4.04L3.28,4Z" /></g><g id="power-settings"><path d="M15,24H17V22H15M16.56,4.44L15.11,5.89C16.84,6.94 18,8.83 18,11A6,6 0 0,1 12,17A6,6 0 0,1 6,11C6,8.83 7.16,6.94 8.88,5.88L7.44,4.44C5.36,5.88 4,8.28 4,11A8,8 0 0,0 12,19A8,8 0 0,0 20,11C20,8.28 18.64,5.88 16.56,4.44M13,2H11V12H13M11,24H13V22H11M7,24H9V22H7V24Z" /></g><g id="power-socket"><path d="M15,15H17V11H15M7,15H9V11H7M11,13H13V9H11M8.83,7H15.2L19,10.8V17H5V10.8M8,5L3,10V19H21V10L16,5H8Z" /></g><g id="presentation"><path d="M2,3H10A2,2 0 0,1 12,1A2,2 0 0,1 14,3H22V5H21V16H15.25L17,22H15L13.25,16H10.75L9,22H7L8.75,16H3V5H2V3M5,5V14H19V5H5Z" /></g><g id="presentation-play"><path d="M2,3H10A2,2 0 0,1 12,1A2,2 0 0,1 14,3H22V5H21V16H15.25L17,22H15L13.25,16H10.75L9,22H7L8.75,16H3V5H2V3M5,5V14H19V5H5M11.85,11.85C11.76,11.94 11.64,12 11.5,12A0.5,0.5 0 0,1 11,11.5V7.5A0.5,0.5 0 0,1 11.5,7C11.64,7 11.76,7.06 11.85,7.15L13.25,8.54C13.57,8.86 13.89,9.18 13.89,9.5C13.89,9.82 13.57,10.14 13.25,10.46L11.85,11.85Z" /></g><g id="printer"><path d="M18,3H6V7H18M19,12A1,1 0 0,1 18,11A1,1 0 0,1 19,10A1,1 0 0,1 20,11A1,1 0 0,1 19,12M16,19H8V14H16M19,8H5A3,3 0 0,0 2,11V17H6V21H18V17H22V11A3,3 0 0,0 19,8Z" /></g><g id="printer-3d"><path d="M19,6A1,1 0 0,0 20,5A1,1 0 0,0 19,4A1,1 0 0,0 18,5A1,1 0 0,0 19,6M19,2A3,3 0 0,1 22,5V11H18V7H6V11H2V5A3,3 0 0,1 5,2H19M18,18.25C18,18.63 17.79,18.96 17.47,19.13L12.57,21.82C12.4,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L6.53,19.13C6.21,18.96 6,18.63 6,18.25V13C6,12.62 6.21,12.29 6.53,12.12L11.43,9.68C11.59,9.56 11.79,9.5 12,9.5C12.21,9.5 12.4,9.56 12.57,9.68L17.47,12.12C17.79,12.29 18,12.62 18,13V18.25M12,11.65L9.04,13L12,14.6L14.96,13L12,11.65M8,17.66L11,19.29V16.33L8,14.71V17.66M16,17.66V14.71L13,16.33V19.29L16,17.66Z" /></g><g id="printer-alert"><path d="M14,4V8H6V4H14M15,13A1,1 0 0,0 16,12A1,1 0 0,0 15,11A1,1 0 0,0 14,12A1,1 0 0,0 15,13M13,19V15H7V19H13M15,9A3,3 0 0,1 18,12V17H15V21H5V17H2V12A3,3 0 0,1 5,9H15M22,7V12H20V7H22M22,14V16H20V14H22Z" /></g><g id="priority-high"><path d="M14,19H22V17H14V19M14,13.5H22V11.5H14V13.5M14,8H22V6H14V8M2,12.5C2,8.92 4.92,6 8.5,6H9V4L12,7L9,10V8H8.5C6,8 4,10 4,12.5C4,15 6,17 8.5,17H12V19H8.5C4.92,19 2,16.08 2,12.5Z" /></g><g id="priority-low"><path d="M14,5H22V7H14V5M14,10.5H22V12.5H14V10.5M14,16H22V18H14V16M2,11.5C2,15.08 4.92,18 8.5,18H9V20L12,17L9,14V16H8.5C6,16 4,14 4,11.5C4,9 6,7 8.5,7H12V5H8.5C4.92,5 2,7.92 2,11.5Z" /></g><g id="professional-hexagon"><path d="M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M5,9V15H6.25V13H7A2,2 0 0,0 9,11A2,2 0 0,0 7,9H5M6.25,12V10H6.75A1,1 0 0,1 7.75,11A1,1 0 0,1 6.75,12H6.25M9.75,9V15H11V13H11.75L12.41,15H13.73L12.94,12.61C13.43,12.25 13.75,11.66 13.75,11A2,2 0 0,0 11.75,9H9.75M11,12V10H11.5A1,1 0 0,1 12.5,11A1,1 0 0,1 11.5,12H11M17,9C15.62,9 14.5,10.34 14.5,12C14.5,13.66 15.62,15 17,15C18.38,15 19.5,13.66 19.5,12C19.5,10.34 18.38,9 17,9M17,10.25C17.76,10.25 18.38,11.03 18.38,12C18.38,12.97 17.76,13.75 17,13.75C16.24,13.75 15.63,12.97 15.63,12C15.63,11.03 16.24,10.25 17,10.25Z" /></g><g id="projector"><path d="M16,6C14.87,6 13.77,6.35 12.84,7H4C2.89,7 2,7.89 2,9V15C2,16.11 2.89,17 4,17H5V18A1,1 0 0,0 6,19H8A1,1 0 0,0 9,18V17H15V18A1,1 0 0,0 16,19H18A1,1 0 0,0 19,18V17H20C21.11,17 22,16.11 22,15V9C22,7.89 21.11,7 20,7H19.15C18.23,6.35 17.13,6 16,6M16,7.5A3.5,3.5 0 0,1 19.5,11A3.5,3.5 0 0,1 16,14.5A3.5,3.5 0 0,1 12.5,11A3.5,3.5 0 0,1 16,7.5M4,9H8V10H4V9M16,9A2,2 0 0,0 14,11A2,2 0 0,0 16,13A2,2 0 0,0 18,11A2,2 0 0,0 16,9M4,11H8V12H4V11M4,13H8V14H4V13Z" /></g><g id="projector-screen"><path d="M4,2A1,1 0 0,0 3,3V4A1,1 0 0,0 4,5H5V14H11V16.59L6.79,20.79L8.21,22.21L11,19.41V22H13V19.41L15.79,22.21L17.21,20.79L13,16.59V14H19V5H20A1,1 0 0,0 21,4V3A1,1 0 0,0 20,2H4Z" /></g><g id="publish"><path d="M5,4V6H19V4H5M5,14H9V20H15V14H19L12,7L5,14Z" /></g><g id="pulse"><path d="M3,13H5.79L10.1,4.79L11.28,13.75L14.5,9.66L17.83,13H21V15H17L14.67,12.67L9.92,18.73L8.94,11.31L7,15H3V13Z" /></g><g id="puzzle"><path d="M20.5,11H19V7C19,5.89 18.1,5 17,5H13V3.5A2.5,2.5 0 0,0 10.5,1A2.5,2.5 0 0,0 8,3.5V5H4A2,2 0 0,0 2,7V10.8H3.5C5,10.8 6.2,12 6.2,13.5C6.2,15 5,16.2 3.5,16.2H2V20A2,2 0 0,0 4,22H7.8V20.5C7.8,19 9,17.8 10.5,17.8C12,17.8 13.2,19 13.2,20.5V22H17A2,2 0 0,0 19,20V16H20.5A2.5,2.5 0 0,0 23,13.5A2.5,2.5 0 0,0 20.5,11Z" /></g><g id="qqchat"><path d="M3.18,13.54C3.76,12.16 4.57,11.14 5.17,10.92C5.16,10.12 5.31,9.62 5.56,9.22C5.56,9.19 5.5,8.86 5.72,8.45C5.87,4.85 8.21,2 12,2C15.79,2 18.13,4.85 18.28,8.45C18.5,8.86 18.44,9.19 18.44,9.22C18.69,9.62 18.84,10.12 18.83,10.92C19.43,11.14 20.24,12.16 20.82,13.55C21.57,15.31 21.69,17 21.09,17.3C20.68,17.5 20.03,17 19.42,16.12C19.18,17.1 18.58,18 17.73,18.71C18.63,19.04 19.21,19.58 19.21,20.19C19.21,21.19 17.63,22 15.69,22C13.93,22 12.5,21.34 12.21,20.5H11.79C11.5,21.34 10.07,22 8.31,22C6.37,22 4.79,21.19 4.79,20.19C4.79,19.58 5.37,19.04 6.27,18.71C5.42,18 4.82,17.1 4.58,16.12C3.97,17 3.32,17.5 2.91,17.3C2.31,17 2.43,15.31 3.18,13.54Z" /></g><g id="qrcode"><path d="M3,11H5V13H3V11M11,5H13V9H11V5M9,11H13V15H11V13H9V11M15,11H17V13H19V11H21V13H19V15H21V19H19V21H17V19H13V21H11V17H15V15H17V13H15V11M19,19V15H17V19H19M15,3H21V9H15V3M17,5V7H19V5H17M3,3H9V9H3V3M5,5V7H7V5H5M3,15H9V21H3V15M5,17V19H7V17H5Z" /></g><g id="qrcode-scan"><path d="M4,4H10V10H4V4M20,4V10H14V4H20M14,15H16V13H14V11H16V13H18V11H20V13H18V15H20V18H18V20H16V18H13V20H11V16H14V15M16,15V18H18V15H16M4,20V14H10V20H4M6,6V8H8V6H6M16,6V8H18V6H16M6,16V18H8V16H6M4,11H6V13H4V11M9,11H13V15H11V13H9V11M11,6H13V10H11V6M2,2V6H0V2A2,2 0 0,1 2,0H6V2H2M22,0A2,2 0 0,1 24,2V6H22V2H18V0H22M2,18V22H6V24H2A2,2 0 0,1 0,22V18H2M22,22V18H24V22A2,2 0 0,1 22,24H18V22H22Z" /></g><g id="quadcopter"><path d="M5.5,1C8,1 10,3 10,5.5C10,6.38 9.75,7.2 9.31,7.9L9.41,8H14.59L14.69,7.9C14.25,7.2 14,6.38 14,5.5C14,3 16,1 18.5,1C21,1 23,3 23,5.5C23,8 21,10 18.5,10C17.62,10 16.8,9.75 16.1,9.31L15,10.41V13.59L16.1,14.69C16.8,14.25 17.62,14 18.5,14C21,14 23,16 23,18.5C23,21 21,23 18.5,23C16,23 14,21 14,18.5C14,17.62 14.25,16.8 14.69,16.1L14.59,16H9.41L9.31,16.1C9.75,16.8 10,17.62 10,18.5C10,21 8,23 5.5,23C3,23 1,21 1,18.5C1,16 3,14 5.5,14C6.38,14 7.2,14.25 7.9,14.69L9,13.59V10.41L7.9,9.31C7.2,9.75 6.38,10 5.5,10C3,10 1,8 1,5.5C1,3 3,1 5.5,1M5.5,3A2.5,2.5 0 0,0 3,5.5A2.5,2.5 0 0,0 5.5,8A2.5,2.5 0 0,0 8,5.5A2.5,2.5 0 0,0 5.5,3M5.5,16A2.5,2.5 0 0,0 3,18.5A2.5,2.5 0 0,0 5.5,21A2.5,2.5 0 0,0 8,18.5A2.5,2.5 0 0,0 5.5,16M18.5,3A2.5,2.5 0 0,0 16,5.5A2.5,2.5 0 0,0 18.5,8A2.5,2.5 0 0,0 21,5.5A2.5,2.5 0 0,0 18.5,3M18.5,16A2.5,2.5 0 0,0 16,18.5A2.5,2.5 0 0,0 18.5,21A2.5,2.5 0 0,0 21,18.5A2.5,2.5 0 0,0 18.5,16M3.91,17.25L5.04,17.91C5.17,17.81 5.33,17.75 5.5,17.75A0.75,0.75 0 0,1 6.25,18.5L6.24,18.6L7.37,19.25L7.09,19.75L5.96,19.09C5.83,19.19 5.67,19.25 5.5,19.25A0.75,0.75 0 0,1 4.75,18.5L4.76,18.4L3.63,17.75L3.91,17.25M3.63,6.25L4.76,5.6L4.75,5.5A0.75,0.75 0 0,1 5.5,4.75C5.67,4.75 5.83,4.81 5.96,4.91L7.09,4.25L7.37,4.75L6.24,5.4L6.25,5.5A0.75,0.75 0 0,1 5.5,6.25C5.33,6.25 5.17,6.19 5.04,6.09L3.91,6.75L3.63,6.25M16.91,4.25L18.04,4.91C18.17,4.81 18.33,4.75 18.5,4.75A0.75,0.75 0 0,1 19.25,5.5L19.24,5.6L20.37,6.25L20.09,6.75L18.96,6.09C18.83,6.19 18.67,6.25 18.5,6.25A0.75,0.75 0 0,1 17.75,5.5L17.76,5.4L16.63,4.75L16.91,4.25M16.63,19.25L17.75,18.5A0.75,0.75 0 0,1 18.5,17.75C18.67,17.75 18.83,17.81 18.96,17.91L20.09,17.25L20.37,17.75L19.25,18.5A0.75,0.75 0 0,1 18.5,19.25C18.33,19.25 18.17,19.19 18.04,19.09L16.91,19.75L16.63,19.25Z" /></g><g id="quality-high"><path d="M14.5,13.5H16.5V10.5H14.5M18,14A1,1 0 0,1 17,15H16.25V16.5H14.75V15H14A1,1 0 0,1 13,14V10A1,1 0 0,1 14,9H17A1,1 0 0,1 18,10M11,15H9.5V13H7.5V15H6V9H7.5V11.5H9.5V9H11M19,4H5C3.89,4 3,4.89 3,6V18A2,2 0 0,0 5,20H19A2,2 0 0,0 21,18V6C21,4.89 20.1,4 19,4Z" /></g><g id="quicktime"><path d="M12,3A9,9 0 0,1 21,12C21,13.76 20.5,15.4 19.62,16.79L21,18.17V20A1,1 0 0,1 20,21H18.18L16.79,19.62C15.41,20.5 13.76,21 12,21A9,9 0 0,1 3,12A9,9 0 0,1 12,3M12,7A5,5 0 0,0 7,12A5,5 0 0,0 12,17C12.65,17 13.26,16.88 13.83,16.65L10.95,13.77C10.17,13 10.17,11.72 10.95,10.94C11.73,10.16 13,10.16 13.78,10.94L16.66,13.82C16.88,13.26 17,12.64 17,12A5,5 0 0,0 12,7Z" /></g><g id="radar"><path d="M19.07,4.93L17.66,6.34C19.1,7.79 20,9.79 20,12A8,8 0 0,1 12,20A8,8 0 0,1 4,12C4,7.92 7.05,4.56 11,4.07V6.09C8.16,6.57 6,9.03 6,12A6,6 0 0,0 12,18A6,6 0 0,0 18,12C18,10.34 17.33,8.84 16.24,7.76L14.83,9.17C15.55,9.9 16,10.9 16,12A4,4 0 0,1 12,16A4,4 0 0,1 8,12C8,10.14 9.28,8.59 11,8.14V10.28C10.4,10.63 10,11.26 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12C14,11.26 13.6,10.62 13,10.28V2H12A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,9.24 20.88,6.74 19.07,4.93Z" /></g><g id="radiator"><path d="M7.95,3L6.53,5.19L7.95,7.4H7.94L5.95,10.5L4.22,9.6L5.64,7.39L4.22,5.19L6.22,2.09L7.95,3M13.95,2.89L12.53,5.1L13.95,7.3L13.94,7.31L11.95,10.4L10.22,9.5L11.64,7.3L10.22,5.1L12.22,2L13.95,2.89M20,2.89L18.56,5.1L20,7.3V7.31L18,10.4L16.25,9.5L17.67,7.3L16.25,5.1L18.25,2L20,2.89M2,22V14A2,2 0 0,1 4,12H20A2,2 0 0,1 22,14V22H20V20H4V22H2M6,14A1,1 0 0,0 5,15V17A1,1 0 0,0 6,18A1,1 0 0,0 7,17V15A1,1 0 0,0 6,14M10,14A1,1 0 0,0 9,15V17A1,1 0 0,0 10,18A1,1 0 0,0 11,17V15A1,1 0 0,0 10,14M14,14A1,1 0 0,0 13,15V17A1,1 0 0,0 14,18A1,1 0 0,0 15,17V15A1,1 0 0,0 14,14M18,14A1,1 0 0,0 17,15V17A1,1 0 0,0 18,18A1,1 0 0,0 19,17V15A1,1 0 0,0 18,14Z" /></g><g id="radio"><path d="M20,6A2,2 0 0,1 22,8V20A2,2 0 0,1 20,22H4A2,2 0 0,1 2,20V8C2,7.15 2.53,6.42 3.28,6.13L15.71,1L16.47,2.83L8.83,6H20M20,8H4V12H16V10H18V12H20V8M7,14A3,3 0 0,0 4,17A3,3 0 0,0 7,20A3,3 0 0,0 10,17A3,3 0 0,0 7,14Z" /></g><g id="radio-handheld"><path d="M9,2A1,1 0 0,0 8,3C8,8.67 8,14.33 8,20C8,21.11 8.89,22 10,22H15C16.11,22 17,21.11 17,20V9C17,7.89 16.11,7 15,7H10V3A1,1 0 0,0 9,2M10,9H15V13H10V9Z" /></g><g id="radio-tower"><path d="M12,10A2,2 0 0,1 14,12C14,12.5 13.82,12.94 13.53,13.29L16.7,22H14.57L12,14.93L9.43,22H7.3L10.47,13.29C10.18,12.94 10,12.5 10,12A2,2 0 0,1 12,10M12,8A4,4 0 0,0 8,12C8,12.5 8.1,13 8.28,13.46L7.4,15.86C6.53,14.81 6,13.47 6,12A6,6 0 0,1 12,6A6,6 0 0,1 18,12C18,13.47 17.47,14.81 16.6,15.86L15.72,13.46C15.9,13 16,12.5 16,12A4,4 0 0,0 12,8M12,4A8,8 0 0,0 4,12C4,14.36 5,16.5 6.64,17.94L5.92,19.94C3.54,18.11 2,15.23 2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12C22,15.23 20.46,18.11 18.08,19.94L17.36,17.94C19,16.5 20,14.36 20,12A8,8 0 0,0 12,4Z" /></g><g id="radioactive"><path d="M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,22C10.05,22 8.22,21.44 6.69,20.47L10,15.47C10.6,15.81 11.28,16 12,16C12.72,16 13.4,15.81 14,15.47L17.31,20.47C15.78,21.44 13.95,22 12,22M2,12C2,7.86 4.5,4.3 8.11,2.78L10.34,8.36C8.96,9 8,10.38 8,12H2M16,12C16,10.38 15.04,9 13.66,8.36L15.89,2.78C19.5,4.3 22,7.86 22,12H16Z" /></g><g id="radiobox-blank"><path d="M12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" /></g><g id="radiobox-marked"><path d="M12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,7A5,5 0 0,0 7,12A5,5 0 0,0 12,17A5,5 0 0,0 17,12A5,5 0 0,0 12,7Z" /></g><g id="raspberrypi"><path d="M20,8H22V10H20V8M4,5H20A2,2 0 0,1 22,7H19V9H5V13H8V16H19V17H22A2,2 0 0,1 20,19H16V20H14V19H11V20H7V19H4A2,2 0 0,1 2,17V7A2,2 0 0,1 4,5M19,15H9V10H19V11H22V13H19V15M13,12V14H15V12H13M5,6V8H6V6H5M7,6V8H8V6H7M9,6V8H10V6H9M11,6V8H12V6H11M13,6V8H14V6H13M15,6V8H16V6H15M20,14H22V16H20V14Z" /></g><g id="ray-end"><path d="M20,9C18.69,9 17.58,9.83 17.17,11H2V13H17.17C17.58,14.17 18.69,15 20,15A3,3 0 0,0 23,12A3,3 0 0,0 20,9Z" /></g><g id="ray-end-arrow"><path d="M1,12L5,16V13H17.17C17.58,14.17 18.69,15 20,15A3,3 0 0,0 23,12A3,3 0 0,0 20,9C18.69,9 17.58,9.83 17.17,11H5V8L1,12Z" /></g><g id="ray-start"><path d="M4,9C5.31,9 6.42,9.83 6.83,11H22V13H6.83C6.42,14.17 5.31,15 4,15A3,3 0 0,1 1,12A3,3 0 0,1 4,9Z" /></g><g id="ray-start-arrow"><path d="M23,12L19,16V13H6.83C6.42,14.17 5.31,15 4,15A3,3 0 0,1 1,12A3,3 0 0,1 4,9C5.31,9 6.42,9.83 6.83,11H19V8L23,12Z" /></g><g id="ray-start-end"><path d="M4,9C5.31,9 6.42,9.83 6.83,11H17.17C17.58,9.83 18.69,9 20,9A3,3 0 0,1 23,12A3,3 0 0,1 20,15C18.69,15 17.58,14.17 17.17,13H6.83C6.42,14.17 5.31,15 4,15A3,3 0 0,1 1,12A3,3 0 0,1 4,9Z" /></g><g id="ray-vertex"><path d="M2,11H9.17C9.58,9.83 10.69,9 12,9C13.31,9 14.42,9.83 14.83,11H22V13H14.83C14.42,14.17 13.31,15 12,15C10.69,15 9.58,14.17 9.17,13H2V11Z" /></g><g id="rdio"><path d="M19.29,10.84C19.35,11.22 19.38,11.61 19.38,12C19.38,16.61 15.5,20.35 10.68,20.35C5.87,20.35 2,16.61 2,12C2,7.39 5.87,3.65 10.68,3.65C11.62,3.65 12.53,3.79 13.38,4.06V9.11C13.38,9.11 10.79,7.69 8.47,9.35C6.15,11 6.59,12.76 6.59,12.76C6.59,12.76 6.7,15.5 9.97,15.5C13.62,15.5 14.66,12.19 14.66,12.19V4.58C15.36,4.93 16,5.36 16.65,5.85C18.2,6.82 19.82,7.44 21.67,7.39C21.67,7.39 22,7.31 22,8C22,8.4 21.88,8.83 21.5,9.25C21.5,9.25 20.78,10.33 19.29,10.84Z" /></g><g id="read"><path d="M21.59,11.59L23,13L13.5,22.5L8.42,17.41L9.83,16L13.5,19.68L21.59,11.59M4,16V3H6L9,3A4,4 0 0,1 13,7C13,8.54 12.13,9.88 10.85,10.55L14,16H12L9.11,11H6V16H4M6,9H9A2,2 0 0,0 11,7A2,2 0 0,0 9,5H6V9Z" /></g><g id="readability"><path d="M12,4C15.15,4 17.81,6.38 18.69,9.65C18,10.15 17.58,10.93 17.5,11.81L17.32,13.91C15.55,13 13.78,12.17 12,12.17C10.23,12.17 8.45,13 6.68,13.91L6.5,11.77C6.42,10.89 6,10.12 5.32,9.61C6.21,6.36 8.86,4 12,4M17.05,17H6.95L6.73,14.47C8.5,13.59 10.24,12.75 12,12.75C13.76,12.75 15.5,13.59 17.28,14.47L17.05,17M5,19V18L3.72,14.5H3.5A2.5,2.5 0 0,1 1,12A2.5,2.5 0 0,1 3.5,9.5C4.82,9.5 5.89,10.5 6,11.81L6.5,18V19H5M19,19H17.5V18L18,11.81C18.11,10.5 19.18,9.5 20.5,9.5A2.5,2.5 0 0,1 23,12A2.5,2.5 0 0,1 20.5,14.5H20.28L19,18V19Z" /></g><g id="receipt"><path d="M3,22L4.5,20.5L6,22L7.5,20.5L9,22L10.5,20.5L12,22L13.5,20.5L15,22L16.5,20.5L18,22L19.5,20.5L21,22V2L19.5,3.5L18,2L16.5,3.5L15,2L13.5,3.5L12,2L10.5,3.5L9,2L7.5,3.5L6,2L4.5,3.5L3,2M18,9H6V7H18M18,13H6V11H18M18,17H6V15H18V17Z" /></g><g id="record"><path d="M19,12C19,15.86 15.86,19 12,19C8.14,19 5,15.86 5,12C5,8.14 8.14,5 12,5C15.86,5 19,8.14 19,12Z" /></g><g id="record-rec"><path d="M12.5,5A7.5,7.5 0 0,0 5,12.5A7.5,7.5 0 0,0 12.5,20A7.5,7.5 0 0,0 20,12.5A7.5,7.5 0 0,0 12.5,5M7,10H9A1,1 0 0,1 10,11V12C10,12.5 9.62,12.9 9.14,12.97L10.31,15H9.15L8,13V15H7M12,10H14V11H12V12H14V13H12V14H14V15H12A1,1 0 0,1 11,14V11A1,1 0 0,1 12,10M16,10H18V11H16V14H18V15H16A1,1 0 0,1 15,14V11A1,1 0 0,1 16,10M8,11V12H9V11" /></g><g id="recycle"><path d="M21.82,15.42L19.32,19.75C18.83,20.61 17.92,21.06 17,21H15V23L12.5,18.5L15,14V16H17.82L15.6,12.15L19.93,9.65L21.73,12.77C22.25,13.54 22.32,14.57 21.82,15.42M9.21,3.06H14.21C15.19,3.06 16.04,3.63 16.45,4.45L17.45,6.19L19.18,5.19L16.54,9.6L11.39,9.69L13.12,8.69L11.71,6.24L9.5,10.09L5.16,7.59L6.96,4.47C7.37,3.64 8.22,3.06 9.21,3.06M5.05,19.76L2.55,15.43C2.06,14.58 2.13,13.56 2.64,12.79L3.64,11.06L1.91,10.06L7.05,10.14L9.7,14.56L7.97,13.56L6.56,16H11V21H7.4C6.47,21.07 5.55,20.61 5.05,19.76Z" /></g><g id="reddit"><path d="M22,11.5C22,10.1 20.9,9 19.5,9C18.9,9 18.3,9.2 17.9,9.6C16.4,8.7 14.6,8.1 12.5,8L13.6,4L17,5A2,2 0 0,0 19,7A2,2 0 0,0 21,5A2,2 0 0,0 19,3C18.3,3 17.6,3.4 17.3,4L13.3,3C13,2.9 12.8,3.1 12.7,3.4L11.5,8C9.5,8.1 7.6,8.7 6.1,9.6C5.7,9.2 5.1,9 4.5,9C3.1,9 2,10.1 2,11.5C2,12.4 2.4,13.1 3.1,13.6L3,14.5C3,18.1 7,21 12,21C17,21 21,18.1 21,14.5L20.9,13.6C21.6,13.1 22,12.4 22,11.5M9,11.8C9.7,11.8 10.2,12.4 10.2,13C10.2,13.6 9.7,14.2 9,14.2C8.3,14.2 7.8,13.7 7.8,13C7.8,12.3 8.3,11.8 9,11.8M15.8,17.2C14,18.3 10,18.3 8.2,17.2C8,17 7.9,16.7 8.1,16.5C8.3,16.3 8.6,16.2 8.8,16.4C10,17.3 14,17.3 15.2,16.4C15.4,16.2 15.7,16.3 15.9,16.5C16.1,16.7 16,17 15.8,17.2M15,14.2C14.3,14.2 13.8,13.6 13.8,13C13.8,12.3 14.4,11.8 15,11.8C15.7,11.8 16.2,12.4 16.2,13C16.2,13.7 15.7,14.2 15,14.2Z" /></g><g id="redo"><path d="M18.4,10.6C16.55,9 14.15,8 11.5,8C6.85,8 2.92,11.03 1.54,15.22L3.9,16C4.95,12.81 7.95,10.5 11.5,10.5C13.45,10.5 15.23,11.22 16.62,12.38L13,16H22V7L18.4,10.6Z" /></g><g id="redo-variant"><path d="M10.5,7A6.5,6.5 0 0,0 4,13.5A6.5,6.5 0 0,0 10.5,20H14V18H10.5C8,18 6,16 6,13.5C6,11 8,9 10.5,9H16.17L13.09,12.09L14.5,13.5L20,8L14.5,2.5L13.08,3.91L16.17,7H10.5M18,18H16V20H18V18Z" /></g><g id="refresh"><path d="M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z" /></g><g id="regex"><path d="M16,16.92C15.67,16.97 15.34,17 15,17C14.66,17 14.33,16.97 14,16.92V13.41L11.5,15.89C11,15.5 10.5,15 10.11,14.5L12.59,12H9.08C9.03,11.67 9,11.34 9,11C9,10.66 9.03,10.33 9.08,10H12.59L10.11,7.5C10.3,7.25 10.5,7 10.76,6.76V6.76C11,6.5 11.25,6.3 11.5,6.11L14,8.59V5.08C14.33,5.03 14.66,5 15,5C15.34,5 15.67,5.03 16,5.08V8.59L18.5,6.11C19,6.5 19.5,7 19.89,7.5L17.41,10H20.92C20.97,10.33 21,10.66 21,11C21,11.34 20.97,11.67 20.92,12H17.41L19.89,14.5C19.7,14.75 19.5,15 19.24,15.24V15.24C19,15.5 18.75,15.7 18.5,15.89L16,13.41V16.92H16V16.92M5,19A2,2 0 0,1 7,17A2,2 0 0,1 9,19A2,2 0 0,1 7,21A2,2 0 0,1 5,19H5Z" /></g><g id="relative-scale"><path d="M20,18H4V6H20M20,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V6C22,4.89 21.1,4 20,4M12,10H10V12H12M8,10H6V12H8M16,14H14V16H16M16,10H14V12H16V10Z" /></g><g id="reload"><path d="M19,12H22.32L17.37,16.95L12.42,12H16.97C17,10.46 16.42,8.93 15.24,7.75C12.9,5.41 9.1,5.41 6.76,7.75C4.42,10.09 4.42,13.9 6.76,16.24C8.6,18.08 11.36,18.47 13.58,17.41L15.05,18.88C12,20.69 8,20.29 5.34,17.65C2.22,14.53 2.23,9.47 5.35,6.35C8.5,3.22 13.53,3.21 16.66,6.34C18.22,7.9 19,9.95 19,12Z" /></g><g id="remote"><path d="M12,0C8.96,0 6.21,1.23 4.22,3.22L5.63,4.63C7.26,3 9.5,2 12,2C14.5,2 16.74,3 18.36,4.64L19.77,3.23C17.79,1.23 15.04,0 12,0M7.05,6.05L8.46,7.46C9.37,6.56 10.62,6 12,6C13.38,6 14.63,6.56 15.54,7.46L16.95,6.05C15.68,4.78 13.93,4 12,4C10.07,4 8.32,4.78 7.05,6.05M12,15A2,2 0 0,1 10,13A2,2 0 0,1 12,11A2,2 0 0,1 14,13A2,2 0 0,1 12,15M15,9H9A1,1 0 0,0 8,10V22A1,1 0 0,0 9,23H15A1,1 0 0,0 16,22V10A1,1 0 0,0 15,9Z" /></g><g id="rename-box"><path d="M18,17H10.5L12.5,15H18M6,17V14.5L13.88,6.65C14.07,6.45 14.39,6.45 14.59,6.65L16.35,8.41C16.55,8.61 16.55,8.92 16.35,9.12L8.47,17M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3Z" /></g><g id="reorder-horizontal"><path d="M3,15H21V13H3V15M3,19H21V17H3V19M3,11H21V9H3V11M3,5V7H21V5H3Z" /></g><g id="reorder-vertical"><path d="M9,3V21H11V3H9M5,3V21H7V3H5M13,3V21H15V3H13M19,3H17V21H19V3Z" /></g><g id="repeat"><path d="M17,17H7V14L3,18L7,22V19H19V13H17M7,7H17V10L21,6L17,2V5H5V11H7V7Z" /></g><g id="repeat-off"><path d="M2,5.27L3.28,4L20,20.72L18.73,22L15.73,19H7V22L3,18L7,14V17H13.73L7,10.27V11H5V8.27L2,5.27M17,13H19V17.18L17,15.18V13M17,5V2L21,6L17,10V7H8.82L6.82,5H17Z" /></g><g id="repeat-once"><path d="M13,15V9H12L10,10V11H11.5V15M17,17H7V14L3,18L7,22V19H19V13H17M7,7H17V10L21,6L17,2V5H5V11H7V7Z" /></g><g id="replay"><path d="M12,5V1L7,6L12,11V7A6,6 0 0,1 18,13A6,6 0 0,1 12,19A6,6 0 0,1 6,13H4A8,8 0 0,0 12,21A8,8 0 0,0 20,13A8,8 0 0,0 12,5Z" /></g><g id="reply"><path d="M10,9V5L3,12L10,19V14.9C15,14.9 18.5,16.5 21,20C20,15 17,10 10,9Z" /></g><g id="reply-all"><path d="M13,9V5L6,12L13,19V14.9C18,14.9 21.5,16.5 24,20C23,15 20,10 13,9M7,8V5L0,12L7,19V16L3,12L7,8Z" /></g><g id="reproduction"><path d="M12.72,13.15L13.62,12.26C13.6,11 14.31,9.44 15.62,8.14C17.57,6.18 20.11,5.55 21.28,6.72C22.45,7.89 21.82,10.43 19.86,12.38C18.56,13.69 17,14.4 15.74,14.38L14.85,15.28C14.5,15.61 14,15.66 13.6,15.41C12.76,15.71 12,16.08 11.56,16.8C11.03,17.68 11.03,19.1 10.47,19.95C9.91,20.81 8.79,21.1 7.61,21.1C6.43,21.1 5,21 3.95,19.5L6.43,19.92C7,20 8.5,19.39 9.05,18.54C9.61,17.68 9.61,16.27 10.14,15.38C10.61,14.6 11.5,14.23 12.43,13.91C12.42,13.64 12.5,13.36 12.72,13.15M7,2A5,5 0 0,1 12,7A5,5 0 0,1 7,12A5,5 0 0,1 2,7A5,5 0 0,1 7,2M7,4A3,3 0 0,0 4,7A3,3 0 0,0 7,10A3,3 0 0,0 10,7A3,3 0 0,0 7,4Z" /></g><g id="resize-bottom-right"><path d="M22,22H20V20H22V22M22,18H20V16H22V18M18,22H16V20H18V22M18,18H16V16H18V18M14,22H12V20H14V22M22,14H20V12H22V14Z" /></g><g id="responsive"><path d="M4,6V16H9V12A2,2 0 0,1 11,10H16A2,2 0 0,1 18,12V16H20V6H4M0,20V18H4A2,2 0 0,1 2,16V6A2,2 0 0,1 4,4H20A2,2 0 0,1 22,6V16A2,2 0 0,1 20,18H24V20H18V20C18,21.11 17.1,22 16,22H11A2,2 0 0,1 9,20H9L0,20M11.5,20A0.5,0.5 0 0,0 11,20.5A0.5,0.5 0 0,0 11.5,21A0.5,0.5 0 0,0 12,20.5A0.5,0.5 0 0,0 11.5,20M15.5,20A0.5,0.5 0 0,0 15,20.5A0.5,0.5 0 0,0 15.5,21A0.5,0.5 0 0,0 16,20.5A0.5,0.5 0 0,0 15.5,20M13,20V21H14V20H13M11,12V19H16V12H11Z" /></g><g id="restore"><path d="M13,3A9,9 0 0,0 4,12H1L4.89,15.89L4.96,16.03L9,12H6A7,7 0 0,1 13,5A7,7 0 0,1 20,12A7,7 0 0,1 13,19C11.07,19 9.32,18.21 8.06,16.94L6.64,18.36C8.27,20 10.5,21 13,21A9,9 0 0,0 22,12A9,9 0 0,0 13,3M12,8V13L16.28,15.54L17,14.33L13.5,12.25V8H12Z" /></g><g id="rewind"><path d="M11.5,12L20,18V6M11,18V6L2.5,12L11,18Z" /></g><g id="ribbon"><path d="M13.41,19.31L16.59,22.5L18,21.07L14.83,17.9M15.54,11.53H15.53L12,15.07L8.47,11.53H8.46V11.53C7.56,10.63 7,9.38 7,8A5,5 0 0,1 12,3A5,5 0 0,1 17,8C17,9.38 16.44,10.63 15.54,11.53M16.9,13C18.2,11.73 19,9.96 19,8A7,7 0 0,0 12,1A7,7 0 0,0 5,8C5,9.96 5.81,11.73 7.1,13V13L10.59,16.5L6,21.07L7.41,22.5L16.9,13Z" /></g><g id="road"><path d="M11,16H13V20H11M11,10H13V14H11M11,4H13V8H11M4,22H20V2H4V22Z" /></g><g id="road-variant"><path d="M18.1,4.8C18,4.3 17.6,4 17.1,4H13L13.2,7H10.8L11,4H6.8C6.3,4 5.9,4.4 5.8,4.8L3.1,18.8C3,19.4 3.5,20 4.1,20H10L10.3,15H13.7L14,20H19.8C20.4,20 20.9,19.4 20.8,18.8L18.1,4.8M10.4,13L10.6,9H13.2L13.4,13H10.4Z" /></g><g id="robot"><path d="M12,2A2,2 0 0,1 14,4C14,4.74 13.6,5.39 13,5.73V7H14A7,7 0 0,1 21,14H22A1,1 0 0,1 23,15V18A1,1 0 0,1 22,19H21V20A2,2 0 0,1 19,22H5A2,2 0 0,1 3,20V19H2A1,1 0 0,1 1,18V15A1,1 0 0,1 2,14H3A7,7 0 0,1 10,7H11V5.73C10.4,5.39 10,4.74 10,4A2,2 0 0,1 12,2M7.5,13A2.5,2.5 0 0,0 5,15.5A2.5,2.5 0 0,0 7.5,18A2.5,2.5 0 0,0 10,15.5A2.5,2.5 0 0,0 7.5,13M16.5,13A2.5,2.5 0 0,0 14,15.5A2.5,2.5 0 0,0 16.5,18A2.5,2.5 0 0,0 19,15.5A2.5,2.5 0 0,0 16.5,13Z" /></g><g id="rocket"><path d="M2.81,14.12L5.64,11.29L8.17,10.79C11.39,6.41 17.55,4.22 19.78,4.22C19.78,6.45 17.59,12.61 13.21,15.83L12.71,18.36L9.88,21.19L9.17,17.66C7.76,17.66 7.76,17.66 7.05,16.95C6.34,16.24 6.34,16.24 6.34,14.83L2.81,14.12M5.64,16.95L7.05,18.36L4.39,21.03H2.97V19.61L5.64,16.95M4.22,15.54L5.46,15.71L3,18.16V16.74L4.22,15.54M8.29,18.54L8.46,19.78L7.26,21H5.84L8.29,18.54M13,9.5A1.5,1.5 0 0,0 11.5,11A1.5,1.5 0 0,0 13,12.5A1.5,1.5 0 0,0 14.5,11A1.5,1.5 0 0,0 13,9.5Z" /></g><g id="rotate-3d"><path d="M12,5C16.97,5 21,7.69 21,11C21,12.68 19.96,14.2 18.29,15.29C19.36,14.42 20,13.32 20,12.13C20,9.29 16.42,7 12,7V10L8,6L12,2V5M12,19C7.03,19 3,16.31 3,13C3,11.32 4.04,9.8 5.71,8.71C4.64,9.58 4,10.68 4,11.88C4,14.71 7.58,17 12,17V14L16,18L12,22V19Z" /></g><g id="rotate-90"><path d="M7.34,6.41L0.86,12.9L7.35,19.38L13.84,12.9L7.34,6.41M3.69,12.9L7.35,9.24L11,12.9L7.34,16.56L3.69,12.9M19.36,6.64C17.61,4.88 15.3,4 13,4V0.76L8.76,5L13,9.24V6C14.79,6 16.58,6.68 17.95,8.05C20.68,10.78 20.68,15.22 17.95,17.95C16.58,19.32 14.79,20 13,20C12.03,20 11.06,19.79 10.16,19.39L8.67,20.88C10,21.62 11.5,22 13,22C15.3,22 17.61,21.12 19.36,19.36C22.88,15.85 22.88,10.15 19.36,6.64Z" /></g><g id="rotate-left"><path d="M13,4.07V1L8.45,5.55L13,10V6.09C15.84,6.57 18,9.03 18,12C18,14.97 15.84,17.43 13,17.91V19.93C16.95,19.44 20,16.08 20,12C20,7.92 16.95,4.56 13,4.07M7.1,18.32C8.26,19.22 9.61,19.76 11,19.93V17.9C10.13,17.75 9.29,17.41 8.54,16.87L7.1,18.32M6.09,13H4.07C4.24,14.39 4.79,15.73 5.69,16.89L7.1,15.47C6.58,14.72 6.23,13.88 6.09,13M7.11,8.53L5.7,7.11C4.8,8.27 4.24,9.61 4.07,11H6.09C6.23,10.13 6.58,9.28 7.11,8.53Z" /></g><g id="rotate-left-variant"><path d="M4,2H7A2,2 0 0,1 9,4V20A2,2 0 0,1 7,22H4A2,2 0 0,1 2,20V4A2,2 0 0,1 4,2M20,15A2,2 0 0,1 22,17V20A2,2 0 0,1 20,22H11V15H20M14,4A8,8 0 0,1 22,12L21.94,13H19.92L20,12A6,6 0 0,0 14,6V9L10,5L14,1V4Z" /></g><g id="rotate-right"><path d="M16.89,15.5L18.31,16.89C19.21,15.73 19.76,14.39 19.93,13H17.91C17.77,13.87 17.43,14.72 16.89,15.5M13,17.9V19.92C14.39,19.75 15.74,19.21 16.9,18.31L15.46,16.87C14.71,17.41 13.87,17.76 13,17.9M19.93,11C19.76,9.61 19.21,8.27 18.31,7.11L16.89,8.53C17.43,9.28 17.77,10.13 17.91,11M15.55,5.55L11,1V4.07C7.06,4.56 4,7.92 4,12C4,16.08 7.05,19.44 11,19.93V17.91C8.16,17.43 6,14.97 6,12C6,9.03 8.16,6.57 11,6.09V10L15.55,5.55Z" /></g><g id="rotate-right-variant"><path d="M10,4V1L14,5L10,9V6A6,6 0 0,0 4,12L4.08,13H2.06L2,12A8,8 0 0,1 10,4M17,2H20A2,2 0 0,1 22,4V20A2,2 0 0,1 20,22H17A2,2 0 0,1 15,20V4A2,2 0 0,1 17,2M4,15H13V22H4A2,2 0 0,1 2,20V17A2,2 0 0,1 4,15Z" /></g><g id="rounded-corner"><path d="M19,19H21V21H19V19M19,17H21V15H19V17M3,13H5V11H3V13M3,17H5V15H3V17M3,9H5V7H3V9M3,5H5V3H3V5M7,5H9V3H7V5M15,21H17V19H15V21M11,21H13V19H11V21M15,21H17V19H15V21M7,21H9V19H7V21M3,21H5V19H3V21M21,8A5,5 0 0,0 16,3H11V5H16A3,3 0 0,1 19,8V13H21V8Z" /></g><g id="router-wireless"><path d="M4,13H20A1,1 0 0,1 21,14V18A1,1 0 0,1 20,19H4A1,1 0 0,1 3,18V14A1,1 0 0,1 4,13M9,17H10V15H9V17M5,15V17H7V15H5M19,6.93L17.6,8.34C16.15,6.89 14.15,6 11.93,6C9.72,6 7.72,6.89 6.27,8.34L4.87,6.93C6.68,5.12 9.18,4 11.93,4C14.69,4 17.19,5.12 19,6.93M16.17,9.76L14.77,11.17C14.04,10.45 13.04,10 11.93,10C10.82,10 9.82,10.45 9.1,11.17L7.7,9.76C8.78,8.67 10.28,8 11.93,8C13.58,8 15.08,8.67 16.17,9.76Z" /></g><g id="routes"><path d="M11,10H5L3,8L5,6H11V3L12,2L13,3V4H19L21,6L19,8H13V10H19L21,12L19,14H13V20A2,2 0 0,1 15,22H9A2,2 0 0,1 11,20V10Z" /></g><g id="rowing"><path d="M8.5,14.5L4,19L5.5,20.5L9,17H11L8.5,14.5M15,1A2,2 0 0,0 13,3A2,2 0 0,0 15,5A2,2 0 0,0 17,3A2,2 0 0,0 15,1M21,21L18,24L15,21V19.5L7.91,12.41C7.6,12.46 7.3,12.5 7,12.5V10.32C8.66,10.35 10.61,9.45 11.67,8.28L13.07,6.73C13.26,6.5 13.5,6.35 13.76,6.23C14.05,6.09 14.38,6 14.72,6H14.75C16,6 17,7 17,8.26V14C17,14.85 16.65,15.62 16.08,16.17L12.5,12.59V10.32C11.87,10.84 11.07,11.34 10.21,11.71L16.5,18H18L21,21Z" /></g><g id="rss"><path d="M6.18,15.64A2.18,2.18 0 0,1 8.36,17.82C8.36,19 7.38,20 6.18,20C5,20 4,19 4,17.82A2.18,2.18 0 0,1 6.18,15.64M4,4.44A15.56,15.56 0 0,1 19.56,20H16.73A12.73,12.73 0 0,0 4,7.27V4.44M4,10.1A9.9,9.9 0 0,1 13.9,20H11.07A7.07,7.07 0 0,0 4,12.93V10.1Z" /></g><g id="rss-box"><path d="M5,3H19A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3M7.5,15A1.5,1.5 0 0,0 6,16.5A1.5,1.5 0 0,0 7.5,18A1.5,1.5 0 0,0 9,16.5A1.5,1.5 0 0,0 7.5,15M6,10V12A6,6 0 0,1 12,18H14A8,8 0 0,0 6,10M6,6V8A10,10 0 0,1 16,18H18A12,12 0 0,0 6,6Z" /></g><g id="ruler"><path d="M1.39,18.36L3.16,16.6L4.58,18L5.64,16.95L4.22,15.54L5.64,14.12L8.11,16.6L9.17,15.54L6.7,13.06L8.11,11.65L9.53,13.06L10.59,12L9.17,10.59L10.59,9.17L13.06,11.65L14.12,10.59L11.65,8.11L13.06,6.7L14.47,8.11L15.54,7.05L14.12,5.64L15.54,4.22L18,6.7L19.07,5.64L16.6,3.16L18.36,1.39L22.61,5.64L5.64,22.61L1.39,18.36Z" /></g><g id="run"><path d="M17.12,10L16.04,8.18L15.31,11.05L17.8,15.59V22H16V17L13.67,13.89L12.07,18.4L7.25,20.5L6.2,19L10.39,16.53L12.91,6.67L10.8,7.33V11H9V5.8L14.42,4.11L14.92,4.03C15.54,4.03 16.08,4.37 16.38,4.87L18.38,8.2H22V10H17.12M17,3.8C16,3.8 15.2,3 15.2,2C15.2,1 16,0.2 17,0.2C18,0.2 18.8,1 18.8,2C18.8,3 18,3.8 17,3.8M7,9V11H4A1,1 0 0,1 3,10A1,1 0 0,1 4,9H7M9.25,13L8.75,15H5A1,1 0 0,1 4,14A1,1 0 0,1 5,13H9.25M7,5V7H3A1,1 0 0,1 2,6A1,1 0 0,1 3,5H7Z" /></g><g id="sale"><path d="M18.65,2.85L19.26,6.71L22.77,8.5L21,12L22.78,15.5L19.24,17.29L18.63,21.15L14.74,20.54L11.97,23.3L9.19,20.5L5.33,21.14L4.71,17.25L1.22,15.47L3,11.97L1.23,8.5L4.74,6.69L5.35,2.86L9.22,3.5L12,0.69L14.77,3.46L18.65,2.85M9.5,7A1.5,1.5 0 0,0 8,8.5A1.5,1.5 0 0,0 9.5,10A1.5,1.5 0 0,0 11,8.5A1.5,1.5 0 0,0 9.5,7M14.5,14A1.5,1.5 0 0,0 13,15.5A1.5,1.5 0 0,0 14.5,17A1.5,1.5 0 0,0 16,15.5A1.5,1.5 0 0,0 14.5,14M8.41,17L17,8.41L15.59,7L7,15.59L8.41,17Z" /></g><g id="satellite"><path d="M5,18L8.5,13.5L11,16.5L14.5,12L19,18M5,12V10A5,5 0 0,0 10,5H12A7,7 0 0,1 5,12M5,5H8A3,3 0 0,1 5,8M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3Z" /></g><g id="satellite-variant"><path d="M11.62,1L17.28,6.67L15.16,8.79L13.04,6.67L11.62,8.09L13.95,10.41L12.79,11.58L13.24,12.04C14.17,11.61 15.31,11.77 16.07,12.54L12.54,16.07C11.77,15.31 11.61,14.17 12.04,13.24L11.58,12.79L10.41,13.95L8.09,11.62L6.67,13.04L8.79,15.16L6.67,17.28L1,11.62L3.14,9.5L5.26,11.62L6.67,10.21L3.84,7.38C3.06,6.6 3.06,5.33 3.84,4.55L4.55,3.84C5.33,3.06 6.6,3.06 7.38,3.84L10.21,6.67L11.62,5.26L9.5,3.14L11.62,1M18,14A4,4 0 0,1 14,18V16A2,2 0 0,0 16,14H18M22,14A8,8 0 0,1 14,22V20A6,6 0 0,0 20,14H22Z" /></g><g id="saxophone"><path d="M4,2A1,1 0 0,0 3,3A1,1 0 0,0 4,4A3,3 0 0,1 7,7V8.66L7,15.5C7,19.1 9.9,22 13.5,22C17.1,22 20,19.1 20,15.5V13A1,1 0 0,0 21,12A1,1 0 0,0 20,11H14A1,1 0 0,0 13,12A1,1 0 0,0 14,13V15A1,1 0 0,1 13,16A1,1 0 0,1 12,15V11A1,1 0 0,0 13,10A1,1 0 0,0 12,9V8A1,1 0 0,0 13,7A1,1 0 0,0 12,6V5.5A3.5,3.5 0 0,0 8.5,2H4Z" /></g><g id="scale"><path d="M8.46,15.06L7.05,16.47L5.68,15.1C4.82,16.21 4.24,17.54 4.06,19H6V21H2V20C2,15.16 5.44,11.13 10,10.2V8.2L2,5V3H22V5L14,8.2V10.2C18.56,11.13 22,15.16 22,20V21H18V19H19.94C19.76,17.54 19.18,16.21 18.32,15.1L16.95,16.47L15.54,15.06L16.91,13.68C15.8,12.82 14.46,12.24 13,12.06V14H11V12.06C9.54,12.24 8.2,12.82 7.09,13.68L8.46,15.06M12,18A2,2 0 0,1 14,20A2,2 0 0,1 12,22C11.68,22 11.38,21.93 11.12,21.79L7.27,20L11.12,18.21C11.38,18.07 11.68,18 12,18Z" /></g><g id="scale-balance"><path d="M12,3C10.73,3 9.6,3.8 9.18,5H3V7H4.95L2,14C1.53,16 3,17 5.5,17C8,17 9.56,16 9,14L6.05,7H9.17C9.5,7.85 10.15,8.5 11,8.83V20H2V22H22V20H13V8.82C13.85,8.5 14.5,7.85 14.82,7H17.95L15,14C14.53,16 16,17 18.5,17C21,17 22.56,16 22,14L19.05,7H21V5H14.83C14.4,3.8 13.27,3 12,3M12,5A1,1 0 0,1 13,6A1,1 0 0,1 12,7A1,1 0 0,1 11,6A1,1 0 0,1 12,5M5.5,10.25L7,14H4L5.5,10.25M18.5,10.25L20,14H17L18.5,10.25Z" /></g><g id="scale-bathroom"><path d="M5,2H19A2,2 0 0,1 21,4V20A2,2 0 0,1 19,22H5A2,2 0 0,1 3,20V4A2,2 0 0,1 5,2M12,4A4,4 0 0,0 8,8H11.26L10.85,5.23L12.9,8H16A4,4 0 0,0 12,4M5,10V20H19V10H5Z" /></g><g id="scanner"><path d="M19.8,10.7L4.2,5L3.5,6.9L17.6,12H5A2,2 0 0,0 3,14V18A2,2 0 0,0 5,20H19A2,2 0 0,0 21,18V12.5C21,11.7 20.5,10.9 19.8,10.7M7,17H5V15H7V17M19,17H9V15H19V17Z" /></g><g id="school"><path d="M12,3L1,9L12,15L21,10.09V17H23V9M5,13.18V17.18L12,21L19,17.18V13.18L12,17L5,13.18Z" /></g><g id="screen-rotation"><path d="M7.5,21.5C4.25,19.94 1.91,16.76 1.55,13H0.05C0.56,19.16 5.71,24 12,24L12.66,23.97L8.85,20.16M14.83,21.19L2.81,9.17L9.17,2.81L21.19,14.83M10.23,1.75C9.64,1.16 8.69,1.16 8.11,1.75L1.75,8.11C1.16,8.7 1.16,9.65 1.75,10.23L13.77,22.25C14.36,22.84 15.31,22.84 15.89,22.25L22.25,15.89C22.84,15.3 22.84,14.35 22.25,13.77L10.23,1.75M16.5,2.5C19.75,4.07 22.09,7.24 22.45,11H23.95C23.44,4.84 18.29,0 12,0L11.34,0.03L15.15,3.84L16.5,2.5Z" /></g><g id="screen-rotation-lock"><path d="M16.8,2.5C16.8,1.56 17.56,0.8 18.5,0.8C19.44,0.8 20.2,1.56 20.2,2.5V3H16.8V2.5M16,9H21A1,1 0 0,0 22,8V4A1,1 0 0,0 21,3V2.5A2.5,2.5 0 0,0 18.5,0A2.5,2.5 0 0,0 16,2.5V3A1,1 0 0,0 15,4V8A1,1 0 0,0 16,9M8.47,20.5C5.2,18.94 2.86,15.76 2.5,12H1C1.5,18.16 6.66,23 12.95,23L13.61,22.97L9.8,19.15L8.47,20.5M23.25,12.77L20.68,10.2L19.27,11.61L21.5,13.83L15.83,19.5L4.5,8.17L10.17,2.5L12.27,4.61L13.68,3.2L11.23,0.75C10.64,0.16 9.69,0.16 9.11,0.75L2.75,7.11C2.16,7.7 2.16,8.65 2.75,9.23L14.77,21.25C15.36,21.84 16.31,21.84 16.89,21.25L23.25,14.89C23.84,14.3 23.84,13.35 23.25,12.77Z" /></g><g id="screwdriver"><path d="M18,1.83C17.5,1.83 17,2 16.59,2.41C13.72,5.28 8,11 8,11L9.5,12.5L6,16H4L2,20L4,22L8,20V18L11.5,14.5L13,16C13,16 18.72,10.28 21.59,7.41C22.21,6.5 22.37,5.37 21.59,4.59L19.41,2.41C19,2 18.5,1.83 18,1.83M18,4L20,6L13,13L11,11L18,4Z" /></g><g id="script"><path d="M14,20A2,2 0 0,0 16,18V5H9A1,1 0 0,0 8,6V16H5V5A3,3 0 0,1 8,2H19A3,3 0 0,1 22,5V6H18V18L18,19A3,3 0 0,1 15,22H5A3,3 0 0,1 2,19V18H12A2,2 0 0,0 14,20Z" /></g><g id="sd"><path d="M18,8H16V4H18M15,8H13V4H15M12,8H10V4H12M18,2H10L4,8V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V4A2,2 0 0,0 18,2Z" /></g><g id="seal"><path d="M20.39,19.37L16.38,18L15,22L11.92,16L9,22L7.62,18L3.61,19.37L6.53,13.37C5.57,12.17 5,10.65 5,9A7,7 0 0,1 12,2A7,7 0 0,1 19,9C19,10.65 18.43,12.17 17.47,13.37L20.39,19.37M7,9L9.69,10.34L9.5,13.34L12,11.68L14.5,13.33L14.33,10.34L17,9L14.32,7.65L14.5,4.67L12,6.31L9.5,4.65L9.67,7.66L7,9Z" /></g><g id="seat-flat"><path d="M22,11V13H9V7H18A4,4 0 0,1 22,11M2,14V16H8V18H16V16H22V14M7.14,12.1C8.3,10.91 8.28,9 7.1,7.86C5.91,6.7 4,6.72 2.86,7.9C1.7,9.09 1.72,11 2.9,12.14C4.09,13.3 6,13.28 7.14,12.1Z" /></g><g id="seat-flat-angled"><path d="M22.25,14.29L21.56,16.18L9.2,11.71L11.28,6.05L19.84,9.14C21.94,9.9 23,12.2 22.25,14.29M1.5,12.14L8,14.5V19H16V17.37L20.5,19L21.21,17.11L2.19,10.25M7.3,10.2C8.79,9.5 9.42,7.69 8.71,6.2C8,4.71 6.2,4.08 4.7,4.8C3.21,5.5 2.58,7.3 3.3,8.8C4,10.29 5.8,10.92 7.3,10.2Z" /></g><g id="seat-individual-suite"><path d="M7,13A3,3 0 0,0 10,10A3,3 0 0,0 7,7A3,3 0 0,0 4,10A3,3 0 0,0 7,13M19,7H11V14H3V7H1V17H23V11A4,4 0 0,0 19,7Z" /></g><g id="seat-legroom-extra"><path d="M4,12V3H2V12A5,5 0 0,0 7,17H13V15H7A3,3 0 0,1 4,12M22.83,17.24C22.45,16.5 21.54,16.27 20.8,16.61L19.71,17.11L16.3,10.13C15.96,9.45 15.27,9 14.5,9H11V3H5V11A3,3 0 0,0 8,14H15L18.41,21L22.13,19.3C22.9,18.94 23.23,18 22.83,17.24Z" /></g><g id="seat-legroom-normal"><path d="M5,12V3H3V12A5,5 0 0,0 8,17H14V15H8A3,3 0 0,1 5,12M20.5,18H19V11A2,2 0 0,0 17,9H12V3H6V11A3,3 0 0,0 9,14H16V21H20.5A1.5,1.5 0 0,0 22,19.5A1.5,1.5 0 0,0 20.5,18Z" /></g><g id="seat-legroom-reduced"><path d="M19.97,19.2C20.15,20.16 19.42,21 18.5,21H14V18L15,14H9A3,3 0 0,1 6,11V3H12V9H17A2,2 0 0,1 19,11L17,18H18.44C19.17,18 19.83,18.5 19.97,19.2M5,12V3H3V12A5,5 0 0,0 8,17H12V15H8A3,3 0 0,1 5,12Z" /></g><g id="seat-recline-extra"><path d="M5.35,5.64C4.45,5 4.23,3.76 4.86,2.85C5.5,1.95 6.74,1.73 7.65,2.36C8.55,3 8.77,4.24 8.14,5.15C7.5,6.05 6.26,6.27 5.35,5.64M16,19H8.93C7.45,19 6.19,17.92 5.97,16.46L4,7H2L4,16.76C4.37,19.2 6.47,21 8.94,21H16M16.23,15H11.35L10.32,10.9C11.9,11.79 13.6,12.44 15.47,12.12V10C13.84,10.3 12.03,9.72 10.78,8.74L9.14,7.47C8.91,7.29 8.65,7.17 8.38,7.09C8.06,7 7.72,6.97 7.39,7.03H7.37C6.14,7.25 5.32,8.42 5.53,9.64L6.88,15.56C7.16,17 8.39,18 9.83,18H16.68L20.5,21L22,19.5" /></g><g id="seat-recline-normal"><path d="M7.59,5.41C6.81,4.63 6.81,3.36 7.59,2.58C8.37,1.8 9.64,1.8 10.42,2.58C11.2,3.36 11.2,4.63 10.42,5.41C9.63,6.2 8.37,6.2 7.59,5.41M6,16V7H4V16A5,5 0 0,0 9,21H15V19H9A3,3 0 0,1 6,16M20,20.07L14.93,15H11.5V11.32C12.9,12.47 15.1,13.5 17,13.5V11.32C15.34,11.34 13.39,10.45 12.33,9.28L10.93,7.73C10.74,7.5 10.5,7.35 10.24,7.23C9.95,7.09 9.62,7 9.28,7H9.25C8,7 7,8 7,9.25V15A3,3 0 0,0 10,18H15.07L18.57,21.5" /></g><g id="security"><path d="M12,12H19C18.47,16.11 15.72,19.78 12,20.92V12H5V6.3L12,3.19M12,1L3,5V11C3,16.55 6.84,21.73 12,23C17.16,21.73 21,16.55 21,11V5L12,1Z" /></g><g id="security-home"><path d="M11,13H13V16H16V11H18L12,6L6,11H8V16H11V13M12,1L21,5V11C21,16.55 17.16,21.74 12,23C6.84,21.74 3,16.55 3,11V5L12,1Z" /></g><g id="security-network"><path d="M13,18H14A1,1 0 0,1 15,19H22V21H15A1,1 0 0,1 14,22H10A1,1 0 0,1 9,21H2V19H9A1,1 0 0,1 10,18H11V16.34C8.07,15.13 6,12 6,8.67V4.67L12,2L18,4.67V8.67C18,12 15.93,15.13 13,16.34V18M12,4L8,5.69V9H12V4M12,9V15C13.91,14.53 16,12.06 16,10V9H12Z" /></g><g id="select"><path d="M4,3H5V5H3V4A1,1 0 0,1 4,3M20,3A1,1 0 0,1 21,4V5H19V3H20M15,5V3H17V5H15M11,5V3H13V5H11M7,5V3H9V5H7M21,20A1,1 0 0,1 20,21H19V19H21V20M15,21V19H17V21H15M11,21V19H13V21H11M7,21V19H9V21H7M4,21A1,1 0 0,1 3,20V19H5V21H4M3,15H5V17H3V15M21,15V17H19V15H21M3,11H5V13H3V11M21,11V13H19V11H21M3,7H5V9H3V7M21,7V9H19V7H21Z" /></g><g id="select-all"><path d="M9,9H15V15H9M7,17H17V7H7M15,5H17V3H15M15,21H17V19H15M19,17H21V15H19M19,9H21V7H19M19,21A2,2 0 0,0 21,19H19M19,13H21V11H19M11,21H13V19H11M9,3H7V5H9M3,17H5V15H3M5,21V19H3A2,2 0 0,0 5,21M19,3V5H21A2,2 0 0,0 19,3M13,3H11V5H13M3,9H5V7H3M7,21H9V19H7M3,13H5V11H3M3,5H5V3A2,2 0 0,0 3,5Z" /></g><g id="select-inverse"><path d="M5,3H7V5H9V3H11V5H13V3H15V5H17V3H19V5H21V7H19V9H21V11H19V13H21V15H19V17H21V19H19V21H17V19H15V21H13V19H11V21H9V19H7V21H5V19H3V17H5V15H3V13H5V11H3V9H5V7H3V5H5V3Z" /></g><g id="select-off"><path d="M1,4.27L2.28,3L21,21.72L19.73,23L17,20.27V21H15V19H15.73L5,8.27V9H3V7H3.73L1,4.27M20,3A1,1 0 0,1 21,4V5H19V3H20M15,5V3H17V5H15M11,5V3H13V5H11M7,5V3H9V5H7M11,21V19H13V21H11M7,21V19H9V21H7M4,21A1,1 0 0,1 3,20V19H5V21H4M3,15H5V17H3V15M21,15V17H19V15H21M3,11H5V13H3V11M21,11V13H19V11H21M21,7V9H19V7H21Z" /></g><g id="selection"><path d="M2,4V7H4V4H2M7,4H4C4,4 4,4 4,4H2C2,2.89 2.9,2 4,2H7V4M22,4V7H20V4H22M17,4H20C20,4 20,4 20,4H22C22,2.89 21.1,2 20,2H17V4M22,20V17H20V20H22M17,20H20C20,20 20,20 20,20H22C22,21.11 21.1,22 20,22H17V20M2,20V17H4V20H2M7,20H4C4,20 4,20 4,20H2C2,21.11 2.9,22 4,22H7V20M10,2H14V4H10V2M10,20H14V22H10V20M20,10H22V14H20V10M2,10H4V14H2V10Z" /></g><g id="send"><path d="M2,21L23,12L2,3V10L17,12L2,14V21Z" /></g><g id="serial-port"><path d="M7,3H17V5H19V8H16V14H8V8H5V5H7V3M17,9H19V14H17V9M11,15H13V22H11V15M5,9H7V14H5V9Z" /></g><g id="server"><path d="M4,1H20A1,1 0 0,1 21,2V6A1,1 0 0,1 20,7H4A1,1 0 0,1 3,6V2A1,1 0 0,1 4,1M4,9H20A1,1 0 0,1 21,10V14A1,1 0 0,1 20,15H4A1,1 0 0,1 3,14V10A1,1 0 0,1 4,9M4,17H20A1,1 0 0,1 21,18V22A1,1 0 0,1 20,23H4A1,1 0 0,1 3,22V18A1,1 0 0,1 4,17M9,5H10V3H9V5M9,13H10V11H9V13M9,21H10V19H9V21M5,3V5H7V3H5M5,11V13H7V11H5M5,19V21H7V19H5Z" /></g><g id="server-minus"><path d="M4,4H20A1,1 0 0,1 21,5V9A1,1 0 0,1 20,10H4A1,1 0 0,1 3,9V5A1,1 0 0,1 4,4M9,8H10V6H9V8M5,6V8H7V6H5M8,16H16V18H8V16Z" /></g><g id="server-network"><path d="M13,18H14A1,1 0 0,1 15,19H22V21H15A1,1 0 0,1 14,22H10A1,1 0 0,1 9,21H2V19H9A1,1 0 0,1 10,18H11V16H4A1,1 0 0,1 3,15V11A1,1 0 0,1 4,10H20A1,1 0 0,1 21,11V15A1,1 0 0,1 20,16H13V18M4,2H20A1,1 0 0,1 21,3V7A1,1 0 0,1 20,8H4A1,1 0 0,1 3,7V3A1,1 0 0,1 4,2M9,6H10V4H9V6M9,14H10V12H9V14M5,4V6H7V4H5M5,12V14H7V12H5Z" /></g><g id="server-network-off"><path d="M13,18H14A1,1 0 0,1 15,19H15.73L13,16.27V18M22,19V20.18L20.82,19H22M21,21.72L19.73,23L17.73,21H15A1,1 0 0,1 14,22H10A1,1 0 0,1 9,21H2V19H9A1,1 0 0,1 10,18H11V16H4A1,1 0 0,1 3,15V11A1,1 0 0,1 4,10H6.73L4.73,8H4A1,1 0 0,1 3,7V6.27L1,4.27L2.28,3L21,21.72M4,2H20A1,1 0 0,1 21,3V7A1,1 0 0,1 20,8H9.82L7,5.18V4H5.82L3.84,2C3.89,2 3.94,2 4,2M20,10A1,1 0 0,1 21,11V15A1,1 0 0,1 20,16H17.82L11.82,10H20M9,6H10V4H9V6M9,14H10V13.27L9,12.27V14M5,12V14H7V12H5Z" /></g><g id="server-off"><path d="M4,1H20A1,1 0 0,1 21,2V6A1,1 0 0,1 20,7H8.82L6.82,5H7V3H5V3.18L3.21,1.39C3.39,1.15 3.68,1 4,1M22,22.72L20.73,24L19.73,23H4A1,1 0 0,1 3,22V18A1,1 0 0,1 4,17H13.73L11.73,15H4A1,1 0 0,1 3,14V10A1,1 0 0,1 4,9H5.73L3.68,6.95C3.38,6.85 3.15,6.62 3.05,6.32L1,4.27L2.28,3L22,22.72M20,9A1,1 0 0,1 21,10V14A1,1 0 0,1 20,15H16.82L10.82,9H20M20,17A1,1 0 0,1 21,18V19.18L18.82,17H20M9,5H10V3H9V5M9,13H9.73L9,12.27V13M9,21H10V19H9V21M5,11V13H7V11H5M5,19V21H7V19H5Z" /></g><g id="server-plus"><path d="M4,4H20A1,1 0 0,1 21,5V9A1,1 0 0,1 20,10H4A1,1 0 0,1 3,9V5A1,1 0 0,1 4,4M9,8H10V6H9V8M5,6V8H7V6H5M8,16H11V13H13V16H16V18H13V21H11V18H8V16Z" /></g><g id="server-remove"><path d="M4,4H20A1,1 0 0,1 21,5V9A1,1 0 0,1 20,10H4A1,1 0 0,1 3,9V5A1,1 0 0,1 4,4M9,8H10V6H9V8M5,6V8H7V6H5M10.59,17L8,14.41L9.41,13L12,15.59L14.59,13L16,14.41L13.41,17L16,19.59L14.59,21L12,18.41L9.41,21L8,19.59L10.59,17Z" /></g><g id="server-security"><path d="M3,1H19A1,1 0 0,1 20,2V6A1,1 0 0,1 19,7H3A1,1 0 0,1 2,6V2A1,1 0 0,1 3,1M3,9H19A1,1 0 0,1 20,10V10.67L17.5,9.56L11,12.44V15H3A1,1 0 0,1 2,14V10A1,1 0 0,1 3,9M3,17H11C11.06,19.25 12,21.4 13.46,23H3A1,1 0 0,1 2,22V18A1,1 0 0,1 3,17M8,5H9V3H8V5M8,13H9V11H8V13M8,21H9V19H8V21M4,3V5H6V3H4M4,11V13H6V11H4M4,19V21H6V19H4M17.5,12L22,14V17C22,19.78 20.08,22.37 17.5,23C14.92,22.37 13,19.78 13,17V14L17.5,12M17.5,13.94L15,15.06V17.72C15,19.26 16.07,20.7 17.5,21.06V13.94Z" /></g><g id="settings"><path d="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z" /></g><g id="settings-box"><path d="M17.25,12C17.25,12.23 17.23,12.46 17.2,12.68L18.68,13.84C18.81,13.95 18.85,14.13 18.76,14.29L17.36,16.71C17.27,16.86 17.09,16.92 16.93,16.86L15.19,16.16C14.83,16.44 14.43,16.67 14,16.85L13.75,18.7C13.72,18.87 13.57,19 13.4,19H10.6C10.43,19 10.28,18.87 10.25,18.7L10,16.85C9.56,16.67 9.17,16.44 8.81,16.16L7.07,16.86C6.91,16.92 6.73,16.86 6.64,16.71L5.24,14.29C5.15,14.13 5.19,13.95 5.32,13.84L6.8,12.68C6.77,12.46 6.75,12.23 6.75,12C6.75,11.77 6.77,11.54 6.8,11.32L5.32,10.16C5.19,10.05 5.15,9.86 5.24,9.71L6.64,7.29C6.73,7.13 6.91,7.07 7.07,7.13L8.81,7.84C9.17,7.56 9.56,7.32 10,7.15L10.25,5.29C10.28,5.13 10.43,5 10.6,5H13.4C13.57,5 13.72,5.13 13.75,5.29L14,7.15C14.43,7.32 14.83,7.56 15.19,7.84L16.93,7.13C17.09,7.07 17.27,7.13 17.36,7.29L18.76,9.71C18.85,9.86 18.81,10.05 18.68,10.16L17.2,11.32C17.23,11.54 17.25,11.77 17.25,12M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3M12,10C10.89,10 10,10.89 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12C14,10.89 13.1,10 12,10Z" /></g><g id="shape-circle-plus"><path d="M11,19A6,6 0 0,0 17,13H19A8,8 0 0,1 11,21A8,8 0 0,1 3,13A8,8 0 0,1 11,5V7A6,6 0 0,0 5,13A6,6 0 0,0 11,19M19,5H22V7H19V10H17V7H14V5H17V2H19V5Z" /></g><g id="shape-plus"><path d="M2,2H11V11H2V2M17.5,2C20,2 22,4 22,6.5C22,9 20,11 17.5,11C15,11 13,9 13,6.5C13,4 15,2 17.5,2M6.5,14L11,22H2L6.5,14M19,17H22V19H19V22H17V19H14V17H17V14H19V17Z" /></g><g id="shape-polygon-plus"><path d="M17,15.7V13H19V17L10,21L3,14L7,5H11V7H8.3L5.4,13.6L10.4,18.6L17,15.7M22,5V7H19V10H17V7H14V5H17V2H19V5H22Z" /></g><g id="shape-rectangle-plus"><path d="M19,6H22V8H19V11H17V8H14V6H17V3H19V6M17,17V14H19V19H3V6H11V8H5V17H17Z" /></g><g id="shape-square-plus"><path d="M19,5H22V7H19V10H17V7H14V5H17V2H19V5M17,19V13H19V21H3V5H11V7H5V19H17Z" /></g><g id="share"><path d="M21,11L14,4V8C7,9 4,14 3,19C5.5,15.5 9,13.9 14,13.9V18L21,11Z" /></g><g id="share-variant"><path d="M18,16.08C17.24,16.08 16.56,16.38 16.04,16.85L8.91,12.7C8.96,12.47 9,12.24 9,12C9,11.76 8.96,11.53 8.91,11.3L15.96,7.19C16.5,7.69 17.21,8 18,8A3,3 0 0,0 21,5A3,3 0 0,0 18,2A3,3 0 0,0 15,5C15,5.24 15.04,5.47 15.09,5.7L8.04,9.81C7.5,9.31 6.79,9 6,9A3,3 0 0,0 3,12A3,3 0 0,0 6,15C6.79,15 7.5,14.69 8.04,14.19L15.16,18.34C15.11,18.55 15.08,18.77 15.08,19C15.08,20.61 16.39,21.91 18,21.91C19.61,21.91 20.92,20.61 20.92,19A2.92,2.92 0 0,0 18,16.08Z" /></g><g id="shield"><path d="M12,1L3,5V11C3,16.55 6.84,21.74 12,23C17.16,21.74 21,16.55 21,11V5L12,1Z" /></g><g id="shield-outline"><path d="M21,11C21,16.55 17.16,21.74 12,23C6.84,21.74 3,16.55 3,11V5L12,1L21,5V11M12,21C15.75,20 19,15.54 19,11.22V6.3L12,3.18L5,6.3V11.22C5,15.54 8.25,20 12,21Z" /></g><g id="shopping"><path d="M12,13A5,5 0 0,1 7,8H9A3,3 0 0,0 12,11A3,3 0 0,0 15,8H17A5,5 0 0,1 12,13M12,3A3,3 0 0,1 15,6H9A3,3 0 0,1 12,3M19,6H17A5,5 0 0,0 12,1A5,5 0 0,0 7,6H5C3.89,6 3,6.89 3,8V20A2,2 0 0,0 5,22H19A2,2 0 0,0 21,20V8C21,6.89 20.1,6 19,6Z" /></g><g id="shopping-music"><path d="M12,3A3,3 0 0,0 9,6H15A3,3 0 0,0 12,3M19,6A2,2 0 0,1 21,8V20A2,2 0 0,1 19,22H5C3.89,22 3,21.1 3,20V8C3,6.89 3.89,6 5,6H7A5,5 0 0,1 12,1A5,5 0 0,1 17,6H19M9,19L16.5,14L9,10V19Z" /></g><g id="shredder"><path d="M6,3V7H8V5H16V7H18V3H6M5,8A3,3 0 0,0 2,11V17H5V14H19V17H22V11A3,3 0 0,0 19,8H5M18,10A1,1 0 0,1 19,11A1,1 0 0,1 18,12A1,1 0 0,1 17,11A1,1 0 0,1 18,10M7,16V21H9V16H7M11,16V20H13V16H11M15,16V21H17V16H15Z" /></g><g id="shuffle"><path d="M14.83,13.41L13.42,14.82L16.55,17.95L14.5,20H20V14.5L17.96,16.54L14.83,13.41M14.5,4L16.54,6.04L4,18.59L5.41,20L17.96,7.46L20,9.5V4M10.59,9.17L5.41,4L4,5.41L9.17,10.58L10.59,9.17Z" /></g><g id="shuffle-disabled"><path d="M16,4.5V7H5V9H16V11.5L19.5,8M16,12.5V15H5V17H16V19.5L19.5,16" /></g><g id="shuffle-variant"><path d="M17,3L22.25,7.5L17,12L22.25,16.5L17,21V18H14.26L11.44,15.18L13.56,13.06L15.5,15H17V12L17,9H15.5L6.5,18H2V15H5.26L14.26,6H17V3M2,6H6.5L9.32,8.82L7.2,10.94L5.26,9H2V6Z" /></g><g id="sigma"><path d="M5,4H18V9H17L16,6H10.06L13.65,11.13L9.54,17H16L17,15H18V20H5L10.6,12L5,4Z" /></g><g id="sigma-lower"><path d="M19,12C19,16.42 15.64,20 11.5,20C7.36,20 4,16.42 4,12C4,7.58 7.36,4 11.5,4H20V6H16.46C18,7.47 19,9.61 19,12M11.5,6C8.46,6 6,8.69 6,12C6,15.31 8.46,18 11.5,18C14.54,18 17,15.31 17,12C17,8.69 14.54,6 11.5,6Z" /></g><g id="sign-caution"><path d="M2,3H22V13H18V21H16V13H8V21H6V13H2V3M18.97,11L20,9.97V7.15L16.15,11H18.97M13.32,11L19.32,5H16.5L10.5,11H13.32M7.66,11L13.66,5H10.83L4.83,11H7.66M5.18,5L4,6.18V9L8,5H5.18Z" /></g><g id="signal"><path d="M3,21H6V18H3M8,21H11V14H8M13,21H16V9H13M18,21H21V3H18V21Z" /></g><g id="signal-variant"><path d="M4,6V4H4.1C12.9,4 20,11.1 20,19.9V20H18V19.9C18,12.2 11.8,6 4,6M4,10V8A12,12 0 0,1 16,20H14A10,10 0 0,0 4,10M4,14V12A8,8 0 0,1 12,20H10A6,6 0 0,0 4,14M4,16A4,4 0 0,1 8,20H4V16Z" /></g><g id="silverware"><path d="M8.1,13.34L3.91,9.16C2.35,7.59 2.35,5.06 3.91,3.5L10.93,10.5L8.1,13.34M14.88,11.53L13.41,13L20.29,19.88L18.88,21.29L12,14.41L5.12,21.29L3.71,19.88L13.47,10.12C12.76,8.59 13.26,6.44 14.85,4.85C16.76,2.93 19.5,2.57 20.96,4.03C22.43,5.5 22.07,8.24 20.15,10.15C18.56,11.74 16.41,12.24 14.88,11.53Z" /></g><g id="silverware-fork"><path d="M5.12,21.29L3.71,19.88L13.36,10.22L13.16,10C12.38,9.23 12.38,7.97 13.16,7.19L17.5,2.82L18.43,3.74L15.19,7L16.15,7.94L19.39,4.69L20.31,5.61L17.06,8.85L18,9.81L21.26,6.56L22.18,7.5L17.81,11.84C17.03,12.62 15.77,12.62 15,11.84L14.78,11.64L5.12,21.29Z" /></g><g id="silverware-spoon"><path d="M14.88,11.53L5.12,21.29L3.71,19.88L13.47,10.12C12.76,8.59 13.26,6.44 14.85,4.85C16.76,2.93 19.5,2.57 20.96,4.03C22.43,5.5 22.07,8.24 20.15,10.15C18.56,11.74 16.41,12.24 14.88,11.53Z" /></g><g id="silverware-variant"><path d="M8.1,13.34L3.91,9.16C2.35,7.59 2.35,5.06 3.91,3.5L10.93,10.5L8.1,13.34M13.41,13L20.29,19.88L18.88,21.29L12,14.41L5.12,21.29L3.71,19.88L13.36,10.22L13.16,10C12.38,9.23 12.38,7.97 13.16,7.19L17.5,2.82L18.43,3.74L15.19,7L16.15,7.94L19.39,4.69L20.31,5.61L17.06,8.85L18,9.81L21.26,6.56L22.18,7.5L17.81,11.84C17.03,12.62 15.77,12.62 15,11.84L14.78,11.64L13.41,13Z" /></g><g id="sim"><path d="M20,4A2,2 0 0,0 18,2H10L4,8V20A2,2 0 0,0 6,22H18C19.11,22 20,21.1 20,20V4M9,19H7V17H9V19M17,19H15V17H17V19M9,15H7V11H9V15M13,19H11V15H13V19M13,13H11V11H13V13M17,15H15V11H17V15Z" /></g><g id="sim-alert"><path d="M13,13H11V8H13M13,17H11V15H13M18,2H10L4,8V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V4A2,2 0 0,0 18,2Z" /></g><g id="sim-off"><path d="M19,5A2,2 0 0,0 17,3H10L7.66,5.34L19,16.68V5M3.65,3.88L2.38,5.15L5,7.77V19A2,2 0 0,0 7,21H17C17.36,21 17.68,20.9 17.97,20.74L19.85,22.62L21.12,21.35L3.65,3.88Z" /></g><g id="sitemap"><path d="M9,2V8H11V11H5C3.89,11 3,11.89 3,13V16H1V22H7V16H5V13H11V16H9V22H15V16H13V13H19V16H17V22H23V16H21V13C21,11.89 20.11,11 19,11H13V8H15V2H9Z" /></g><g id="skip-backward"><path d="M20,5V19L13,12M6,5V19H4V5M13,5V19L6,12" /></g><g id="skip-forward"><path d="M4,5V19L11,12M18,5V19H20V5M11,5V19L18,12" /></g><g id="skip-next"><path d="M16,18H18V6H16M6,18L14.5,12L6,6V18Z" /></g><g id="skip-next-circle"><path d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M8,8L13,12L8,16M14,8H16V16H14" /></g><g id="skip-next-circle-outline"><path d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4M8,8V16L13,12M14,8V16H16V8" /></g><g id="skip-previous"><path d="M6,18V6H8V18H6M9.5,12L18,6V18L9.5,12Z" /></g><g id="skip-previous-circle"><path d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M8,8H10V16H8M16,8V16L11,12" /></g><g id="skip-previous-circle-outline"><path d="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4C7.59,4 4,7.59 4,12C4,16.41 7.59,20 12,20C16.41,20 20,16.41 20,12C20,7.59 16.41,4 12,4M16,8V16L11,12M10,8V16H8V8" /></g><g id="skull"><path d="M12,2A9,9 0 0,0 3,11C3,14.03 4.53,16.82 7,18.47V22H9V19H11V22H13V19H15V22H17V18.46C19.47,16.81 21,14 21,11A9,9 0 0,0 12,2M8,11A2,2 0 0,1 10,13A2,2 0 0,1 8,15A2,2 0 0,1 6,13A2,2 0 0,1 8,11M16,11A2,2 0 0,1 18,13A2,2 0 0,1 16,15A2,2 0 0,1 14,13A2,2 0 0,1 16,11M12,14L13.5,17H10.5L12,14Z" /></g><g id="skype"><path d="M18,6C20.07,8.04 20.85,10.89 20.36,13.55C20.77,14.27 21,15.11 21,16A5,5 0 0,1 16,21C15.11,21 14.27,20.77 13.55,20.36C10.89,20.85 8.04,20.07 6,18C3.93,15.96 3.15,13.11 3.64,10.45C3.23,9.73 3,8.89 3,8A5,5 0 0,1 8,3C8.89,3 9.73,3.23 10.45,3.64C13.11,3.15 15.96,3.93 18,6M12.04,17.16C14.91,17.16 16.34,15.78 16.34,13.92C16.34,12.73 15.78,11.46 13.61,10.97L11.62,10.53C10.86,10.36 10,10.13 10,9.42C10,8.7 10.6,8.2 11.7,8.2C13.93,8.2 13.72,9.73 14.83,9.73C15.41,9.73 15.91,9.39 15.91,8.8C15.91,7.43 13.72,6.4 11.86,6.4C9.85,6.4 7.7,7.26 7.7,9.54C7.7,10.64 8.09,11.81 10.25,12.35L12.94,13.03C13.75,13.23 13.95,13.68 13.95,14.1C13.95,14.78 13.27,15.45 12.04,15.45C9.63,15.45 9.96,13.6 8.67,13.6C8.09,13.6 7.67,14 7.67,14.57C7.67,15.68 9,17.16 12.04,17.16Z" /></g><g id="skype-business"><path d="M12.03,16.53C9.37,16.53 8.18,15.22 8.18,14.24C8.18,13.74 8.55,13.38 9.06,13.38C10.2,13.38 9.91,15 12.03,15C13.12,15 13.73,14.43 13.73,13.82C13.73,13.46 13.55,13.06 12.83,12.88L10.46,12.29C8.55,11.81 8.2,10.78 8.2,9.81C8.2,7.79 10.1,7.03 11.88,7.03C13.5,7.03 15.46,7.94 15.46,9.15C15.46,9.67 15,9.97 14.5,9.97C13.5,9.97 13.7,8.62 11.74,8.62C10.77,8.62 10.23,9.06 10.23,9.69C10.23,10.32 11,10.5 11.66,10.68L13.42,11.07C15.34,11.5 15.83,12.62 15.83,13.67C15.83,15.31 14.57,16.53 12.03,16.53M18,6C20.07,8.04 20.85,10.89 20.36,13.55C20.77,14.27 21,15.11 21,16A5,5 0 0,1 16,21C15.11,21 14.27,20.77 13.55,20.36C10.89,20.85 8.04,20.07 6,18C3.93,15.96 3.15,13.11 3.64,10.45C3.23,9.73 3,8.89 3,8A5,5 0 0,1 8,3C8.89,3 9.73,3.23 10.45,3.64C13.11,3.15 15.96,3.93 18,6M8,5A3,3 0 0,0 5,8C5,8.79 5.3,9.5 5.8,10.04C5.1,12.28 5.63,14.82 7.4,16.6C9.18,18.37 11.72,18.9 13.96,18.2C14.5,18.7 15.21,19 16,19A3,3 0 0,0 19,16C19,15.21 18.7,14.5 18.2,13.96C18.9,11.72 18.37,9.18 16.6,7.4C14.82,5.63 12.28,5.1 10.04,5.8C9.5,5.3 8.79,5 8,5Z" /></g><g id="slack"><path d="M10.23,11.16L12.91,10.27L13.77,12.84L11.09,13.73L10.23,11.16M17.69,13.71C18.23,13.53 18.5,12.94 18.34,12.4C18.16,11.86 17.57,11.56 17.03,11.75L15.73,12.18L14.87,9.61L16.17,9.17C16.71,9 17,8.4 16.82,7.86C16.64,7.32 16.05,7 15.5,7.21L14.21,7.64L13.76,6.3C13.58,5.76 13,5.46 12.45,5.65C11.91,5.83 11.62,6.42 11.8,6.96L12.25,8.3L9.57,9.19L9.12,7.85C8.94,7.31 8.36,7 7.81,7.2C7.27,7.38 7,7.97 7.16,8.5L7.61,9.85L6.31,10.29C5.77,10.47 5.5,11.06 5.66,11.6C5.8,12 6.19,12.3 6.61,12.31L6.97,12.25L8.27,11.82L9.13,14.39L7.83,14.83C7.29,15 7,15.6 7.18,16.14C7.32,16.56 7.71,16.84 8.13,16.85L8.5,16.79L9.79,16.36L10.24,17.7C10.38,18.13 10.77,18.4 11.19,18.41L11.55,18.35C12.09,18.17 12.38,17.59 12.2,17.04L11.75,15.7L14.43,14.81L14.88,16.15C15,16.57 15.41,16.84 15.83,16.85L16.19,16.8C16.73,16.62 17,16.03 16.84,15.5L16.39,14.15L17.69,13.71M21.17,9.25C23.23,16.12 21.62,19.1 14.75,21.17C7.88,23.23 4.9,21.62 2.83,14.75C0.77,7.88 2.38,4.9 9.25,2.83C16.12,0.77 19.1,2.38 21.17,9.25Z" /></g><g id="sleep"><path d="M23,12H17V10L20.39,6H17V4H23V6L19.62,10H23V12M15,16H9V14L12.39,10H9V8H15V10L11.62,14H15V16M7,20H1V18L4.39,14H1V12H7V14L3.62,18H7V20Z" /></g><g id="sleep-off"><path d="M2,5.27L3.28,4L20,20.72L18.73,22L12.73,16H9V14L9.79,13.06L2,5.27M23,12H17V10L20.39,6H17V4H23V6L19.62,10H23V12M9.82,8H15V10L13.54,11.72L9.82,8M7,20H1V18L4.39,14H1V12H7V14L3.62,18H7V20Z" /></g><g id="smoking"><path d="M7,19H22V15H7M2,19H5V15H2M10,4V5A3,3 0 0,1 7,8A5,5 0 0,0 2,13H4A3,3 0 0,1 7,10A5,5 0 0,0 12,5V4H10Z" /></g><g id="smoking-off"><path d="M15.82,14L19.82,18H22V14M2,18H5V14H2M3.28,4L2,5.27L4.44,7.71C2.93,8.61 2,10.24 2,12H4C4,10.76 4.77,9.64 5.93,9.2L10.73,14H7V18H14.73L18.73,22L20,20.72M10,3V4C10,5.09 9.4,6.1 8.45,6.62L9.89,8.07C11.21,7.13 12,5.62 12,4V3H10Z" /></g><g id="snapchat"><path d="M12,20.45C10.81,20.45 10.1,19.94 9.47,19.5C9,19.18 8.58,18.87 8.08,18.79C6.93,18.73 6.59,18.79 5.97,18.9C5.86,18.9 5.73,18.87 5.68,18.69C5.5,17.94 5.45,17.73 5.32,17.71C4,17.5 3.19,17.2 3.03,16.83C3,16.6 3.07,16.5 3.18,16.5C4.25,16.31 5.2,15.75 6,14.81C6.63,14.09 6.93,13.39 6.96,13.32C7.12,13 7.15,12.72 7.06,12.5C6.89,12.09 6.31,11.91 5.68,11.7C5.34,11.57 4.79,11.29 4.86,10.9C4.92,10.62 5.29,10.42 5.81,10.46C6.16,10.62 6.46,10.7 6.73,10.7C7.06,10.7 7.21,10.58 7.25,10.54C7.14,8.78 7.05,7.25 7.44,6.38C8.61,3.76 11.08,3.55 12,3.55C12.92,3.55 15.39,3.76 16.56,6.38C16.95,7.25 16.86,8.78 16.75,10.54C16.79,10.58 16.94,10.7 17.27,10.7C17.54,10.7 17.84,10.62 18.19,10.46C18.71,10.42 19.08,10.62 19.14,10.9C19.21,11.29 18.66,11.57 18.32,11.7C17.69,11.91 17.11,12.09 16.94,12.5C16.85,12.72 16.88,13 17.04,13.32C17.07,13.39 17.37,14.09 18,14.81C18.8,15.75 19.75,16.31 20.82,16.5C20.93,16.5 21,16.6 20.97,16.83C20.81,17.2 20,17.5 18.68,17.71C18.55,17.73 18.5,17.94 18.32,18.69C18.27,18.87 18.14,18.9 18.03,18.9C17.41,18.79 17.07,18.73 15.92,18.79C15.42,18.87 15,19.18 14.53,19.5C13.9,19.94 13.19,20.45 12,20.45Z" /></g><g id="snowman"><path d="M17,17A5,5 0 0,1 12,22A5,5 0 0,1 7,17C7,15.5 7.65,14.17 8.69,13.25C8.26,12.61 8,11.83 8,11C8,10.86 8,10.73 8,10.59L5.04,8.87L4.83,8.71L2.29,9.39L2.03,8.43L4.24,7.84L2.26,6.69L2.76,5.82L4.74,6.97L4.15,4.75L5.11,4.5L5.8,7.04L6.04,7.14L8.73,8.69C9.11,8.15 9.62,7.71 10.22,7.42C9.5,6.87 9,6 9,5A3,3 0 0,1 12,2A3,3 0 0,1 15,5C15,6 14.5,6.87 13.78,7.42C14.38,7.71 14.89,8.15 15.27,8.69L17.96,7.14L18.2,7.04L18.89,4.5L19.85,4.75L19.26,6.97L21.24,5.82L21.74,6.69L19.76,7.84L21.97,8.43L21.71,9.39L19.17,8.71L18.96,8.87L16,10.59V11C16,11.83 15.74,12.61 15.31,13.25C16.35,14.17 17,15.5 17,17Z" /></g><g id="soccer"><path d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,3C13.76,3 15.4,3.53 16.78,4.41L16.5,5H13L12,5L10.28,4.16L10.63,3.13C11.08,3.05 11.53,3 12,3M9.53,3.38L9.19,4.41L6.63,5.69L5.38,5.94C6.5,4.73 7.92,3.84 9.53,3.38M13,6H16L18.69,9.59L17.44,12.16L14.81,12.78L11.53,8.94L13,6M6.16,6.66L7,10L5.78,13.06L3.22,13.94C3.08,13.31 3,12.67 3,12C3,10.1 3.59,8.36 4.59,6.91L6.16,6.66M20.56,9.22C20.85,10.09 21,11.03 21,12C21,13.44 20.63,14.79 20.03,16H19L18.16,12.66L19.66,9.66L20.56,9.22M8,10H11L13.81,13.28L12,16L8.84,16.78L6.53,13.69L8,10M12,17L15,19L14.13,20.72C13.44,20.88 12.73,21 12,21C10.25,21 8.63,20.5 7.25,19.63L8.41,17.91L12,17M19,17H19.5C18.5,18.5 17,19.67 15.31,20.34L16,19L19,17Z" /></g><g id="sofa"><path d="M7,6H9A2,2 0 0,1 11,8V12H5V8A2,2 0 0,1 7,6M15,6H17A2,2 0 0,1 19,8V12H13V8A2,2 0 0,1 15,6M1,9H2A1,1 0 0,1 3,10V12A2,2 0 0,0 5,14H19A2,2 0 0,0 21,12V11L21,10A1,1 0 0,1 22,9H23A1,1 0 0,1 24,10V19H21V17H3V19H0V10A1,1 0 0,1 1,9Z" /></g><g id="solid"><path d="M0,0H24V24H0" /></g><g id="sort"><path d="M10,13V11H18V13H10M10,19V17H14V19H10M10,7V5H22V7H10M6,17H8.5L5,20.5L1.5,17H4V7H1.5L5,3.5L8.5,7H6V17Z" /></g><g id="sort-alphabetical"><path d="M9.25,5L12.5,1.75L15.75,5H9.25M15.75,19L12.5,22.25L9.25,19H15.75M8.89,14.3H6L5.28,17H2.91L6,7H9L12.13,17H9.67L8.89,14.3M6.33,12.68H8.56L7.93,10.56L7.67,9.59L7.42,8.63H7.39L7.17,9.6L6.93,10.58L6.33,12.68M13.05,17V15.74L17.8,8.97V8.91H13.5V7H20.73V8.34L16.09,15V15.08H20.8V17H13.05Z" /></g><g id="sort-ascending"><path d="M10,11V13H18V11H10M10,5V7H14V5H10M10,17V19H22V17H10M6,7H8.5L5,3.5L1.5,7H4V20H6V7Z" /></g><g id="sort-descending"><path d="M10,13V11H18V13H10M10,19V17H14V19H10M10,7V5H22V7H10M6,17H8.5L5,20.5L1.5,17H4V4H6V17Z" /></g><g id="sort-numeric"><path d="M7.78,7C9.08,7.04 10,7.53 10.57,8.46C11.13,9.4 11.41,10.56 11.39,11.95C11.4,13.5 11.09,14.73 10.5,15.62C9.88,16.5 8.95,16.97 7.71,17C6.45,16.96 5.54,16.5 4.96,15.56C4.38,14.63 4.09,13.45 4.09,12C4.09,10.55 4.39,9.36 5,8.44C5.59,7.5 6.5,7.04 7.78,7M7.75,8.63C7.31,8.63 6.96,8.9 6.7,9.46C6.44,10 6.32,10.87 6.32,12C6.31,13.15 6.44,14 6.69,14.54C6.95,15.1 7.31,15.37 7.77,15.37C8.69,15.37 9.16,14.24 9.17,12C9.17,9.77 8.7,8.65 7.75,8.63M13.33,17V15.22L13.76,15.24L14.3,15.22L15.34,15.03C15.68,14.92 16,14.78 16.26,14.58C16.59,14.35 16.86,14.08 17.07,13.76C17.29,13.45 17.44,13.12 17.53,12.78L17.5,12.77C17.05,13.19 16.38,13.4 15.47,13.41C14.62,13.4 13.91,13.15 13.34,12.65C12.77,12.15 12.5,11.43 12.46,10.5C12.47,9.5 12.81,8.69 13.47,8.03C14.14,7.37 15,7.03 16.12,7C17.37,7.04 18.29,7.45 18.88,8.24C19.47,9 19.76,10 19.76,11.19C19.75,12.15 19.61,13 19.32,13.76C19.03,14.5 18.64,15.13 18.12,15.64C17.66,16.06 17.11,16.38 16.47,16.61C15.83,16.83 15.12,16.96 14.34,17H13.33M16.06,8.63C15.65,8.64 15.32,8.8 15.06,9.11C14.81,9.42 14.68,9.84 14.68,10.36C14.68,10.8 14.8,11.16 15.03,11.46C15.27,11.77 15.63,11.92 16.11,11.93C16.43,11.93 16.7,11.86 16.92,11.74C17.14,11.61 17.3,11.46 17.41,11.28C17.5,11.17 17.53,10.97 17.53,10.71C17.54,10.16 17.43,9.69 17.2,9.28C16.97,8.87 16.59,8.65 16.06,8.63M9.25,5L12.5,1.75L15.75,5H9.25M15.75,19L12.5,22.25L9.25,19H15.75Z" /></g><g id="sort-variant"><path d="M3,13H15V11H3M3,6V8H21V6M3,18H9V16H3V18Z" /></g><g id="soundcloud"><path d="M11.56,8.87V17H20.32V17C22.17,16.87 23,15.73 23,14.33C23,12.85 21.88,11.66 20.38,11.66C20,11.66 19.68,11.74 19.35,11.88C19.11,9.54 17.12,7.71 14.67,7.71C13.5,7.71 12.39,8.15 11.56,8.87M10.68,9.89C10.38,9.71 10.06,9.57 9.71,9.5V17H11.1V9.34C10.95,9.5 10.81,9.7 10.68,9.89M8.33,9.35V17H9.25V9.38C9.06,9.35 8.87,9.34 8.67,9.34C8.55,9.34 8.44,9.34 8.33,9.35M6.5,10V17H7.41V9.54C7.08,9.65 6.77,9.81 6.5,10M4.83,12.5C4.77,12.5 4.71,12.44 4.64,12.41V17H5.56V10.86C5.19,11.34 4.94,11.91 4.83,12.5M2.79,12.22V16.91C3,16.97 3.24,17 3.5,17H3.72V12.14C3.64,12.13 3.56,12.12 3.5,12.12C3.24,12.12 3,12.16 2.79,12.22M1,14.56C1,15.31 1.34,15.97 1.87,16.42V12.71C1.34,13.15 1,13.82 1,14.56Z" /></g><g id="source-branch"><path d="M13,14C9.64,14 8.54,15.35 8.18,16.24C9.25,16.7 10,17.76 10,19A3,3 0 0,1 7,22A3,3 0 0,1 4,19C4,17.69 4.83,16.58 6,16.17V7.83C4.83,7.42 4,6.31 4,5A3,3 0 0,1 7,2A3,3 0 0,1 10,5C10,6.31 9.17,7.42 8,7.83V13.12C8.88,12.47 10.16,12 12,12C14.67,12 15.56,10.66 15.85,9.77C14.77,9.32 14,8.25 14,7A3,3 0 0,1 17,4A3,3 0 0,1 20,7C20,8.34 19.12,9.5 17.91,9.86C17.65,11.29 16.68,14 13,14M7,18A1,1 0 0,0 6,19A1,1 0 0,0 7,20A1,1 0 0,0 8,19A1,1 0 0,0 7,18M7,4A1,1 0 0,0 6,5A1,1 0 0,0 7,6A1,1 0 0,0 8,5A1,1 0 0,0 7,4M17,6A1,1 0 0,0 16,7A1,1 0 0,0 17,8A1,1 0 0,0 18,7A1,1 0 0,0 17,6Z" /></g><g id="source-fork"><path d="M6,2A3,3 0 0,1 9,5C9,6.28 8.19,7.38 7.06,7.81C7.15,8.27 7.39,8.83 8,9.63C9,10.92 11,12.83 12,14.17C13,12.83 15,10.92 16,9.63C16.61,8.83 16.85,8.27 16.94,7.81C15.81,7.38 15,6.28 15,5A3,3 0 0,1 18,2A3,3 0 0,1 21,5C21,6.32 20.14,7.45 18.95,7.85C18.87,8.37 18.64,9 18,9.83C17,11.17 15,13.08 14,14.38C13.39,15.17 13.15,15.73 13.06,16.19C14.19,16.62 15,17.72 15,19A3,3 0 0,1 12,22A3,3 0 0,1 9,19C9,17.72 9.81,16.62 10.94,16.19C10.85,15.73 10.61,15.17 10,14.38C9,13.08 7,11.17 6,9.83C5.36,9 5.13,8.37 5.05,7.85C3.86,7.45 3,6.32 3,5A3,3 0 0,1 6,2M6,4A1,1 0 0,0 5,5A1,1 0 0,0 6,6A1,1 0 0,0 7,5A1,1 0 0,0 6,4M18,4A1,1 0 0,0 17,5A1,1 0 0,0 18,6A1,1 0 0,0 19,5A1,1 0 0,0 18,4M12,18A1,1 0 0,0 11,19A1,1 0 0,0 12,20A1,1 0 0,0 13,19A1,1 0 0,0 12,18Z" /></g><g id="source-merge"><path d="M7,3A3,3 0 0,1 10,6C10,7.29 9.19,8.39 8.04,8.81C8.58,13.81 13.08,14.77 15.19,14.96C15.61,13.81 16.71,13 18,13A3,3 0 0,1 21,16A3,3 0 0,1 18,19C16.69,19 15.57,18.16 15.16,17C10.91,16.8 9.44,15.19 8,13.39V15.17C9.17,15.58 10,16.69 10,18A3,3 0 0,1 7,21A3,3 0 0,1 4,18C4,16.69 4.83,15.58 6,15.17V8.83C4.83,8.42 4,7.31 4,6A3,3 0 0,1 7,3M7,5A1,1 0 0,0 6,6A1,1 0 0,0 7,7A1,1 0 0,0 8,6A1,1 0 0,0 7,5M7,17A1,1 0 0,0 6,18A1,1 0 0,0 7,19A1,1 0 0,0 8,18A1,1 0 0,0 7,17M18,15A1,1 0 0,0 17,16A1,1 0 0,0 18,17A1,1 0 0,0 19,16A1,1 0 0,0 18,15Z" /></g><g id="source-pull"><path d="M6,3A3,3 0 0,1 9,6C9,7.31 8.17,8.42 7,8.83V15.17C8.17,15.58 9,16.69 9,18A3,3 0 0,1 6,21A3,3 0 0,1 3,18C3,16.69 3.83,15.58 5,15.17V8.83C3.83,8.42 3,7.31 3,6A3,3 0 0,1 6,3M6,5A1,1 0 0,0 5,6A1,1 0 0,0 6,7A1,1 0 0,0 7,6A1,1 0 0,0 6,5M6,17A1,1 0 0,0 5,18A1,1 0 0,0 6,19A1,1 0 0,0 7,18A1,1 0 0,0 6,17M21,18A3,3 0 0,1 18,21A3,3 0 0,1 15,18C15,16.69 15.83,15.58 17,15.17V7H15V10.25L10.75,6L15,1.75V5H17A2,2 0 0,1 19,7V15.17C20.17,15.58 21,16.69 21,18M18,17A1,1 0 0,0 17,18A1,1 0 0,0 18,19A1,1 0 0,0 19,18A1,1 0 0,0 18,17Z" /></g><g id="speaker"><path d="M12,12A3,3 0 0,0 9,15A3,3 0 0,0 12,18A3,3 0 0,0 15,15A3,3 0 0,0 12,12M12,20A5,5 0 0,1 7,15A5,5 0 0,1 12,10A5,5 0 0,1 17,15A5,5 0 0,1 12,20M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8C10.89,8 10,7.1 10,6C10,4.89 10.89,4 12,4M17,2H7C5.89,2 5,2.89 5,4V20A2,2 0 0,0 7,22H17A2,2 0 0,0 19,20V4C19,2.89 18.1,2 17,2Z" /></g><g id="speaker-off"><path d="M2,5.27L3.28,4L21,21.72L19.73,23L18.27,21.54C17.93,21.83 17.5,22 17,22H7C5.89,22 5,21.1 5,20V8.27L2,5.27M12,18A3,3 0 0,1 9,15C9,14.24 9.28,13.54 9.75,13L8.33,11.6C7.5,12.5 7,13.69 7,15A5,5 0 0,0 12,20C13.31,20 14.5,19.5 15.4,18.67L14,17.25C13.45,17.72 12.76,18 12,18M17,15A5,5 0 0,0 12,10H11.82L5.12,3.3C5.41,2.54 6.14,2 7,2H17A2,2 0 0,1 19,4V17.18L17,15.17V15M12,4C10.89,4 10,4.89 10,6A2,2 0 0,0 12,8A2,2 0 0,0 14,6C14,4.89 13.1,4 12,4Z" /></g><g id="speedometer"><path d="M12,16A3,3 0 0,1 9,13C9,11.88 9.61,10.9 10.5,10.39L20.21,4.77L14.68,14.35C14.18,15.33 13.17,16 12,16M12,3C13.81,3 15.5,3.5 16.97,4.32L14.87,5.53C14,5.19 13,5 12,5A8,8 0 0,0 4,13C4,15.21 4.89,17.21 6.34,18.65H6.35C6.74,19.04 6.74,19.67 6.35,20.06C5.96,20.45 5.32,20.45 4.93,20.07V20.07C3.12,18.26 2,15.76 2,13A10,10 0 0,1 12,3M22,13C22,15.76 20.88,18.26 19.07,20.07V20.07C18.68,20.45 18.05,20.45 17.66,20.06C17.27,19.67 17.27,19.04 17.66,18.65V18.65C19.11,17.2 20,15.21 20,13C20,12 19.81,11 19.46,10.1L20.67,8C21.5,9.5 22,11.18 22,13Z" /></g><g id="spellcheck"><path d="M21.59,11.59L13.5,19.68L9.83,16L8.42,17.41L13.5,22.5L23,13M6.43,11L8.5,5.5L10.57,11M12.45,16H14.54L9.43,3H7.57L2.46,16H4.55L5.67,13H11.31L12.45,16Z" /></g><g id="spotify"><path d="M17.9,10.9C14.7,9 9.35,8.8 6.3,9.75C5.8,9.9 5.3,9.6 5.15,9.15C5,8.65 5.3,8.15 5.75,8C9.3,6.95 15.15,7.15 18.85,9.35C19.3,9.6 19.45,10.2 19.2,10.65C18.95,11 18.35,11.15 17.9,10.9M17.8,13.7C17.55,14.05 17.1,14.2 16.75,13.95C14.05,12.3 9.95,11.8 6.8,12.8C6.4,12.9 5.95,12.7 5.85,12.3C5.75,11.9 5.95,11.45 6.35,11.35C10,10.25 14.5,10.8 17.6,12.7C17.9,12.85 18.05,13.35 17.8,13.7M16.6,16.45C16.4,16.75 16.05,16.85 15.75,16.65C13.4,15.2 10.45,14.9 6.95,15.7C6.6,15.8 6.3,15.55 6.2,15.25C6.1,14.9 6.35,14.6 6.65,14.5C10.45,13.65 13.75,14 16.35,15.6C16.7,15.75 16.75,16.15 16.6,16.45M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" /></g><g id="spotlight"><path d="M2,6L7.09,8.55C6.4,9.5 6,10.71 6,12C6,13.29 6.4,14.5 7.09,15.45L2,18V6M6,3H18L15.45,7.09C14.5,6.4 13.29,6 12,6C10.71,6 9.5,6.4 8.55,7.09L6,3M22,6V18L16.91,15.45C17.6,14.5 18,13.29 18,12C18,10.71 17.6,9.5 16.91,8.55L22,6M18,21H6L8.55,16.91C9.5,17.6 10.71,18 12,18C13.29,18 14.5,17.6 15.45,16.91L18,21M12,8A4,4 0 0,1 16,12A4,4 0 0,1 12,16A4,4 0 0,1 8,12A4,4 0 0,1 12,8M12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12A2,2 0 0,0 12,10Z" /></g><g id="spotlight-beam"><path d="M9,16.5L9.91,15.59L15.13,20.8L14.21,21.71L9,16.5M15.5,10L16.41,9.09L21.63,14.3L20.71,15.21L15.5,10M6.72,2.72L10.15,6.15L6.15,10.15L2.72,6.72C1.94,5.94 1.94,4.67 2.72,3.89L3.89,2.72C4.67,1.94 5.94,1.94 6.72,2.72M14.57,7.5L15.28,8.21L8.21,15.28L7.5,14.57L6.64,11.07L11.07,6.64L14.57,7.5Z" /></g><g id="spray"><path d="M10,4H12V6H10V4M7,3H9V5H7V3M7,6H9V8H7V6M6,8V10H4V8H6M6,5V7H4V5H6M6,2V4H4V2H6M13,22A2,2 0 0,1 11,20V10A2,2 0 0,1 13,8V7H14V4H17V7H18V8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H13M13,10V20H18V10H13Z" /></g><g id="square-inc"><path d="M6,3H18A3,3 0 0,1 21,6V18A3,3 0 0,1 18,21H6A3,3 0 0,1 3,18V6A3,3 0 0,1 6,3M7,6A1,1 0 0,0 6,7V17A1,1 0 0,0 7,18H17A1,1 0 0,0 18,17V7A1,1 0 0,0 17,6H7M9.5,9H14.5A0.5,0.5 0 0,1 15,9.5V14.5A0.5,0.5 0 0,1 14.5,15H9.5A0.5,0.5 0 0,1 9,14.5V9.5A0.5,0.5 0 0,1 9.5,9Z" /></g><g id="square-inc-cash"><path d="M5.5,0H18.5A5.5,5.5 0 0,1 24,5.5V18.5A5.5,5.5 0 0,1 18.5,24H5.5A5.5,5.5 0 0,1 0,18.5V5.5A5.5,5.5 0 0,1 5.5,0M15.39,15.18C15.39,16.76 14.5,17.81 12.85,17.95V12.61C14.55,13.13 15.39,13.66 15.39,15.18M11.65,6V10.88C10.34,10.5 9.03,9.93 9.03,8.43C9.03,6.94 10.18,6.12 11.65,6M15.5,7.6L16.5,6.8C15.62,5.66 14.4,4.92 12.85,4.77V3.8H11.65V3.8L11.65,4.75C9.5,4.89 7.68,6.17 7.68,8.5C7.68,11 9.74,11.78 11.65,12.29V17.96C10.54,17.84 9.29,17.31 8.43,16.03L7.3,16.78C8.2,18.12 9.76,19 11.65,19.14V20.2H12.07L12.85,20.2V19.16C15.35,19 16.7,17.34 16.7,15.14C16.7,12.58 14.81,11.76 12.85,11.19V6.05C14,6.22 14.85,6.76 15.5,7.6Z" /></g><g id="stackexchange"><path d="M4,14.04V11H20V14.04H4M4,10V7H20V10H4M17.46,2C18.86,2 20,3.18 20,4.63V6H4V4.63C4,3.18 5.14,2 6.54,2H17.46M4,15H20V16.35C20,17.81 18.86,19 17.46,19H16.5L13,22V19H6.54C5.14,19 4,17.81 4,16.35V15Z" /></g><g id="stackoverflow"><path d="M17.36,20.2V14.82H19.15V22H3V14.82H4.8V20.2H17.36M6.77,14.32L7.14,12.56L15.93,14.41L15.56,16.17L6.77,14.32M7.93,10.11L8.69,8.5L16.83,12.28L16.07,13.9L7.93,10.11M10.19,6.12L11.34,4.74L18.24,10.5L17.09,11.87L10.19,6.12M14.64,1.87L20,9.08L18.56,10.15L13.2,2.94L14.64,1.87M6.59,18.41V16.61H15.57V18.41H6.59Z" /></g><g id="stairs"><path d="M15,5V9H11V13H7V17H3V20H10V16H14V12H18V8H22V5H15Z" /></g><g id="star"><path d="M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z" /></g><g id="star-circle"><path d="M16.23,18L12,15.45L7.77,18L8.89,13.19L5.16,9.96L10.08,9.54L12,5L13.92,9.53L18.84,9.95L15.11,13.18L16.23,18M12,2C6.47,2 2,6.5 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" /></g><g id="star-half"><path d="M12,15.89V6.59L13.71,10.63L18.09,11L14.77,13.88L15.76,18.16M22,9.74L14.81,9.13L12,2.5L9.19,9.13L2,9.74L7.45,14.47L5.82,21.5L12,17.77L18.18,21.5L16.54,14.47L22,9.74Z" /></g><g id="star-off"><path d="M2,5.27L3.28,4L20,20.72L18.73,22L17.05,20.31L12,17.27L5.82,21L7.45,13.97L2,9.24L5.66,8.93L2,5.27M12,2L14.81,8.62L22,9.24L16.54,13.97L16.77,14.95L9.56,7.74L12,2Z" /></g><g id="star-outline"><path d="M12,15.39L8.24,17.66L9.23,13.38L5.91,10.5L10.29,10.13L12,6.09L13.71,10.13L18.09,10.5L14.77,13.38L15.76,17.66M22,9.24L14.81,8.63L12,2L9.19,8.63L2,9.24L7.45,13.97L5.82,21L12,17.27L18.18,21L16.54,13.97L22,9.24Z" /></g><g id="steam"><path d="M20.14,7.79C21.33,7.79 22.29,8.75 22.29,9.93C22.29,11.11 21.33,12.07 20.14,12.07A2.14,2.14 0 0,1 18,9.93C18,8.75 18.96,7.79 20.14,7.79M3,6.93A3,3 0 0,1 6,9.93V10.24L12.33,13.54C12.84,13.15 13.46,12.93 14.14,12.93L16.29,9.93C16.29,7.8 18,6.07 20.14,6.07A3.86,3.86 0 0,1 24,9.93A3.86,3.86 0 0,1 20.14,13.79L17.14,15.93A3,3 0 0,1 14.14,18.93C12.5,18.93 11.14,17.59 11.14,15.93C11.14,15.89 11.14,15.85 11.14,15.82L4.64,12.44C4.17,12.75 3.6,12.93 3,12.93A3,3 0 0,1 0,9.93A3,3 0 0,1 3,6.93M15.03,14.94C15.67,15.26 15.92,16.03 15.59,16.67C15.27,17.3 14.5,17.55 13.87,17.23L12.03,16.27C12.19,17.29 13.08,18.07 14.14,18.07C15.33,18.07 16.29,17.11 16.29,15.93C16.29,14.75 15.33,13.79 14.14,13.79C13.81,13.79 13.5,13.86 13.22,14L15.03,14.94M3,7.79C1.82,7.79 0.86,8.75 0.86,9.93C0.86,11.11 1.82,12.07 3,12.07C3.24,12.07 3.5,12.03 3.7,11.95L2.28,11.22C1.64,10.89 1.39,10.12 1.71,9.5C2.04,8.86 2.81,8.6 3.44,8.93L5.14,9.81C5.08,8.68 4.14,7.79 3,7.79M20.14,6.93C18.5,6.93 17.14,8.27 17.14,9.93A3,3 0 0,0 20.14,12.93A3,3 0 0,0 23.14,9.93A3,3 0 0,0 20.14,6.93Z" /></g><g id="steering"><path d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,4C16.1,4 19.5,7.1 20,11H17C16.5,9.9 14.4,9 12,9C9.6,9 7.5,9.9 7,11H4C4.5,7.1 7.9,4 12,4M4,13H7C7.2,14.3 8.2,16.6 11,17V20C7.4,19.6 4.4,16.6 4,13M13,20V17C15.8,16.6 16.7,14.3 17,13H20C19.6,16.6 16.6,19.6 13,20Z" /></g><g id="step-backward"><path d="M19,5V19H16V5M14,5V19L3,12" /></g><g id="step-backward-2"><path d="M17,5H14V19H17V5M12,5L1,12L12,19V5M22,5H19V19H22V5Z" /></g><g id="step-forward"><path d="M5,5V19H8V5M10,5V19L21,12" /></g><g id="step-forward-2"><path d="M7,5H10V19H7V5M12,5L23,12L12,19V5M2,5H5V19H2V5Z" /></g><g id="stethoscope"><path d="M19,8C19.56,8 20,8.43 20,9A1,1 0 0,1 19,10C18.43,10 18,9.55 18,9C18,8.43 18.43,8 19,8M2,2V11C2,13.96 4.19,16.5 7.14,16.91C7.76,19.92 10.42,22 13.5,22A6.5,6.5 0 0,0 20,15.5V11.81C21.16,11.39 22,10.29 22,9A3,3 0 0,0 19,6A3,3 0 0,0 16,9C16,10.29 16.84,11.4 18,11.81V15.41C18,17.91 16,19.91 13.5,19.91C11.5,19.91 9.82,18.7 9.22,16.9C12,16.3 14,13.8 14,11V2H10V5H12V11A4,4 0 0,1 8,15A4,4 0 0,1 4,11V5H6V2H2Z" /></g><g id="sticker"><path d="M12.12,18.46L18.3,12.28C16.94,12.59 15.31,13.2 14.07,14.46C13.04,15.5 12.39,16.83 12.12,18.46M20.75,10H21.05C21.44,10 21.79,10.27 21.93,10.64C22.07,11 22,11.43 21.7,11.71L11.7,21.71C11.5,21.9 11.26,22 11,22L10.64,21.93C10.27,21.79 10,21.44 10,21.05C9.84,17.66 10.73,14.96 12.66,13.03C15.5,10.2 19.62,10 20.75,10M12,2C16.5,2 20.34,5 21.58,9.11L20,9H19.42C18.24,6.07 15.36,4 12,4A8,8 0 0,0 4,12C4,15.36 6.07,18.24 9,19.42C8.97,20.13 9,20.85 9.11,21.57C5,20.33 2,16.5 2,12C2,6.47 6.5,2 12,2Z" /></g><g id="stocking"><path d="M17,2A2,2 0 0,1 19,4V7A2,2 0 0,1 17,9V17C17,17.85 16.5,18.57 15.74,18.86L9.5,21.77C8.5,22.24 7.29,21.81 6.83,20.81L6,19C5.5,18 5.95,16.8 6.95,16.34L10,14.91V9A2,2 0 0,1 8,7V4A2,2 0 0,1 10,2H17M10,4V7H17V4H10Z" /></g><g id="stop"><path d="M18,18H6V6H18V18Z" /></g><g id="stop-circle"><path d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M9,9H15V15H9" /></g><g id="stop-circle-outline"><path d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4M9,9V15H15V9" /></g><g id="store"><path d="M12,18H6V14H12M21,14V12L20,7H4L3,12V14H4V20H14V14H18V20H20V14M20,4H4V6H20V4Z" /></g><g id="store-24-hour"><path d="M16,12H15V10H13V7H14V9H15V7H16M11,10H9V11H11V12H8V9H10V8H8V7H11M19,7V4H5V7H2V20H10V16H14V20H22V7H19Z" /></g><g id="stove"><path d="M6,14H8L11,17H9L6,14M4,4H5V3A1,1 0 0,1 6,2H10A1,1 0 0,1 11,3V4H13V3A1,1 0 0,1 14,2H18A1,1 0 0,1 19,3V4H20A2,2 0 0,1 22,6V19A2,2 0 0,1 20,21V22H17V21H7V22H4V21A2,2 0 0,1 2,19V6A2,2 0 0,1 4,4M18,7A1,1 0 0,1 19,8A1,1 0 0,1 18,9A1,1 0 0,1 17,8A1,1 0 0,1 18,7M14,7A1,1 0 0,1 15,8A1,1 0 0,1 14,9A1,1 0 0,1 13,8A1,1 0 0,1 14,7M20,6H4V10H20V6M4,19H20V12H4V19M6,7A1,1 0 0,1 7,8A1,1 0 0,1 6,9A1,1 0 0,1 5,8A1,1 0 0,1 6,7M13,14H15L18,17H16L13,14Z" /></g><g id="subdirectory-arrow-left"><path d="M11,9L12.42,10.42L8.83,14H18V4H20V16H8.83L12.42,19.58L11,21L5,15L11,9Z" /></g><g id="subdirectory-arrow-right"><path d="M19,15L13,21L11.58,19.58L15.17,16H4V4H6V14H15.17L11.58,10.42L13,9L19,15Z" /></g><g id="subway"><path d="M8.5,15A1,1 0 0,1 9.5,16A1,1 0 0,1 8.5,17A1,1 0 0,1 7.5,16A1,1 0 0,1 8.5,15M7,9H17V14H7V9M15.5,15A1,1 0 0,1 16.5,16A1,1 0 0,1 15.5,17A1,1 0 0,1 14.5,16A1,1 0 0,1 15.5,15M18,15.88V9C18,6.38 15.32,6 12,6C9,6 6,6.37 6,9V15.88A2.62,2.62 0 0,0 8.62,18.5L7.5,19.62V20H9.17L10.67,18.5H13.5L15,20H16.5V19.62L15.37,18.5C16.82,18.5 18,17.33 18,15.88M17.8,2.8C20.47,3.84 22,6.05 22,8.86V22H2V8.86C2,6.05 3.53,3.84 6.2,2.8C8,2.09 10.14,2 12,2C13.86,2 16,2.09 17.8,2.8Z" /></g><g id="subway-variant"><path d="M18,11H13V6H18M16.5,17A1.5,1.5 0 0,1 15,15.5A1.5,1.5 0 0,1 16.5,14A1.5,1.5 0 0,1 18,15.5A1.5,1.5 0 0,1 16.5,17M11,11H6V6H11M7.5,17A1.5,1.5 0 0,1 6,15.5A1.5,1.5 0 0,1 7.5,14A1.5,1.5 0 0,1 9,15.5A1.5,1.5 0 0,1 7.5,17M12,2C7.58,2 4,2.5 4,6V15.5A3.5,3.5 0 0,0 7.5,19L6,20.5V21H18V20.5L16.5,19A3.5,3.5 0 0,0 20,15.5V6C20,2.5 16.42,2 12,2Z" /></g><g id="sunglasses"><path d="M7,17H4C2.38,17 0.96,15.74 0.76,14.14L0.26,11.15C0.15,10.3 0.39,9.5 0.91,8.92C1.43,8.34 2.19,8 3,8H9C9.83,8 10.58,8.35 11.06,8.96C11.17,9.11 11.27,9.27 11.35,9.45C11.78,9.36 12.22,9.36 12.64,9.45C12.72,9.27 12.82,9.11 12.94,8.96C13.41,8.35 14.16,8 15,8H21C21.81,8 22.57,8.34 23.09,8.92C23.6,9.5 23.84,10.3 23.74,11.11L23.23,14.18C23.04,15.74 21.61,17 20,17H17C15.44,17 13.92,15.81 13.54,14.3L12.64,11.59C12.26,11.31 11.73,11.31 11.35,11.59L10.43,14.37C10.07,15.82 8.56,17 7,17Z" /></g><g id="surround-sound"><path d="M20,4H4A2,2 0 0,0 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V6A2,2 0 0,0 20,4M7.76,16.24L6.35,17.65C4.78,16.1 4,14.05 4,12C4,9.95 4.78,7.9 6.34,6.34L7.75,7.75C6.59,8.93 6,10.46 6,12C6,13.54 6.59,15.07 7.76,16.24M12,16A4,4 0 0,1 8,12A4,4 0 0,1 12,8A4,4 0 0,1 16,12A4,4 0 0,1 12,16M17.66,17.66L16.25,16.25C17.41,15.07 18,13.54 18,12C18,10.46 17.41,8.93 16.24,7.76L17.65,6.35C19.22,7.9 20,9.95 20,12C20,14.05 19.22,16.1 17.66,17.66M12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12A2,2 0 0,0 12,10Z" /></g><g id="swap-horizontal"><path d="M21,9L17,5V8H10V10H17V13M7,11L3,15L7,19V16H14V14H7V11Z" /></g><g id="swap-vertical"><path d="M9,3L5,7H8V14H10V7H13M16,17V10H14V17H11L15,21L19,17H16Z" /></g><g id="swim"><path d="M2,18C4.22,17 6.44,16 8.67,16C10.89,16 13.11,18 15.33,18C17.56,18 19.78,16 22,16V19C19.78,19 17.56,21 15.33,21C13.11,21 10.89,19 8.67,19C6.44,19 4.22,20 2,21V18M8.67,13C7.89,13 7.12,13.12 6.35,13.32L11.27,9.88L10.23,8.64C10.09,8.47 10,8.24 10,8C10,7.66 10.17,7.35 10.44,7.17L16.16,3.17L17.31,4.8L12.47,8.19L17.7,14.42C16.91,14.75 16.12,15 15.33,15C13.11,15 10.89,13 8.67,13M18,7A2,2 0 0,1 20,9A2,2 0 0,1 18,11A2,2 0 0,1 16,9A2,2 0 0,1 18,7Z" /></g><g id="switch"><path d="M13,18H14A1,1 0 0,1 15,19H22V21H15A1,1 0 0,1 14,22H10A1,1 0 0,1 9,21H2V19H9A1,1 0 0,1 10,18H11V16H8A1,1 0 0,1 7,15V3A1,1 0 0,1 8,2H16A1,1 0 0,1 17,3V15A1,1 0 0,1 16,16H13V18M13,6H14V4H13V6M9,4V6H11V4H9M9,8V10H11V8H9M9,12V14H11V12H9Z" /></g><g id="sword"><path d="M6.92,5H5L14,14L15,13.06M19.96,19.12L19.12,19.96C18.73,20.35 18.1,20.35 17.71,19.96L14.59,16.84L11.91,19.5L10.5,18.09L11.92,16.67L3,7.75V3H7.75L16.67,11.92L18.09,10.5L19.5,11.91L16.83,14.58L19.95,17.7C20.35,18.1 20.35,18.73 19.96,19.12Z" /></g><g id="sync"><path d="M12,18A6,6 0 0,1 6,12C6,11 6.25,10.03 6.7,9.2L5.24,7.74C4.46,8.97 4,10.43 4,12A8,8 0 0,0 12,20V23L16,19L12,15M12,4V1L8,5L12,9V6A6,6 0 0,1 18,12C18,13 17.75,13.97 17.3,14.8L18.76,16.26C19.54,15.03 20,13.57 20,12A8,8 0 0,0 12,4Z" /></g><g id="sync-alert"><path d="M11,13H13V7H11M21,4H15V10L17.24,7.76C18.32,8.85 19,10.34 19,12C19,14.61 17.33,16.83 15,17.65V19.74C18.45,18.85 21,15.73 21,12C21,9.79 20.09,7.8 18.64,6.36M11,17H13V15H11M3,12C3,14.21 3.91,16.2 5.36,17.64L3,20H9V14L6.76,16.24C5.68,15.15 5,13.66 5,12C5,9.39 6.67,7.17 9,6.35V4.26C5.55,5.15 3,8.27 3,12Z" /></g><g id="sync-off"><path d="M20,4H14V10L16.24,7.76C17.32,8.85 18,10.34 18,12C18,13 17.75,13.94 17.32,14.77L18.78,16.23C19.55,15 20,13.56 20,12C20,9.79 19.09,7.8 17.64,6.36L20,4M2.86,5.41L5.22,7.77C4.45,9 4,10.44 4,12C4,14.21 4.91,16.2 6.36,17.64L4,20H10V14L7.76,16.24C6.68,15.15 6,13.66 6,12C6,11 6.25,10.06 6.68,9.23L14.76,17.31C14.5,17.44 14.26,17.56 14,17.65V19.74C14.79,19.53 15.54,19.2 16.22,18.78L18.58,21.14L19.85,19.87L4.14,4.14L2.86,5.41M10,6.35V4.26C9.2,4.47 8.45,4.8 7.77,5.22L9.23,6.68C9.5,6.56 9.73,6.44 10,6.35Z" /></g><g id="tab"><path d="M19,19H5V5H12V9H19M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3Z" /></g><g id="tab-unselected"><path d="M15,21H17V19H15M11,21H13V19H11M19,13H21V11H19M19,21A2,2 0 0,0 21,19H19M7,5H9V3H7M19,17H21V15H19M19,3H11V9H21V5C21,3.89 20.1,3 19,3M5,21V19H3A2,2 0 0,0 5,21M3,17H5V15H3M7,21H9V19H7M3,5H5V3C3.89,3 3,3.89 3,5M3,13H5V11H3M3,9H5V7H3V9Z" /></g><g id="table"><path d="M5,4H19A2,2 0 0,1 21,6V18A2,2 0 0,1 19,20H5A2,2 0 0,1 3,18V6A2,2 0 0,1 5,4M5,8V12H11V8H5M13,8V12H19V8H13M5,14V18H11V14H5M13,14V18H19V14H13Z" /></g><g id="table-column-plus-after"><path d="M11,2A2,2 0 0,1 13,4V20A2,2 0 0,1 11,22H2V2H11M4,10V14H11V10H4M4,16V20H11V16H4M4,4V8H11V4H4M15,11H18V8H20V11H23V13H20V16H18V13H15V11Z" /></g><g id="table-column-plus-before"><path d="M13,2A2,2 0 0,0 11,4V20A2,2 0 0,0 13,22H22V2H13M20,10V14H13V10H20M20,16V20H13V16H20M20,4V8H13V4H20M9,11H6V8H4V11H1V13H4V16H6V13H9V11Z" /></g><g id="table-column-remove"><path d="M4,2H11A2,2 0 0,1 13,4V20A2,2 0 0,1 11,22H4A2,2 0 0,1 2,20V4A2,2 0 0,1 4,2M4,10V14H11V10H4M4,16V20H11V16H4M4,4V8H11V4H4M17.59,12L15,9.41L16.41,8L19,10.59L21.59,8L23,9.41L20.41,12L23,14.59L21.59,16L19,13.41L16.41,16L15,14.59L17.59,12Z" /></g><g id="table-column-width"><path d="M5,8H19A2,2 0 0,1 21,10V20A2,2 0 0,1 19,22H5A2,2 0 0,1 3,20V10A2,2 0 0,1 5,8M5,12V15H11V12H5M13,12V15H19V12H13M5,17V20H11V17H5M13,17V20H19V17H13M11,2H21V6H19V4H13V6H11V2Z" /></g><g id="table-edit"><path d="M21.7,13.35L20.7,14.35L18.65,12.3L19.65,11.3C19.86,11.08 20.21,11.08 20.42,11.3L21.7,12.58C21.92,12.79 21.92,13.14 21.7,13.35M12,18.94L18.07,12.88L20.12,14.93L14.06,21H12V18.94M4,2H18A2,2 0 0,1 20,4V8.17L16.17,12H12V16.17L10.17,18H4A2,2 0 0,1 2,16V4A2,2 0 0,1 4,2M4,6V10H10V6H4M12,6V10H18V6H12M4,12V16H10V12H4Z" /></g><g id="table-large"><path d="M4,3H20A2,2 0 0,1 22,5V20A2,2 0 0,1 20,22H4A2,2 0 0,1 2,20V5A2,2 0 0,1 4,3M4,7V10H8V7H4M10,7V10H14V7H10M20,10V7H16V10H20M4,12V15H8V12H4M4,20H8V17H4V20M10,12V15H14V12H10M10,20H14V17H10V20M20,20V17H16V20H20M20,12H16V15H20V12Z" /></g><g id="table-row-height"><path d="M3,5H15A2,2 0 0,1 17,7V17A2,2 0 0,1 15,19H3A2,2 0 0,1 1,17V7A2,2 0 0,1 3,5M3,9V12H8V9H3M10,9V12H15V9H10M3,14V17H8V14H3M10,14V17H15V14H10M23,14V7H19V9H21V12H19V14H23Z" /></g><g id="table-row-plus-after"><path d="M22,10A2,2 0 0,1 20,12H4A2,2 0 0,1 2,10V3H4V5H8V3H10V5H14V3H16V5H20V3H22V10M4,10H8V7H4V10M10,10H14V7H10V10M20,10V7H16V10H20M11,14H13V17H16V19H13V22H11V19H8V17H11V14Z" /></g><g id="table-row-plus-before"><path d="M22,14A2,2 0 0,0 20,12H4A2,2 0 0,0 2,14V21H4V19H8V21H10V19H14V21H16V19H20V21H22V14M4,14H8V17H4V14M10,14H14V17H10V14M20,14V17H16V14H20M11,10H13V7H16V5H13V2H11V5H8V7H11V10Z" /></g><g id="table-row-remove"><path d="M9.41,13L12,15.59L14.59,13L16,14.41L13.41,17L16,19.59L14.59,21L12,18.41L9.41,21L8,19.59L10.59,17L8,14.41L9.41,13M22,9A2,2 0 0,1 20,11H4A2,2 0 0,1 2,9V6A2,2 0 0,1 4,4H20A2,2 0 0,1 22,6V9M4,9H8V6H4V9M10,9H14V6H10V9M16,9H20V6H16V9Z" /></g><g id="tablet"><path d="M19,18H5V6H19M21,4H3C1.89,4 1,4.89 1,6V18A2,2 0 0,0 3,20H21A2,2 0 0,0 23,18V6C23,4.89 22.1,4 21,4Z" /></g><g id="tablet-android"><path d="M19.25,19H4.75V3H19.25M14,22H10V21H14M18,0H6A3,3 0 0,0 3,3V21A3,3 0 0,0 6,24H18A3,3 0 0,0 21,21V3A3,3 0 0,0 18,0Z" /></g><g id="tablet-ipad"><path d="M19,19H4V3H19M11.5,23A1.5,1.5 0 0,1 10,21.5A1.5,1.5 0 0,1 11.5,20A1.5,1.5 0 0,1 13,21.5A1.5,1.5 0 0,1 11.5,23M18.5,0H4.5A2.5,2.5 0 0,0 2,2.5V21.5A2.5,2.5 0 0,0 4.5,24H18.5A2.5,2.5 0 0,0 21,21.5V2.5A2.5,2.5 0 0,0 18.5,0Z" /></g><g id="tag"><path d="M5.5,7A1.5,1.5 0 0,1 4,5.5A1.5,1.5 0 0,1 5.5,4A1.5,1.5 0 0,1 7,5.5A1.5,1.5 0 0,1 5.5,7M21.41,11.58L12.41,2.58C12.05,2.22 11.55,2 11,2H4C2.89,2 2,2.89 2,4V11C2,11.55 2.22,12.05 2.59,12.41L11.58,21.41C11.95,21.77 12.45,22 13,22C13.55,22 14.05,21.77 14.41,21.41L21.41,14.41C21.78,14.05 22,13.55 22,13C22,12.44 21.77,11.94 21.41,11.58Z" /></g><g id="tag-faces"><path d="M15,18C11.68,18 9,15.31 9,12C9,8.68 11.68,6 15,6A6,6 0 0,1 21,12A6,6 0 0,1 15,18M4,13A1,1 0 0,1 3,12A1,1 0 0,1 4,11A1,1 0 0,1 5,12A1,1 0 0,1 4,13M22,3H7.63C6.97,3 6.38,3.32 6,3.81L0,12L6,20.18C6.38,20.68 6.97,21 7.63,21H22A2,2 0 0,0 24,19V5C24,3.89 23.1,3 22,3M13,11A1,1 0 0,0 14,10A1,1 0 0,0 13,9A1,1 0 0,0 12,10A1,1 0 0,0 13,11M15,16C16.86,16 18.35,14.72 18.8,13H11.2C11.65,14.72 13.14,16 15,16M17,11A1,1 0 0,0 18,10A1,1 0 0,0 17,9A1,1 0 0,0 16,10A1,1 0 0,0 17,11Z" /></g><g id="tag-heart"><path d="M21.41,11.58L12.41,2.58C12.05,2.22 11.55,2 11,2H4A2,2 0 0,0 2,4V11C2,11.55 2.22,12.05 2.59,12.42L11.59,21.42C11.95,21.78 12.45,22 13,22C13.55,22 14.05,21.78 14.41,21.41L21.41,14.41C21.78,14.05 22,13.55 22,13C22,12.45 21.77,11.94 21.41,11.58M5.5,7A1.5,1.5 0 0,1 4,5.5A1.5,1.5 0 0,1 5.5,4A1.5,1.5 0 0,1 7,5.5A1.5,1.5 0 0,1 5.5,7M17.27,15.27L13,19.54L8.73,15.27C8.28,14.81 8,14.19 8,13.5A2.5,2.5 0 0,1 10.5,11C11.19,11 11.82,11.28 12.27,11.74L13,12.46L13.73,11.73C14.18,11.28 14.81,11 15.5,11A2.5,2.5 0 0,1 18,13.5C18,14.19 17.72,14.82 17.27,15.27Z" /></g><g id="tag-multiple"><path d="M5.5,9A1.5,1.5 0 0,0 7,7.5A1.5,1.5 0 0,0 5.5,6A1.5,1.5 0 0,0 4,7.5A1.5,1.5 0 0,0 5.5,9M17.41,11.58C17.77,11.94 18,12.44 18,13C18,13.55 17.78,14.05 17.41,14.41L12.41,19.41C12.05,19.77 11.55,20 11,20C10.45,20 9.95,19.78 9.58,19.41L2.59,12.42C2.22,12.05 2,11.55 2,11V6C2,4.89 2.89,4 4,4H9C9.55,4 10.05,4.22 10.41,4.58L17.41,11.58M13.54,5.71L14.54,4.71L21.41,11.58C21.78,11.94 22,12.45 22,13C22,13.55 21.78,14.05 21.42,14.41L16.04,19.79L15.04,18.79L20.75,13L13.54,5.71Z" /></g><g id="tag-outline"><path d="M5.5,7A1.5,1.5 0 0,0 7,5.5A1.5,1.5 0 0,0 5.5,4A1.5,1.5 0 0,0 4,5.5A1.5,1.5 0 0,0 5.5,7M21.41,11.58C21.77,11.94 22,12.44 22,13C22,13.55 21.78,14.05 21.41,14.41L14.41,21.41C14.05,21.77 13.55,22 13,22C12.45,22 11.95,21.77 11.58,21.41L2.59,12.41C2.22,12.05 2,11.55 2,11V4C2,2.89 2.89,2 4,2H11C11.55,2 12.05,2.22 12.41,2.58L21.41,11.58M13,20L20,13L11.5,4.5L4.5,11.5L13,20Z" /></g><g id="tag-text-outline"><path d="M5.5,7A1.5,1.5 0 0,0 7,5.5A1.5,1.5 0 0,0 5.5,4A1.5,1.5 0 0,0 4,5.5A1.5,1.5 0 0,0 5.5,7M21.41,11.58C21.77,11.94 22,12.44 22,13C22,13.55 21.78,14.05 21.41,14.41L14.41,21.41C14.05,21.77 13.55,22 13,22C12.45,22 11.95,21.77 11.58,21.41L2.59,12.41C2.22,12.05 2,11.55 2,11V4C2,2.89 2.89,2 4,2H11C11.55,2 12.05,2.22 12.41,2.58L21.41,11.58M13,20L20,13L11.5,4.5L4.5,11.5L13,20M10.09,8.91L11.5,7.5L17,13L15.59,14.41L10.09,8.91M7.59,11.41L9,10L13,14L11.59,15.41L7.59,11.41Z" /></g><g id="target"><path d="M11,2V4.07C7.38,4.53 4.53,7.38 4.07,11H2V13H4.07C4.53,16.62 7.38,19.47 11,19.93V22H13V19.93C16.62,19.47 19.47,16.62 19.93,13H22V11H19.93C19.47,7.38 16.62,4.53 13,4.07V2M11,6.08V8H13V6.09C15.5,6.5 17.5,8.5 17.92,11H16V13H17.91C17.5,15.5 15.5,17.5 13,17.92V16H11V17.91C8.5,17.5 6.5,15.5 6.08,13H8V11H6.09C6.5,8.5 8.5,6.5 11,6.08M12,11A1,1 0 0,0 11,12A1,1 0 0,0 12,13A1,1 0 0,0 13,12A1,1 0 0,0 12,11Z" /></g><g id="taxi"><path d="M5,11L6.5,6.5H17.5L19,11M17.5,16A1.5,1.5 0 0,1 16,14.5A1.5,1.5 0 0,1 17.5,13A1.5,1.5 0 0,1 19,14.5A1.5,1.5 0 0,1 17.5,16M6.5,16A1.5,1.5 0 0,1 5,14.5A1.5,1.5 0 0,1 6.5,13A1.5,1.5 0 0,1 8,14.5A1.5,1.5 0 0,1 6.5,16M18.92,6C18.72,5.42 18.16,5 17.5,5H15V3H9V5H6.5C5.84,5 5.28,5.42 5.08,6L3,12V20A1,1 0 0,0 4,21H5A1,1 0 0,0 6,20V19H18V20A1,1 0 0,0 19,21H20A1,1 0 0,0 21,20V12L18.92,6Z" /></g><g id="teamviewer"><path d="M19,3A2,2 0 0,1 21,5V19C21,20.11 20.1,21 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3H19M12,5A7,7 0 0,0 5,12A7,7 0 0,0 12,19A7,7 0 0,0 19,12A7,7 0 0,0 12,5M7,12L10,9V11H14V9L17,12L14,15V13H10V15L7,12Z" /></g><g id="telegram"><path d="M9.78,18.65L10.06,14.42L17.74,7.5C18.08,7.19 17.67,7.04 17.22,7.31L7.74,13.3L3.64,12C2.76,11.75 2.75,11.14 3.84,10.7L19.81,4.54C20.54,4.21 21.24,4.72 20.96,5.84L18.24,18.65C18.05,19.56 17.5,19.78 16.74,19.36L12.6,16.3L10.61,18.23C10.38,18.46 10.19,18.65 9.78,18.65Z" /></g><g id="television"><path d="M20,17H4V5H20M20,3H4C2.89,3 2,3.89 2,5V17A2,2 0 0,0 4,19H8V21H16V19H20A2,2 0 0,0 22,17V5C22,3.89 21.1,3 20,3Z" /></g><g id="television-guide"><path d="M21,17V5H3V17H21M21,3A2,2 0 0,1 23,5V17A2,2 0 0,1 21,19H16V21H8V19H3A2,2 0 0,1 1,17V5A2,2 0 0,1 3,3H21M5,7H11V11H5V7M5,13H11V15H5V13M13,7H19V9H13V7M13,11H19V15H13V11Z" /></g><g id="temperature-celsius"><path d="M16.5,5C18.05,5 19.5,5.47 20.69,6.28L19.53,9.17C18.73,8.44 17.67,8 16.5,8C14,8 12,10 12,12.5C12,15 14,17 16.5,17C17.53,17 18.47,16.66 19.23,16.08L20.37,18.93C19.24,19.61 17.92,20 16.5,20A7.5,7.5 0 0,1 9,12.5A7.5,7.5 0 0,1 16.5,5M6,3A3,3 0 0,1 9,6A3,3 0 0,1 6,9A3,3 0 0,1 3,6A3,3 0 0,1 6,3M6,5A1,1 0 0,0 5,6A1,1 0 0,0 6,7A1,1 0 0,0 7,6A1,1 0 0,0 6,5Z" /></g><g id="temperature-fahrenheit"><path d="M11,20V5H20V8H14V11H19V14H14V20H11M6,3A3,3 0 0,1 9,6A3,3 0 0,1 6,9A3,3 0 0,1 3,6A3,3 0 0,1 6,3M6,5A1,1 0 0,0 5,6A1,1 0 0,0 6,7A1,1 0 0,0 7,6A1,1 0 0,0 6,5Z" /></g><g id="temperature-kelvin"><path d="M7,5H10V11L15,5H19L13.88,10.78L19,20H15.38L11.76,13.17L10,15.15V20H7V5Z" /></g><g id="tennis"><path d="M12,2C14.5,2 16.75,2.9 18.5,4.4C16.36,6.23 15,8.96 15,12C15,15.04 16.36,17.77 18.5,19.6C16.75,21.1 14.5,22 12,22C9.5,22 7.25,21.1 5.5,19.6C7.64,17.77 9,15.04 9,12C9,8.96 7.64,6.23 5.5,4.4C7.25,2.9 9.5,2 12,2M22,12C22,14.32 21.21,16.45 19.88,18.15C18.12,16.68 17,14.47 17,12C17,9.53 18.12,7.32 19.88,5.85C21.21,7.55 22,9.68 22,12M2,12C2,9.68 2.79,7.55 4.12,5.85C5.88,7.32 7,9.53 7,12C7,14.47 5.88,16.68 4.12,18.15C2.79,16.45 2,14.32 2,12Z" /></g><g id="tent"><path d="M4,6C4,7.19 4.39,8.27 5,9A3,3 0 0,1 2,6A3,3 0 0,1 5,3C4.39,3.73 4,4.81 4,6M2,21V19H4.76L12,4.78L19.24,19H22V21H2M12,9.19L7,19H17L12,9.19Z" /></g><g id="terrain"><path d="M14,6L10.25,11L13.1,14.8L11.5,16C9.81,13.75 7,10 7,10L1,18H23L14,6Z" /></g><g id="test-tube"><path d="M7,2V4H8V18A4,4 0 0,0 12,22A4,4 0 0,0 16,18V4H17V2H7M11,16C10.4,16 10,15.6 10,15C10,14.4 10.4,14 11,14C11.6,14 12,14.4 12,15C12,15.6 11.6,16 11,16M13,12C12.4,12 12,11.6 12,11C12,10.4 12.4,10 13,10C13.6,10 14,10.4 14,11C14,11.6 13.6,12 13,12M14,7H10V4H14V7Z" /></g><g id="text-shadow"><path d="M3,3H16V6H11V18H8V6H3V3M12,7H14V9H12V7M15,7H17V9H15V7M18,7H20V9H18V7M12,10H14V12H12V10M12,13H14V15H12V13M12,16H14V18H12V16M12,19H14V21H12V19Z" /></g><g id="text-to-speech"><path d="M8,7A2,2 0 0,1 10,9V14A2,2 0 0,1 8,16A2,2 0 0,1 6,14V9A2,2 0 0,1 8,7M14,14C14,16.97 11.84,19.44 9,19.92V22H7V19.92C4.16,19.44 2,16.97 2,14H4A4,4 0 0,0 8,18A4,4 0 0,0 12,14H14M21.41,9.41L17.17,13.66L18.18,10H14A2,2 0 0,1 12,8V4A2,2 0 0,1 14,2H20A2,2 0 0,1 22,4V8C22,8.55 21.78,9.05 21.41,9.41Z" /></g><g id="text-to-speech-off"><path d="M2,5.27L3.28,4L20,20.72L18.73,22L13.38,16.65C12.55,18.35 10.93,19.59 9,19.92V22H7V19.92C4.16,19.44 2,16.97 2,14H4A4,4 0 0,0 8,18C9.82,18 11.36,16.78 11.84,15.11L10,13.27V14A2,2 0 0,1 8,16A2,2 0 0,1 6,14V9.27L2,5.27M21.41,9.41L17.17,13.66L18.18,10H14A2,2 0 0,1 12,8V4A2,2 0 0,1 14,2H20A2,2 0 0,1 22,4V8C22,8.55 21.78,9.05 21.41,9.41Z" /></g><g id="textbox"><path d="M17,7H22V17H17V19A1,1 0 0,0 18,20H20V22H17.5C16.95,22 16,21.55 16,21C16,21.55 15.05,22 14.5,22H12V20H14A1,1 0 0,0 15,19V5A1,1 0 0,0 14,4H12V2H14.5C15.05,2 16,2.45 16,3C16,2.45 16.95,2 17.5,2H20V4H18A1,1 0 0,0 17,5V7M2,7H13V9H4V15H13V17H2V7M20,15V9H17V15H20Z" /></g><g id="texture"><path d="M9.29,21H12.12L21,12.12V9.29M19,21C19.55,21 20.05,20.78 20.41,20.41C20.78,20.05 21,19.55 21,19V17L17,21M5,3A2,2 0 0,0 3,5V7L7,3M11.88,3L3,11.88V14.71L14.71,3M19.5,3.08L3.08,19.5C3.17,19.85 3.35,20.16 3.59,20.41C3.84,20.65 4.15,20.83 4.5,20.92L20.93,4.5C20.74,3.8 20.2,3.26 19.5,3.08Z" /></g><g id="theater"><path d="M4,15H6A2,2 0 0,1 8,17V19H9V17A2,2 0 0,1 11,15H13A2,2 0 0,1 15,17V19H16V17A2,2 0 0,1 18,15H20A2,2 0 0,1 22,17V19H23V22H1V19H2V17A2,2 0 0,1 4,15M11,7L15,10L11,13V7M4,2H20A2,2 0 0,1 22,4V13.54C21.41,13.19 20.73,13 20,13V4H4V13C3.27,13 2.59,13.19 2,13.54V4A2,2 0 0,1 4,2Z" /></g><g id="theme-light-dark"><path d="M7.5,2C5.71,3.15 4.5,5.18 4.5,7.5C4.5,9.82 5.71,11.85 7.53,13C4.46,13 2,10.54 2,7.5A5.5,5.5 0 0,1 7.5,2M19.07,3.5L20.5,4.93L4.93,20.5L3.5,19.07L19.07,3.5M12.89,5.93L11.41,5L9.97,6L10.39,4.3L9,3.24L10.75,3.12L11.33,1.47L12,3.1L13.73,3.13L12.38,4.26L12.89,5.93M9.59,9.54L8.43,8.81L7.31,9.59L7.65,8.27L6.56,7.44L7.92,7.35L8.37,6.06L8.88,7.33L10.24,7.36L9.19,8.23L9.59,9.54M19,13.5A5.5,5.5 0 0,1 13.5,19C12.28,19 11.15,18.6 10.24,17.93L17.93,10.24C18.6,11.15 19,12.28 19,13.5M14.6,20.08L17.37,18.93L17.13,22.28L14.6,20.08M18.93,17.38L20.08,14.61L22.28,17.15L18.93,17.38M20.08,12.42L18.94,9.64L22.28,9.88L20.08,12.42M9.63,18.93L12.4,20.08L9.87,22.27L9.63,18.93Z" /></g><g id="thermometer"><path d="M17,17A5,5 0 0,1 12,22A5,5 0 0,1 7,17C7,15.36 7.79,13.91 9,13V5A3,3 0 0,1 12,2A3,3 0 0,1 15,5V13C16.21,13.91 17,15.36 17,17M11,8V14.17C9.83,14.58 9,15.69 9,17A3,3 0 0,0 12,20A3,3 0 0,0 15,17C15,15.69 14.17,14.58 13,14.17V8H11Z" /></g><g id="thermometer-lines"><path d="M17,3H21V5H17V3M17,7H21V9H17V7M17,11H21V13H17.75L17,12.1V11M21,15V17H19C19,16.31 18.9,15.63 18.71,15H21M17,17A5,5 0 0,1 12,22A5,5 0 0,1 7,17C7,15.36 7.79,13.91 9,13V5A3,3 0 0,1 12,2A3,3 0 0,1 15,5V13C16.21,13.91 17,15.36 17,17M11,8V14.17C9.83,14.58 9,15.69 9,17A3,3 0 0,0 12,20A3,3 0 0,0 15,17C15,15.69 14.17,14.58 13,14.17V8H11M7,3V5H3V3H7M7,7V9H3V7H7M7,11V12.1L6.25,13H3V11H7M3,15H5.29C5.1,15.63 5,16.31 5,17H3V15Z" /></g><g id="thumb-down"><path d="M19,15H23V3H19M15,3H6C5.17,3 4.46,3.5 4.16,4.22L1.14,11.27C1.05,11.5 1,11.74 1,12V13.91L1,14A2,2 0 0,0 3,16H9.31L8.36,20.57C8.34,20.67 8.33,20.77 8.33,20.88C8.33,21.3 8.5,21.67 8.77,21.94L9.83,23L16.41,16.41C16.78,16.05 17,15.55 17,15V5C17,3.89 16.1,3 15,3Z" /></g><g id="thumb-down-outline"><path d="M19,15V3H23V15H19M15,3A2,2 0 0,1 17,5V15C17,15.55 16.78,16.05 16.41,16.41L9.83,23L8.77,21.94C8.5,21.67 8.33,21.3 8.33,20.88L8.36,20.57L9.31,16H3C1.89,16 1,15.1 1,14V13.91L1,12C1,11.74 1.05,11.5 1.14,11.27L4.16,4.22C4.46,3.5 5.17,3 6,3H15M15,5H5.97L3,12V14H11.78L10.65,19.32L15,14.97V5Z" /></g><g id="thumb-up"><path d="M23,10C23,8.89 22.1,8 21,8H14.68L15.64,3.43C15.66,3.33 15.67,3.22 15.67,3.11C15.67,2.7 15.5,2.32 15.23,2.05L14.17,1L7.59,7.58C7.22,7.95 7,8.45 7,9V19A2,2 0 0,0 9,21H18C18.83,21 19.54,20.5 19.84,19.78L22.86,12.73C22.95,12.5 23,12.26 23,12V10.08L23,10M1,21H5V9H1V21Z" /></g><g id="thumb-up-outline"><path d="M5,9V21H1V9H5M9,21A2,2 0 0,1 7,19V9C7,8.45 7.22,7.95 7.59,7.59L14.17,1L15.23,2.06C15.5,2.33 15.67,2.7 15.67,3.11L15.64,3.43L14.69,8H21C22.11,8 23,8.9 23,10V10.09L23,12C23,12.26 22.95,12.5 22.86,12.73L19.84,19.78C19.54,20.5 18.83,21 18,21H9M9,19H18.03L21,12V10H12.21L13.34,4.68L9,9.03V19Z" /></g><g id="thumbs-up-down"><path d="M22.5,10.5H15.75C15.13,10.5 14.6,10.88 14.37,11.41L12.11,16.7C12.04,16.87 12,17.06 12,17.25V18.5A1,1 0 0,0 13,19.5H18.18L17.5,22.68V22.92C17.5,23.23 17.63,23.5 17.83,23.72L18.62,24.5L23.56,19.56C23.83,19.29 24,18.91 24,18.5V12A1.5,1.5 0 0,0 22.5,10.5M12,6.5A1,1 0 0,0 11,5.5H5.82L6.5,2.32V2.09C6.5,1.78 6.37,1.5 6.17,1.29L5.38,0.5L0.44,5.44C0.17,5.71 0,6.09 0,6.5V13A1.5,1.5 0 0,0 1.5,14.5H8.25C8.87,14.5 9.4,14.12 9.63,13.59L11.89,8.3C11.96,8.13 12,7.94 12,7.75V6.5Z" /></g><g id="ticket"><path d="M15.58,16.8L12,14.5L8.42,16.8L9.5,12.68L6.21,10L10.46,9.74L12,5.8L13.54,9.74L17.79,10L14.5,12.68M20,12C20,10.89 20.9,10 22,10V6C22,4.89 21.1,4 20,4H4A2,2 0 0,0 2,6V10C3.11,10 4,10.9 4,12A2,2 0 0,1 2,14V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V14A2,2 0 0,1 20,12Z" /></g><g id="ticket-account"><path d="M20,12A2,2 0 0,0 22,14V18A2,2 0 0,1 20,20H4A2,2 0 0,1 2,18V14C3.11,14 4,13.1 4,12A2,2 0 0,0 2,10V6C2,4.89 2.9,4 4,4H20A2,2 0 0,1 22,6V10A2,2 0 0,0 20,12M16.5,16.25C16.5,14.75 13.5,14 12,14C10.5,14 7.5,14.75 7.5,16.25V17H16.5V16.25M12,12.25A2.25,2.25 0 0,0 14.25,10A2.25,2.25 0 0,0 12,7.75A2.25,2.25 0 0,0 9.75,10A2.25,2.25 0 0,0 12,12.25Z" /></g><g id="ticket-confirmation"><path d="M13,8.5H11V6.5H13V8.5M13,13H11V11H13V13M13,17.5H11V15.5H13V17.5M22,10V6C22,4.89 21.1,4 20,4H4A2,2 0 0,0 2,6V10C3.11,10 4,10.9 4,12A2,2 0 0,1 2,14V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V14A2,2 0 0,1 20,12A2,2 0 0,1 22,10Z" /></g><g id="ticket-percent"><path d="M4,4A2,2 0 0,0 2,6V10C3.11,10 4,10.9 4,12A2,2 0 0,1 2,14V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V14A2,2 0 0,1 20,12C20,10.89 20.9,10 22,10V6C22,4.89 21.1,4 20,4H4M15.5,7L17,8.5L8.5,17L7,15.5L15.5,7M8.81,7.04C9.79,7.04 10.58,7.83 10.58,8.81A1.77,1.77 0 0,1 8.81,10.58C7.83,10.58 7.04,9.79 7.04,8.81A1.77,1.77 0 0,1 8.81,7.04M15.19,13.42C16.17,13.42 16.96,14.21 16.96,15.19A1.77,1.77 0 0,1 15.19,16.96C14.21,16.96 13.42,16.17 13.42,15.19A1.77,1.77 0 0,1 15.19,13.42Z" /></g><g id="tie"><path d="M6,2L10,6L7,17L12,22L17,17L14,6L18,2Z" /></g><g id="timelapse"><path d="M12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M16.24,7.76C15.07,6.58 13.53,6 12,6V12L7.76,16.24C10.1,18.58 13.9,18.58 16.24,16.24C18.59,13.9 18.59,10.1 16.24,7.76Z" /></g><g id="timer"><path d="M12,20A7,7 0 0,1 5,13A7,7 0 0,1 12,6A7,7 0 0,1 19,13A7,7 0 0,1 12,20M19.03,7.39L20.45,5.97C20,5.46 19.55,5 19.04,4.56L17.62,6C16.07,4.74 14.12,4 12,4A9,9 0 0,0 3,13A9,9 0 0,0 12,22C17,22 21,17.97 21,13C21,10.88 20.26,8.93 19.03,7.39M11,14H13V8H11M15,1H9V3H15V1Z" /></g><g id="timer-10"><path d="M12.9,13.22C12.9,13.82 12.86,14.33 12.78,14.75C12.7,15.17 12.58,15.5 12.42,15.77C12.26,16.03 12.06,16.22 11.83,16.34C11.6,16.46 11.32,16.5 11,16.5C10.71,16.5 10.43,16.46 10.19,16.34C9.95,16.22 9.75,16.03 9.59,15.77C9.43,15.5 9.3,15.17 9.21,14.75C9.12,14.33 9.08,13.82 9.08,13.22V10.72C9.08,10.12 9.12,9.61 9.21,9.2C9.3,8.79 9.42,8.46 9.59,8.2C9.75,7.95 9.95,7.77 10.19,7.65C10.43,7.54 10.7,7.5 11,7.5C11.31,7.5 11.58,7.54 11.81,7.65C12.05,7.76 12.25,7.94 12.41,8.2C12.57,8.45 12.7,8.78 12.78,9.19C12.86,9.6 12.91,10.11 12.91,10.71V13.22M13.82,7.05C13.5,6.65 13.07,6.35 12.59,6.17C12.12,6 11.58,5.9 11,5.9C10.42,5.9 9.89,6 9.41,6.17C8.93,6.35 8.5,6.64 8.18,7.05C7.84,7.46 7.58,8 7.39,8.64C7.21,9.29 7.11,10.09 7.11,11.03V12.95C7.11,13.89 7.2,14.69 7.39,15.34C7.58,16 7.84,16.53 8.19,16.94C8.53,17.35 8.94,17.65 9.42,17.83C9.9,18 10.43,18.11 11,18.11C11.6,18.11 12.13,18 12.6,17.83C13.08,17.65 13.5,17.35 13.82,16.94C14.16,16.53 14.42,16 14.6,15.34C14.78,14.69 14.88,13.89 14.88,12.95V11.03C14.88,10.09 14.79,9.29 14.6,8.64C14.42,8 14.16,7.45 13.82,7.05M23.78,14.37C23.64,14.09 23.43,13.84 23.15,13.63C22.87,13.42 22.54,13.24 22.14,13.1C21.74,12.96 21.29,12.83 20.79,12.72C20.44,12.65 20.15,12.57 19.92,12.5C19.69,12.41 19.5,12.33 19.37,12.24C19.23,12.15 19.14,12.05 19.09,11.94C19.04,11.83 19,11.7 19,11.55C19,11.41 19.04,11.27 19.1,11.14C19.16,11 19.25,10.89 19.37,10.8C19.5,10.7 19.64,10.62 19.82,10.56C20,10.5 20.22,10.47 20.46,10.47C20.71,10.47 20.93,10.5 21.12,10.58C21.31,10.65 21.47,10.75 21.6,10.87C21.73,11 21.82,11.13 21.89,11.29C21.95,11.45 22,11.61 22,11.78H23.94C23.94,11.39 23.86,11.03 23.7,10.69C23.54,10.35 23.31,10.06 23,9.81C22.71,9.56 22.35,9.37 21.92,9.22C21.5,9.07 21,9 20.46,9C19.95,9 19.5,9.07 19.07,9.21C18.66,9.35 18.3,9.54 18,9.78C17.72,10 17.5,10.3 17.34,10.62C17.18,10.94 17.11,11.27 17.11,11.63C17.11,12 17.19,12.32 17.34,12.59C17.5,12.87 17.7,13.11 18,13.32C18.25,13.53 18.58,13.7 18.96,13.85C19.34,14 19.77,14.11 20.23,14.21C20.62,14.29 20.94,14.38 21.18,14.47C21.42,14.56 21.61,14.66 21.75,14.76C21.88,14.86 21.97,15 22,15.1C22.07,15.22 22.09,15.35 22.09,15.5C22.09,15.81 21.96,16.06 21.69,16.26C21.42,16.46 21.03,16.55 20.5,16.55C20.3,16.55 20.09,16.53 19.88,16.47C19.67,16.42 19.5,16.34 19.32,16.23C19.15,16.12 19,15.97 18.91,15.79C18.8,15.61 18.74,15.38 18.73,15.12H16.84C16.84,15.5 16.92,15.83 17.08,16.17C17.24,16.5 17.47,16.82 17.78,17.1C18.09,17.37 18.47,17.59 18.93,17.76C19.39,17.93 19.91,18 20.5,18C21.04,18 21.5,17.95 21.95,17.82C22.38,17.69 22.75,17.5 23.06,17.28C23.37,17.05 23.6,16.77 23.77,16.45C23.94,16.13 24,15.78 24,15.39C24,15 23.93,14.65 23.78,14.37M0,7.72V9.4L3,8.4V18H5V6H4.75L0,7.72Z" /></g><g id="timer-3"><path d="M20.87,14.37C20.73,14.09 20.5,13.84 20.24,13.63C19.96,13.42 19.63,13.24 19.23,13.1C18.83,12.96 18.38,12.83 17.88,12.72C17.53,12.65 17.24,12.57 17,12.5C16.78,12.41 16.6,12.33 16.46,12.24C16.32,12.15 16.23,12.05 16.18,11.94C16.13,11.83 16.1,11.7 16.1,11.55C16.1,11.4 16.13,11.27 16.19,11.14C16.25,11 16.34,10.89 16.46,10.8C16.58,10.7 16.73,10.62 16.91,10.56C17.09,10.5 17.31,10.47 17.55,10.47C17.8,10.47 18,10.5 18.21,10.58C18.4,10.65 18.56,10.75 18.69,10.87C18.82,11 18.91,11.13 19,11.29C19.04,11.45 19.08,11.61 19.08,11.78H21.03C21.03,11.39 20.95,11.03 20.79,10.69C20.63,10.35 20.4,10.06 20.1,9.81C19.8,9.56 19.44,9.37 19,9.22C18.58,9.07 18.09,9 17.55,9C17.04,9 16.57,9.07 16.16,9.21C15.75,9.35 15.39,9.54 15.1,9.78C14.81,10 14.59,10.3 14.43,10.62C14.27,10.94 14.2,11.27 14.2,11.63C14.2,12 14.28,12.31 14.43,12.59C14.58,12.87 14.8,13.11 15.07,13.32C15.34,13.53 15.67,13.7 16.05,13.85C16.43,14 16.86,14.11 17.32,14.21C17.71,14.29 18.03,14.38 18.27,14.47C18.5,14.56 18.7,14.66 18.84,14.76C18.97,14.86 19.06,15 19.11,15.1C19.16,15.22 19.18,15.35 19.18,15.5C19.18,15.81 19.05,16.06 18.78,16.26C18.5,16.46 18.12,16.55 17.61,16.55C17.39,16.55 17.18,16.53 16.97,16.47C16.76,16.42 16.57,16.34 16.41,16.23C16.24,16.12 16.11,15.97 16,15.79C15.89,15.61 15.83,15.38 15.82,15.12H13.93C13.93,15.5 14,15.83 14.17,16.17C14.33,16.5 14.56,16.82 14.87,17.1C15.18,17.37 15.56,17.59 16,17.76C16.5,17.93 17,18 17.6,18C18.13,18 18.61,17.95 19.04,17.82C19.47,17.69 19.84,17.5 20.15,17.28C20.46,17.05 20.69,16.77 20.86,16.45C21.03,16.13 21.11,15.78 21.11,15.39C21.09,15 21,14.65 20.87,14.37M11.61,12.97C11.45,12.73 11.25,12.5 11,12.32C10.74,12.13 10.43,11.97 10.06,11.84C10.36,11.7 10.63,11.54 10.86,11.34C11.09,11.14 11.28,10.93 11.43,10.7C11.58,10.47 11.7,10.24 11.77,10C11.85,9.75 11.88,9.5 11.88,9.26C11.88,8.71 11.79,8.22 11.6,7.8C11.42,7.38 11.16,7.03 10.82,6.74C10.5,6.46 10.09,6.24 9.62,6.1C9.17,5.97 8.65,5.9 8.09,5.9C7.54,5.9 7.03,6 6.57,6.14C6.1,6.31 5.7,6.54 5.37,6.83C5.04,7.12 4.77,7.46 4.59,7.86C4.39,8.25 4.3,8.69 4.3,9.15H6.28C6.28,8.89 6.33,8.66 6.42,8.46C6.5,8.26 6.64,8.08 6.8,7.94C6.97,7.8 7.16,7.69 7.38,7.61C7.6,7.53 7.84,7.5 8.11,7.5C8.72,7.5 9.17,7.65 9.47,7.96C9.77,8.27 9.91,8.71 9.91,9.28C9.91,9.55 9.87,9.8 9.79,10C9.71,10.24 9.58,10.43 9.41,10.59C9.24,10.75 9.03,10.87 8.78,10.96C8.53,11.05 8.23,11.09 7.89,11.09H6.72V12.66H7.9C8.24,12.66 8.54,12.7 8.81,12.77C9.08,12.85 9.31,12.96 9.5,13.12C9.69,13.28 9.84,13.5 9.94,13.73C10.04,13.97 10.1,14.27 10.1,14.6C10.1,15.22 9.92,15.69 9.57,16C9.22,16.35 8.73,16.5 8.12,16.5C7.83,16.5 7.56,16.47 7.32,16.38C7.08,16.3 6.88,16.18 6.71,16C6.54,15.86 6.41,15.68 6.32,15.46C6.23,15.24 6.18,15 6.18,14.74H4.19C4.19,15.29 4.3,15.77 4.5,16.19C4.72,16.61 5,16.96 5.37,17.24C5.73,17.5 6.14,17.73 6.61,17.87C7.08,18 7.57,18.08 8.09,18.08C8.66,18.08 9.18,18 9.67,17.85C10.16,17.7 10.58,17.47 10.93,17.17C11.29,16.87 11.57,16.5 11.77,16.07C11.97,15.64 12.07,15.14 12.07,14.59C12.07,14.3 12.03,14 11.96,13.73C11.88,13.5 11.77,13.22 11.61,12.97Z" /></g><g id="timer-off"><path d="M12,20A7,7 0 0,1 5,13C5,11.72 5.35,10.5 5.95,9.5L15.5,19.04C14.5,19.65 13.28,20 12,20M3,4L1.75,5.27L4.5,8.03C3.55,9.45 3,11.16 3,13A9,9 0 0,0 12,22C13.84,22 15.55,21.45 17,20.5L19.5,23L20.75,21.73L13.04,14L3,4M11,9.44L13,11.44V8H11M15,1H9V3H15M19.04,4.55L17.62,5.97C16.07,4.74 14.12,4 12,4C10.17,4 8.47,4.55 7.05,5.5L8.5,6.94C9.53,6.35 10.73,6 12,6A7,7 0 0,1 19,13C19,14.27 18.65,15.47 18.06,16.5L19.5,17.94C20.45,16.53 21,14.83 21,13C21,10.88 20.26,8.93 19.03,7.39L20.45,5.97L19.04,4.55Z" /></g><g id="timer-sand"><path d="M20,2V4H18V8.41L14.41,12L18,15.59V20H20V22H4V20H6V15.59L9.59,12L6,8.41V4H4V2H20M16,16.41L13,13.41V10.59L16,7.59V4H8V7.59L11,10.59V13.41L8,16.41V17H10L12,15L14,17H16V16.41M12,9L10,7H14L12,9Z" /></g><g id="timer-sand-empty"><path d="M20,2V4H18V8.41L14.41,12L18,15.59V20H20V22H4V20H6V15.59L9.59,12L6,8.41V4H4V2H20M16,16.41L13,13.41V10.59L16,7.59V4H8V7.59L11,10.59V13.41L8,16.41V20H16V16.41Z" /></g><g id="timetable"><path d="M14,12H15.5V14.82L17.94,16.23L17.19,17.53L14,15.69V12M4,2H18A2,2 0 0,1 20,4V10.1C21.24,11.36 22,13.09 22,15A7,7 0 0,1 15,22C13.09,22 11.36,21.24 10.1,20H4A2,2 0 0,1 2,18V4A2,2 0 0,1 4,2M4,15V18H8.67C8.24,17.09 8,16.07 8,15H4M4,8H10V5H4V8M18,8V5H12V8H18M4,13H8.29C8.63,11.85 9.26,10.82 10.1,10H4V13M15,10.15A4.85,4.85 0 0,0 10.15,15C10.15,17.68 12.32,19.85 15,19.85A4.85,4.85 0 0,0 19.85,15C19.85,12.32 17.68,10.15 15,10.15Z" /></g><g id="toggle-switch"><path d="M17,7A5,5 0 0,1 22,12A5,5 0 0,1 17,17A5,5 0 0,1 12,12A5,5 0 0,1 17,7M4,14A2,2 0 0,1 2,12A2,2 0 0,1 4,10H10V14H4Z" /></g><g id="toggle-switch-off"><path d="M7,7A5,5 0 0,1 12,12A5,5 0 0,1 7,17A5,5 0 0,1 2,12A5,5 0 0,1 7,7M20,14H14V10H20A2,2 0 0,1 22,12A2,2 0 0,1 20,14M7,9A3,3 0 0,0 4,12A3,3 0 0,0 7,15A3,3 0 0,0 10,12A3,3 0 0,0 7,9Z" /></g><g id="tooltip"><path d="M4,2H20A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H16L12,22L8,18H4A2,2 0 0,1 2,16V4A2,2 0 0,1 4,2Z" /></g><g id="tooltip-edit"><path d="M4,2H20A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H16L12,22L8,18H4A2,2 0 0,1 2,16V4A2,2 0 0,1 4,2M18,14V12H12.5L10.5,14H18M6,14H8.5L15.35,7.12C15.55,6.93 15.55,6.61 15.35,6.41L13.59,4.65C13.39,4.45 13.07,4.45 12.88,4.65L6,11.53V14Z" /></g><g id="tooltip-image"><path d="M4,2H20A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H16L12,22L8,18H4A2,2 0 0,1 2,16V4A2,2 0 0,1 4,2M19,15V7L15,11L13,9L7,15H19M7,5A2,2 0 0,0 5,7A2,2 0 0,0 7,9A2,2 0 0,0 9,7A2,2 0 0,0 7,5Z" /></g><g id="tooltip-outline"><path d="M4,2H20A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H16L12,22L8,18H4A2,2 0 0,1 2,16V4A2,2 0 0,1 4,2M4,4V16H8.83L12,19.17L15.17,16H20V4H4Z" /></g><g id="tooltip-outline-plus"><path d="M4,2H20A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H16L12,22L8,18H4A2,2 0 0,1 2,16V4A2,2 0 0,1 4,2M4,4V16H8.83L12,19.17L15.17,16H20V4H4M11,6H13V9H16V11H13V14H11V11H8V9H11V6Z" /></g><g id="tooltip-text"><path d="M4,2H20A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H16L12,22L8,18H4A2,2 0 0,1 2,16V4A2,2 0 0,1 4,2M5,5V7H19V5H5M5,9V11H15V9H5M5,13V15H17V13H5Z" /></g><g id="tooth"><path d="M7,2C4,2 2,5 2,8C2,10.11 3,13 4,14C5,15 6,22 8,22C12.54,22 10,15 12,15C14,15 11.46,22 16,22C18,22 19,15 20,14C21,13 22,10.11 22,8C22,5 20,2 17,2C14,2 14,3 12,3C10,3 10,2 7,2M7,4C9,4 10,5 12,5C14,5 15,4 17,4C18.67,4 20,6 20,8C20,9.75 19.14,12.11 18.19,13.06C17.33,13.92 16.06,19.94 15.5,19.94C15.29,19.94 15,18.88 15,17.59C15,15.55 14.43,13 12,13C9.57,13 9,15.55 9,17.59C9,18.88 8.71,19.94 8.5,19.94C7.94,19.94 6.67,13.92 5.81,13.06C4.86,12.11 4,9.75 4,8C4,6 5.33,4 7,4Z" /></g><g id="tor"><path d="M12,14C11,14 9,15 9,16C9,18 12,18 12,18V17A1,1 0 0,1 11,16A1,1 0 0,1 12,15V14M12,19C12,19 8,18.5 8,16.5C8,13.5 11,12.75 12,12.75V11.5C11,11.5 7,13 7,16C7,20 12,20 12,20V19M10.07,7.03L11.26,7.56C11.69,5.12 12.84,3.5 12.84,3.5C12.41,4.53 12.13,5.38 11.95,6.05C13.16,3.55 15.61,2 15.61,2C14.43,3.18 13.56,4.46 12.97,5.53C14.55,3.85 16.74,2.75 16.74,2.75C14.05,4.47 12.84,7.2 12.54,7.96L13.09,8.04C13.09,8.56 13.09,9.04 13.34,9.42C14.1,11.31 18,11.47 18,16C18,20.53 13.97,22 11.83,22C9.69,22 5,21.03 5,16C5,10.97 9.95,10.93 10.83,8.92C10.95,8.54 10.07,7.03 10.07,7.03Z" /></g><g id="tower-beach"><path d="M17,4V8H18V10H17.64L21,23H18.93L18.37,20.83L12,17.15L5.63,20.83L5.07,23H3L6.36,10H6V8H7V4H6V3L18,1V4H17M7.28,14.43L6.33,18.12L10,16L7.28,14.43M15.57,10H8.43L7.8,12.42L12,14.85L16.2,12.42L15.57,10M17.67,18.12L16.72,14.43L14,16L17.67,18.12Z" /></g><g id="tower-fire"><path d="M17,4V8H18V10H17.64L21,23H18.93L18.37,20.83L12,17.15L5.63,20.83L5.07,23H3L6.36,10H6V8H7V4H6V3L12,1L18,3V4H17M7.28,14.43L6.33,18.12L10,16L7.28,14.43M15.57,10H8.43L7.8,12.42L12,14.85L16.2,12.42L15.57,10M17.67,18.12L16.72,14.43L14,16L17.67,18.12Z" /></g><g id="traffic-light"><path d="M12,9A2,2 0 0,1 10,7C10,5.89 10.9,5 12,5C13.11,5 14,5.89 14,7A2,2 0 0,1 12,9M12,14A2,2 0 0,1 10,12C10,10.89 10.9,10 12,10C13.11,10 14,10.89 14,12A2,2 0 0,1 12,14M12,19A2,2 0 0,1 10,17C10,15.89 10.9,15 12,15C13.11,15 14,15.89 14,17A2,2 0 0,1 12,19M20,10H17V8.86C18.72,8.41 20,6.86 20,5H17V4A1,1 0 0,0 16,3H8A1,1 0 0,0 7,4V5H4C4,6.86 5.28,8.41 7,8.86V10H4C4,11.86 5.28,13.41 7,13.86V15H4C4,16.86 5.28,18.41 7,18.86V20A1,1 0 0,0 8,21H16A1,1 0 0,0 17,20V18.86C18.72,18.41 20,16.86 20,15H17V13.86C18.72,13.41 20,11.86 20,10Z" /></g><g id="train"><path d="M18,10H6V5H18M12,17C10.89,17 10,16.1 10,15C10,13.89 10.89,13 12,13A2,2 0 0,1 14,15A2,2 0 0,1 12,17M4,15.5A3.5,3.5 0 0,0 7.5,19L6,20.5V21H18V20.5L16.5,19A3.5,3.5 0 0,0 20,15.5V5C20,1.5 16.42,1 12,1C7.58,1 4,1.5 4,5V15.5Z" /></g><g id="tram"><path d="M17,18C16.4,18 16,17.6 16,17C16,16.4 16.4,16 17,16C17.6,16 18,16.4 18,17C18,17.6 17.6,18 17,18M6.7,10.7L7,7.3C7,6.6 7.6,6 8.3,6H15.6C16.4,6 17,6.6 17,7.3L17.3,10.6C17.3,11.3 16.7,11.9 16,11.9H8C7.3,12 6.7,11.4 6.7,10.7M7,18C6.4,18 6,17.6 6,17C6,16.4 6.4,16 7,16C7.6,16 8,16.4 8,17C8,17.6 7.6,18 7,18M19,6A2,2 0 0,0 17,4H15A2,2 0 0,0 13,2H11A2,2 0 0,0 9,4H7A2,2 0 0,0 5,6L4,18A2,2 0 0,0 6,20H8L7,22H17.1L16.1,20H18A2,2 0 0,0 20,18L19,6Z" /></g><g id="transcribe"><path d="M20,5A2,2 0 0,1 22,7V17A2,2 0 0,1 20,19H4C2.89,19 2,18.1 2,17V7C2,5.89 2.89,5 4,5H20M18,17V15H12.5L10.5,17H18M6,17H8.5L15.35,10.12C15.55,9.93 15.55,9.61 15.35,9.41L13.59,7.65C13.39,7.45 13.07,7.45 12.88,7.65L6,14.53V17Z" /></g><g id="transcribe-close"><path d="M12,23L8,19H16L12,23M20,3A2,2 0 0,1 22,5V15A2,2 0 0,1 20,17H4A2,2 0 0,1 2,15V5A2,2 0 0,1 4,3H20M18,15V13H12.5L10.5,15H18M6,15H8.5L15.35,8.12C15.55,7.93 15.55,7.61 15.35,7.42L13.59,5.65C13.39,5.45 13.07,5.45 12.88,5.65L6,12.53V15Z" /></g><g id="transfer"><path d="M3,8H5V16H3V8M7,8H9V16H7V8M11,8H13V16H11V8M15,19.25V4.75L22.25,12L15,19.25Z" /></g><g id="transit-transfer"><path d="M16.5,15.5H22V17H16.5V18.75L14,16.25L16.5,13.75V15.5M19.5,19.75V18L22,20.5L19.5,23V21.25H14V19.75H19.5M9.5,5.5A2,2 0 0,1 7.5,3.5A2,2 0 0,1 9.5,1.5A2,2 0 0,1 11.5,3.5A2,2 0 0,1 9.5,5.5M5.75,8.9L4,9.65V13H2V8.3L7.25,6.15C7.5,6.05 7.75,6 8,6C8.7,6 9.35,6.35 9.7,6.95L10.65,8.55C11.55,10 13.15,11 15,11V13C12.8,13 10.85,12 9.55,10.4L8.95,13.4L11,15.45V23H9V17L6.85,15L5.1,23H3L5.75,8.9Z" /></g><g id="translate"><path d="M12.87,15.07L10.33,12.56L10.36,12.53C12.1,10.59 13.34,8.36 14.07,6H17V4H10V2H8V4H1V6H12.17C11.5,7.92 10.44,9.75 9,11.35C8.07,10.32 7.3,9.19 6.69,8H4.69C5.42,9.63 6.42,11.17 7.67,12.56L2.58,17.58L4,19L9,14L12.11,17.11L12.87,15.07M18.5,10H16.5L12,22H14L15.12,19H19.87L21,22H23L18.5,10M15.88,17L17.5,12.67L19.12,17H15.88Z" /></g><g id="tree"><path d="M11,21V16.74C10.53,16.91 10.03,17 9.5,17C7,17 5,15 5,12.5C5,11.23 5.5,10.09 6.36,9.27C6.13,8.73 6,8.13 6,7.5C6,5 8,3 10.5,3C12.06,3 13.44,3.8 14.25,5C14.33,5 14.41,5 14.5,5A5.5,5.5 0 0,1 20,10.5A5.5,5.5 0 0,1 14.5,16C14,16 13.5,15.93 13,15.79V21H11Z" /></g><g id="trello"><path d="M4,3H20A1,1 0 0,1 21,4V20A1,1 0 0,1 20,21H4A1,1 0 0,1 3,20V4A1,1 0 0,1 4,3M5.5,5A0.5,0.5 0 0,0 5,5.5V17.5A0.5,0.5 0 0,0 5.5,18H10.5A0.5,0.5 0 0,0 11,17.5V5.5A0.5,0.5 0 0,0 10.5,5H5.5M13.5,5A0.5,0.5 0 0,0 13,5.5V11.5A0.5,0.5 0 0,0 13.5,12H18.5A0.5,0.5 0 0,0 19,11.5V5.5A0.5,0.5 0 0,0 18.5,5H13.5Z" /></g><g id="trending-down"><path d="M16,18L18.29,15.71L13.41,10.83L9.41,14.83L2,7.41L3.41,6L9.41,12L13.41,8L19.71,14.29L22,12V18H16Z" /></g><g id="trending-neutral"><path d="M22,12L18,8V11H3V13H18V16L22,12Z" /></g><g id="trending-up"><path d="M16,6L18.29,8.29L13.41,13.17L9.41,9.17L2,16.59L3.41,18L9.41,12L13.41,16L19.71,9.71L22,12V6H16Z" /></g><g id="triangle"><path d="M1,21H23L12,2" /></g><g id="triangle-outline"><path d="M12,2L1,21H23M12,6L19.53,19H4.47" /></g><g id="trophy"><path d="M20.2,2H19.5H18C17.1,2 16,3 16,4H8C8,3 6.9,2 6,2H4.5H3.8H2V11C2,12 3,13 4,13H6.2C6.6,15 7.9,16.7 11,17V19.1C8.8,19.3 8,20.4 8,21.7V22H16V21.7C16,20.4 15.2,19.3 13,19.1V17C16.1,16.7 17.4,15 17.8,13H20C21,13 22,12 22,11V2H20.2M4,11V4H6V6V11C5.1,11 4.3,11 4,11M20,11C19.7,11 18.9,11 18,11V6V4H20V11Z" /></g><g id="trophy-award"><path d="M15.2,10.7L16.6,16L12,12.2L7.4,16L8.8,10.8L4.6,7.3L10,7L12,2L14,7L19.4,7.3L15.2,10.7M14,19.1H13V16L12,15L11,16V19.1H10A2,2 0 0,0 8,21.1V22.1H16V21.1A2,2 0 0,0 14,19.1Z" /></g><g id="trophy-outline"><path d="M2,2V11C2,12 3,13 4,13H6.2C6.6,15 7.9,16.7 11,17V19.1C8.8,19.3 8,20.4 8,21.7V22H16V21.7C16,20.4 15.2,19.3 13,19.1V17C16.1,16.7 17.4,15 17.8,13H20C21,13 22,12 22,11V2H18C17.1,2 16,3 16,4H8C8,3 6.9,2 6,2H2M4,4H6V6L6,11H4V4M18,4H20V11H18V6L18,4M8,6H16V11.5C16,13.43 15.42,15 12,15C8.59,15 8,13.43 8,11.5V6Z" /></g><g id="trophy-variant"><path d="M20.2,4H20H17V2H7V4H4.5H3.8H2V11C2,12 3,13 4,13H7.2C7.6,14.9 8.6,16.6 11,16.9V19C8,19.2 8,20.3 8,21.6V22H16V21.7C16,20.4 16,19.3 13,19.1V17C15.5,16.7 16.5,15 16.8,13.1H20C21,13.1 22,12.1 22,11.1V4H20.2M4,11V6H7V8V11C5.6,11 4.4,11 4,11M20,11C19.6,11 18.4,11 17,11V6H18H20V11Z" /></g><g id="trophy-variant-outline"><path d="M7,2V4H2V11C2,12 3,13 4,13H7.2C7.6,14.9 8.6,16.6 11,16.9V19C8,19.2 8,20.3 8,21.6V22H16V21.7C16,20.4 16,19.3 13,19.1V17C15.5,16.7 16.5,15 16.8,13.1H20C21,13.1 22,12.1 22,11.1V4H17V2H7M9,4H15V12A3,3 0 0,1 12,15C10,15 9,13.66 9,12V4M4,6H7V8L7,11H4V6M17,6H20V11H17V6Z" /></g><g id="truck"><path d="M18,18.5A1.5,1.5 0 0,1 16.5,17A1.5,1.5 0 0,1 18,15.5A1.5,1.5 0 0,1 19.5,17A1.5,1.5 0 0,1 18,18.5M19.5,9.5L21.46,12H17V9.5M6,18.5A1.5,1.5 0 0,1 4.5,17A1.5,1.5 0 0,1 6,15.5A1.5,1.5 0 0,1 7.5,17A1.5,1.5 0 0,1 6,18.5M20,8H17V4H3C1.89,4 1,4.89 1,6V17H3A3,3 0 0,0 6,20A3,3 0 0,0 9,17H15A3,3 0 0,0 18,20A3,3 0 0,0 21,17H23V12L20,8Z" /></g><g id="truck-delivery"><path d="M3,4A2,2 0 0,0 1,6V17H3A3,3 0 0,0 6,20A3,3 0 0,0 9,17H15A3,3 0 0,0 18,20A3,3 0 0,0 21,17H23V12L20,8H17V4M10,6L14,10L10,14V11H4V9H10M17,9.5H19.5L21.47,12H17M6,15.5A1.5,1.5 0 0,1 7.5,17A1.5,1.5 0 0,1 6,18.5A1.5,1.5 0 0,1 4.5,17A1.5,1.5 0 0,1 6,15.5M18,15.5A1.5,1.5 0 0,1 19.5,17A1.5,1.5 0 0,1 18,18.5A1.5,1.5 0 0,1 16.5,17A1.5,1.5 0 0,1 18,15.5Z" /></g><g id="tshirt-crew"><path d="M16,21H8A1,1 0 0,1 7,20V12.07L5.7,13.12C5.31,13.5 4.68,13.5 4.29,13.12L1.46,10.29C1.07,9.9 1.07,9.27 1.46,8.88L7.34,3H9C9,4.1 10.34,5 12,5C13.66,5 15,4.1 15,3H16.66L22.54,8.88C22.93,9.27 22.93,9.9 22.54,10.29L19.71,13.12C19.32,13.5 18.69,13.5 18.3,13.12L17,12.07V20A1,1 0 0,1 16,21M20.42,9.58L16.11,5.28C15.8,5.63 15.43,5.94 15,6.2C14.16,6.7 13.13,7 12,7C10.3,7 8.79,6.32 7.89,5.28L3.58,9.58L5,11L8,9H9V19H15V9H16L19,11L20.42,9.58Z" /></g><g id="tshirt-v"><path d="M16,21H8A1,1 0 0,1 7,20V12.07L5.7,13.12C5.31,13.5 4.68,13.5 4.29,13.12L1.46,10.29C1.07,9.9 1.07,9.27 1.46,8.88L7.34,3H9C9,4.1 10,6 12,7.25C14,6 15,4.1 15,3H16.66L22.54,8.88C22.93,9.27 22.93,9.9 22.54,10.29L19.71,13.12C19.32,13.5 18.69,13.5 18.3,13.12L17,12.07V20A1,1 0 0,1 16,21M20.42,9.58L16.11,5.28C15,7 14,8.25 12,9.25C10,8.25 9,7 7.89,5.28L3.58,9.58L5,11L8,9H9V19H15V9H16L19,11L20.42,9.58Z" /></g><g id="tumblr"><path d="M16,11H13V14.9C13,15.63 13.14,16 14.1,16H16V19C16,19 14.97,19.1 13.9,19.1C11.25,19.1 10,17.5 10,15.7V11H8V8.2C10.41,8 10.62,6.16 10.8,5H13V8H16M20,2H4C2.89,2 2,2.89 2,4V20A2,2 0 0,0 4,22H20A2,2 0 0,0 22,20V4C22,2.89 21.1,2 20,2Z" /></g><g id="tumblr-reblog"><path d="M3.75,17L8,12.75V16H18V11.5L20,9.5V16A2,2 0 0,1 18,18H8V21.25L3.75,17M20.25,7L16,11.25V8H6V12.5L4,14.5V8A2,2 0 0,1 6,6H16V2.75L20.25,7Z" /></g><g id="tune"><path d="M3,17V19H9V17H3M3,5V7H13V5H3M13,21V19H21V17H13V15H11V21H13M7,9V11H3V13H7V15H9V9H7M21,13V11H11V13H21M15,9H17V7H21V5H17V3H15V9Z" /></g><g id="tune-vertical"><path d="M5,3V12H3V14H5V21H7V14H9V12H7V3M11,3V8H9V10H11V21H13V10H15V8H13V3M17,3V14H15V16H17V21H19V16H21V14H19V3" /></g><g id="twitch"><path d="M4,2H22V14L17,19H13L10,22H7V19H2V6L4,2M20,13V4H6V16H9V19L12,16H17L20,13M15,7H17V12H15V7M12,7V12H10V7H12Z" /></g><g id="twitter"><path d="M22.46,6C21.69,6.35 20.86,6.58 20,6.69C20.88,6.16 21.56,5.32 21.88,4.31C21.05,4.81 20.13,5.16 19.16,5.36C18.37,4.5 17.26,4 16,4C13.65,4 11.73,5.92 11.73,8.29C11.73,8.63 11.77,8.96 11.84,9.27C8.28,9.09 5.11,7.38 3,4.79C2.63,5.42 2.42,6.16 2.42,6.94C2.42,8.43 3.17,9.75 4.33,10.5C3.62,10.5 2.96,10.3 2.38,10C2.38,10 2.38,10 2.38,10.03C2.38,12.11 3.86,13.85 5.82,14.24C5.46,14.34 5.08,14.39 4.69,14.39C4.42,14.39 4.15,14.36 3.89,14.31C4.43,16 6,17.26 7.89,17.29C6.43,18.45 4.58,19.13 2.56,19.13C2.22,19.13 1.88,19.11 1.54,19.07C3.44,20.29 5.7,21 8.12,21C16,21 20.33,14.46 20.33,8.79C20.33,8.6 20.33,8.42 20.32,8.23C21.16,7.63 21.88,6.87 22.46,6Z" /></g><g id="twitter-box"><path d="M17.71,9.33C17.64,13.95 14.69,17.11 10.28,17.31C8.46,17.39 7.15,16.81 6,16.08C7.34,16.29 9,15.76 9.9,15C8.58,14.86 7.81,14.19 7.44,13.12C7.82,13.18 8.22,13.16 8.58,13.09C7.39,12.69 6.54,11.95 6.5,10.41C6.83,10.57 7.18,10.71 7.64,10.74C6.75,10.23 6.1,8.38 6.85,7.16C8.17,8.61 9.76,9.79 12.37,9.95C11.71,7.15 15.42,5.63 16.97,7.5C17.63,7.38 18.16,7.14 18.68,6.86C18.47,7.5 18.06,7.97 17.56,8.33C18.1,8.26 18.59,8.13 19,7.92C18.75,8.45 18.19,8.93 17.71,9.33M20,2H4A2,2 0 0,0 2,4V20A2,2 0 0,0 4,22H20A2,2 0 0,0 22,20V4C22,2.89 21.1,2 20,2Z" /></g><g id="twitter-circle"><path d="M17.71,9.33C18.19,8.93 18.75,8.45 19,7.92C18.59,8.13 18.1,8.26 17.56,8.33C18.06,7.97 18.47,7.5 18.68,6.86C18.16,7.14 17.63,7.38 16.97,7.5C15.42,5.63 11.71,7.15 12.37,9.95C9.76,9.79 8.17,8.61 6.85,7.16C6.1,8.38 6.75,10.23 7.64,10.74C7.18,10.71 6.83,10.57 6.5,10.41C6.54,11.95 7.39,12.69 8.58,13.09C8.22,13.16 7.82,13.18 7.44,13.12C7.81,14.19 8.58,14.86 9.9,15C9,15.76 7.34,16.29 6,16.08C7.15,16.81 8.46,17.39 10.28,17.31C14.69,17.11 17.64,13.95 17.71,9.33M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z" /></g><g id="twitter-retweet"><path d="M6,5.75L10.25,10H7V16H13.5L15.5,18H7A2,2 0 0,1 5,16V10H1.75L6,5.75M18,18.25L13.75,14H17V8H10.5L8.5,6H17A2,2 0 0,1 19,8V14H22.25L18,18.25Z" /></g><g id="ubuntu"><path d="M22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12M14.34,7.74C14.92,8.07 15.65,7.87 16,7.3C16.31,6.73 16.12,6 15.54,5.66C14.97,5.33 14.23,5.5 13.9,6.1C13.57,6.67 13.77,7.41 14.34,7.74M11.88,15.5C11.35,15.5 10.85,15.39 10.41,15.18L9.57,16.68C10.27,17 11.05,17.22 11.88,17.22C12.37,17.22 12.83,17.15 13.28,17.03C13.36,16.54 13.64,16.1 14.1,15.84C14.56,15.57 15.08,15.55 15.54,15.72C16.43,14.85 17,13.66 17.09,12.33L15.38,12.31C15.22,14.1 13.72,15.5 11.88,15.5M11.88,8.5C13.72,8.5 15.22,9.89 15.38,11.69L17.09,11.66C17,10.34 16.43,9.15 15.54,8.28C15.08,8.45 14.55,8.42 14.1,8.16C13.64,7.9 13.36,7.45 13.28,6.97C12.83,6.85 12.37,6.78 11.88,6.78C11.05,6.78 10.27,6.97 9.57,7.32L10.41,8.82C10.85,8.61 11.35,8.5 11.88,8.5M8.37,12C8.37,10.81 8.96,9.76 9.86,9.13L9,7.65C7.94,8.36 7.15,9.43 6.83,10.69C7.21,11 7.45,11.47 7.45,12C7.45,12.53 7.21,13 6.83,13.31C7.15,14.56 7.94,15.64 9,16.34L9.86,14.87C8.96,14.24 8.37,13.19 8.37,12M14.34,16.26C13.77,16.59 13.57,17.32 13.9,17.9C14.23,18.47 14.97,18.67 15.54,18.34C16.12,18 16.31,17.27 16,16.7C15.65,16.12 14.92,15.93 14.34,16.26M5.76,10.8C5.1,10.8 4.56,11.34 4.56,12C4.56,12.66 5.1,13.2 5.76,13.2C6.43,13.2 6.96,12.66 6.96,12C6.96,11.34 6.43,10.8 5.76,10.8Z" /></g><g id="umbraco"><path d="M8.6,8.6L7.17,8.38C6.5,11.67 6.46,14.24 7.61,15.5C8.6,16.61 11.89,16.61 11.89,16.61C11.89,16.61 15.29,16.61 16.28,15.5C17.43,14.24 17.38,11.67 16.72,8.38L15.29,8.6C15.29,8.6 16.54,13.88 14.69,14.69C13.81,15.07 11.89,15.07 11.89,15.07C11.89,15.07 10.08,15.07 9.2,14.69C7.35,13.88 8.6,8.6 8.6,8.6M12,3A9,9 0 0,1 21,12A9,9 0 0,1 12,21A9,9 0 0,1 3,12A9,9 0 0,1 12,3Z" /></g><g id="umbrella"><path d="M12,2A9,9 0 0,0 3,11H11V19A1,1 0 0,1 10,20A1,1 0 0,1 9,19H7A3,3 0 0,0 10,22A3,3 0 0,0 13,19V11H21A9,9 0 0,0 12,2Z" /></g><g id="umbrella-outline"><path d="M12,4C15.09,4 17.82,6.04 18.7,9H5.3C6.18,6.03 8.9,4 12,4M12,2A9,9 0 0,0 3,11H11V19A1,1 0 0,1 10,20A1,1 0 0,1 9,19H7A3,3 0 0,0 10,22A3,3 0 0,0 13,19V11H21A9,9 0 0,0 12,2Z" /></g><g id="undo"><path d="M12.5,8C9.85,8 7.45,9 5.6,10.6L2,7V16H11L7.38,12.38C8.77,11.22 10.54,10.5 12.5,10.5C16.04,10.5 19.05,12.81 20.1,16L22.47,15.22C21.08,11.03 17.15,8 12.5,8Z" /></g><g id="undo-variant"><path d="M13.5,7A6.5,6.5 0 0,1 20,13.5A6.5,6.5 0 0,1 13.5,20H10V18H13.5C16,18 18,16 18,13.5C18,11 16,9 13.5,9H7.83L10.91,12.09L9.5,13.5L4,8L9.5,2.5L10.92,3.91L7.83,7H13.5M6,18H8V20H6V18Z" /></g><g id="unfold-less"><path d="M16.59,5.41L15.17,4L12,7.17L8.83,4L7.41,5.41L12,10M7.41,18.59L8.83,20L12,16.83L15.17,20L16.58,18.59L12,14L7.41,18.59Z" /></g><g id="unfold-more"><path d="M12,18.17L8.83,15L7.42,16.41L12,21L16.59,16.41L15.17,15M12,5.83L15.17,9L16.58,7.59L12,3L7.41,7.59L8.83,9L12,5.83Z" /></g><g id="ungroup"><path d="M2,2H6V3H13V2H17V6H16V9H18V8H22V12H21V18H22V22H18V21H12V22H8V18H9V16H6V17H2V13H3V6H2V2M18,12V11H16V13H17V17H13V16H11V18H12V19H18V18H19V12H18M13,6V5H6V6H5V13H6V14H9V12H8V8H12V9H14V6H13M12,12H11V14H13V13H14V11H12V12Z" /></g><g id="unity"><path d="M9.11,17H6.5L1.59,12L6.5,7H9.11L10.42,4.74L17.21,3L19.08,9.74L17.77,12L19.08,14.26L17.21,21L10.42,19.26L9.11,17M9.25,16.75L14.38,18.13L11.42,13H5.5L9.25,16.75M16.12,17.13L17.5,12L16.12,6.87L13.15,12L16.12,17.13M9.25,7.25L5.5,11H11.42L14.38,5.87L9.25,7.25Z" /></g><g id="untappd"><path d="M14.41,4C14.41,4 14.94,4.39 14.97,4.71C14.97,4.81 14.73,4.85 14.68,4.93C14.62,5 14.7,5.15 14.65,5.21C14.59,5.26 14.5,5.26 14.41,5.41C14.33,5.56 12.07,10.09 11.73,10.63C11.59,11.03 11.47,12.46 11.37,12.66C11.26,12.85 6.34,19.84 6.16,20.05C5.67,20.63 4.31,20.3 3.28,19.56C2.3,18.86 1.74,17.7 2.11,17.16C2.27,16.93 7.15,9.92 7.29,9.75C7.44,9.58 8.75,9 9.07,8.71C9.47,8.22 12.96,4.54 13.07,4.42C13.18,4.3 13.15,4.2 13.18,4.13C13.22,4.06 13.38,4.08 13.43,4C13.5,3.93 13.39,3.71 13.5,3.68C13.59,3.64 13.96,3.67 14.41,4M10.85,4.44L11.74,5.37L10.26,6.94L9.46,5.37C9.38,5.22 9.28,5.22 9.22,5.17C9.17,5.11 9.24,4.97 9.19,4.89C9.13,4.81 8.9,4.83 8.9,4.73C8.9,4.62 9.05,4.28 9.5,3.96C9.5,3.96 10.06,3.6 10.37,3.68C10.47,3.71 10.43,3.95 10.5,4C10.54,4.1 10.7,4.08 10.73,4.15C10.77,4.21 10.73,4.32 10.85,4.44M21.92,17.15C22.29,17.81 21.53,19 20.5,19.7C19.5,20.39 18.21,20.54 17.83,20C17.66,19.78 12.67,12.82 12.56,12.62C12.45,12.43 12.32,11 12.18,10.59L12.15,10.55C12.45,10 13.07,8.77 13.73,7.47C14.3,8.06 14.75,8.56 14.88,8.72C15.21,9 16.53,9.58 16.68,9.75C16.82,9.92 21.78,16.91 21.92,17.15Z" /></g><g id="update"><path d="M21,10.12H14.22L16.96,7.3C14.23,4.6 9.81,4.5 7.08,7.2C4.35,9.91 4.35,14.28 7.08,17C9.81,19.7 14.23,19.7 16.96,17C18.32,15.65 19,14.08 19,12.1H21C21,14.08 20.12,16.65 18.36,18.39C14.85,21.87 9.15,21.87 5.64,18.39C2.14,14.92 2.11,9.28 5.62,5.81C9.13,2.34 14.76,2.34 18.27,5.81L21,3V10.12M12.5,8V12.25L16,14.33L15.28,15.54L11,13V8H12.5Z" /></g><g id="upload"><path d="M9,16V10H5L12,3L19,10H15V16H9M5,20V18H19V20H5Z" /></g><g id="usb"><path d="M15,7V11H16V13H13V5H15L12,1L9,5H11V13H8V10.93C8.7,10.56 9.2,9.85 9.2,9C9.2,7.78 8.21,6.8 7,6.8C5.78,6.8 4.8,7.78 4.8,9C4.8,9.85 5.3,10.56 6,10.93V13A2,2 0 0,0 8,15H11V18.05C10.29,18.41 9.8,19.15 9.8,20A2.2,2.2 0 0,0 12,22.2A2.2,2.2 0 0,0 14.2,20C14.2,19.15 13.71,18.41 13,18.05V15H16A2,2 0 0,0 18,13V11H19V7H15Z" /></g><g id="vector-arrange-above"><path d="M3,1C1.89,1 1,1.89 1,3V14C1,15.11 1.89,16 3,16C6.67,16 10.33,16 14,16C15.11,16 16,15.11 16,14C16,10.33 16,6.67 16,3C16,1.89 15.11,1 14,1H3M3,3H14V14H3V3M18,7V9H20V20H9V18H7V20C7,21.11 7.89,22 9,22H20C21.11,22 22,21.11 22,20V9C22,7.89 21.11,7 20,7H18Z" /></g><g id="vector-arrange-below"><path d="M20,22C21.11,22 22,21.11 22,20V9C22,7.89 21.11,7 20,7C16.33,7 12.67,7 9,7C7.89,7 7,7.89 7,9C7,12.67 7,16.33 7,20C7,21.11 7.89,22 9,22H20M20,20H9V9H20V20M5,16V14H3V3H14V5H16V3C16,1.89 15.11,1 14,1H3C1.89,1 1,1.89 1,3V14C1,15.11 1.89,16 3,16H5Z" /></g><g id="vector-circle"><path d="M9,2V4.06C6.72,4.92 4.92,6.72 4.05,9H2V15H4.06C4.92,17.28 6.72,19.09 9,19.95V22H15V19.94C17.28,19.08 19.09,17.28 19.95,15H22V9H19.94C19.08,6.72 17.28,4.92 15,4.05V2M11,4H13V6H11M9,6.25V8H15V6.25C16.18,6.86 17.14,7.82 17.75,9H16V15H17.75C17.14,16.18 16.18,17.14 15,17.75V16H9V17.75C7.82,17.14 6.86,16.18 6.25,15H8V9H6.25C6.86,7.82 7.82,6.86 9,6.25M4,11H6V13H4M18,11H20V13H18M11,18H13V20H11" /></g><g id="vector-circle-variant"><path d="M22,9H19.97C18.7,5.41 15.31,3 11.5,3A9,9 0 0,0 2.5,12C2.5,17 6.53,21 11.5,21C15.31,21 18.7,18.6 20,15H22M20,11V13H18V11M17.82,15C16.66,17.44 14.2,19 11.5,19C7.64,19 4.5,15.87 4.5,12C4.5,8.14 7.64,5 11.5,5C14.2,5 16.66,6.57 17.81,9H16V15" /></g><g id="vector-combine"><path d="M3,1C1.89,1 1,1.89 1,3V14C1,15.11 1.89,16 3,16C4.33,16 7,16 7,16C7,16 7,18.67 7,20C7,21.11 7.89,22 9,22H20C21.11,22 22,21.11 22,20V9C22,7.89 21.11,7 20,7C18.67,7 16,7 16,7C16,7 16,4.33 16,3C16,1.89 15.11,1 14,1H3M3,3H14C14,4.33 14,7 14,7H9C7.89,7 7,7.89 7,9V14C7,14 4.33,14 3,14V3M9,9H14V14H9V9M16,9C16,9 18.67,9 20,9V20H9C9,18.67 9,16 9,16H14C15.11,16 16,15.11 16,14V9Z" /></g><g id="vector-curve"><path d="M18.5,2A1.5,1.5 0 0,1 20,3.5A1.5,1.5 0 0,1 18.5,5C18.27,5 18.05,4.95 17.85,4.85L14.16,8.55L14.5,9C16.69,7.74 19.26,7 22,7L23,7.03V9.04L22,9C19.42,9 17,9.75 15,11.04A3.96,3.96 0 0,1 11.04,15C9.75,17 9,19.42 9,22L9.04,23H7.03L7,22C7,19.26 7.74,16.69 9,14.5L8.55,14.16L4.85,17.85C4.95,18.05 5,18.27 5,18.5A1.5,1.5 0 0,1 3.5,20A1.5,1.5 0 0,1 2,18.5A1.5,1.5 0 0,1 3.5,17C3.73,17 3.95,17.05 4.15,17.15L7.84,13.45C7.31,12.78 7,11.92 7,11A4,4 0 0,1 11,7C11.92,7 12.78,7.31 13.45,7.84L17.15,4.15C17.05,3.95 17,3.73 17,3.5A1.5,1.5 0 0,1 18.5,2M11,9A2,2 0 0,0 9,11A2,2 0 0,0 11,13A2,2 0 0,0 13,11A2,2 0 0,0 11,9Z" /></g><g id="vector-difference"><path d="M3,1C1.89,1 1,1.89 1,3V14C1,15.11 1.89,16 3,16H5V14H3V3H14V5H16V3C16,1.89 15.11,1 14,1H3M9,7C7.89,7 7,7.89 7,9V11H9V9H11V7H9M13,7V9H14V10H16V7H13M18,7V9H20V20H9V18H7V20C7,21.11 7.89,22 9,22H20C21.11,22 22,21.11 22,20V9C22,7.89 21.11,7 20,7H18M14,12V14H12V16H14C15.11,16 16,15.11 16,14V12H14M7,13V16H10V14H9V13H7Z" /></g><g id="vector-difference-ab"><path d="M3,1C1.89,1 1,1.89 1,3V5H3V3H5V1H3M7,1V3H10V1H7M12,1V3H14V5H16V3C16,1.89 15.11,1 14,1H12M1,7V10H3V7H1M14,7C14,7 14,11.67 14,14C11.67,14 7,14 7,14C7,14 7,18 7,20C7,21.11 7.89,22 9,22H20C21.11,22 22,21.11 22,20V9C22,7.89 21.11,7 20,7C18,7 14,7 14,7M16,9H20V20H9V16H14C15.11,16 16,15.11 16,14V9M1,12V14C1,15.11 1.89,16 3,16H5V14H3V12H1Z" /></g><g id="vector-difference-ba"><path d="M20,22C21.11,22 22,21.11 22,20V18H20V20H18V22H20M16,22V20H13V22H16M11,22V20H9V18H7V20C7,21.11 7.89,22 9,22H11M22,16V13H20V16H22M9,16C9,16 9,11.33 9,9C11.33,9 16,9 16,9C16,9 16,5 16,3C16,1.89 15.11,1 14,1H3C1.89,1 1,1.89 1,3V14C1,15.11 1.89,16 3,16C5,16 9,16 9,16M7,14H3V3H14V7H9C7.89,7 7,7.89 7,9V14M22,11V9C22,7.89 21.11,7 20,7H18V9H20V11H22Z" /></g><g id="vector-intersection"><path d="M3.14,1A2.14,2.14 0 0,0 1,3.14V5H3V3H5V1H3.14M7,1V3H10V1H7M12,1V3H14V5H16V3.14C16,1.96 15.04,1 13.86,1H12M1,7V10H3V7H1M9,7C7.89,7 7,7.89 7,9C7,11.33 7,16 7,16C7,16 11.57,16 13.86,16A2.14,2.14 0 0,0 16,13.86C16,11.57 16,7 16,7C16,7 11.33,7 9,7M18,7V9H20V11H22V9C22,7.89 21.11,7 20,7H18M9,9H14V14H9V9M1,12V13.86C1,15.04 1.96,16 3.14,16H5V14H3V12H1M20,13V16H22V13H20M7,18V20C7,21.11 7.89,22 9,22H11V20H9V18H7M20,18V20H18V22H20C21.11,22 22,21.11 22,20V18H20M13,20V22H16V20H13Z" /></g><g id="vector-line"><path d="M15,3V7.59L7.59,15H3V21H9V16.42L16.42,9H21V3M17,5H19V7H17M5,17H7V19H5" /></g><g id="vector-point"><path d="M12,20L7,22L12,11L17,22L12,20M8,2H16V5H22V7H16V10H8V7H2V5H8V2M10,4V8H14V4H10Z" /></g><g id="vector-polygon"><path d="M2,2V8H4.28L5.57,16H4V22H10V20.06L15,20.05V22H21V16H19.17L20,9H22V3H16V6.53L14.8,8H9.59L8,5.82V2M4,4H6V6H4M18,5H20V7H18M6.31,8H7.11L9,10.59V14H15V10.91L16.57,9H18L17.16,16H15V18.06H10V16H7.6M11,10H13V12H11M6,18H8V20H6M17,18H19V20H17" /></g><g id="vector-polyline"><path d="M16,2V8H17.08L14.95,13H14.26L12,9.97V5H6V11H6.91L4.88,16H2V22H8V16H7.04L9.07,11H10.27L12,13.32V19H18V13H17.12L19.25,8H22V2M18,4H20V6H18M8,7H10V9H8M14,15H16V17H14M4,18H6V20H4" /></g><g id="vector-rectangle"><path d="M2,4H8V6H16V4H22V10H20V14H22V20H16V18H8V20H2V14H4V10H2V4M16,10V8H8V10H6V14H8V16H16V14H18V10H16M4,6V8H6V6H4M18,6V8H20V6H18M4,16V18H6V16H4M18,16V18H20V16H18Z" /></g><g id="vector-selection"><path d="M3,1H5V3H3V5H1V3A2,2 0 0,1 3,1M14,1A2,2 0 0,1 16,3V5H14V3H12V1H14M20,7A2,2 0 0,1 22,9V11H20V9H18V7H20M22,20A2,2 0 0,1 20,22H18V20H20V18H22V20M20,13H22V16H20V13M13,9V7H16V10H14V9H13M13,22V20H16V22H13M9,22A2,2 0 0,1 7,20V18H9V20H11V22H9M7,16V13H9V14H10V16H7M7,3V1H10V3H7M3,16A2,2 0 0,1 1,14V12H3V14H5V16H3M1,7H3V10H1V7M9,7H11V9H9V11H7V9A2,2 0 0,1 9,7M16,14A2,2 0 0,1 14,16H12V14H14V12H16V14Z" /></g><g id="vector-square"><path d="M2,2H8V4H16V2H22V8H20V16H22V22H16V20H8V22H2V16H4V8H2V2M16,8V6H8V8H6V16H8V18H16V16H18V8H16M4,4V6H6V4H4M18,4V6H20V4H18M4,18V20H6V18H4M18,18V20H20V18H18Z" /></g><g id="vector-triangle"><path d="M9,3V9H9.73L5.79,16H2V22H8V20H16V22H22V16H18.21L14.27,9H15V3M11,5H13V7H11M12,9.04L16,16.15V18H8V16.15M4,18H6V20H4M18,18H20V20H18" /></g><g id="vector-union"><path d="M3,1C1.89,1 1,1.89 1,3V14C1,15.11 1.89,16 3,16H7V20C7,21.11 7.89,22 9,22H20C21.11,22 22,21.11 22,20V9C22,7.89 21.11,7 20,7H16V3C16,1.89 15.11,1 14,1H3M3,3H14V9H20V20H9V14H3V3Z" /></g><g id="verified"><path d="M10,17L6,13L7.41,11.59L10,14.17L16.59,7.58L18,9M12,1L3,5V11C3,16.55 6.84,21.74 12,23C17.16,21.74 21,16.55 21,11V5L12,1Z" /></g><g id="vibrate"><path d="M16,19H8V5H16M16.5,3H7.5A1.5,1.5 0 0,0 6,4.5V19.5A1.5,1.5 0 0,0 7.5,21H16.5A1.5,1.5 0 0,0 18,19.5V4.5A1.5,1.5 0 0,0 16.5,3M19,17H21V7H19M22,9V15H24V9M3,17H5V7H3M0,15H2V9H0V15Z" /></g><g id="video"><path d="M17,10.5V7A1,1 0 0,0 16,6H4A1,1 0 0,0 3,7V17A1,1 0 0,0 4,18H16A1,1 0 0,0 17,17V13.5L21,17.5V6.5L17,10.5Z" /></g><g id="video-off"><path d="M3.27,2L2,3.27L4.73,6H4A1,1 0 0,0 3,7V17A1,1 0 0,0 4,18H16C16.2,18 16.39,17.92 16.54,17.82L19.73,21L21,19.73M21,6.5L17,10.5V7A1,1 0 0,0 16,6H9.82L21,17.18V6.5Z" /></g><g id="video-switch"><path d="M13,15.5V13H7V15.5L3.5,12L7,8.5V11H13V8.5L16.5,12M18,9.5V6A1,1 0 0,0 17,5H3A1,1 0 0,0 2,6V18A1,1 0 0,0 3,19H17A1,1 0 0,0 18,18V14.5L22,18.5V5.5L18,9.5Z" /></g><g id="view-agenda"><path d="M20,3H3A1,1 0 0,0 2,4V10A1,1 0 0,0 3,11H20A1,1 0 0,0 21,10V4A1,1 0 0,0 20,3M20,13H3A1,1 0 0,0 2,14V20A1,1 0 0,0 3,21H20A1,1 0 0,0 21,20V14A1,1 0 0,0 20,13Z" /></g><g id="view-array"><path d="M8,18H17V5H8M18,5V18H21V5M4,18H7V5H4V18Z" /></g><g id="view-carousel"><path d="M18,6V17H22V6M2,17H6V6H2M7,19H17V4H7V19Z" /></g><g id="view-column"><path d="M16,5V18H21V5M4,18H9V5H4M10,18H15V5H10V18Z" /></g><g id="view-dashboard"><path d="M13,3V9H21V3M13,21H21V11H13M3,21H11V15H3M3,13H11V3H3V13Z" /></g><g id="view-day"><path d="M2,3V6H21V3M20,8H3A1,1 0 0,0 2,9V15A1,1 0 0,0 3,16H20A1,1 0 0,0 21,15V9A1,1 0 0,0 20,8M2,21H21V18H2V21Z" /></g><g id="view-grid"><path d="M3,11H11V3H3M3,21H11V13H3M13,21H21V13H13M13,3V11H21V3" /></g><g id="view-headline"><path d="M4,5V7H21V5M4,11H21V9H4M4,19H21V17H4M4,15H21V13H4V15Z" /></g><g id="view-list"><path d="M9,5V9H21V5M9,19H21V15H9M9,14H21V10H9M4,9H8V5H4M4,19H8V15H4M4,14H8V10H4V14Z" /></g><g id="view-module"><path d="M16,5V11H21V5M10,11H15V5H10M16,18H21V12H16M10,18H15V12H10M4,18H9V12H4M4,11H9V5H4V11Z" /></g><g id="view-quilt"><path d="M10,5V11H21V5M16,18H21V12H16M4,18H9V5H4M10,18H15V12H10V18Z" /></g><g id="view-stream"><path d="M4,5V11H21V5M4,18H21V12H4V18Z" /></g><g id="view-week"><path d="M13,5H10A1,1 0 0,0 9,6V18A1,1 0 0,0 10,19H13A1,1 0 0,0 14,18V6A1,1 0 0,0 13,5M20,5H17A1,1 0 0,0 16,6V18A1,1 0 0,0 17,19H20A1,1 0 0,0 21,18V6A1,1 0 0,0 20,5M6,5H3A1,1 0 0,0 2,6V18A1,1 0 0,0 3,19H6A1,1 0 0,0 7,18V6A1,1 0 0,0 6,5Z" /></g><g id="vimeo"><path d="M22,7.42C21.91,9.37 20.55,12.04 17.92,15.44C15.2,19 12.9,20.75 11,20.75C9.85,20.75 8.86,19.67 8.05,17.5C7.5,15.54 7,13.56 6.44,11.58C5.84,9.42 5.2,8.34 4.5,8.34C4.36,8.34 3.84,8.66 2.94,9.29L2,8.07C3,7.2 3.96,6.33 4.92,5.46C6.24,4.32 7.23,3.72 7.88,3.66C9.44,3.5 10.4,4.58 10.76,6.86C11.15,9.33 11.42,10.86 11.57,11.46C12,13.5 12.5,14.5 13.05,14.5C13.47,14.5 14.1,13.86 14.94,12.53C15.78,11.21 16.23,10.2 16.29,9.5C16.41,8.36 15.96,7.79 14.94,7.79C14.46,7.79 13.97,7.9 13.46,8.12C14.44,4.89 16.32,3.32 19.09,3.41C21.15,3.47 22.12,4.81 22,7.42Z" /></g><g id="vine"><path d="M19.89,11.95C19.43,12.06 19,12.1 18.57,12.1C16.3,12.1 14.55,10.5 14.55,7.76C14.55,6.41 15.08,5.7 15.82,5.7C16.5,5.7 17,6.33 17,7.61C17,8.34 16.79,9.14 16.65,9.61C16.65,9.61 17.35,10.83 19.26,10.46C19.67,9.56 19.89,8.39 19.89,7.36C19.89,4.6 18.5,3 15.91,3C13.26,3 11.71,5.04 11.71,7.72C11.71,10.38 12.95,12.67 15,13.71C14.14,15.43 13.04,16.95 11.9,18.1C9.82,15.59 7.94,12.24 7.17,5.7H4.11C5.53,16.59 9.74,20.05 10.86,20.72C11.5,21.1 12.03,21.08 12.61,20.75C13.5,20.24 16.23,17.5 17.74,14.34C18.37,14.33 19.13,14.26 19.89,14.09V11.95Z" /></g><g id="violin"><path d="M11,2A1,1 0 0,0 10,3V5L10,9A0.5,0.5 0 0,0 10.5,9.5H12A0.5,0.5 0 0,1 12.5,10A0.5,0.5 0 0,1 12,10.5H10.5C9.73,10.5 9,9.77 9,9V5.16C7.27,5.6 6,7.13 6,9V10.5A2.5,2.5 0 0,1 8.5,13A2.5,2.5 0 0,1 6,15.5V17C6,19.77 8.23,22 11,22H13C15.77,22 18,19.77 18,17V15.5A2.5,2.5 0 0,1 15.5,13A2.5,2.5 0 0,1 18,10.5V9C18,6.78 16.22,5 14,5V3A1,1 0 0,0 13,2H11M10.75,16.5H13.25L12.75,20H11.25L10.75,16.5Z" /></g><g id="visualstudio"><path d="M17,8.5L12.25,12.32L17,16V8.5M4.7,18.4L2,16.7V7.7L5,6.7L9.3,10.03L18,2L22,4.5V20L17,22L9.34,14.66L4.7,18.4M5,14L6.86,12.28L5,10.5V14Z" /></g><g id="vk"><path d="M19.54,14.6C21.09,16.04 21.41,16.73 21.46,16.82C22.1,17.88 20.76,17.96 20.76,17.96L18.18,18C18.18,18 17.62,18.11 16.9,17.61C15.93,16.95 15,15.22 14.31,15.45C13.6,15.68 13.62,17.23 13.62,17.23C13.62,17.23 13.62,17.45 13.46,17.62C13.28,17.81 12.93,17.74 12.93,17.74H11.78C11.78,17.74 9.23,18 7,15.67C4.55,13.13 2.39,8.13 2.39,8.13C2.39,8.13 2.27,7.83 2.4,7.66C2.55,7.5 2.97,7.5 2.97,7.5H5.73C5.73,7.5 6,7.5 6.17,7.66C6.32,7.77 6.41,8 6.41,8C6.41,8 6.85,9.11 7.45,10.13C8.6,12.12 9.13,12.55 9.5,12.34C10.1,12.03 9.93,9.53 9.93,9.53C9.93,9.53 9.94,8.62 9.64,8.22C9.41,7.91 8.97,7.81 8.78,7.79C8.62,7.77 8.88,7.41 9.21,7.24C9.71,7 10.58,7 11.62,7C12.43,7 12.66,7.06 12.97,7.13C13.93,7.36 13.6,8.25 13.6,10.37C13.6,11.06 13.5,12 13.97,12.33C14.18,12.47 14.7,12.35 16,10.16C16.6,9.12 17.06,7.89 17.06,7.89C17.06,7.89 17.16,7.68 17.31,7.58C17.47,7.5 17.69,7.5 17.69,7.5H20.59C20.59,7.5 21.47,7.4 21.61,7.79C21.76,8.2 21.28,9.17 20.09,10.74C18.15,13.34 17.93,13.1 19.54,14.6Z" /></g><g id="vk-box"><path d="M5,3H19A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3M17.24,14.03C16.06,12.94 16.22,13.11 17.64,11.22C18.5,10.07 18.85,9.37 18.74,9.07C18.63,8.79 18,8.86 18,8.86L15.89,8.88C15.89,8.88 15.73,8.85 15.62,8.92C15.5,9 15.43,9.15 15.43,9.15C15.43,9.15 15.09,10.04 14.65,10.8C13.71,12.39 13.33,12.47 13.18,12.38C12.83,12.15 12.91,11.45 12.91,10.95C12.91,9.41 13.15,8.76 12.46,8.6C12.23,8.54 12.06,8.5 11.47,8.5C10.72,8.5 10.08,8.5 9.72,8.68C9.5,8.8 9.29,9.06 9.41,9.07C9.55,9.09 9.86,9.16 10.03,9.39C10.25,9.68 10.24,10.34 10.24,10.34C10.24,10.34 10.36,12.16 9.95,12.39C9.66,12.54 9.27,12.22 8.44,10.78C8,10.04 7.68,9.22 7.68,9.22L7.5,9L7.19,8.85H5.18C5.18,8.85 4.88,8.85 4.77,9C4.67,9.1 4.76,9.32 4.76,9.32C4.76,9.32 6.33,12.96 8.11,14.8C9.74,16.5 11.59,16.31 11.59,16.31H12.43C12.43,16.31 12.68,16.36 12.81,16.23C12.93,16.1 12.93,15.94 12.93,15.94C12.93,15.94 12.91,14.81 13.43,14.65C13.95,14.5 14.61,15.73 15.31,16.22C15.84,16.58 16.24,16.5 16.24,16.5L18.12,16.47C18.12,16.47 19.1,16.41 18.63,15.64C18.6,15.58 18.36,15.07 17.24,14.03Z" /></g><g id="vk-circle"><path d="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M17.24,14.03C16.06,12.94 16.22,13.11 17.64,11.22C18.5,10.07 18.85,9.37 18.74,9.07C18.63,8.79 18,8.86 18,8.86L15.89,8.88C15.89,8.88 15.73,8.85 15.62,8.92C15.5,9 15.43,9.15 15.43,9.15C15.43,9.15 15.09,10.04 14.65,10.8C13.71,12.39 13.33,12.47 13.18,12.38C12.83,12.15 12.91,11.45 12.91,10.95C12.91,9.41 13.15,8.76 12.46,8.6C12.23,8.54 12.06,8.5 11.47,8.5C10.72,8.5 10.08,8.5 9.72,8.68C9.5,8.8 9.29,9.06 9.41,9.07C9.55,9.09 9.86,9.16 10.03,9.39C10.25,9.68 10.24,10.34 10.24,10.34C10.24,10.34 10.36,12.16 9.95,12.39C9.66,12.54 9.27,12.22 8.44,10.78C8,10.04 7.68,9.22 7.68,9.22L7.5,9L7.19,8.85H5.18C5.18,8.85 4.88,8.85 4.77,9C4.67,9.1 4.76,9.32 4.76,9.32C4.76,9.32 6.33,12.96 8.11,14.8C9.74,16.5 11.59,16.31 11.59,16.31H12.43C12.43,16.31 12.68,16.36 12.81,16.23C12.93,16.1 12.93,15.94 12.93,15.94C12.93,15.94 12.91,14.81 13.43,14.65C13.95,14.5 14.61,15.73 15.31,16.22C15.84,16.58 16.24,16.5 16.24,16.5L18.12,16.47C18.12,16.47 19.1,16.41 18.63,15.64C18.6,15.58 18.36,15.07 17.24,14.03Z" /></g><g id="vlc"><path d="M12,1C11.58,1 11.19,1.23 11,1.75L9.88,4.88C10.36,5.4 11.28,5.5 12,5.5C12.72,5.5 13.64,5.4 14.13,4.88L13,1.75C12.82,1.25 12.42,1 12,1M8.44,8.91L7,12.91C8.07,14.27 10.26,14.5 12,14.5C13.74,14.5 15.93,14.27 17,12.91L15.56,8.91C14.76,9.83 13.24,10 12,10C10.76,10 9.24,9.83 8.44,8.91M5.44,15C4.62,15 3.76,15.65 3.53,16.44L2.06,21.56C1.84,22.35 2.3,23 3.13,23H20.88C21.7,23 22.16,22.35 21.94,21.56L20.47,16.44C20.24,15.65 19.38,15 18.56,15H17.75L18.09,15.97C18.21,16.29 18.29,16.69 18.09,16.97C16.84,18.7 14.14,19 12,19C9.86,19 7.16,18.7 5.91,16.97C5.71,16.69 5.79,16.29 5.91,15.97L6.25,15H5.44Z" /></g><g id="voice"><path d="M9,5A4,4 0 0,1 13,9A4,4 0 0,1 9,13A4,4 0 0,1 5,9A4,4 0 0,1 9,5M9,15C11.67,15 17,16.34 17,19V21H1V19C1,16.34 6.33,15 9,15M16.76,5.36C18.78,7.56 18.78,10.61 16.76,12.63L15.08,10.94C15.92,9.76 15.92,8.23 15.08,7.05L16.76,5.36M20.07,2C24,6.05 23.97,12.11 20.07,16L18.44,14.37C21.21,11.19 21.21,6.65 18.44,3.63L20.07,2Z" /></g><g id="voicemail"><path d="M18.5,15A3.5,3.5 0 0,1 15,11.5A3.5,3.5 0 0,1 18.5,8A3.5,3.5 0 0,1 22,11.5A3.5,3.5 0 0,1 18.5,15M5.5,15A3.5,3.5 0 0,1 2,11.5A3.5,3.5 0 0,1 5.5,8A3.5,3.5 0 0,1 9,11.5A3.5,3.5 0 0,1 5.5,15M18.5,6A5.5,5.5 0 0,0 13,11.5C13,12.83 13.47,14.05 14.26,15H9.74C10.53,14.05 11,12.83 11,11.5A5.5,5.5 0 0,0 5.5,6A5.5,5.5 0 0,0 0,11.5A5.5,5.5 0 0,0 5.5,17H18.5A5.5,5.5 0 0,0 24,11.5A5.5,5.5 0 0,0 18.5,6Z" /></g><g id="volume-high"><path d="M14,3.23V5.29C16.89,6.15 19,8.83 19,12C19,15.17 16.89,17.84 14,18.7V20.77C18,19.86 21,16.28 21,12C21,7.72 18,4.14 14,3.23M16.5,12C16.5,10.23 15.5,8.71 14,7.97V16C15.5,15.29 16.5,13.76 16.5,12M3,9V15H7L12,20V4L7,9H3Z" /></g><g id="volume-low"><path d="M7,9V15H11L16,20V4L11,9H7Z" /></g><g id="volume-medium"><path d="M5,9V15H9L14,20V4L9,9M18.5,12C18.5,10.23 17.5,8.71 16,7.97V16C17.5,15.29 18.5,13.76 18.5,12Z" /></g><g id="volume-off"><path d="M12,4L9.91,6.09L12,8.18M4.27,3L3,4.27L7.73,9H3V15H7L12,20V13.27L16.25,17.53C15.58,18.04 14.83,18.46 14,18.7V20.77C15.38,20.45 16.63,19.82 17.68,18.96L19.73,21L21,19.73L12,10.73M19,12C19,12.94 18.8,13.82 18.46,14.64L19.97,16.15C20.62,14.91 21,13.5 21,12C21,7.72 18,4.14 14,3.23V5.29C16.89,6.15 19,8.83 19,12M16.5,12C16.5,10.23 15.5,8.71 14,7.97V10.18L16.45,12.63C16.5,12.43 16.5,12.21 16.5,12Z" /></g><g id="vpn"><path d="M9,5H15L12,8L9,5M10.5,14.66C10.2,15 10,15.5 10,16A2,2 0 0,0 12,18A2,2 0 0,0 14,16C14,15.45 13.78,14.95 13.41,14.59L14.83,13.17C15.55,13.9 16,14.9 16,16A4,4 0 0,1 12,20A4,4 0 0,1 8,16C8,14.93 8.42,13.96 9.1,13.25L9.09,13.24L16.17,6.17V6.17C16.89,5.45 17.89,5 19,5A4,4 0 0,1 23,9A4,4 0 0,1 19,13C17.9,13 16.9,12.55 16.17,11.83L17.59,10.41C17.95,10.78 18.45,11 19,11A2,2 0 0,0 21,9A2,2 0 0,0 19,7C18.45,7 17.95,7.22 17.59,7.59L10.5,14.66M6.41,7.59C6.05,7.22 5.55,7 5,7A2,2 0 0,0 3,9A2,2 0 0,0 5,11C5.55,11 6.05,10.78 6.41,10.41L7.83,11.83C7.1,12.55 6.1,13 5,13A4,4 0 0,1 1,9A4,4 0 0,1 5,5C6.11,5 7.11,5.45 7.83,6.17V6.17L10.59,8.93L9.17,10.35L6.41,7.59Z" /></g><g id="walk"><path d="M14.12,10H19V8.2H15.38L13.38,4.87C13.08,4.37 12.54,4.03 11.92,4.03C11.74,4.03 11.58,4.06 11.42,4.11L6,5.8V11H7.8V7.33L9.91,6.67L6,22H7.8L10.67,13.89L13,17V22H14.8V15.59L12.31,11.05L13.04,8.18M14,3.8C15,3.8 15.8,3 15.8,2C15.8,1 15,0.2 14,0.2C13,0.2 12.2,1 12.2,2C12.2,3 13,3.8 14,3.8Z" /></g><g id="wallet"><path d="M21,18V19A2,2 0 0,1 19,21H5C3.89,21 3,20.1 3,19V5A2,2 0 0,1 5,3H19A2,2 0 0,1 21,5V6H12C10.89,6 10,6.9 10,8V16A2,2 0 0,0 12,18M12,16H22V8H12M16,13.5A1.5,1.5 0 0,1 14.5,12A1.5,1.5 0 0,1 16,10.5A1.5,1.5 0 0,1 17.5,12A1.5,1.5 0 0,1 16,13.5Z" /></g><g id="wallet-giftcard"><path d="M20,14H4V8H9.08L7,10.83L8.62,12L11,8.76L12,7.4L13,8.76L15.38,12L17,10.83L14.92,8H20M20,19H4V17H20M9,4A1,1 0 0,1 10,5A1,1 0 0,1 9,6A1,1 0 0,1 8,5A1,1 0 0,1 9,4M15,4A1,1 0 0,1 16,5A1,1 0 0,1 15,6A1,1 0 0,1 14,5A1,1 0 0,1 15,4M20,6H17.82C17.93,5.69 18,5.35 18,5A3,3 0 0,0 15,2C13.95,2 13.04,2.54 12.5,3.35L12,4L11.5,3.34C10.96,2.54 10.05,2 9,2A3,3 0 0,0 6,5C6,5.35 6.07,5.69 6.18,6H4C2.89,6 2,6.89 2,8V19C2,20.11 2.89,21 4,21H20C21.11,21 22,20.11 22,19V8C22,6.89 21.11,6 20,6Z" /></g><g id="wallet-membership"><path d="M20,10H4V4H20M20,15H4V13H20M20,2H4C2.89,2 2,2.89 2,4V15C2,16.11 2.89,17 4,17H8V22L12,20L16,22V17H20C21.11,17 22,16.11 22,15V4C22,2.89 21.11,2 20,2Z" /></g><g id="wallet-travel"><path d="M20,14H4V8H7V10H9V8H15V10H17V8H20M20,19H4V17H20M9,4H15V6H9M20,6H17V4C17,2.89 16.11,2 15,2H9C7.89,2 7,2.89 7,4V6H4C2.89,6 2,6.89 2,8V19C2,20.11 2.89,21 4,21H20C21.11,21 22,20.11 22,19V8C22,6.89 21.11,6 20,6Z" /></g><g id="wan"><path d="M12,2A8,8 0 0,0 4,10C4,14.03 7,17.42 11,17.93V19H10A1,1 0 0,0 9,20H2V22H9A1,1 0 0,0 10,23H14A1,1 0 0,0 15,22H22V20H15A1,1 0 0,0 14,19H13V17.93C17,17.43 20,14.03 20,10A8,8 0 0,0 12,2M12,4C12,4 12.74,5.28 13.26,7H10.74C11.26,5.28 12,4 12,4M9.77,4.43C9.5,4.93 9.09,5.84 8.74,7H6.81C7.5,5.84 8.5,4.93 9.77,4.43M14.23,4.44C15.5,4.94 16.5,5.84 17.19,7H15.26C14.91,5.84 14.5,4.93 14.23,4.44M6.09,9H8.32C8.28,9.33 8.25,9.66 8.25,10C8.25,10.34 8.28,10.67 8.32,11H6.09C6.03,10.67 6,10.34 6,10C6,9.66 6.03,9.33 6.09,9M10.32,9H13.68C13.72,9.33 13.75,9.66 13.75,10C13.75,10.34 13.72,10.67 13.68,11H10.32C10.28,10.67 10.25,10.34 10.25,10C10.25,9.66 10.28,9.33 10.32,9M15.68,9H17.91C17.97,9.33 18,9.66 18,10C18,10.34 17.97,10.67 17.91,11H15.68C15.72,10.67 15.75,10.34 15.75,10C15.75,9.66 15.72,9.33 15.68,9M6.81,13H8.74C9.09,14.16 9.5,15.07 9.77,15.56C8.5,15.06 7.5,14.16 6.81,13M10.74,13H13.26C12.74,14.72 12,16 12,16C12,16 11.26,14.72 10.74,13M15.26,13H17.19C16.5,14.16 15.5,15.07 14.23,15.57C14.5,15.07 14.91,14.16 15.26,13Z" /></g><g id="watch"><path d="M6,12A6,6 0 0,1 12,6A6,6 0 0,1 18,12A6,6 0 0,1 12,18A6,6 0 0,1 6,12M20,12C20,9.45 18.81,7.19 16.95,5.73L16,0H8L7.05,5.73C5.19,7.19 4,9.45 4,12C4,14.54 5.19,16.81 7.05,18.27L8,24H16L16.95,18.27C18.81,16.81 20,14.54 20,12Z" /></g><g id="watch-export"><path d="M14,11H19L16.5,8.5L17.92,7.08L22.84,12L17.92,16.92L16.5,15.5L19,13H14V11M12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.4,6 14.69,6.5 15.71,7.29L17.13,5.87L16.95,5.73L16,0H8L7.05,5.73C5.19,7.19 4,9.46 4,12C4,14.55 5.19,16.81 7.05,18.27L8,24H16L16.95,18.27L17.13,18.13L15.71,16.71C14.69,17.5 13.4,18 12,18Z" /></g><g id="watch-import"><path d="M2,11H7L4.5,8.5L5.92,7.08L10.84,12L5.92,16.92L4.5,15.5L7,13H2V11M12,18A6,6 0 0,0 18,12A6,6 0 0,0 12,6C10.6,6 9.31,6.5 8.29,7.29L6.87,5.87L7.05,5.73L8,0H16L16.95,5.73C18.81,7.19 20,9.45 20,12C20,14.54 18.81,16.81 16.95,18.27L16,24H8L7.05,18.27L6.87,18.13L8.29,16.71C9.31,17.5 10.6,18 12,18Z" /></g><g id="watch-vibrate"><path d="M3,17V7H5V17H3M19,17V7H21V17H19M22,9H24V15H22V9M0,15V9H2V15H0M17.96,11.97C17.96,13.87 17.07,15.57 15.68,16.67L14.97,20.95H9L8.27,16.67C6.88,15.57 6,13.87 6,11.97C6,10.07 6.88,8.37 8.27,7.28L9,3H14.97L15.68,7.28C17.07,8.37 17.96,10.07 17.96,11.97M7.5,11.97C7.5,14.45 9.5,16.46 11.97,16.46A4.49,4.49 0 0,0 16.46,11.97C16.46,9.5 14.45,7.5 11.97,7.5A4.47,4.47 0 0,0 7.5,11.97Z" /></g><g id="water"><path d="M12,20A6,6 0 0,1 6,14C6,10 12,3.25 12,3.25C12,3.25 18,10 18,14A6,6 0 0,1 12,20Z" /></g><g id="water-off"><path d="M17.12,17.12L12.5,12.5L5.27,5.27L4,6.55L7.32,9.87C6.55,11.32 6,12.79 6,14A6,6 0 0,0 12,20C13.5,20 14.9,19.43 15.96,18.5L18.59,21.13L19.86,19.86L17.12,17.12M18,14C18,10 12,3.2 12,3.2C12,3.2 10.67,4.71 9.27,6.72L17.86,15.31C17.95,14.89 18,14.45 18,14Z" /></g><g id="water-percent"><path d="M12,3.25C12,3.25 6,10 6,14C6,17.32 8.69,20 12,20A6,6 0 0,0 18,14C18,10 12,3.25 12,3.25M14.47,9.97L15.53,11.03L9.53,17.03L8.47,15.97M9.75,10A1.25,1.25 0 0,1 11,11.25A1.25,1.25 0 0,1 9.75,12.5A1.25,1.25 0 0,1 8.5,11.25A1.25,1.25 0 0,1 9.75,10M14.25,14.5A1.25,1.25 0 0,1 15.5,15.75A1.25,1.25 0 0,1 14.25,17A1.25,1.25 0 0,1 13,15.75A1.25,1.25 0 0,1 14.25,14.5Z" /></g><g id="water-pump"><path d="M19,14.5C19,14.5 21,16.67 21,18A2,2 0 0,1 19,20A2,2 0 0,1 17,18C17,16.67 19,14.5 19,14.5M5,18V9A2,2 0 0,1 3,7A2,2 0 0,1 5,5V4A2,2 0 0,1 7,2H9A2,2 0 0,1 11,4V5H19A2,2 0 0,1 21,7V9L21,11A1,1 0 0,1 22,12A1,1 0 0,1 21,13H17A1,1 0 0,1 16,12A1,1 0 0,1 17,11V9H11V18H12A2,2 0 0,1 14,20V22H2V20A2,2 0 0,1 4,18H5Z" /></g><g id="watermark"><path d="M21,3H3A2,2 0 0,0 1,5V19A2,2 0 0,0 3,21H21A2,2 0 0,0 23,19V5A2,2 0 0,0 21,3M21,19H12V13H21V19Z" /></g><g id="weather-cloudy"><path d="M6,19A5,5 0 0,1 1,14A5,5 0 0,1 6,9C7,6.65 9.3,5 12,5C15.43,5 18.24,7.66 18.5,11.03L19,11A4,4 0 0,1 23,15A4,4 0 0,1 19,19H6M19,13H17V12A5,5 0 0,0 12,7C9.5,7 7.45,8.82 7.06,11.19C6.73,11.07 6.37,11 6,11A3,3 0 0,0 3,14A3,3 0 0,0 6,17H19A2,2 0 0,0 21,15A2,2 0 0,0 19,13Z" /></g><g id="weather-fog"><path d="M3,15H13A1,1 0 0,1 14,16A1,1 0 0,1 13,17H3A1,1 0 0,1 2,16A1,1 0 0,1 3,15M16,15H21A1,1 0 0,1 22,16A1,1 0 0,1 21,17H16A1,1 0 0,1 15,16A1,1 0 0,1 16,15M1,12A5,5 0 0,1 6,7C7,4.65 9.3,3 12,3C15.43,3 18.24,5.66 18.5,9.03L19,9C21.19,9 22.97,10.76 23,13H21A2,2 0 0,0 19,11H17V10A5,5 0 0,0 12,5C9.5,5 7.45,6.82 7.06,9.19C6.73,9.07 6.37,9 6,9A3,3 0 0,0 3,12C3,12.35 3.06,12.69 3.17,13H1.1L1,12M3,19H5A1,1 0 0,1 6,20A1,1 0 0,1 5,21H3A1,1 0 0,1 2,20A1,1 0 0,1 3,19M8,19H21A1,1 0 0,1 22,20A1,1 0 0,1 21,21H8A1,1 0 0,1 7,20A1,1 0 0,1 8,19Z" /></g><g id="weather-hail"><path d="M6,14A1,1 0 0,1 7,15A1,1 0 0,1 6,16A5,5 0 0,1 1,11A5,5 0 0,1 6,6C7,3.65 9.3,2 12,2C15.43,2 18.24,4.66 18.5,8.03L19,8A4,4 0 0,1 23,12A4,4 0 0,1 19,16H18A1,1 0 0,1 17,15A1,1 0 0,1 18,14H19A2,2 0 0,0 21,12A2,2 0 0,0 19,10H17V9A5,5 0 0,0 12,4C9.5,4 7.45,5.82 7.06,8.19C6.73,8.07 6.37,8 6,8A3,3 0 0,0 3,11A3,3 0 0,0 6,14M10,18A2,2 0 0,1 12,20A2,2 0 0,1 10,22A2,2 0 0,1 8,20A2,2 0 0,1 10,18M14.5,16A1.5,1.5 0 0,1 16,17.5A1.5,1.5 0 0,1 14.5,19A1.5,1.5 0 0,1 13,17.5A1.5,1.5 0 0,1 14.5,16M10.5,12A1.5,1.5 0 0,1 12,13.5A1.5,1.5 0 0,1 10.5,15A1.5,1.5 0 0,1 9,13.5A1.5,1.5 0 0,1 10.5,12Z" /></g><g id="weather-lightning"><path d="M6,16A5,5 0 0,1 1,11A5,5 0 0,1 6,6C7,3.65 9.3,2 12,2C15.43,2 18.24,4.66 18.5,8.03L19,8A4,4 0 0,1 23,12A4,4 0 0,1 19,16H18A1,1 0 0,1 17,15A1,1 0 0,1 18,14H19A2,2 0 0,0 21,12A2,2 0 0,0 19,10H17V9A5,5 0 0,0 12,4C9.5,4 7.45,5.82 7.06,8.19C6.73,8.07 6.37,8 6,8A3,3 0 0,0 3,11A3,3 0 0,0 6,14H7A1,1 0 0,1 8,15A1,1 0 0,1 7,16H6M12,11H15L13,15H15L11.25,22L12,17H9.5L12,11Z" /></g><g id="weather-lightning-rainy"><path d="M4.5,13.59C5,13.87 5.14,14.5 4.87,14.96C4.59,15.44 4,15.6 3.5,15.33V15.33C2,14.47 1,12.85 1,11A5,5 0 0,1 6,6C7,3.65 9.3,2 12,2C15.43,2 18.24,4.66 18.5,8.03L19,8A4,4 0 0,1 23,12A4,4 0 0,1 19,16A1,1 0 0,1 18,15A1,1 0 0,1 19,14A2,2 0 0,0 21,12A2,2 0 0,0 19,10H17V9A5,5 0 0,0 12,4C9.5,4 7.45,5.82 7.06,8.19C6.73,8.07 6.37,8 6,8A3,3 0 0,0 3,11C3,12.11 3.6,13.08 4.5,13.6V13.59M9.5,11H12.5L10.5,15H12.5L8.75,22L9.5,17H7L9.5,11M17.5,18.67C17.5,19.96 16.5,21 15.25,21C14,21 13,19.96 13,18.67C13,17.12 15.25,14.5 15.25,14.5C15.25,14.5 17.5,17.12 17.5,18.67Z" /></g><g id="weather-night"><path d="M17.75,4.09L15.22,6.03L16.13,9.09L13.5,7.28L10.87,9.09L11.78,6.03L9.25,4.09L12.44,4L13.5,1L14.56,4L17.75,4.09M21.25,11L19.61,12.25L20.2,14.23L18.5,13.06L16.8,14.23L17.39,12.25L15.75,11L17.81,10.95L18.5,9L19.19,10.95L21.25,11M18.97,15.95C19.8,15.87 20.69,17.05 20.16,17.8C19.84,18.25 19.5,18.67 19.08,19.07C15.17,23 8.84,23 4.94,19.07C1.03,15.17 1.03,8.83 4.94,4.93C5.34,4.53 5.76,4.17 6.21,3.85C6.96,3.32 8.14,4.21 8.06,5.04C7.79,7.9 8.75,10.87 10.95,13.06C13.14,15.26 16.1,16.22 18.97,15.95M17.33,17.97C14.5,17.81 11.7,16.64 9.53,14.5C7.36,12.31 6.2,9.5 6.04,6.68C3.23,9.82 3.34,14.64 6.35,17.66C9.37,20.67 14.19,20.78 17.33,17.97Z" /></g><g id="weather-partlycloudy"><path d="M12.74,5.47C15.1,6.5 16.35,9.03 15.92,11.46C17.19,12.56 18,14.19 18,16V16.17C18.31,16.06 18.65,16 19,16A3,3 0 0,1 22,19A3,3 0 0,1 19,22H6A4,4 0 0,1 2,18A4,4 0 0,1 6,14H6.27C5,12.45 4.6,10.24 5.5,8.26C6.72,5.5 9.97,4.24 12.74,5.47M11.93,7.3C10.16,6.5 8.09,7.31 7.31,9.07C6.85,10.09 6.93,11.22 7.41,12.13C8.5,10.83 10.16,10 12,10C12.7,10 13.38,10.12 14,10.34C13.94,9.06 13.18,7.86 11.93,7.3M13.55,3.64C13,3.4 12.45,3.23 11.88,3.12L14.37,1.82L15.27,4.71C14.76,4.29 14.19,3.93 13.55,3.64M6.09,4.44C5.6,4.79 5.17,5.19 4.8,5.63L4.91,2.82L7.87,3.5C7.25,3.71 6.65,4.03 6.09,4.44M18,9.71C17.91,9.12 17.78,8.55 17.59,8L19.97,9.5L17.92,11.73C18.03,11.08 18.05,10.4 18,9.71M3.04,11.3C3.11,11.9 3.24,12.47 3.43,13L1.06,11.5L3.1,9.28C3,9.93 2.97,10.61 3.04,11.3M19,18H16V16A4,4 0 0,0 12,12A4,4 0 0,0 8,16H6A2,2 0 0,0 4,18A2,2 0 0,0 6,20H19A1,1 0 0,0 20,19A1,1 0 0,0 19,18Z" /></g><g id="weather-pouring"><path d="M9,12C9.53,12.14 9.85,12.69 9.71,13.22L8.41,18.05C8.27,18.59 7.72,18.9 7.19,18.76C6.65,18.62 6.34,18.07 6.5,17.54L7.78,12.71C7.92,12.17 8.47,11.86 9,12M13,12C13.53,12.14 13.85,12.69 13.71,13.22L11.64,20.95C11.5,21.5 10.95,21.8 10.41,21.66C9.88,21.5 9.56,20.97 9.7,20.43L11.78,12.71C11.92,12.17 12.47,11.86 13,12M17,12C17.53,12.14 17.85,12.69 17.71,13.22L16.41,18.05C16.27,18.59 15.72,18.9 15.19,18.76C14.65,18.62 14.34,18.07 14.5,17.54L15.78,12.71C15.92,12.17 16.47,11.86 17,12M17,10V9A5,5 0 0,0 12,4C9.5,4 7.45,5.82 7.06,8.19C6.73,8.07 6.37,8 6,8A3,3 0 0,0 3,11C3,12.11 3.6,13.08 4.5,13.6V13.59C5,13.87 5.14,14.5 4.87,14.96C4.59,15.43 4,15.6 3.5,15.32V15.33C2,14.47 1,12.85 1,11A5,5 0 0,1 6,6C7,3.65 9.3,2 12,2C15.43,2 18.24,4.66 18.5,8.03L19,8A4,4 0 0,1 23,12C23,13.5 22.2,14.77 21,15.46V15.46C20.5,15.73 19.91,15.57 19.63,15.09C19.36,14.61 19.5,14 20,13.72V13.73C20.6,13.39 21,12.74 21,12A2,2 0 0,0 19,10H17Z" /></g><g id="weather-rainy"><path d="M6,14A1,1 0 0,1 7,15A1,1 0 0,1 6,16A5,5 0 0,1 1,11A5,5 0 0,1 6,6C7,3.65 9.3,2 12,2C15.43,2 18.24,4.66 18.5,8.03L19,8A4,4 0 0,1 23,12A4,4 0 0,1 19,16H18A1,1 0 0,1 17,15A1,1 0 0,1 18,14H19A2,2 0 0,0 21,12A2,2 0 0,0 19,10H17V9A5,5 0 0,0 12,4C9.5,4 7.45,5.82 7.06,8.19C6.73,8.07 6.37,8 6,8A3,3 0 0,0 3,11A3,3 0 0,0 6,14M14.83,15.67C16.39,17.23 16.39,19.5 14.83,21.08C14.05,21.86 13,22 12,22C11,22 9.95,21.86 9.17,21.08C7.61,19.5 7.61,17.23 9.17,15.67L12,11L14.83,15.67M13.41,16.69L12,14.25L10.59,16.69C9.8,17.5 9.8,18.7 10.59,19.5C11,19.93 11.5,20 12,20C12.5,20 13,19.93 13.41,19.5C14.2,18.7 14.2,17.5 13.41,16.69Z" /></g><g id="weather-snowy"><path d="M6,14A1,1 0 0,1 7,15A1,1 0 0,1 6,16A5,5 0 0,1 1,11A5,5 0 0,1 6,6C7,3.65 9.3,2 12,2C15.43,2 18.24,4.66 18.5,8.03L19,8A4,4 0 0,1 23,12A4,4 0 0,1 19,16H18A1,1 0 0,1 17,15A1,1 0 0,1 18,14H19A2,2 0 0,0 21,12A2,2 0 0,0 19,10H17V9A5,5 0 0,0 12,4C9.5,4 7.45,5.82 7.06,8.19C6.73,8.07 6.37,8 6,8A3,3 0 0,0 3,11A3,3 0 0,0 6,14M7.88,18.07L10.07,17.5L8.46,15.88C8.07,15.5 8.07,14.86 8.46,14.46C8.85,14.07 9.5,14.07 9.88,14.46L11.5,16.07L12.07,13.88C12.21,13.34 12.76,13.03 13.29,13.17C13.83,13.31 14.14,13.86 14,14.4L13.41,16.59L15.6,16C16.14,15.86 16.69,16.17 16.83,16.71C16.97,17.24 16.66,17.79 16.12,17.93L13.93,18.5L15.54,20.12C15.93,20.5 15.93,21.15 15.54,21.54C15.15,21.93 14.5,21.93 14.12,21.54L12.5,19.93L11.93,22.12C11.79,22.66 11.24,22.97 10.71,22.83C10.17,22.69 9.86,22.14 10,21.6L10.59,19.41L8.4,20C7.86,20.14 7.31,19.83 7.17,19.29C7.03,18.76 7.34,18.21 7.88,18.07Z" /></g><g id="weather-snowy-rainy"><path d="M18.5,18.67C18.5,19.96 17.5,21 16.25,21C15,21 14,19.96 14,18.67C14,17.12 16.25,14.5 16.25,14.5C16.25,14.5 18.5,17.12 18.5,18.67M4,17.36C3.86,16.82 4.18,16.25 4.73,16.11L7,15.5L5.33,13.86C4.93,13.46 4.93,12.81 5.33,12.4C5.73,12 6.4,12 6.79,12.4L8.45,14.05L9.04,11.8C9.18,11.24 9.75,10.92 10.29,11.07C10.85,11.21 11.17,11.78 11,12.33L10.42,14.58L12.67,14C13.22,13.83 13.79,14.15 13.93,14.71C14.08,15.25 13.76,15.82 13.2,15.96L10.95,16.55L12.6,18.21C13,18.6 13,19.27 12.6,19.67C12.2,20.07 11.54,20.07 11.15,19.67L9.5,18L8.89,20.27C8.75,20.83 8.18,21.14 7.64,21C7.08,20.86 6.77,20.29 6.91,19.74L7.5,17.5L5.26,18.09C4.71,18.23 4.14,17.92 4,17.36M1,11A5,5 0 0,1 6,6C7,3.65 9.3,2 12,2C15.43,2 18.24,4.66 18.5,8.03L19,8A4,4 0 0,1 23,12A4,4 0 0,1 19,16A1,1 0 0,1 18,15A1,1 0 0,1 19,14A2,2 0 0,0 21,12A2,2 0 0,0 19,10H17V9A5,5 0 0,0 12,4C9.5,4 7.45,5.82 7.06,8.19C6.73,8.07 6.37,8 6,8A3,3 0 0,0 3,11C3,11.85 3.35,12.61 3.91,13.16C4.27,13.55 4.26,14.16 3.88,14.54C3.5,14.93 2.85,14.93 2.47,14.54C1.56,13.63 1,12.38 1,11Z" /></g><g id="weather-sunny"><path d="M12,7A5,5 0 0,1 17,12A5,5 0 0,1 12,17A5,5 0 0,1 7,12A5,5 0 0,1 12,7M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9M12,2L14.39,5.42C13.65,5.15 12.84,5 12,5C11.16,5 10.35,5.15 9.61,5.42L12,2M3.34,7L7.5,6.65C6.9,7.16 6.36,7.78 5.94,8.5C5.5,9.24 5.25,10 5.11,10.79L3.34,7M3.36,17L5.12,13.23C5.26,14 5.53,14.78 5.95,15.5C6.37,16.24 6.91,16.86 7.5,17.37L3.36,17M20.65,7L18.88,10.79C18.74,10 18.47,9.23 18.05,8.5C17.63,7.78 17.1,7.15 16.5,6.64L20.65,7M20.64,17L16.5,17.36C17.09,16.85 17.62,16.22 18.04,15.5C18.46,14.77 18.73,14 18.87,13.21L20.64,17M12,22L9.59,18.56C10.33,18.83 11.14,19 12,19C12.82,19 13.63,18.83 14.37,18.56L12,22Z" /></g><g id="weather-sunset"><path d="M3,12H7A5,5 0 0,1 12,7A5,5 0 0,1 17,12H21A1,1 0 0,1 22,13A1,1 0 0,1 21,14H3A1,1 0 0,1 2,13A1,1 0 0,1 3,12M5,16H19A1,1 0 0,1 20,17A1,1 0 0,1 19,18H5A1,1 0 0,1 4,17A1,1 0 0,1 5,16M17,20A1,1 0 0,1 18,21A1,1 0 0,1 17,22H7A1,1 0 0,1 6,21A1,1 0 0,1 7,20H17M15,12A3,3 0 0,0 12,9A3,3 0 0,0 9,12H15M12,2L14.39,5.42C13.65,5.15 12.84,5 12,5C11.16,5 10.35,5.15 9.61,5.42L12,2M3.34,7L7.5,6.65C6.9,7.16 6.36,7.78 5.94,8.5C5.5,9.24 5.25,10 5.11,10.79L3.34,7M20.65,7L18.88,10.79C18.74,10 18.47,9.23 18.05,8.5C17.63,7.78 17.1,7.15 16.5,6.64L20.65,7Z" /></g><g id="weather-sunset-down"><path d="M3,12H7A5,5 0 0,1 12,7A5,5 0 0,1 17,12H21A1,1 0 0,1 22,13A1,1 0 0,1 21,14H3A1,1 0 0,1 2,13A1,1 0 0,1 3,12M15,12A3,3 0 0,0 12,9A3,3 0 0,0 9,12H15M12,2L14.39,5.42C13.65,5.15 12.84,5 12,5C11.16,5 10.35,5.15 9.61,5.42L12,2M3.34,7L7.5,6.65C6.9,7.16 6.36,7.78 5.94,8.5C5.5,9.24 5.25,10 5.11,10.79L3.34,7M20.65,7L18.88,10.79C18.74,10 18.47,9.23 18.05,8.5C17.63,7.78 17.1,7.15 16.5,6.64L20.65,7M12.71,20.71L15.82,17.6C16.21,17.21 16.21,16.57 15.82,16.18C15.43,15.79 14.8,15.79 14.41,16.18L12,18.59L9.59,16.18C9.2,15.79 8.57,15.79 8.18,16.18C7.79,16.57 7.79,17.21 8.18,17.6L11.29,20.71C11.5,20.9 11.74,21 12,21C12.26,21 12.5,20.9 12.71,20.71Z" /></g><g id="weather-sunset-up"><path d="M3,12H7A5,5 0 0,1 12,7A5,5 0 0,1 17,12H21A1,1 0 0,1 22,13A1,1 0 0,1 21,14H3A1,1 0 0,1 2,13A1,1 0 0,1 3,12M15,12A3,3 0 0,0 12,9A3,3 0 0,0 9,12H15M12,2L14.39,5.42C13.65,5.15 12.84,5 12,5C11.16,5 10.35,5.15 9.61,5.42L12,2M3.34,7L7.5,6.65C6.9,7.16 6.36,7.78 5.94,8.5C5.5,9.24 5.25,10 5.11,10.79L3.34,7M20.65,7L18.88,10.79C18.74,10 18.47,9.23 18.05,8.5C17.63,7.78 17.1,7.15 16.5,6.64L20.65,7M12.71,16.3L15.82,19.41C16.21,19.8 16.21,20.43 15.82,20.82C15.43,21.21 14.8,21.21 14.41,20.82L12,18.41L9.59,20.82C9.2,21.21 8.57,21.21 8.18,20.82C7.79,20.43 7.79,19.8 8.18,19.41L11.29,16.3C11.5,16.1 11.74,16 12,16C12.26,16 12.5,16.1 12.71,16.3Z" /></g><g id="weather-windy"><path d="M4,10A1,1 0 0,1 3,9A1,1 0 0,1 4,8H12A2,2 0 0,0 14,6A2,2 0 0,0 12,4C11.45,4 10.95,4.22 10.59,4.59C10.2,5 9.56,5 9.17,4.59C8.78,4.2 8.78,3.56 9.17,3.17C9.9,2.45 10.9,2 12,2A4,4 0 0,1 16,6A4,4 0 0,1 12,10H4M19,12A1,1 0 0,0 20,11A1,1 0 0,0 19,10C18.72,10 18.47,10.11 18.29,10.29C17.9,10.68 17.27,10.68 16.88,10.29C16.5,9.9 16.5,9.27 16.88,8.88C17.42,8.34 18.17,8 19,8A3,3 0 0,1 22,11A3,3 0 0,1 19,14H5A1,1 0 0,1 4,13A1,1 0 0,1 5,12H19M18,18H4A1,1 0 0,1 3,17A1,1 0 0,1 4,16H18A3,3 0 0,1 21,19A3,3 0 0,1 18,22C17.17,22 16.42,21.66 15.88,21.12C15.5,20.73 15.5,20.1 15.88,19.71C16.27,19.32 16.9,19.32 17.29,19.71C17.47,19.89 17.72,20 18,20A1,1 0 0,0 19,19A1,1 0 0,0 18,18Z" /></g><g id="weather-windy-variant"><path d="M6,6L6.69,6.06C7.32,3.72 9.46,2 12,2A5.5,5.5 0 0,1 17.5,7.5L17.42,8.45C17.88,8.16 18.42,8 19,8A3,3 0 0,1 22,11A3,3 0 0,1 19,14H6A4,4 0 0,1 2,10A4,4 0 0,1 6,6M6,8A2,2 0 0,0 4,10A2,2 0 0,0 6,12H19A1,1 0 0,0 20,11A1,1 0 0,0 19,10H15.5V7.5A3.5,3.5 0 0,0 12,4A3.5,3.5 0 0,0 8.5,7.5V8H6M18,18H4A1,1 0 0,1 3,17A1,1 0 0,1 4,16H18A3,3 0 0,1 21,19A3,3 0 0,1 18,22C17.17,22 16.42,21.66 15.88,21.12C15.5,20.73 15.5,20.1 15.88,19.71C16.27,19.32 16.9,19.32 17.29,19.71C17.47,19.89 17.72,20 18,20A1,1 0 0,0 19,19A1,1 0 0,0 18,18Z" /></g><g id="web"><path d="M16.36,14C16.44,13.34 16.5,12.68 16.5,12C16.5,11.32 16.44,10.66 16.36,10H19.74C19.9,10.64 20,11.31 20,12C20,12.69 19.9,13.36 19.74,14M14.59,19.56C15.19,18.45 15.65,17.25 15.97,16H18.92C17.96,17.65 16.43,18.93 14.59,19.56M14.34,14H9.66C9.56,13.34 9.5,12.68 9.5,12C9.5,11.32 9.56,10.65 9.66,10H14.34C14.43,10.65 14.5,11.32 14.5,12C14.5,12.68 14.43,13.34 14.34,14M12,19.96C11.17,18.76 10.5,17.43 10.09,16H13.91C13.5,17.43 12.83,18.76 12,19.96M8,8H5.08C6.03,6.34 7.57,5.06 9.4,4.44C8.8,5.55 8.35,6.75 8,8M5.08,16H8C8.35,17.25 8.8,18.45 9.4,19.56C7.57,18.93 6.03,17.65 5.08,16M4.26,14C4.1,13.36 4,12.69 4,12C4,11.31 4.1,10.64 4.26,10H7.64C7.56,10.66 7.5,11.32 7.5,12C7.5,12.68 7.56,13.34 7.64,14M12,4.03C12.83,5.23 13.5,6.57 13.91,8H10.09C10.5,6.57 11.17,5.23 12,4.03M18.92,8H15.97C15.65,6.75 15.19,5.55 14.59,4.44C16.43,5.07 17.96,6.34 18.92,8M12,2C6.47,2 2,6.5 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" /></g><g id="webcam"><path d="M12,2A7,7 0 0,1 19,9A7,7 0 0,1 12,16A7,7 0 0,1 5,9A7,7 0 0,1 12,2M12,4A5,5 0 0,0 7,9A5,5 0 0,0 12,14A5,5 0 0,0 17,9A5,5 0 0,0 12,4M12,6A3,3 0 0,1 15,9A3,3 0 0,1 12,12A3,3 0 0,1 9,9A3,3 0 0,1 12,6M6,22A2,2 0 0,1 4,20C4,19.62 4.1,19.27 4.29,18.97L6.11,15.81C7.69,17.17 9.75,18 12,18C14.25,18 16.31,17.17 17.89,15.81L19.71,18.97C19.9,19.27 20,19.62 20,20A2,2 0 0,1 18,22H6Z" /></g><g id="webhook"><path d="M10.46,19C9,21.07 6.15,21.59 4.09,20.15C2.04,18.71 1.56,15.84 3,13.75C3.87,12.5 5.21,11.83 6.58,11.77L6.63,13.2C5.72,13.27 4.84,13.74 4.27,14.56C3.27,16 3.58,17.94 4.95,18.91C6.33,19.87 8.26,19.5 9.26,18.07C9.57,17.62 9.75,17.13 9.82,16.63V15.62L15.4,15.58L15.47,15.47C16,14.55 17.15,14.23 18.05,14.75C18.95,15.27 19.26,16.43 18.73,17.35C18.2,18.26 17.04,18.58 16.14,18.06C15.73,17.83 15.44,17.46 15.31,17.04L11.24,17.06C11.13,17.73 10.87,18.38 10.46,19M17.74,11.86C20.27,12.17 22.07,14.44 21.76,16.93C21.45,19.43 19.15,21.2 16.62,20.89C15.13,20.71 13.9,19.86 13.19,18.68L14.43,17.96C14.92,18.73 15.75,19.28 16.75,19.41C18.5,19.62 20.05,18.43 20.26,16.76C20.47,15.09 19.23,13.56 17.5,13.35C16.96,13.29 16.44,13.36 15.97,13.53L15.12,13.97L12.54,9.2H12.32C11.26,9.16 10.44,8.29 10.47,7.25C10.5,6.21 11.4,5.4 12.45,5.44C13.5,5.5 14.33,6.35 14.3,7.39C14.28,7.83 14.11,8.23 13.84,8.54L15.74,12.05C16.36,11.85 17.04,11.78 17.74,11.86M8.25,9.14C7.25,6.79 8.31,4.1 10.62,3.12C12.94,2.14 15.62,3.25 16.62,5.6C17.21,6.97 17.09,8.47 16.42,9.67L15.18,8.95C15.6,8.14 15.67,7.15 15.27,6.22C14.59,4.62 12.78,3.85 11.23,4.5C9.67,5.16 8.97,7 9.65,8.6C9.93,9.26 10.4,9.77 10.97,10.11L11.36,10.32L8.29,15.31C8.32,15.36 8.36,15.42 8.39,15.5C8.88,16.41 8.54,17.56 7.62,18.05C6.71,18.54 5.56,18.18 5.06,17.24C4.57,16.31 4.91,15.16 5.83,14.67C6.22,14.46 6.65,14.41 7.06,14.5L9.37,10.73C8.9,10.3 8.5,9.76 8.25,9.14Z" /></g><g id="wechat"><path d="M9.5,4C5.36,4 2,6.69 2,10C2,11.89 3.08,13.56 4.78,14.66L4,17L6.5,15.5C7.39,15.81 8.37,16 9.41,16C9.15,15.37 9,14.7 9,14C9,10.69 12.13,8 16,8C16.19,8 16.38,8 16.56,8.03C15.54,5.69 12.78,4 9.5,4M6.5,6.5A1,1 0 0,1 7.5,7.5A1,1 0 0,1 6.5,8.5A1,1 0 0,1 5.5,7.5A1,1 0 0,1 6.5,6.5M11.5,6.5A1,1 0 0,1 12.5,7.5A1,1 0 0,1 11.5,8.5A1,1 0 0,1 10.5,7.5A1,1 0 0,1 11.5,6.5M16,9C12.69,9 10,11.24 10,14C10,16.76 12.69,19 16,19C16.67,19 17.31,18.92 17.91,18.75L20,20L19.38,18.13C20.95,17.22 22,15.71 22,14C22,11.24 19.31,9 16,9M14,11.5A1,1 0 0,1 15,12.5A1,1 0 0,1 14,13.5A1,1 0 0,1 13,12.5A1,1 0 0,1 14,11.5M18,11.5A1,1 0 0,1 19,12.5A1,1 0 0,1 18,13.5A1,1 0 0,1 17,12.5A1,1 0 0,1 18,11.5Z" /></g><g id="weight"><path d="M12,3A4,4 0 0,1 16,7C16,7.73 15.81,8.41 15.46,9H18C18.95,9 19.75,9.67 19.95,10.56C21.96,18.57 22,18.78 22,19A2,2 0 0,1 20,21H4A2,2 0 0,1 2,19C2,18.78 2.04,18.57 4.05,10.56C4.25,9.67 5.05,9 6,9H8.54C8.19,8.41 8,7.73 8,7A4,4 0 0,1 12,3M12,5A2,2 0 0,0 10,7A2,2 0 0,0 12,9A2,2 0 0,0 14,7A2,2 0 0,0 12,5Z" /></g><g id="weight-kilogram"><path d="M12,3A4,4 0 0,1 16,7C16,7.73 15.81,8.41 15.46,9H18C18.95,9 19.75,9.67 19.95,10.56C21.96,18.57 22,18.78 22,19A2,2 0 0,1 20,21H4A2,2 0 0,1 2,19C2,18.78 2.04,18.57 4.05,10.56C4.25,9.67 5.05,9 6,9H8.54C8.19,8.41 8,7.73 8,7A4,4 0 0,1 12,3M12,5A2,2 0 0,0 10,7A2,2 0 0,0 12,9A2,2 0 0,0 14,7A2,2 0 0,0 12,5M9.04,15.44L10.4,18H12.11L10.07,14.66L11.95,11.94H10.2L8.87,14.33H8.39V11.94H6.97V18H8.39V15.44H9.04M17.31,17.16V14.93H14.95V16.04H15.9V16.79L15.55,16.93L14.94,17C14.59,17 14.31,16.85 14.11,16.6C13.92,16.34 13.82,16 13.82,15.59V14.34C13.82,13.93 13.92,13.6 14.12,13.35C14.32,13.09 14.58,12.97 14.91,12.97C15.24,12.97 15.5,13.05 15.64,13.21C15.8,13.37 15.9,13.61 15.95,13.93H17.27L17.28,13.9C17.23,13.27 17,12.77 16.62,12.4C16.23,12.04 15.64,11.86 14.86,11.86C14.14,11.86 13.56,12.09 13.1,12.55C12.64,13 12.41,13.61 12.41,14.34V15.6C12.41,16.34 12.65,16.94 13.12,17.4C13.58,17.86 14.19,18.09 14.94,18.09C15.53,18.09 16.03,18 16.42,17.81C16.81,17.62 17.11,17.41 17.31,17.16Z" /></g><g id="whatsapp"><path d="M16.75,13.96C17,14.09 17.16,14.16 17.21,14.26C17.27,14.37 17.25,14.87 17,15.44C16.8,16 15.76,16.54 15.3,16.56C14.84,16.58 14.83,16.92 12.34,15.83C9.85,14.74 8.35,12.08 8.23,11.91C8.11,11.74 7.27,10.53 7.31,9.3C7.36,8.08 8,7.5 8.26,7.26C8.5,7 8.77,6.97 8.94,7H9.41C9.56,7 9.77,6.94 9.96,7.45L10.65,9.32C10.71,9.45 10.75,9.6 10.66,9.76L10.39,10.17L10,10.59C9.88,10.71 9.74,10.84 9.88,11.09C10,11.35 10.5,12.18 11.2,12.87C12.11,13.75 12.91,14.04 13.15,14.17C13.39,14.31 13.54,14.29 13.69,14.13L14.5,13.19C14.69,12.94 14.85,13 15.08,13.08L16.75,13.96M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22C10.03,22 8.2,21.43 6.65,20.45L2,22L3.55,17.35C2.57,15.8 2,13.97 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12C4,13.72 4.54,15.31 5.46,16.61L4.5,19.5L7.39,18.54C8.69,19.46 10.28,20 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z" /></g><g id="wheelchair-accessibility"><path d="M18.4,11.2L14.3,11.4L16.6,8.8C16.8,8.5 16.9,8 16.8,7.5C16.7,7.2 16.6,6.9 16.3,6.7L10.9,3.5C10.5,3.2 9.9,3.3 9.5,3.6L6.8,6.1C6.3,6.6 6.2,7.3 6.7,7.8C7.1,8.3 7.9,8.3 8.4,7.9L10.4,6.1L12.3,7.2L8.1,11.5C8,11.6 8,11.7 7.9,11.7C7.4,11.9 6.9,12.1 6.5,12.4L8,13.9C8.5,13.7 9,13.5 9.5,13.5C11.4,13.5 13,15.1 13,17C13,17.6 12.9,18.1 12.6,18.5L14.1,20C14.7,19.1 15,18.1 15,17C15,15.8 14.6,14.6 13.9,13.7L17.2,13.4L17,18.2C16.9,18.9 17.4,19.4 18.1,19.5H18.2C18.8,19.5 19.3,19 19.4,18.4L19.6,12.5C19.6,12.2 19.5,11.8 19.3,11.6C19,11.3 18.7,11.2 18.4,11.2M18,5.5A2,2 0 0,0 20,3.5A2,2 0 0,0 18,1.5A2,2 0 0,0 16,3.5A2,2 0 0,0 18,5.5M12.5,21.6C11.6,22.2 10.6,22.5 9.5,22.5C6.5,22.5 4,20 4,17C4,15.9 4.3,14.9 4.9,14L6.4,15.5C6.2,16 6,16.5 6,17C6,18.9 7.6,20.5 9.5,20.5C10.1,20.5 10.6,20.4 11,20.1L12.5,21.6Z" /></g><g id="white-balance-auto"><path d="M10.3,16L9.6,14H6.4L5.7,16H3.8L7,7H9L12.2,16M22,7L20.8,13.29L19.3,7H17.7L16.21,13.29L15,7H14.24C12.77,5.17 10.5,4 8,4A8,8 0 0,0 0,12A8,8 0 0,0 8,20C11.13,20 13.84,18.19 15.15,15.57L15.25,16H17L18.5,9.9L20,16H21.75L23.8,7M6.85,12.65H9.15L8,9L6.85,12.65Z" /></g><g id="white-balance-incandescent"><path d="M17.24,18.15L19.04,19.95L20.45,18.53L18.66,16.74M20,12.5H23V10.5H20M15,6.31V1.5H9V6.31C7.21,7.35 6,9.28 6,11.5A6,6 0 0,0 12,17.5A6,6 0 0,0 18,11.5C18,9.28 16.79,7.35 15,6.31M4,10.5H1V12.5H4M11,22.45C11.32,22.45 13,22.45 13,22.45V19.5H11M3.55,18.53L4.96,19.95L6.76,18.15L5.34,16.74L3.55,18.53Z" /></g><g id="white-balance-iridescent"><path d="M4.96,19.95L6.76,18.15L5.34,16.74L3.55,18.53M3.55,4.46L5.34,6.26L6.76,4.84L4.96,3.05M20.45,18.53L18.66,16.74L17.24,18.15L19.04,19.95M13,22.45V19.5H11V22.45C11.32,22.45 13,22.45 13,22.45M19.04,3.05L17.24,4.84L18.66,6.26L20.45,4.46M11,3.5H13V0.55H11M5,14.5H19V8.5H5V14.5Z" /></g><g id="white-balance-sunny"><path d="M3.55,18.54L4.96,19.95L6.76,18.16L5.34,16.74M11,22.45C11.32,22.45 13,22.45 13,22.45V19.5H11M12,5.5A6,6 0 0,0 6,11.5A6,6 0 0,0 12,17.5A6,6 0 0,0 18,11.5C18,8.18 15.31,5.5 12,5.5M20,12.5H23V10.5H20M17.24,18.16L19.04,19.95L20.45,18.54L18.66,16.74M20.45,4.46L19.04,3.05L17.24,4.84L18.66,6.26M13,0.55H11V3.5H13M4,10.5H1V12.5H4M6.76,4.84L4.96,3.05L3.55,4.46L5.34,6.26L6.76,4.84Z" /></g><g id="widgets"><path d="M3,3H11V7.34L16.66,1.69L22.31,7.34L16.66,13H21V21H13V13H16.66L11,7.34V11H3V3M3,13H11V21H3V13Z" /></g><g id="wifi"><path d="M12,21L15.6,16.2C14.6,15.45 13.35,15 12,15C10.65,15 9.4,15.45 8.4,16.2L12,21M12,3C7.95,3 4.21,4.34 1.2,6.6L3,9C5.5,7.12 8.62,6 12,6C15.38,6 18.5,7.12 21,9L22.8,6.6C19.79,4.34 16.05,3 12,3M12,9C9.3,9 6.81,9.89 4.8,11.4L6.6,13.8C8.1,12.67 9.97,12 12,12C14.03,12 15.9,12.67 17.4,13.8L19.2,11.4C17.19,9.89 14.7,9 12,9Z" /></g><g id="wifi-off"><path d="M2.28,3L1,4.27L2.47,5.74C2.04,6 1.61,6.29 1.2,6.6L3,9C3.53,8.6 4.08,8.25 4.66,7.93L6.89,10.16C6.15,10.5 5.44,10.91 4.8,11.4L6.6,13.8C7.38,13.22 8.26,12.77 9.2,12.47L11.75,15C10.5,15.07 9.34,15.5 8.4,16.2L12,21L14.46,17.73L17.74,21L19,19.72M12,3C9.85,3 7.8,3.38 5.9,4.07L8.29,6.47C9.5,6.16 10.72,6 12,6C15.38,6 18.5,7.11 21,9L22.8,6.6C19.79,4.34 16.06,3 12,3M12,9C11.62,9 11.25,9 10.88,9.05L14.07,12.25C15.29,12.53 16.43,13.07 17.4,13.8L19.2,11.4C17.2,9.89 14.7,9 12,9Z" /></g><g id="wii"><path d="M17.84,16.94H15.97V10.79H17.84V16.94M18,8.58C18,9.19 17.5,9.69 16.9,9.69A1.11,1.11 0 0,1 15.79,8.58C15.79,7.96 16.29,7.46 16.9,7.46C17.5,7.46 18,7.96 18,8.58M21.82,16.94H19.94V10.79H21.82V16.94M22,8.58C22,9.19 21.5,9.69 20.88,9.69A1.11,1.11 0 0,1 19.77,8.58C19.77,7.96 20.27,7.46 20.88,7.46C21.5,7.46 22,7.96 22,8.58M12.9,8.05H14.9L12.78,15.5C12.78,15.5 12.5,17.04 11.28,17.04C10.07,17.04 9.79,15.5 9.79,15.5L8.45,10.64L7.11,15.5C7.11,15.5 6.82,17.04 5.61,17.04C4.4,17.04 4.12,15.5 4.12,15.5L2,8.05H4L5.72,14.67L7.11,9.3C7.43,7.95 8.45,7.97 8.45,7.97C8.45,7.97 9.47,7.95 9.79,9.3L11.17,14.67L12.9,8.05Z" /></g><g id="wiiu"><path d="M2,15.96C2,18.19 3.54,19.5 5.79,19.5H18.57C20.47,19.5 22,18.2 22,16.32V6.97C22,5.83 21.15,4.6 20.11,4.6H17.15V12.3C17.15,18.14 6.97,18.09 6.97,12.41V4.5H4.72C3.26,4.5 2,5.41 2,6.85V15.96M9.34,11.23C9.34,15.74 14.66,15.09 14.66,11.94V4.5H9.34V11.23Z" /></g><g id="wikipedia"><path d="M14.97,18.95L12.41,12.92C11.39,14.91 10.27,17 9.31,18.95C9.3,18.96 8.84,18.95 8.84,18.95C7.37,15.5 5.85,12.1 4.37,8.68C4.03,7.84 2.83,6.5 2,6.5C2,6.4 2,6.18 2,6.05H7.06V6.5C6.46,6.5 5.44,6.9 5.7,7.55C6.42,9.09 8.94,15.06 9.63,16.58C10.1,15.64 11.43,13.16 12,12.11C11.55,11.23 10.13,7.93 9.71,7.11C9.39,6.57 8.58,6.5 7.96,6.5C7.96,6.35 7.97,6.25 7.96,6.06L12.42,6.07V6.47C11.81,6.5 11.24,6.71 11.5,7.29C12.1,8.53 12.45,9.42 13,10.57C13.17,10.23 14.07,8.38 14.5,7.41C14.76,6.76 14.37,6.5 13.29,6.5C13.3,6.38 13.3,6.17 13.3,6.07C14.69,6.06 16.78,6.06 17.15,6.05V6.47C16.44,6.5 15.71,6.88 15.33,7.46L13.5,11.3C13.68,11.81 15.46,15.76 15.65,16.2L19.5,7.37C19.2,6.65 18.34,6.5 18,6.5C18,6.37 18,6.2 18,6.05L22,6.08V6.1L22,6.5C21.12,6.5 20.57,7 20.25,7.75C19.45,9.54 17,15.24 15.4,18.95C15.4,18.95 14.97,18.95 14.97,18.95Z" /></g><g id="window-close"><path d="M13.46,12L19,17.54V19H17.54L12,13.46L6.46,19H5V17.54L10.54,12L5,6.46V5H6.46L12,10.54L17.54,5H19V6.46L13.46,12Z" /></g><g id="window-closed"><path d="M6,11H10V9H14V11H18V4H6V11M18,13H6V20H18V13M6,2H18A2,2 0 0,1 20,4V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2Z" /></g><g id="window-maximize"><path d="M4,4H20V20H4V4M6,8V18H18V8H6Z" /></g><g id="window-minimize"><path d="M20,14H4V10H20" /></g><g id="window-open"><path d="M6,8H10V6H14V8H18V4H6V8M18,10H6V15H18V10M6,20H18V17H6V20M6,2H18A2,2 0 0,1 20,4V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2Z" /></g><g id="window-restore"><path d="M4,8H8V4H20V16H16V20H4V8M16,8V14H18V6H10V8H16M6,12V18H14V12H6Z" /></g><g id="windows"><path d="M3,12V6.75L9,5.43V11.91L3,12M20,3V11.75L10,11.9V5.21L20,3M3,13L9,13.09V19.9L3,18.75V13M20,13.25V22L10,20.09V13.1L20,13.25Z" /></g><g id="wordpress"><path d="M12.2,15.5L9.65,21.72C10.4,21.9 11.19,22 12,22C12.84,22 13.66,21.9 14.44,21.7M20.61,7.06C20.8,7.96 20.76,9.05 20.39,10.25C19.42,13.37 17,19 16.1,21.13C19.58,19.58 22,16.12 22,12.1C22,10.26 21.5,8.53 20.61,7.06M4.31,8.64C4.31,8.64 3.82,8 3.31,8H2.78C2.28,9.13 2,10.62 2,12C2,16.09 4.5,19.61 8.12,21.11M3.13,7.14C4.8,4.03 8.14,2 12,2C14.5,2 16.78,3.06 18.53,4.56C18.03,4.46 17.5,4.57 16.93,4.89C15.64,5.63 15.22,7.71 16.89,8.76C17.94,9.41 18.31,11.04 18.27,12.04C18.24,13.03 15.85,17.61 15.85,17.61L13.5,9.63C13.5,9.63 13.44,9.07 13.44,8.91C13.44,8.71 13.5,8.46 13.63,8.31C13.72,8.22 13.85,8 14,8H15.11V7.14H9.11V8H9.3C9.5,8 9.69,8.29 9.87,8.47C10.09,8.7 10.37,9.55 10.7,10.43L11.57,13.3L9.69,17.63L7.63,8.97C7.63,8.97 7.69,8.37 7.82,8.27C7.9,8.2 8,8 8.17,8H8.22V7.14H3.13Z" /></g><g id="worker"><path d="M12,15C7.58,15 4,16.79 4,19V21H20V19C20,16.79 16.42,15 12,15M8,9A4,4 0 0,0 12,13A4,4 0 0,0 16,9M11.5,2C11.2,2 11,2.21 11,2.5V5.5H10V3C10,3 7.75,3.86 7.75,6.75C7.75,6.75 7,6.89 7,8H17C16.95,6.89 16.25,6.75 16.25,6.75C16.25,3.86 14,3 14,3V5.5H13V2.5C13,2.21 12.81,2 12.5,2H11.5Z" /></g><g id="wrap"><path d="M21,5H3V7H21V5M3,19H10V17H3V19M3,13H18C19,13 20,13.43 20,15C20,16.57 19,17 18,17H16V15L12,18L16,21V19H18C20.95,19 22,17.73 22,15C22,12.28 21,11 18,11H3V13Z" /></g><g id="wrench"><path d="M22.7,19L13.6,9.9C14.5,7.6 14,4.9 12.1,3C10.1,1 7.1,0.6 4.7,1.7L9,6L6,9L1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1C4.8,14 7.5,14.5 9.8,13.6L18.9,22.7C19.3,23.1 19.9,23.1 20.3,22.7L22.6,20.4C23.1,20 23.1,19.3 22.7,19Z" /></g><g id="wunderlist"><path d="M17,17.5L12,15L7,17.5V5H5V19H19V5H17V17.5M12,12.42L14.25,13.77L13.65,11.22L15.64,9.5L13,9.27L12,6.86L11,9.27L8.36,9.5L10.35,11.22L9.75,13.77L12,12.42M5,3H19A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3Z" /></g><g id="xaml"><path d="M18.93,12L15.46,18H8.54L5.07,12L8.54,6H15.46L18.93,12M23.77,12L19.73,19L18,18L21.46,12L18,6L19.73,5L23.77,12M0.23,12L4.27,5L6,6L2.54,12L6,18L4.27,19L0.23,12Z" /></g><g id="xbox"><path d="M6.43,3.72C6.5,3.66 6.57,3.6 6.62,3.56C8.18,2.55 10,2 12,2C13.88,2 15.64,2.5 17.14,3.42C17.25,3.5 17.54,3.69 17.7,3.88C16.25,2.28 12,5.7 12,5.7C10.5,4.57 9.17,3.8 8.16,3.5C7.31,3.29 6.73,3.5 6.46,3.7M19.34,5.21C19.29,5.16 19.24,5.11 19.2,5.06C18.84,4.66 18.38,4.56 18,4.59C17.61,4.71 15.9,5.32 13.8,7.31C13.8,7.31 16.17,9.61 17.62,11.96C19.07,14.31 19.93,16.16 19.4,18.73C21,16.95 22,14.59 22,12C22,9.38 21,7 19.34,5.21M15.73,12.96C15.08,12.24 14.13,11.21 12.86,9.95C12.59,9.68 12.3,9.4 12,9.1C12,9.1 11.53,9.56 10.93,10.17C10.16,10.94 9.17,11.95 8.61,12.54C7.63,13.59 4.81,16.89 4.65,18.74C4.65,18.74 4,17.28 5.4,13.89C6.3,11.68 9,8.36 10.15,7.28C10.15,7.28 9.12,6.14 7.82,5.35L7.77,5.32C7.14,4.95 6.46,4.66 5.8,4.62C5.13,4.67 4.71,5.16 4.71,5.16C3.03,6.95 2,9.35 2,12A10,10 0 0,0 12,22C14.93,22 17.57,20.74 19.4,18.73C19.4,18.73 19.19,17.4 17.84,15.5C17.53,15.07 16.37,13.69 15.73,12.96Z" /></g><g id="xbox-controller"><path d="M8.75,15.75C6.75,15.75 6,18 4,19C2,19 0.5,16 4.5,7.5H4.75L5.19,6.67C5.19,6.67 8,5 9.33,6.23H14.67C16,5 18.81,6.67 18.81,6.67L19.25,7.5H19.5C23.5,16 22,19 20,19C18,18 17.25,15.75 15.25,15.75H8.75M12,7A1,1 0 0,0 11,8A1,1 0 0,0 12,9A1,1 0 0,0 13,8A1,1 0 0,0 12,7Z" /></g><g id="xbox-controller-off"><path d="M2,5.27L3.28,4L20,20.72L18.73,22L12.5,15.75H8.75C6.75,15.75 6,18 4,19C2,19 0.5,16.04 4.42,7.69L2,5.27M9.33,6.23H14.67C16,5 18.81,6.67 18.81,6.67L19.25,7.5H19.5C23,15 22.28,18.2 20.69,18.87L7.62,5.8C8.25,5.73 8.87,5.81 9.33,6.23M12,7A1,1 0 0,0 11,8A1,1 0 0,0 12,9A1,1 0 0,0 13,8A1,1 0 0,0 12,7Z" /></g><g id="xda"><path d="M-0.05,16.79L3.19,12.97L-0.05,9.15L1.5,7.86L4.5,11.41L7.5,7.86L9.05,9.15L5.81,12.97L9.05,16.79L7.5,18.07L4.5,14.5L1.5,18.07L-0.05,16.79M24,17A1,1 0 0,1 23,18H20A2,2 0 0,1 18,16V14A2,2 0 0,1 20,12H22V10H18V8H23A1,1 0 0,1 24,9M22,14H20V16H22V14M16,17A1,1 0 0,1 15,18H12A2,2 0 0,1 10,16V10A2,2 0 0,1 12,8H14V5H16V17M14,16V10H12V16H14Z" /></g><g id="xing"><path d="M17.67,2C17.24,2 17.05,2.27 16.9,2.55C16.9,2.55 10.68,13.57 10.5,13.93L14.58,21.45C14.72,21.71 14.94,22 15.38,22H18.26C18.44,22 18.57,21.93 18.64,21.82C18.72,21.69 18.72,21.53 18.64,21.37L14.57,13.92L20.96,2.63C21.04,2.47 21.04,2.31 20.97,2.18C20.89,2.07 20.76,2 20.58,2M5.55,5.95C5.38,5.95 5.23,6 5.16,6.13C5.08,6.26 5.09,6.41 5.18,6.57L7.12,9.97L4.06,15.37C4,15.53 4,15.69 4.06,15.82C4.13,15.94 4.26,16 4.43,16H7.32C7.75,16 7.96,15.72 8.11,15.45C8.11,15.45 11.1,10.16 11.22,9.95L9.24,6.5C9.1,6.24 8.88,5.95 8.43,5.95" /></g><g id="xing-box"><path d="M4.8,3C3.8,3 3,3.8 3,4.8V19.2C3,20.2 3.8,21 4.8,21H19.2C20.2,21 21,20.2 21,19.2V4.8C21,3.8 20.2,3 19.2,3M16.07,5H18.11C18.23,5 18.33,5.04 18.37,5.13C18.43,5.22 18.43,5.33 18.37,5.44L13.9,13.36L16.75,18.56C16.81,18.67 16.81,18.78 16.75,18.87C16.7,18.95 16.61,19 16.5,19H14.47C14.16,19 14,18.79 13.91,18.61L11.04,13.35C11.18,13.1 15.53,5.39 15.53,5.39C15.64,5.19 15.77,5 16.07,5M7.09,7.76H9.1C9.41,7.76 9.57,7.96 9.67,8.15L11.06,10.57C10.97,10.71 8.88,14.42 8.88,14.42C8.77,14.61 8.63,14.81 8.32,14.81H6.3C6.18,14.81 6.09,14.76 6.04,14.67C6,14.59 6,14.47 6.04,14.36L8.18,10.57L6.82,8.2C6.77,8.09 6.75,8 6.81,7.89C6.86,7.81 6.96,7.76 7.09,7.76Z" /></g><g id="xing-circle"><path d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M15.85,6H17.74C17.86,6 17.94,6.04 18,6.12C18.04,6.2 18.04,6.3 18,6.41L13.84,13.76L16.5,18.59C16.53,18.69 16.53,18.8 16.5,18.88C16.43,18.96 16.35,19 16.24,19H14.36C14.07,19 13.93,18.81 13.84,18.64L11.17,13.76C11.31,13.5 15.35,6.36 15.35,6.36C15.45,6.18 15.57,6 15.85,6M7.5,8.57H9.39C9.67,8.57 9.81,8.75 9.9,8.92L11.19,11.17C11.12,11.3 9.17,14.75 9.17,14.75C9.07,14.92 8.94,15.11 8.66,15.11H6.78C6.67,15.11 6.59,15.06 6.54,15C6.5,14.9 6.5,14.8 6.54,14.69L8.53,11.17L7.27,9C7.21,8.87 7.2,8.77 7.25,8.69C7.3,8.61 7.39,8.57 7.5,8.57Z" /></g><g id="xml"><path d="M12.89,3L14.85,3.4L11.11,21L9.15,20.6L12.89,3M19.59,12L16,8.41V5.58L22.42,12L16,18.41V15.58L19.59,12M1.58,12L8,5.58V8.41L4.41,12L8,15.58V18.41L1.58,12Z" /></g><g id="yeast"><path d="M18,14A4,4 0 0,1 22,18A4,4 0 0,1 18,22A4,4 0 0,1 14,18L14.09,17.15C14.05,16.45 13.92,15.84 13.55,15.5C13.35,15.3 13.07,15.19 12.75,15.13C11.79,15.68 10.68,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3A6.5,6.5 0 0,1 16,9.5C16,10.68 15.68,11.79 15.13,12.75C15.19,13.07 15.3,13.35 15.5,13.55C15.84,13.92 16.45,14.05 17.15,14.09L18,14M7.5,10A1.5,1.5 0 0,1 9,11.5A1.5,1.5 0 0,1 7.5,13A1.5,1.5 0 0,1 6,11.5A1.5,1.5 0 0,1 7.5,10M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z" /></g><g id="yelp"><path d="M10.59,2C11.23,2 11.5,2.27 11.58,2.97L11.79,6.14L12.03,10.29C12.05,10.64 12,11 11.86,11.32C11.64,11.77 11.14,11.89 10.73,11.58C10.5,11.39 10.31,11.14 10.15,10.87L6.42,4.55C6.06,3.94 6.17,3.54 6.77,3.16C7.5,2.68 9.73,2 10.59,2M14.83,14.85L15.09,14.91L18.95,16.31C19.61,16.55 19.79,16.92 19.5,17.57C19.06,18.7 18.34,19.66 17.42,20.45C16.96,20.85 16.5,20.78 16.21,20.28L13.94,16.32C13.55,15.61 14.03,14.8 14.83,14.85M4.5,14C4.5,13.26 4.5,12.55 4.75,11.87C4.97,11.2 5.33,11 6,11.27L9.63,12.81C10.09,13 10.35,13.32 10.33,13.84C10.3,14.36 9.97,14.58 9.53,14.73L5.85,15.94C5.15,16.17 4.79,15.96 4.64,15.25C4.55,14.83 4.47,14.4 4.5,14M11.97,21C11.95,21.81 11.6,22.12 10.81,22C9.77,21.8 8.81,21.4 7.96,20.76C7.54,20.44 7.45,19.95 7.76,19.53L10.47,15.97C10.7,15.67 11.03,15.6 11.39,15.74C11.77,15.88 11.97,16.18 11.97,16.59V21M14.45,13.32C13.73,13.33 13.23,12.5 13.64,11.91C14.47,10.67 15.35,9.46 16.23,8.26C16.5,7.85 16.94,7.82 17.31,8.16C18.24,9 18.91,10 19.29,11.22C19.43,11.67 19.25,12.08 18.83,12.2L15.09,13.17L14.45,13.32Z" /></g><g id="yin-yang"><path d="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A4,4 0 0,1 8,16A4,4 0 0,1 12,12A4,4 0 0,0 16,8A4,4 0 0,0 12,4M12,6.5A1.5,1.5 0 0,1 13.5,8A1.5,1.5 0 0,1 12,9.5A1.5,1.5 0 0,1 10.5,8A1.5,1.5 0 0,1 12,6.5M12,14.5A1.5,1.5 0 0,0 10.5,16A1.5,1.5 0 0,0 12,17.5A1.5,1.5 0 0,0 13.5,16A1.5,1.5 0 0,0 12,14.5Z" /></g><g id="youtube-play"><path d="M10,16.5V7.5L16,12M20,4.4C19.4,4.2 15.7,4 12,4C8.3,4 4.6,4.19 4,4.38C2.44,4.9 2,8.4 2,12C2,15.59 2.44,19.1 4,19.61C4.6,19.81 8.3,20 12,20C15.7,20 19.4,19.81 20,19.61C21.56,19.1 22,15.59 22,12C22,8.4 21.56,4.91 20,4.4Z" /></g><g id="zip-box"><path d="M14,17H12V15H10V13H12V15H14M14,9H12V11H14V13H12V11H10V9H12V7H10V5H12V7H14M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3Z" /></g></defs></svg>
\ No newline at end of file
ui/src/thingsboard.ico 0(+0 -0)
diff --git a/ui/src/thingsboard.ico b/ui/src/thingsboard.ico
new file mode 100644
index 0000000..8564792
Binary files /dev/null and b/ui/src/thingsboard.ico differ
ui/src/vendor/css.js/css.js 672(+672 -0)
diff --git a/ui/src/vendor/css.js/css.js b/ui/src/vendor/css.js/css.js
new file mode 100644
index 0000000..8c4160a
--- /dev/null
+++ b/ui/src/vendor/css.js/css.js
@@ -0,0 +1,672 @@
+/* eslint-disable */
+
+/* jshint unused:false */
+/* global base64_decode, CSSWizardView, window, console, jQuery */
+var fi = function () {
+
+ this.cssImportStatements = [];
+ this.cssKeyframeStatements = [];
+
+ this.cssRegex = new RegExp('([\\s\\S]*?){([\\s\\S]*?)}', 'gi');
+ this.cssMediaQueryRegex = '((@media [\\s\\S]*?){([\\s\\S]*?}\\s*?)})';
+ this.cssKeyframeRegex = '((@.*?keyframes [\\s\\S]*?){([\\s\\S]*?}\\s*?)})';
+ this.combinedCSSRegex = '((\\s*?@media[\\s\\S]*?){([\\s\\S]*?)}\\s*?})|(([\\s\\S]*?){([\\s\\S]*?)})'; //to match css & media queries together
+ this.cssCommentsRegex = '(\\/\\*[\\s\\S]*?\\*\\/)';
+ this.cssImportStatementRegex = new RegExp('@import .*?;', 'gi');
+};
+
+/*
+ Strip outs css comments and returns cleaned css string
+
+ @param css, the original css string to be stipped out of comments
+
+ @return cleanedCSS contains no css comments
+ */
+fi.prototype.stripComments = function (cssString) {
+ var regex = new RegExp(this.cssCommentsRegex, 'gi');
+
+ return cssString.replace(regex, '');
+};
+
+/*
+ Parses given css string, and returns css object
+ keys as selectors and values are css rules
+ eliminates all css comments before parsing
+
+ @param source css string to be parsed
+
+ @return object css
+ */
+fi.prototype.parseCSS = function (source) {
+
+ if (source === undefined) {
+ return [];
+ }
+
+ var css = [];
+ //strip out comments
+ //source = this.stripComments(source);
+
+ //get import statements
+
+ while (true) {
+ var imports = this.cssImportStatementRegex.exec(source);
+ if (imports !== null) {
+ this.cssImportStatements.push(imports[0]);
+ css.push({
+ selector: '@imports',
+ type: 'imports',
+ styles: imports[0]
+ });
+ } else {
+ break;
+ }
+ }
+ source = source.replace(this.cssImportStatementRegex, '');
+ //get keyframe statements
+ var keyframesRegex = new RegExp(this.cssKeyframeRegex, 'gi');
+ var arr;
+ while (true) {
+ arr = keyframesRegex.exec(source);
+ if (arr === null) {
+ break;
+ }
+ css.push({
+ selector: '@keyframes',
+ type: 'keyframes',
+ styles: arr[0]
+ });
+ }
+ source = source.replace(keyframesRegex, '');
+
+ //unified regex
+ var unified = new RegExp(this.combinedCSSRegex, 'gi');
+
+ while (true) {
+ arr = unified.exec(source);
+ if (arr === null) {
+ break;
+ }
+ var selector = '';
+ if (arr[2] === undefined) {
+ selector = arr[5].split('\r\n').join('\n').trim();
+ } else {
+ selector = arr[2].split('\r\n').join('\n').trim();
+ }
+
+ /*
+ fetch comments and associate it with current selector
+ */
+ var commentsRegex = new RegExp(this.cssCommentsRegex, 'gi');
+ var comments = commentsRegex.exec(selector);
+ if (comments !== null) {
+ selector = selector.replace(commentsRegex, '').trim();
+ }
+
+ //determine the type
+ if (selector.indexOf('@media') !== -1) {
+ //we have a media query
+ var cssObject = {
+ selector: selector,
+ type: 'media',
+ subStyles: this.parseCSS(arr[3] + '\n}') //recursively parse media query inner css
+ };
+ if (comments !== null) {
+ cssObject.comments = comments[0];
+ }
+ css.push(cssObject);
+ } else {
+ //we have standart css
+ var rules = this.parseRules(arr[6]);
+ var style = {
+ selector: selector,
+ rules: rules
+ };
+ if (selector === '@font-face') {
+ style.type = 'font-face';
+ }
+ if (comments !== null) {
+ style.comments = comments[0];
+ }
+ css.push(style);
+ }
+ }
+
+ return css;
+};
+
+/*
+ parses given string containing css directives
+ and returns an array of objects containing ruleName:ruleValue pairs
+
+ @param rules, css directive string example
+ \n\ncolor:white;\n font-size:18px;\n
+ */
+fi.prototype.parseRules = function (rules) {
+ //convert all windows style line endings to unix style line endings
+ rules = rules.split('\r\n').join('\n');
+ var ret = [];
+
+ rules = rules.split(';');
+
+ //proccess rules line by line
+ for (var i = 0; i < rules.length; i++) {
+ var line = rules[i];
+
+ //determine if line is a valid css directive, ie color:white;
+ line = line.trim();
+ if (line.indexOf(':') !== -1) {
+ //line contains :
+ line = line.split(':');
+ var cssDirective = line[0].trim();
+ var cssValue = line.slice(1).join(':').trim();
+
+ //more checks
+ if (cssDirective.length < 1 || cssValue.length < 1) {
+ continue; //there is no css directive or value that is of length 1 or 0
+ // PLAIN WRONG WHAT ABOUT margin:0; ?
+ }
+
+ //push rule
+ ret.push({
+ directive: cssDirective,
+ value: cssValue
+ });
+ } else {
+ //if there is no ':', but what if it was mis splitted value which starts with base64
+ if (line.trim().substr(0, 7) == 'base64,') { //hack :)
+ ret[ret.length - 1].value += line.trim();
+ } else {
+ //add rule, even if it is defective
+ if (line.length > 0) {
+ ret.push({
+ directive: '',
+ value: line,
+ defective: true
+ });
+ }
+ }
+ }
+ }
+
+ return ret; //we are done!
+};
+/*
+ just returns the rule having given directive
+ if not found returns false;
+ */
+fi.prototype.findCorrespondingRule = function (rules, directive, value) {
+ if (value === undefined) {
+ value = false;
+ }
+ var ret = false;
+ for (var i = 0; i < rules.length; i++) {
+ if (rules[i].directive == directive) {
+ ret = rules[i];
+ if (value === rules[i].value) {
+ break;
+ }
+ }
+ }
+ return ret;
+};
+
+/*
+ Finds styles that have given selector, compress them,
+ and returns them
+ */
+fi.prototype.findBySelector = function (cssObjectArray, selector, contains) {
+ if (contains === undefined) {
+ contains = false;
+ }
+
+ var found = [];
+ for (var i = 0; i < cssObjectArray.length; i++) {
+ if (contains === false) {
+ if (cssObjectArray[i].selector === selector) {
+ found.push(cssObjectArray[i]);
+ }
+ } else {
+ if (cssObjectArray[i].selector.indexOf(selector) !== -1) {
+ found.push(cssObjectArray[i]);
+ }
+ }
+
+ }
+ if (found.length < 2) {
+ return found;
+ } else {
+ var base = found[0];
+ for (i = 1; i < found.length; i++) {
+ this.intelligentCSSPush([base], found[i]);
+ }
+ return [base]; //we are done!! all properties merged into base!
+ }
+};
+
+/*
+ deletes cssObjects having given selector, and returns new array
+ */
+fi.prototype.deleteBySelector = function (cssObjectArray, selector) {
+ var ret = [];
+ for (var i = 0; i < cssObjectArray.length; i++) {
+ if (cssObjectArray[i].selector !== selector) {
+ ret.push(cssObjectArray[i]);
+ }
+ }
+ return ret;
+};
+
+/*
+ Compresses given cssObjectArray and tries to minimize
+ selector redundence.
+ */
+fi.prototype.compressCSS = function (cssObjectArray) {
+ var compressed = [];
+ var done = {};
+ for (var i = 0; i < cssObjectArray.length; i++) {
+ var obj = cssObjectArray[i];
+ if (done[obj.selector] === true) {
+ continue;
+ }
+
+ var found = this.findBySelector(cssObjectArray, obj.selector); //found compressed
+ if (found.length !== 0) {
+ compressed.push(found[0]);
+ done[obj.selector] = true;
+ }
+ }
+ return compressed;
+};
+
+/*
+ Received 2 css objects with following structure
+ {
+ rules : [{directive:"", value:""}, {directive:"", value:""}, ...]
+ selector : "SOMESELECTOR"
+ }
+
+ returns the changed(new,removed,updated) values on css1 parameter, on same structure
+
+ if two css objects are the same, then returns false
+
+ if a css directive exists in css1 and css2, and its value is different, it is included in diff
+ if a css directive exists in css1 and not css2, it is then included in diff
+ if a css directive exists in css2 but not css1, then it is deleted in css1, it would be included in diff but will be marked as type='DELETED'
+
+ @object css1 css object
+ @object css2 css object
+
+ @return diff css object contains changed values in css1 in regards to css2 see test input output in /test/data/css.js
+ */
+fi.prototype.cssDiff = function (css1, css2) {
+ if (css1.selector !== css2.selector) {
+ return false;
+ }
+
+ //if one of them is media query return false, because diff function can not operate on media queries
+ if ((css1.type === 'media' || css2.type === 'media')) {
+ return false;
+ }
+
+ var diff = {
+ selector: css1.selector,
+ rules: []
+ };
+ var rule1, rule2;
+ for (var i = 0; i < css1.rules.length; i++) {
+ rule1 = css1.rules[i];
+ //find rule2 which has the same directive as rule1
+ rule2 = this.findCorrespondingRule(css2.rules, rule1.directive, rule1.value);
+ if (rule2 === false) {
+ //rule1 is a new rule in css1
+ diff.rules.push(rule1);
+ } else {
+ //rule2 was found only push if its value is different too
+ if (rule1.value !== rule2.value) {
+ diff.rules.push(rule1);
+ }
+ }
+ }
+
+ //now for rules exists in css2 but not in css1, which means deleted rules
+ for (var ii = 0; ii < css2.rules.length; ii++) {
+ rule2 = css2.rules[ii];
+ //find rule2 which has the same directive as rule1
+ rule1 = this.findCorrespondingRule(css1.rules, rule2.directive);
+ if (rule1 === false) {
+ //rule1 is a new rule
+ rule2.type = 'DELETED'; //mark it as a deleted rule, so that other merge operations could be true
+ diff.rules.push(rule2);
+ }
+ }
+
+
+ if (diff.rules.length === 0) {
+ return false;
+ }
+ return diff;
+};
+
+/*
+ Merges 2 different css objects together
+ using intelligentCSSPush,
+
+ @param cssObjectArray, target css object array
+ @param newArray, source array that will be pushed into cssObjectArray parameter
+ @param reverse, [optional], if given true, first parameter will be traversed on reversed order
+ effectively giving priority to the styles in newArray
+ */
+fi.prototype.intelligentMerge = function (cssObjectArray, newArray, reverse) {
+ if (reverse === undefined) {
+ reverse = false;
+ }
+
+
+ for (var i = 0; i < newArray.length; i++) {
+ this.intelligentCSSPush(cssObjectArray, newArray[i], reverse);
+ }
+ for (i = 0; i < cssObjectArray.length; i++) {
+ var cobj = cssObjectArray[i];
+ if (cobj.type === 'media' || (cobj.type === 'keyframes')) {
+ continue;
+ }
+ cobj.rules = this.compactRules(cobj.rules);
+ }
+};
+
+/*
+ inserts new css objects into a bigger css object
+ with same selectors groupped together
+
+ @param cssObjectArray, array of bigger css object to be pushed into
+ @param minimalObject, single css object
+ @param reverse [optional] default is false, if given, cssObjectArray will be reversly traversed
+ resulting more priority in minimalObject's styles
+ */
+fi.prototype.intelligentCSSPush = function (cssObjectArray, minimalObject, reverse) {
+ var pushSelector = minimalObject.selector;
+ //find correct selector if not found just push minimalObject into cssObject
+ var cssObject = false;
+
+ if (reverse === undefined) {
+ reverse = false;
+ }
+
+ if (reverse === false) {
+ for (var i = 0; i < cssObjectArray.length; i++) {
+ if (cssObjectArray[i].selector === minimalObject.selector) {
+ cssObject = cssObjectArray[i];
+ break;
+ }
+ }
+ } else {
+ for (var j = cssObjectArray.length - 1; j > -1; j--) {
+ if (cssObjectArray[j].selector === minimalObject.selector) {
+ cssObject = cssObjectArray[j];
+ break;
+ }
+ }
+ }
+
+ if (cssObject === false) {
+ cssObjectArray.push(minimalObject); //just push, because cssSelector is new
+ } else {
+ if (minimalObject.type !== 'media') {
+ for (var ii = 0; ii < minimalObject.rules.length; ii++) {
+ var rule = minimalObject.rules[ii];
+ //find rule inside cssObject
+ var oldRule = this.findCorrespondingRule(cssObject.rules, rule.directive);
+ if (oldRule === false) {
+ cssObject.rules.push(rule);
+ } else if (rule.type == 'DELETED') {
+ oldRule.type = 'DELETED';
+ } else {
+ //rule found just update value
+
+ oldRule.value = rule.value;
+ }
+ }
+ } else {
+ cssObject.subStyles = minimalObject.subStyles; //TODO, make this intelligent too
+ }
+
+ }
+};
+
+/*
+ filter outs rule objects whose type param equal to DELETED
+
+ @param rules, array of rules
+
+ @returns rules array, compacted by deleting all unneccessary rules
+ */
+fi.prototype.compactRules = function (rules) {
+ var newRules = [];
+ for (var i = 0; i < rules.length; i++) {
+ if (rules[i].type !== 'DELETED') {
+ newRules.push(rules[i]);
+ }
+ }
+ return newRules;
+};
+/*
+ computes string for ace editor using this.css or given cssBase optional parameter
+
+ @param [optional] cssBase, if given computes cssString from cssObject array
+ */
+fi.prototype.getCSSForEditor = function (cssBase, depth) {
+ if (depth === undefined) {
+ depth = 0;
+ }
+ var ret = '';
+ if (cssBase === undefined) {
+ cssBase = this.css;
+ }
+ //append imports
+ for (var i = 0; i < cssBase.length; i++) {
+ if (cssBase[i].type == 'imports') {
+ ret += cssBase[i].styles + '\n\n';
+ }
+ }
+ for (i = 0; i < cssBase.length; i++) {
+ var tmp = cssBase[i];
+ if (tmp.selector === undefined) { //temporarily omit media queries
+ continue;
+ }
+ var comments = "";
+ if (tmp.comments !== undefined) {
+ comments = tmp.comments + '\n';
+ }
+
+ if (tmp.type == 'media') { //also put media queries to output
+ ret += comments + tmp.selector + '{\n';
+ ret += this.getCSSForEditor(tmp.subStyles, depth + 1);
+ ret += '}\n\n';
+ } else if (tmp.type !== 'keyframes' && tmp.type !== 'imports') {
+ ret += this.getSpaces(depth) + comments + tmp.selector + ' {\n';
+ ret += this.getCSSOfRules(tmp.rules, depth + 1);
+ ret += this.getSpaces(depth) + '}\n\n';
+ }
+ }
+
+ //append keyFrames
+ for (i = 0; i < cssBase.length; i++) {
+ if (cssBase[i].type == 'keyframes') {
+ ret += cssBase[i].styles + '\n\n';
+ }
+ }
+
+ return ret;
+};
+
+fi.prototype.getImports = function (cssObjectArray) {
+ var imps = [];
+ for (var i = 0; i < cssObjectArray.length; i++) {
+ if (cssObjectArray[i].type == 'imports') {
+ imps.push(cssObjectArray[i].styles);
+ }
+ }
+ return imps;
+};
+/*
+ given rules array, returns visually formatted css string
+ to be used inside editor
+ */
+fi.prototype.getCSSOfRules = function (rules, depth) {
+ var ret = '';
+ for (var i = 0; i < rules.length; i++) {
+ if (rules[i] === undefined) {
+ continue;
+ }
+ if (rules[i].defective === undefined) {
+ ret += this.getSpaces(depth) + rules[i].directive + ' : ' + rules[i].value + ';\n';
+ } else {
+ ret += this.getSpaces(depth) + rules[i].value + ';\n';
+ }
+
+ }
+ return ret || '\n';
+};
+
+/*
+ A very simple helper function returns number of spaces appended in a single string,
+ the number depends input parameter, namely input*2
+ */
+fi.prototype.getSpaces = function (num) {
+ var ret = '';
+ for (var i = 0; i < num * 4; i++) {
+ ret += ' ';
+ }
+ return ret;
+};
+
+/*
+ Given css string or objectArray, parses it and then for every selector,
+ prepends this.cssPreviewNamespace to prevent css collision issues
+
+ @returns css string in which this.cssPreviewNamespace prepended
+ */
+fi.prototype.applyNamespacing = function (css, forcedNamespace) {
+ var cssObjectArray = css;
+ var namespaceClass = '.' + this.cssPreviewNamespace;
+ if (forcedNamespace !== undefined) {
+ namespaceClass = forcedNamespace;
+ }
+
+ if (typeof css === 'string') {
+ cssObjectArray = this.parseCSS(css);
+ }
+
+ for (var i = 0; i < cssObjectArray.length; i++) {
+ var obj = cssObjectArray[i];
+
+ //bypass namespacing for @font-face @keyframes @import
+ if (obj.selector.indexOf('@font-face') > -1 || obj.selector.indexOf('keyframes') > -1 || obj.selector.indexOf('@import') > -1 || obj.selector.indexOf('.form-all') > -1 || obj.selector.indexOf('#stage') > -1) {
+ continue;
+ }
+
+ if (obj.type !== 'media') {
+ var selector = obj.selector.split(',');
+ var newSelector = [];
+ for (var j = 0; j < selector.length; j++) {
+ if (selector[j].indexOf('.supernova') === -1) { //do not apply namespacing to selectors including supernova
+ newSelector.push(namespaceClass + ' ' + selector[j]);
+ } else {
+ newSelector.push(selector[j]);
+ }
+ }
+ obj.selector = newSelector.join(',');
+ } else {
+ obj.subStyles = this.applyNamespacing(obj.subStyles, forcedNamespace); //handle media queries as well
+ }
+ }
+
+ return cssObjectArray;
+};
+
+/*
+ given css string or object array, clears possible namespacing from
+ all of the selectors inside the css
+ */
+fi.prototype.clearNamespacing = function (css, returnObj) {
+ if (returnObj === undefined) {
+ returnObj = false;
+ }
+ var cssObjectArray = css;
+ var namespaceClass = '.' + this.cssPreviewNamespace;
+ if (typeof css === 'string') {
+ cssObjectArray = this.parseCSS(css);
+ }
+
+ for (var i = 0; i < cssObjectArray.length; i++) {
+ var obj = cssObjectArray[i];
+
+ if (obj.type !== 'media') {
+ var selector = obj.selector.split(',');
+ var newSelector = [];
+ for (var j = 0; j < selector.length; j++) {
+ newSelector.push(selector[j].split(namespaceClass + ' ').join(''));
+ }
+ obj.selector = newSelector.join(',');
+ } else {
+ obj.subStyles = this.clearNamespacing(obj.subStyles, true); //handle media queries as well
+ }
+ }
+ if (returnObj === false) {
+ return this.getCSSForEditor(cssObjectArray);
+ } else {
+ return cssObjectArray;
+ }
+
+};
+
+/*
+ creates a new style tag (also destroys the previous one)
+ and injects given css string into that css tag
+ */
+fi.prototype.createStyleElement = function (id, css, format) {
+ if (format === undefined) {
+ format = false;
+ }
+
+ if (this.testMode === false && format !== 'nonamespace') {
+ //apply namespacing classes
+ css = this.applyNamespacing(css);
+ }
+
+ if (typeof css != 'string') {
+ css = this.getCSSForEditor(css);
+ }
+ //apply formatting for css
+ if (format === true) {
+ css = this.getCSSForEditor(this.parseCSS(css));
+ }
+
+ if (this.testMode !== false) {
+ return this.testMode('create style #' + id, css); //if test mode, just pass result to callback
+ }
+
+ var __el = document.getElementById(id);
+ if (__el) {
+ __el.parentNode.removeChild(__el);
+ }
+
+ var head = document.head || document.getElementsByTagName('head')[0],
+ style = document.createElement('style');
+
+ style.id = id;
+ style.type = 'text/css';
+
+ head.appendChild(style);
+
+ if (style.styleSheet && !style.sheet) {
+ style.styleSheet.cssText = css;
+ } else {
+ style.appendChild(document.createTextNode(css));
+ }
+};
+
+export default fi;
+
+/* eslint-enable */
\ No newline at end of file
ui/src/vendor/css.js/css.min.js 2(+2 -0)
diff --git a/ui/src/vendor/css.js/css.min.js b/ui/src/vendor/css.js/css.min.js
new file mode 100644
index 0000000..b0e02da
--- /dev/null
+++ b/ui/src/vendor/css.js/css.min.js
@@ -0,0 +1,2 @@
+/*! css.js 19-04-2016 */
+!function(a){"use strict";var b=function(){this.cssImportStatements=[],this.cssKeyframeStatements=[],this.cssRegex=new RegExp("([\\s\\S]*?){([\\s\\S]*?)}","gi"),this.cssMediaQueryRegex="((@media [\\s\\S]*?){([\\s\\S]*?}\\s*?)})",this.cssKeyframeRegex="((@.*?keyframes [\\s\\S]*?){([\\s\\S]*?}\\s*?)})",this.combinedCSSRegex="((\\s*?(?:\\/\\*[\\s\\S]*?\\*\\/)?\\s*?@media[\\s\\S]*?){([\\s\\S]*?)}\\s*?})|(([\\s\\S]*?){([\\s\\S]*?)})",this.cssCommentsRegex="(\\/\\*[\\s\\S]*?\\*\\/)",this.cssImportStatementRegex=new RegExp("@import .*?;","gi")};b.prototype.stripComments=function(a){var b=new RegExp(this.cssCommentsRegex,"gi");return a.replace(b,"")},b.prototype.parseCSS=function(a){if(void 0===a)return[];for(var b=[];;){var c=this.cssImportStatementRegex.exec(a);if(null===c)break;this.cssImportStatements.push(c[0]),b.push({selector:"@imports",type:"imports",styles:c[0]})}a=a.replace(this.cssImportStatementRegex,"");for(var d,e=new RegExp(this.cssKeyframeRegex,"gi");;){if(d=e.exec(a),null===d)break;b.push({selector:"@keyframes",type:"keyframes",styles:d[0]})}a=a.replace(e,"");for(var f=new RegExp(this.combinedCSSRegex,"gi");;){if(d=f.exec(a),null===d)break;var g="";g=void 0===d[2]?d[5].split("\r\n").join("\n").trim():d[2].split("\r\n").join("\n").trim();var h=new RegExp(this.cssCommentsRegex,"gi"),i=h.exec(g);if(null!==i&&(g=g.replace(h,"").trim()),g=g.replace(/\n+/,"\n"),-1!==g.indexOf("@media")){var j={selector:g,type:"media",subStyles:this.parseCSS(d[3]+"\n}")};null!==i&&(j.comments=i[0]),b.push(j)}else{var k=this.parseRules(d[6]),l={selector:g,rules:k};"@font-face"===g&&(l.type="font-face"),null!==i&&(l.comments=i[0]),b.push(l)}}return b},b.prototype.parseRules=function(a){a=a.split("\r\n").join("\n");var b=[];a=a.split(";");for(var c=0;c<a.length;c++){var d=a[c];if(d=d.trim(),-1!==d.indexOf(":")){d=d.split(":");var e=d[0].trim(),f=d.slice(1).join(":").trim();if(e.length<1||f.length<1)continue;b.push({directive:e,value:f})}else"base64,"===d.trim().substr(0,7)?b[b.length-1].value+=d.trim():d.length>0&&b.push({directive:"",value:d,defective:!0})}return b},b.prototype.findCorrespondingRule=function(a,b,c){void 0===c&&(c=!1);for(var d=!1,e=0;e<a.length&&(a[e].directive!==b||(d=a[e],c!==a[e].value));e++);return d},b.prototype.findBySelector=function(a,b,c){void 0===c&&(c=!1);for(var d=[],e=0;e<a.length;e++)c===!1?a[e].selector===b&&d.push(a[e]):-1!==a[e].selector.indexOf(b)&&d.push(a[e]);if(d.length<2)return d;var f=d[0];for(e=1;e<d.length;e++)this.intelligentCSSPush([f],d[e]);return[f]},b.prototype.deleteBySelector=function(a,b){for(var c=[],d=0;d<a.length;d++)a[d].selector!==b&&c.push(a[d]);return c},b.prototype.compressCSS=function(a){for(var b=[],c={},d=0;d<a.length;d++){var e=a[d];if(c[e.selector]!==!0){var f=this.findBySelector(a,e.selector);0!==f.length&&(b.push(f[0]),c[e.selector]=!0)}}return b},b.prototype.cssDiff=function(a,b){if(a.selector!==b.selector)return!1;if("media"===a.type||"media"===b.type)return!1;for(var c,d,e={selector:a.selector,rules:[]},f=0;f<a.rules.length;f++)c=a.rules[f],d=this.findCorrespondingRule(b.rules,c.directive,c.value),d===!1?e.rules.push(c):c.value!==d.value&&e.rules.push(c);for(var g=0;g<b.rules.length;g++)d=b.rules[g],c=this.findCorrespondingRule(a.rules,d.directive),c===!1&&(d.type="DELETED",e.rules.push(d));return 0===e.rules.length?!1:e},b.prototype.intelligentMerge=function(a,b,c){void 0===c&&(c=!1);for(var d=0;d<b.length;d++)this.intelligentCSSPush(a,b[d],c);for(d=0;d<a.length;d++){var e=a[d];"media"!==e.type&&"keyframes"!==e.type&&(e.rules=this.compactRules(e.rules))}},b.prototype.intelligentCSSPush=function(a,b,c){var d=(b.selector,!1);if(void 0===c&&(c=!1),c===!1){for(var e=0;e<a.length;e++)if(a[e].selector===b.selector){d=a[e];break}}else for(var f=a.length-1;f>-1;f--)if(a[f].selector===b.selector){d=a[f];break}if(d===!1)a.push(b);else if("media"!==b.type)for(var g=0;g<b.rules.length;g++){var h=b.rules[g],i=this.findCorrespondingRule(d.rules,h.directive);i===!1?d.rules.push(h):"DELETED"===h.type?i.type="DELETED":i.value=h.value}else d.subStyles=d.subStyles.concat(b.subStyles)},b.prototype.compactRules=function(a){for(var b=[],c=0;c<a.length;c++)"DELETED"!==a[c].type&&b.push(a[c]);return b},b.prototype.getCSSForEditor=function(a,b){void 0===b&&(b=0);var c="";void 0===a&&(a=this.css);for(var d=0;d<a.length;d++)"imports"===a[d].type&&(c+=a[d].styles+"\n\n");for(d=0;d<a.length;d++){var e=a[d];if(void 0!==e.selector){var f="";void 0!==e.comments&&(f=e.comments+"\n"),"media"===e.type?(c+=f+e.selector+"{\n",c+=this.getCSSForEditor(e.subStyles,b+1),c+="}\n\n"):"keyframes"!==e.type&&"imports"!==e.type&&(c+=this.getSpaces(b)+f+e.selector+" {\n",c+=this.getCSSOfRules(e.rules,b+1),c+=this.getSpaces(b)+"}\n\n")}}for(d=0;d<a.length;d++)"keyframes"===a[d].type&&(c+=a[d].styles+"\n\n");return c},b.prototype.getImports=function(a){for(var b=[],c=0;c<a.length;c++)"imports"===a[c].type&&b.push(a[c].styles);return b},b.prototype.getCSSOfRules=function(a,b){for(var c="",d=0;d<a.length;d++)void 0!==a[d]&&(c+=void 0===a[d].defective?this.getSpaces(b)+a[d].directive+": "+a[d].value+";\n":this.getSpaces(b)+a[d].value+";\n");return c||"\n"},b.prototype.getSpaces=function(a){for(var b="",c=0;4*a>c;c++)b+=" ";return b},b.prototype.applyNamespacing=function(a,b){var c=a,d="."+this.cssPreviewNamespace;void 0!==b&&(d=b),"string"==typeof a&&(c=this.parseCSS(a));for(var e=0;e<c.length;e++){var f=c[e];if(!(f.selector.indexOf("@font-face")>-1||f.selector.indexOf("keyframes")>-1||f.selector.indexOf("@import")>-1||f.selector.indexOf(".form-all")>-1||f.selector.indexOf("#stage")>-1))if("media"!==f.type){for(var g=f.selector.split(","),h=[],i=0;i<g.length;i++)-1===g[i].indexOf(".supernova")?h.push(d+" "+g[i]):h.push(g[i]);f.selector=h.join(",")}else f.subStyles=this.applyNamespacing(f.subStyles,b)}return c},b.prototype.clearNamespacing=function(a,b){void 0===b&&(b=!1);var c=a,d="."+this.cssPreviewNamespace;"string"==typeof a&&(c=this.parseCSS(a));for(var e=0;e<c.length;e++){var f=c[e];if("media"!==f.type){for(var g=f.selector.split(","),h=[],i=0;i<g.length;i++)h.push(g[i].split(d+" ").join(""));f.selector=h.join(",")}else f.subStyles=this.clearNamespacing(f.subStyles,!0)}return b===!1?this.getCSSForEditor(c):c},b.prototype.createStyleElement=function(a,b,c){if(void 0===c&&(c=!1),this.testMode===!1&&"nonamespace"!==c&&(b=this.applyNamespacing(b)),"string"!=typeof b&&(b=this.getCSSForEditor(b)),c===!0&&(b=this.getCSSForEditor(this.parseCSS(b))),this.testMode!==!1)return this.testMode("create style #"+a,b);var d=document.getElementById(a);d&&d.parentNode.removeChild(d);var e=document.head||document.getElementsByTagName("head")[0],f=document.createElement("style");f.id=a,f.type="text/css",e.appendChild(f),f.styleSheet&&!f.sheet?f.styleSheet.cssText=b:f.appendChild(document.createTextNode(b))},a.cssjs=b}(this);
\ No newline at end of file
ui/webpack.config.dev.js 129(+129 -0)
diff --git a/ui/webpack.config.dev.js b/ui/webpack.config.dev.js
new file mode 100644
index 0000000..a114c29
--- /dev/null
+++ b/ui/webpack.config.dev.js
@@ -0,0 +1,129 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable */
+
+const HtmlWebpackPlugin = require('html-webpack-plugin');
+const ExtractTextPlugin = require('extract-text-webpack-plugin');
+const CopyWebpackPlugin = require('copy-webpack-plugin');
+const webpack = require('webpack');
+const path = require('path');
+
+/* devtool: 'cheap-module-eval-source-map', */
+
+module.exports = {
+ devtool: 'source-map',
+ entry: [
+ './src/app/app.js',
+ 'webpack-hot-middleware/client?reload=true',
+ ],
+ output: {
+ path: path.resolve(__dirname, 'target/generated-resources/public/static'),
+ publicPath: '/',
+ filename: 'bundle.js',
+ },
+ plugins: [
+ new webpack.ProvidePlugin({
+ $: "jquery",
+ jQuery: "jquery",
+ "window.jQuery": "jquery",
+ tinycolor: "tinycolor2",
+ tv4: "tv4",
+ moment: "moment"
+ }),
+ new CopyWebpackPlugin([
+ { from: './src/locale', to: 'locale' },
+ { from: './src/thingsboard.ico', to: 'thingsboard.ico' }
+ ]),
+ new webpack.HotModuleReplacementPlugin(),
+ new HtmlWebpackPlugin({
+ template: './src/index.html',
+ filename: 'index.html',
+ title: 'Thingsboard',
+ inject: 'body',
+ }),
+ new webpack.optimize.OccurrenceOrderPlugin(),
+ new webpack.NoErrorsPlugin(),
+ new ExtractTextPlugin('style.[contentHash].css', {
+ allChunks: true,
+ }),
+ new webpack.DefinePlugin({
+ '__DEVTOOLS__': false,
+ 'process.env': {
+ NODE_ENV: JSON.stringify('development'),
+ },
+ }),
+ ],
+ node: {
+ tls: "empty",
+ fs: "empty"
+ },
+ module: {
+ loaders: [
+ {
+ test: /\.jsx$/,
+ loader: 'babel',
+ exclude: /node_modules/,
+ include: __dirname,
+ },
+ {
+ test: /\.js$/,
+ loaders: ['ng-annotate', 'babel'],
+ exclude: /node_modules/,
+ include: __dirname,
+ },
+ {
+ test: /\.js$/,
+ loader: "eslint-loader?{parser: 'babel-eslint'}",
+ exclude: /node_modules|vendor/,
+ include: __dirname,
+ },
+ {
+ test: /\.css$/,
+ loader: ExtractTextPlugin.extract('style-loader', 'css-loader'),
+ },
+ {
+ test: /\.scss$/,
+ loader: ExtractTextPlugin.extract('style-loader', 'css-loader!postcss-loader!sass-loader'),
+ },
+ {
+ test: /\.less$/,
+ loader: ExtractTextPlugin.extract('style-loader', 'css-loader!postcss-loader!less-loader'),
+ },
+ {
+ test: /\.tpl\.html$/,
+ loader: 'ngtemplate?relativeTo=' + (path.resolve(__dirname, './src/app')) + '/!html!html-minifier-loader'
+ },
+ {
+ test: /\.(svg)(\?v=[0-9]+\.[0-9]+\.[0-9]+)?$/,
+ loader: 'url?limit=8192'
+ },
+ {
+ test: /\.(png|jpe?g|gif|woff|woff2|ttf|otf|eot|ico)(\?v=[0-9]+\.[0-9]+\.[0-9]+)?$/,
+ loaders: [
+ 'url?limit=8192',
+ 'img?minimize'
+ ]
+ },
+ ],
+ },
+ 'html-minifier-loader': {
+ caseSensitive: true,
+ removeComments: true,
+ collapseWhitespace: false,
+ preventAttributesEscaping: true,
+ removeEmptyAttributes: false
+ }
+};
ui/webpack.config.js 22(+22 -0)
diff --git a/ui/webpack.config.js b/ui/webpack.config.js
new file mode 100644
index 0000000..9b4a5cd
--- /dev/null
+++ b/ui/webpack.config.js
@@ -0,0 +1,22 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable */
+
+if (process.env.NODE_ENV === 'production') {
+ module.exports = require('./webpack.config.prod');
+} else {
+ module.exports = require('./webpack.config.dev');
+}
ui/webpack.config.prod.js 125(+125 -0)
diff --git a/ui/webpack.config.prod.js b/ui/webpack.config.prod.js
new file mode 100644
index 0000000..7d6fce1
--- /dev/null
+++ b/ui/webpack.config.prod.js
@@ -0,0 +1,125 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* eslint-disable */
+
+const HtmlWebpackPlugin = require('html-webpack-plugin');
+const ExtractTextPlugin = require('extract-text-webpack-plugin');
+const CopyWebpackPlugin = require('copy-webpack-plugin');
+const webpack = require('webpack');
+const path = require('path');
+
+module.exports = {
+ devtool: 'source-map',
+ entry: ['./src/app/app.js'],
+ output: {
+ path: path.resolve(__dirname, 'target/generated-resources/public/static'),
+ publicPath: '/static/',
+ filename: 'bundle.[hash].js',
+ },
+ plugins: [
+ new webpack.ProvidePlugin({
+ $: "jquery",
+ jQuery: "jquery",
+ "window.jQuery": "jquery",
+ tinycolor: "tinycolor2",
+ tv4: "tv4",
+ moment: "moment"
+ }),
+ new CopyWebpackPlugin([
+ {from: './src/locale', to: 'locale'},
+ {from: './src/thingsboard.ico', to: 'thingsboard.ico'}
+ ]),
+ new HtmlWebpackPlugin({
+ template: './src/index.html',
+ filename: '../index.html',
+ title: 'Thingsboard',
+ inject: 'body',
+ }),
+ new webpack.optimize.OccurrenceOrderPlugin(),
+ new webpack.NoErrorsPlugin(),
+ new webpack.optimize.DedupePlugin(),
+ new webpack.optimize.CommonsChunkPlugin('vendor', 'vendor.[hash].js'),
+ new ExtractTextPlugin('style.[contentHash].css', {
+ allChunks: true,
+ }),
+ new webpack.DefinePlugin({
+ '__DEVTOOLS__': false,
+ 'process.env': {
+ NODE_ENV: JSON.stringify('production'),
+ },
+ }),
+ ],
+ node: {
+ tls: "empty",
+ fs: "empty"
+ },
+ module: {
+ loaders: [
+ {
+ test: /\.jsx$/,
+ loader: 'babel',
+ exclude: /node_modules/,
+ include: __dirname,
+ },
+ {
+ test: /\.js$/,
+ loaders: ['ng-annotate', 'babel'],
+ exclude: /node_modules/,
+ include: __dirname,
+ },
+ {
+ test: /\.js$/,
+ loader: "eslint-loader?{parser: 'babel-eslint'}",
+ exclude: /node_modules|vendor/,
+ include: __dirname,
+ },
+ {
+ test: /\.css$/,
+ loader: ExtractTextPlugin.extract('style-loader', 'css-loader'),
+ },
+ {
+ test: /\.scss$/,
+ loader: ExtractTextPlugin.extract('style-loader', 'css-loader!postcss-loader!sass-loader'),
+ },
+ {
+ test: /\.less$/,
+ loader: ExtractTextPlugin.extract('style-loader', 'css-loader!postcss-loader!less-loader'),
+ },
+ {
+ test: /\.tpl\.html$/,
+ loader: 'ngtemplate?relativeTo=' + (path.resolve(__dirname, './src/app')) + '/!html!html-minifier-loader'
+ },
+ {
+ test: /\.(svg)(\?v=[0-9]+\.[0-9]+\.[0-9]+)?$/,
+ loader: 'url?limit=8192'
+ },
+ {
+ test: /\.(png|jpe?g|gif|woff|woff2|ttf|otf|eot|ico)(\?v=[0-9]+\.[0-9]+\.[0-9]+)?$/,
+ loaders: [
+ 'url?limit=8192',
+ 'img?minimize'
+ ]
+ },
+ ],
+ },
+ 'html-minifier-loader': {
+ caseSensitive: true,
+ removeComments: true,
+ collapseWhitespace: false,
+ preventAttributesEscaping: true,
+ removeEmptyAttributes: false
+ }
+};