Merge remote-tracking branch 'origin/master' into develop/2.5

This commit is contained in:
Andrii Shvaika 2020-04-14 16:17:27 +03:00
commit 7c24b850fb
23 changed files with 97 additions and 50 deletions

View File

@ -330,13 +330,13 @@
"name": "Web Camera Input",
"descriptor": {
"type": "latest",
"sizeX": 9.5,
"sizeY": 6.5,
"sizeX": 7.5,
"sizeY": 3,
"resources": [],
"templateHtml": "<tb-web-camera-widget ctx=\"ctx\">\n</tb-web-camera-widget>",
"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}",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "{}",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Web Camera\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"imageFormat\": {\n \"title\": \"Image Format\",\n \"type\": \"string\",\n \"default\": \"image/png\"\n },\n \"imageQuality\":{\n \"title\":\"Image quality that use lossy compression such as jpeg and webp\",\n \"type\":\"number\",\n \"default\": 0.92,\n \"min\": 0,\n \"max\": 1\n },\n \"maxWidth\": {\n \"title\": \"The maximal image width\",\n \"type\": \"number\",\n \"default\": 640\n }, \n \"maxHeight\": {\n \"title\": \"The maximal image heigth\",\n \"type\": \"number\",\n \"default\": 480\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n {\n \"key\": \"imageFormat\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"image/jpeg\",\n \"label\": \"JPEG\"\n },\n {\n \"value\": \"image/png\",\n \"label\": \"PNG\"\n },\n {\n \"value\": \"image/webp\",\n \"label\": \"WEBP\"\n }\n ]\n },\n \"imageQuality\",\n \"maxWidth\",\n \"maxHeight\"\n ]\n}",
"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\":\"Web Camera Input\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
}

View File

@ -201,6 +201,7 @@ public class AuthController extends BaseController {
@ResponseBody
public JsonNode activateUser(
@RequestBody JsonNode activateRequest,
@RequestParam(required = false, defaultValue = "true") boolean sendActivationMail,
HttpServletRequest request) throws ThingsboardException {
try {
String activateToken = activateRequest.get("activateToken").asText();
@ -215,10 +216,12 @@ public class AuthController extends BaseController {
String loginUrl = String.format("%s/login", baseUrl);
String email = user.getEmail();
try {
mailService.sendAccountActivatedEmail(loginUrl, email);
} catch (Exception e) {
log.info("Unable to send account activation email [{}]", e.getMessage());
if (sendActivationMail) {
try {
mailService.sendAccountActivatedEmail(loginUrl, email);
} catch (Exception e) {
log.info("Unable to send account activation email [{}]", e.getMessage());
}
}
JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);

View File

@ -277,6 +277,7 @@ public class DefaultMailService implements MailService {
} else {
message = exception.getMessage();
}
log.warn("Unable to send mail: {}", message);
return new ThingsboardException(String.format("Unable to send mail: %s", message),
ThingsboardErrorCode.GENERAL);
}

View File

@ -435,6 +435,10 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
if (user.authority === 'CUSTOMER_USER') {
entityId.id = user.customerId;
}
} else if (entityType === types.aliasEntityType.current_tenant){
let user = userService.getCurrentUser();
entityId.entityType = types.entityType.tenant;
entityId.id = user.tenantId;
}
return entityId;
}
@ -806,6 +810,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
entityTypes.dashboard = types.entityType.dashboard;
if (useAliasEntityTypes) {
entityTypes.current_customer = types.aliasEntityType.current_customer;
entityTypes.current_tenant = types.aliasEntityType.current_tenant;
}
break;
case 'CUSTOMER_USER':

View File

@ -85,9 +85,12 @@ function LoginService($http, $q) {
return deferred.promise;
}
function activate(activateToken, password) {
function activate(activateToken, password, sendActivationMail) {
var deferred = $q.defer();
var url = '/api/noauth/activate';
if(sendActivationMail === true || sendActivationMail === false) {
url += '?sendActivationMail=' + sendActivationMail;
}
$http.post(url, {activateToken: activateToken, password: password}).then(function success(response) {
deferred.resolve(response);
}, function fail() {

View File

@ -408,7 +408,8 @@ export default angular.module('thingsboard.types', [])
}
},
aliasEntityType: {
current_customer: "CURRENT_CUSTOMER"
current_customer: "CURRENT_CUSTOMER",
current_tenant: "CURRENT_TENANT"
},
entityTypeTranslations: {
"DEVICE": {
@ -474,6 +475,10 @@ export default angular.module('thingsboard.types', [])
"CURRENT_CUSTOMER": {
type: 'entity.type-current-customer',
list: 'entity.type-current-customer'
},
"CURRENT_TENANT": {
type: 'entity.type-current-tenant',
list: 'entity.type-current-tenant'
}
},
entityField: {

View File

@ -83,9 +83,9 @@ class ThingsboardAceEditor extends React.Component {
fixAceEditor(editor);
}
onToggleFull(groupId) {
onToggleFull() {
this.setState({ isFull: !this.state.isFull });
this.props.onToggleFullscreen(groupId);
this.props.onToggleFullscreen();
this.updateAceEditorSize = true;
}
@ -140,7 +140,7 @@ class ThingsboardAceEditor extends React.Component {
<div className="title-panel">
<label>{this.props.mode}</label>
<FlatButton style={ styles.tidyButtonStyle } className="tidy-button" label={'Tidy'} onTouchTap={this.onTidy}/>
<FlatButton style={ styles.tidyButtonStyle } className="tidy-button" label={this.state.isFull ? 'Exit fullscreen' : 'Fullscreen'} onTouchTap={() => this.onToggleFull(this.props.groupId)}/>
<FlatButton style={ styles.tidyButtonStyle } className="tidy-button" label={this.state.isFull ? 'Exit fullscreen' : 'Fullscreen'} onTouchTap={this.onToggleFull}/>
</div>
<AceEditor mode={this.props.mode}
height={this.state.isFull ? "100%" : "150px"}

View File

@ -131,7 +131,7 @@ class ThingsboardArray extends React.Component {
}
let forms = this.props.form.items.map(function(form, index){
var copy = this.copyWithIndex(form, i);
return this.props.builder(copy, this.props.groupId, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.onIconClick, this.props.onToggleFullscreen, this.props.mapper, this.props.builder);
return this.props.builder(copy, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.onIconClick, this.props.onToggleFullscreen, this.props.mapper, this.props.builder);
}.bind(this));
arrays.push(
<li key={keys[i]} className="list-group-item">

View File

@ -19,7 +19,7 @@ class ThingsboardFieldSet extends React.Component {
render() {
let forms = this.props.form.items.map(function(form, index){
return this.props.builder(form, this.props.groupId, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.onIconClick, this.props.onToggleFullscreen, this.props.mapper, this.props.builder);
return this.props.builder(form, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.onIconClick, this.props.onToggleFullscreen, this.props.mapper, this.props.builder);
}.bind(this));
return (

View File

@ -40,9 +40,6 @@ class ThingsboardSchemaForm extends React.Component {
constructor(props) {
super(props);
this.state = {
groupId: null,
};
this.mapper = {
'number': ThingsboardNumber,
@ -88,15 +85,12 @@ class ThingsboardSchemaForm extends React.Component {
this.props.onIconClick(event);
}
onToggleFullscreen(groupId) {
this.setState({
groupId: groupId
});
onToggleFullscreen() {
this.props.onToggleFullscreen();
}
builder(form, groupId, model, index, onChange, onColorClick, onIconClick, onToggleFullscreen, mapper) {
builder(form, model, index, onChange, onColorClick, onIconClick, onToggleFullscreen, mapper) {
var type = form.type;
let Field = this.mapper[type];
if(!Field) {
@ -109,21 +103,21 @@ class ThingsboardSchemaForm extends React.Component {
return null;
}
}
return <Field model={model} groupId={groupId} form={form} key={index} onChange={onChange} onColorClick={onColorClick} onIconClick={onIconClick} onToggleFullscreen={onToggleFullscreen} mapper={mapper} builder={this.builder}/>
return <Field model={model} form={form} key={index} onChange={onChange} onColorClick={onColorClick} onIconClick={onIconClick} onToggleFullscreen={onToggleFullscreen} mapper={mapper} builder={this.builder}/>
}
createSchema(theForm, groupId) {
createSchema(theForm) {
let merged = utils.merge(this.props.schema, theForm, 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, groupId, this.props.model, index, this.onChange, this.onColorClick, this.onIconClick, this.onToggleFullscreen, mapper);
return this.builder(form, this.props.model, index, this.onChange, this.onColorClick, this.onIconClick, this.onToggleFullscreen, mapper);
}.bind(this));
let formClass = 'SchemaForm';
if (this.props.isFullscreen && groupId === this.state.groupId) {
if (this.props.isFullscreen) {
formClass += ' SchemaFormFullscreen';
}
@ -136,7 +130,7 @@ class ThingsboardSchemaForm extends React.Component {
if(this.props.groupInfoes&&this.props.groupInfoes.length>0){
let content=[];
for(let info of this.props.groupInfoes){
let forms = this.createSchema(this.props.form[info.formIndex], info.formIndex);
let forms = this.createSchema(this.props.form[info.formIndex]);
let item = <ThingsboardSchemaGroup key={content.length} forms={forms} info={info}></ThingsboardSchemaGroup>;
content.push(item);
}
@ -166,8 +160,8 @@ class ThingsboardSchemaGroup extends React.Component{
render() {
let theCla = "pull-right fa fa-chevron-down md-toggle-icon"+(this.state.showGroup?"":" tb-toggled")
return (<section className="md-whiteframe-z1" style={{marginTop: '10px'}}>
<div className='SchemaGroupname md-button-toggle' onClick={this.toogleGroup.bind(this)}>{this.props.info.GroupTitle}<span className={theCla}></span></div>
<div style={{padding: '20px'}} className={this.state.showGroup?"":"invisible"}>{this.props.forms}</div>
</section>);
<div className='SchemaGroupname md-button-toggle' onClick={this.toogleGroup.bind(this)}>{this.props.info.GroupTitle}<span className={theCla}></span></div>
<div style={{padding: '20px'}} className={this.state.showGroup?"":"invisible"}>{this.props.forms}</div>
</section>);
}
}

View File

@ -24,15 +24,12 @@ $input-label-float-scale: .75 !default;
.tb-fullscreen {
[name="ReactSchemaForm"] {
.SchemaForm {
display: none;
&.SchemaFormFullscreen {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: block;
> div:not(.fullscreen-form-field) {
display: none !important;

View File

@ -152,7 +152,7 @@ export default function WidgetController($scope, $state, $timeout, $window, $ocL
var entityInfo = getActiveEntityInfo();
var entityId = entityInfo ? entityInfo.entityId : null;
var entityName = entityInfo ? entityInfo.entityName : null;
var entityLabel = entityInfo && entityInfo.label ? entityInfo.label : null;
var entityLabel = entityInfo && entityInfo.entityLabel ? entityInfo.entityLabel : null;
handleWidgetAction($event, this.descriptor, entityId, entityName, null, entityLabel);
}
widgetContext.customHeaderActions.push(headerAction);

View File

@ -15,7 +15,7 @@
limitations under the License.
-->
<md-dialog aria-label="{{ 'dashboard.manage-states' | translate }}" style="width: 620px;">
<md-dialog aria-label="{{ 'dashboard.manage-states' | translate }}" style="min-width: 600px;">
<form name="theForm" ng-submit="vm.save()">
<md-toolbar>
<div class="md-toolbar-tools">
@ -72,7 +72,7 @@
</md-toolbar>
<md-table-container>
<table md-table>
<thead fix-head md-head md-order="vm.query.order" md-on-reorder="vm.onReorder">
<thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder">
<tr md-row>
<th md-column md-order-by="name"><span translate>dashboard.state-name</span></th>
<th md-column md-order-by="id"><span translate>dashboard.state-id</span></th>

View File

@ -22,13 +22,14 @@ import entitySelectTemplate from './entity-select.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function EntitySelect($compile, $templateCache, entityService) {
export default function EntitySelect($compile, $templateCache, entityService, types) {
var linker = function (scope, element, attrs, ngModelCtrl) {
var template = $templateCache.get(entitySelectTemplate);
element.html(template);
scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false;
scope.entityTypeCurrentTenant = types.aliasEntityType.current_tenant;
var entityTypes = entityService.prepareAllowedEntityTypesList(scope.allowedEntityTypes, scope.useAliasEntityTypes);
@ -48,7 +49,8 @@ export default function EntitySelect($compile, $templateCache, entityService) {
scope.updateView = function () {
if (!scope.disabled) {
var value = ngModelCtrl.$viewValue;
if (scope.model && scope.model.entityType && scope.model.entityId) {
if (scope.model && scope.model.entityType &&
(scope.model.entityId || scope.model.entityType === scope.entityTypeCurrentTenant)) {
if (!value) {
value = {};
}

View File

@ -25,11 +25,11 @@
allowed-entity-types="allowedEntityTypes"
ng-model="model.entityType">
</tb-entity-type-select>
<tb-entity-autocomplete flex ng-if="model.entityType"
<tb-entity-autocomplete flex ng-if="model.entityType && model.entityType !== entityTypeCurrentTenant"
the-form="theForm"
ng-disabled="disabled"
tb-required="tbRequired"
entity-type="model.entityType"
ng-model="model.entityId">
</tb-entity-autocomplete>
</div>
</div>

View File

@ -770,6 +770,7 @@
"list-of-rulenodes": "{ count, plural, 1 {Jeden uzel pravidla} other {Seznam # uzlů pravidel} }",
"rulenode-name-starts-with": "Uzly pravidel, jejichž název začíná '{{prefix}}'",
"type-current-customer": "Stávající zákazník",
"type-current-tenant": "Stávající tenant",
"search": "Vyhledat entity",
"selected-entities": "{ count, plural, 1 {1 entita} other {# entit} } zvoleno",
"entity-name": "Název entity",

View File

@ -811,6 +811,7 @@
"list-of-rulenodes": "{ count, plural, 1 {One rule node} other {List of # rule nodes} }",
"rulenode-name-starts-with": "Rule nodes whose names start with '{{prefix}}'",
"type-current-customer": "Current Customer",
"type-current-tenant": "Current Tenant",
"search": "Search entities",
"selected-entities": "{ count, plural, 1 {1 entity} other {# entities} } selected",
"entity-name": "Entity name",

View File

@ -809,6 +809,7 @@
"list-of-rulenodes": "{ count, plural, 1 {Одно правило} other {Список из # правил} }",
"rulenode-name-starts-with": "Правила, чьи названия начинаются с '{{prefix}}'",
"type-current-customer": "Текущий клиент",
"type-current-tenant": "Текущий владелец",
"search": "Поиск объектов",
"selected-entities": "Выбран(ы) { count, plural, 1 {1 объект} few {# объекта} other {# объектов} }",
"entity-name": "Название объекта",

View File

@ -942,6 +942,7 @@
"list-of-rulenodes": "{ count, plural, 1 {Одне правило} other {Список # правил} }",
"rulenode-name-starts-with": "Список правил, імена яких починаються '{{prefix}}'",
"type-current-customer": "Поточний клієнт",
"type-current-tenant": "Поточний власник",
"search": "Пошук сутностей",
"selected-entities": "{ count, plural, 1 {1 сутність} other {# сутності} } вибрано",
"entity-name": "Ім'я сутності",

View File

@ -20,7 +20,7 @@ import logoSvg from '../../svg/logo_title_white.svg';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function LoginController(toast, loginService, userService, types, $state/*, $rootScope, $log, $translate*/) {
export default function LoginController(toast, loginService, userService, types, $state, $stateParams/*, $rootScope, $log, $translate*/) {
var vm = this;
vm.logoSvg = logoSvg;
@ -32,6 +32,12 @@ export default function LoginController(toast, loginService, userService, types,
vm.login = login;
if ($stateParams.username && $stateParams.password) {
vm.user.name = $stateParams.username;
vm.user.password = $stateParams.password;
doLogin();
}
function doLogin() {
loginService.login(vm.user).then(function success(response) {
var token = response.data.token;

View File

@ -25,7 +25,7 @@ import createPasswordTemplate from './create-password.tpl.html';
/*@ngInject*/
export default function LoginRoutes($stateProvider) {
$stateProvider.state('login', {
url: '/login',
url: '/login?username&password',
module: 'public',
views: {
"@": {

View File

@ -52,6 +52,11 @@ function WebCameraWidgetController($element, $scope, $window, types, utils, attr
let canvas = null;
let photoCamera = null;
let dataKeyType = "";
let width = 640;
let height = 480;
const DEFAULT_IMAGE_TYPE = 'image/jpeg';
const DEFAULT_IMAGE_QUALITY = 0.92;
vm.getStream = getStream;
vm.createPhoto = createPhoto;
@ -79,6 +84,8 @@ function WebCameraWidgetController($element, $scope, $window, types, utils, attr
vm.isEntityDetected = true;
}
}
width = vm.ctx.settings.maxWidth ? vm.ctx.settings.maxWidth : 640;
height = vm.ctx.settings.maxHeight ? vm.ctx.settings.maxWidth : 480;
if (datasource.dataKeys.length) {
$scope.currentKey = datasource.dataKeys[0].name;
dataKeyType = datasource.dataKeys[0].type;
@ -93,6 +100,24 @@ function WebCameraWidgetController($element, $scope, $window, types, utils, attr
}
});
function getVideoAspectRatio() {
if (videoElement.videoWidth && videoElement.videoWidth > 0 &&
videoElement.videoHeight && videoElement.videoHeight > 0) {
return videoElement.videoWidth / videoElement.videoHeight;
}
return width / height;
}
vm.videoWidth = function() {
const videoRatio = getVideoAspectRatio();
return Math.min(width, height * videoRatio);
}
vm.videoHeight = function() {
const videoRatio = getVideoAspectRatio();
return Math.min(height, width / videoRatio);
}
function hasGetUserMedia() {
return !!($window.navigator.mediaDevices && $window.navigator.mediaDevices.getUserMedia);
}
@ -157,10 +182,12 @@ function WebCameraWidgetController($element, $scope, $window, types, utils, attr
}
function createPhoto() {
canvas.width = videoElement.videoWidth;
canvas.height = videoElement.videoHeight;
canvas.getContext('2d').drawImage(videoElement, 0, 0);
vm.previewPhoto = canvas.toDataURL('image/png');
canvas.width = vm.videoWidth();
canvas.height = vm.videoHeight();
canvas.getContext('2d').drawImage(videoElement, 0, 0, vm.videoWidth(), vm.videoHeight());
const mimeType = vm.ctx.settings.imageFormat ? vm.ctx.settings.imageFormat : DEFAULT_IMAGE_TYPE;
const quality = vm.ctx.settings.imageQuality ? vm.ctx.settings.imageQuality : DEFAULT_IMAGE_QUALITY;
vm.previewPhoto = canvas.toDataURL(mimeType, quality);
vm.isPreviewPhoto = true;
}