Entity relations management.

This commit is contained in:
Igor Kulikov 2017-05-29 11:47:43 +03:00
parent 90ef91e3a1
commit a708509aac
61 changed files with 1516 additions and 145 deletions

View File

@ -21,6 +21,7 @@ import org.springframework.web.bind.annotation.*;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntityRelationInfo;
import org.thingsboard.server.dao.relation.EntityRelationsQuery;
import org.thingsboard.server.exception.ThingsboardErrorCode;
import org.thingsboard.server.exception.ThingsboardException;
@ -127,6 +128,21 @@ public class EntityRelationController extends BaseController {
}
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/relations/info", method = RequestMethod.GET, params = {"fromId", "fromType"})
@ResponseBody
public List<EntityRelationInfo> findInfoByFrom(@RequestParam("fromId") String strFromId, @RequestParam("fromType") String strFromType) throws ThingsboardException {
checkParameter("fromId", strFromId);
checkParameter("fromType", strFromType);
EntityId entityId = EntityIdFactory.getByTypeAndId(strFromType, strFromId);
checkEntityId(entityId);
try {
return checkNotNull(relationService.findInfoByFrom(entityId).get());
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/relations", method = RequestMethod.GET, params = {"fromId", "fromType", "relationType"})
@ResponseBody

View File

@ -20,7 +20,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import com.fasterxml.jackson.databind.JsonNode;
public class Customer extends ContactBased<CustomerId>{
public class Customer extends ContactBased<CustomerId> implements HasName {
private static final long serialVersionUID = -1599722990298929275L;
@ -59,6 +59,11 @@ public class Customer extends ContactBased<CustomerId>{
this.title = title;
}
@Override
public String getName() {
return title;
}
public JsonNode getAdditionalInfo() {
return additionalInfo;
}

View File

@ -19,7 +19,7 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.TenantId;
public class DashboardInfo extends SearchTextBased<DashboardId> {
public class DashboardInfo extends SearchTextBased<DashboardId> implements HasName {
private TenantId tenantId;
private CustomerId customerId;
@ -64,6 +64,11 @@ public class DashboardInfo extends SearchTextBased<DashboardId> {
this.title = title;
}
@Override
public String getName() {
return title;
}
@Override
public String getSearchText() {
return title;

View File

@ -21,7 +21,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import com.fasterxml.jackson.databind.JsonNode;
public class Device extends SearchTextBased<DeviceId> {
public class Device extends SearchTextBased<DeviceId> implements HasName {
private static final long serialVersionUID = 2807343040519543363L;
@ -64,6 +64,7 @@ public class Device extends SearchTextBased<DeviceId> {
this.customerId = customerId;
}
@Override
public String getName() {
return name;
}

View File

@ -0,0 +1,22 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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.common.data;
public interface HasName {
String getName();
}

View File

@ -19,7 +19,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import com.fasterxml.jackson.databind.JsonNode;
public class Tenant extends ContactBased<TenantId>{
public class Tenant extends ContactBased<TenantId> implements HasName {
private static final long serialVersionUID = 8057243243859922101L;
@ -50,6 +50,11 @@ public class Tenant extends ContactBased<TenantId>{
this.title = title;
}
@Override
public String getName() {
return title;
}
public String getRegion() {
return region;
}

View File

@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.security.Authority;
import com.fasterxml.jackson.databind.JsonNode;
public class User extends SearchTextBased<UserId> {
public class User extends SearchTextBased<UserId> implements HasName {
private static final long serialVersionUID = 8250339805336035966L;
@ -77,6 +77,11 @@ public class User extends SearchTextBased<UserId> {
this.email = email;
}
@Override
public String getName() {
return email;
}
public Authority getAuthority() {
return authority;
}

View File

@ -18,13 +18,14 @@ package org.thingsboard.server.common.data.alarm;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.id.EntityId;
/**
* Created by ashvayka on 11.05.17.
*/
@Data
public class Alarm extends BaseData<AlarmId> {
public class Alarm extends BaseData<AlarmId> implements HasName {
private long startTs;
private long endTs;
@ -37,4 +38,8 @@ public class Alarm extends BaseData<AlarmId> {
private JsonNode details;
private boolean propagate;
@Override
public String getName() {
return type;
}
}

View File

@ -16,12 +16,13 @@
package org.thingsboard.server.common.data.asset;
import com.fasterxml.jackson.databind.JsonNode;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.SearchTextBased;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
public class Asset extends SearchTextBased<AssetId> {
public class Asset extends SearchTextBased<AssetId> implements HasName {
private static final long serialVersionUID = 2807343040519543363L;
@ -64,6 +65,7 @@ public class Asset extends SearchTextBased<AssetId> {
this.customerId = customerId;
}
@Override
public String getName() {
return name;
}

View File

@ -15,13 +15,14 @@
*/
package org.thingsboard.server.common.data.plugin;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.SearchTextBased;
import org.thingsboard.server.common.data.id.PluginId;
import org.thingsboard.server.common.data.id.TenantId;
import com.fasterxml.jackson.databind.JsonNode;
public class PluginMetaData extends SearchTextBased<PluginId> {
public class PluginMetaData extends SearchTextBased<PluginId> implements HasName {
private static final long serialVersionUID = 1L;
@ -75,6 +76,7 @@ public class PluginMetaData extends SearchTextBased<PluginId> {
this.tenantId = tenantId;
}
@Override
public String getName() {
return name;
}

View File

@ -47,11 +47,11 @@ public class EntityRelation {
this.additionalInfo = additionalInfo;
}
public EntityRelation(EntityRelation device) {
this.from = device.getFrom();
this.to = device.getTo();
this.type = device.getType();
this.additionalInfo = device.getAdditionalInfo();
public EntityRelation(EntityRelation entityRelation) {
this.from = entityRelation.getFrom();
this.to = entityRelation.getTo();
this.type = entityRelation.getType();
this.additionalInfo = entityRelation.getAdditionalInfo();
}
public EntityId getFrom() {

View File

@ -0,0 +1,59 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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.common.data.relation;
public class EntityRelationInfo extends EntityRelation {
private static final long serialVersionUID = 2807343097519543363L;
private String toName;
public EntityRelationInfo() {
super();
}
public EntityRelationInfo(EntityRelation entityRelation) {
super(entityRelation);
}
public String getToName() {
return toName;
}
public void setToName(String toName) {
this.toName = toName;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
EntityRelationInfo that = (EntityRelationInfo) o;
return toName != null ? toName.equals(that.toName) : that.toName == null;
}
@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + (toName != null ? toName.hashCode() : 0);
return result;
}
}

View File

@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.rule;
import lombok.Data;
import lombok.ToString;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.SearchTextBased;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.RuleId;
@ -26,7 +27,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
@Data
public class RuleMetaData extends SearchTextBased<RuleId> {
public class RuleMetaData extends SearchTextBased<RuleId> implements HasName {
private static final long serialVersionUID = -5656679015122935465L;
@ -66,4 +67,9 @@ public class RuleMetaData extends SearchTextBased<RuleId> {
return name;
}
@Override
public String getName() {
return name;
}
}

View File

@ -28,6 +28,10 @@ import java.util.Optional;
*/
public interface AlarmService {
Alarm findAlarmById(AlarmId alarmId);
ListenableFuture<Alarm> findAlarmByIdAsync(AlarmId alarmId);
Optional<Alarm> saveIfNotExists(Alarm alarm);
ListenableFuture<Boolean> updateAlarm(Alarm alarm);

View File

@ -0,0 +1,66 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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.alarm;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmId;
import org.thingsboard.server.common.data.alarm.AlarmQuery;
import org.thingsboard.server.common.data.page.TimePageData;
import java.util.Optional;
@Service
@Slf4j
public class BaseAlarmService implements AlarmService {
@Override
public Alarm findAlarmById(AlarmId alarmId) {
return null;
}
@Override
public ListenableFuture<Alarm> findAlarmByIdAsync(AlarmId alarmId) {
return null;
}
@Override
public Optional<Alarm> saveIfNotExists(Alarm alarm) {
return null;
}
@Override
public ListenableFuture<Boolean> updateAlarm(Alarm alarm) {
return null;
}
@Override
public ListenableFuture<Boolean> ackAlarm(Alarm alarm) {
return null;
}
@Override
public ListenableFuture<Boolean> clearAlarm(AlarmId alarmId) {
return null;
}
@Override
public ListenableFuture<TimePageData<Alarm>> findAlarms(AlarmQuery query) {
return null;
}
}

View File

@ -35,7 +35,7 @@ import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.dao.customer.CustomerDao;
import org.thingsboard.server.dao.entity.BaseEntityService;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.model.*;
import org.thingsboard.server.dao.relation.EntitySearchDirection;
@ -55,7 +55,7 @@ import static org.thingsboard.server.dao.service.Validator.*;
@Service
@Slf4j
public class BaseAssetService extends BaseEntityService implements AssetService {
public class BaseAssetService extends AbstractEntityService implements AssetService {
@Autowired
private AssetDao assetDao;

View File

@ -31,17 +31,15 @@ import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.asset.Asset;
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.entity.BaseEntityService;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.AssetEntity;
import org.thingsboard.server.dao.model.CustomerEntity;
import org.thingsboard.server.dao.model.TenantEntity;
import org.thingsboard.server.dao.service.DataValidator;
@ -53,7 +51,7 @@ import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.service.Validator;
@Service
@Slf4j
public class CustomerServiceImpl extends BaseEntityService implements CustomerService {
public class CustomerServiceImpl extends AbstractEntityService implements CustomerService {
private static final String PUBLIC_CUSTOMER_TITLE = "Public";

View File

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.dashboard;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.id.CustomerId;
@ -27,8 +28,12 @@ public interface DashboardService {
public Dashboard findDashboardById(DashboardId dashboardId);
public ListenableFuture<Dashboard> findDashboardByIdAsync(DashboardId dashboardId);
public DashboardInfo findDashboardInfoById(DashboardId dashboardId);
public ListenableFuture<DashboardInfo> findDashboardInfoByIdAsync(DashboardId dashboardId);
public Dashboard saveDashboard(Dashboard dashboard);
public Dashboard assignDashboardToCustomer(DashboardId dashboardId, CustomerId customerId);

View File

@ -17,9 +17,13 @@ package org.thingsboard.server.dao.dashboard;
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 java.util.List;
import com.google.common.base.Function;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.common.data.Dashboard;
@ -30,7 +34,7 @@ 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.entity.BaseEntityService;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.model.*;
import org.thingsboard.server.dao.service.DataValidator;
@ -42,7 +46,7 @@ import org.thingsboard.server.dao.service.Validator;
@Service
@Slf4j
public class DashboardServiceImpl extends BaseEntityService implements DashboardService {
public class DashboardServiceImpl extends AbstractEntityService implements DashboardService {
@Autowired
private DashboardDao dashboardDao;
@ -64,6 +68,14 @@ public class DashboardServiceImpl extends BaseEntityService implements Dashboard
return getData(dashboardEntity);
}
@Override
public ListenableFuture<Dashboard> findDashboardByIdAsync(DashboardId dashboardId) {
log.trace("Executing findDashboardByIdAsync [{}]", dashboardId);
validateId(dashboardId, "Incorrect dashboardId " + dashboardId);
ListenableFuture<DashboardEntity> dashboardEntity = dashboardDao.findByIdAsync(dashboardId.getId());
return Futures.transform(dashboardEntity, (Function<? super DashboardEntity, ? extends Dashboard>) input -> getData(input));
}
@Override
public DashboardInfo findDashboardInfoById(DashboardId dashboardId) {
log.trace("Executing findDashboardInfoById [{}]", dashboardId);
@ -72,6 +84,14 @@ public class DashboardServiceImpl extends BaseEntityService implements Dashboard
return getData(dashboardInfoEntity);
}
@Override
public ListenableFuture<DashboardInfo> findDashboardInfoByIdAsync(DashboardId dashboardId) {
log.trace("Executing findDashboardInfoByIdAsync [{}]", dashboardId);
validateId(dashboardId, "Incorrect dashboardId " + dashboardId);
ListenableFuture<DashboardInfoEntity> dashboardInfoEntity = dashboardInfoDao.findByIdAsync(dashboardId.getId());
return Futures.transform(dashboardInfoEntity, (Function<? super DashboardInfoEntity, ? extends DashboardInfo>) input -> getData(input));
}
@Override
public Dashboard saveDashboard(Dashboard dashboard) {
log.trace("Executing saveDashboard [{}]", dashboard);

View File

@ -37,7 +37,7 @@ import org.thingsboard.server.common.data.relation.EntityRelation;
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.entity.BaseEntityService;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.model.CustomerEntity;
import org.thingsboard.server.dao.model.DeviceEntity;
@ -58,7 +58,7 @@ import static org.thingsboard.server.dao.service.Validator.*;
@Service
@Slf4j
public class DeviceServiceImpl extends BaseEntityService implements DeviceService {
public class DeviceServiceImpl extends AbstractEntityService implements DeviceService {
@Autowired
private DeviceDao deviceDao;

View File

@ -0,0 +1,36 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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.entity;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.dao.relation.RelationService;
@Slf4j
public abstract class AbstractEntityService {
@Autowired
protected RelationService relationService;
protected void deleteEntityRelations(EntityId entityId) {
log.trace("Executing deleteEntityRelations [{}]", entityId);
relationService.deleteEntityRelations(entityId);
}
}

View File

@ -15,23 +15,102 @@
*/
package org.thingsboard.server.dao.entity;
import com.google.common.base.Function;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.dao.relation.RelationService;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.*;
import org.thingsboard.server.common.data.alarm.AlarmId;
import org.thingsboard.server.common.data.id.*;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetService;
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.plugin.PluginService;
import org.thingsboard.server.dao.rule.RuleService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.user.UserService;
/**
* Created by ashvayka on 04.05.17.
*/
@Service
@Slf4j
public class BaseEntityService {
public class BaseEntityService extends AbstractEntityService implements EntityService {
@Autowired
protected RelationService relationService;
private AssetService assetService;
protected void deleteEntityRelations(EntityId entityId) {
log.trace("Executing deleteEntityRelations [{}]", entityId);
relationService.deleteEntityRelations(entityId);
@Autowired
private DeviceService deviceService;
@Autowired
private RuleService ruleService;
@Autowired
private PluginService pluginService;
@Autowired
private TenantService tenantService;
@Autowired
private CustomerService customerService;
@Autowired
private UserService userService;
@Autowired
private DashboardService dashboardService;
@Autowired
private AlarmService alarmService;
@Override
public void deleteEntityRelations(EntityId entityId) {
super.deleteEntityRelations(entityId);
}
@Override
public ListenableFuture<String> fetchEntityNameAsync(EntityId entityId) {
log.trace("Executing fetchEntityNameAsync [{}]", entityId);
ListenableFuture<String> entityName;
ListenableFuture<? extends HasName> hasName;
switch (entityId.getEntityType()) {
case ASSET:
hasName = assetService.findAssetByIdAsync(new AssetId(entityId.getId()));
break;
case DEVICE:
hasName = deviceService.findDeviceByIdAsync(new DeviceId(entityId.getId()));
break;
case RULE:
hasName = ruleService.findRuleByIdAsync(new RuleId(entityId.getId()));
break;
case PLUGIN:
hasName = pluginService.findPluginByIdAsync(new PluginId(entityId.getId()));
break;
case TENANT:
hasName = tenantService.findTenantByIdAsync(new TenantId(entityId.getId()));
break;
case CUSTOMER:
hasName = customerService.findCustomerByIdAsync(new CustomerId(entityId.getId()));
break;
case USER:
hasName = userService.findUserByIdAsync(new UserId(entityId.getId()));
break;
case DASHBOARD:
hasName = dashboardService.findDashboardInfoByIdAsync(new DashboardId(entityId.getId()));
break;
case ALARM:
hasName = alarmService.findAlarmByIdAsync(new AlarmId(entityId.getId()));
break;
default:
throw new IllegalStateException("Not Implemented!");
}
entityName = Futures.transform(hasName, (Function<HasName, String>) hasName1 -> hasName1.getName() );
return entityName;
}
}

View File

@ -0,0 +1,28 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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.entity;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.id.EntityId;
public interface EntityService {
ListenableFuture<String> fetchEntityNameAsync(EntityId entityId);
void deleteEntityRelations(EntityId entityId);
}

View File

@ -22,7 +22,6 @@ 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.PluginId;
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;
@ -30,9 +29,8 @@ 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.entity.BaseEntityService;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.DatabaseException;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
@ -55,7 +53,7 @@ import static org.thingsboard.server.dao.service.Validator.validateId;
@Service
@Slf4j
public class BasePluginService extends BaseEntityService implements PluginService {
public class BasePluginService extends AbstractEntityService implements PluginService {
//TODO: move to a better place.
public static final TenantId SYSTEM_TENANT = new TenantId(ModelConstants.NULL_UUID);

View File

@ -23,9 +23,24 @@ 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.BaseData;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntityRelationInfo;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.plugin.PluginService;
import org.thingsboard.server.dao.rule.RuleService;
import org.thingsboard.server.dao.tenant.TenantService;
import javax.annotation.Nullable;
import java.util.*;
@ -41,6 +56,9 @@ public class BaseRelationService implements RelationService {
@Autowired
private RelationDao relationDao;
@Autowired
private EntityService entityService;
@Override
public ListenableFuture<Boolean> checkRelation(EntityId from, EntityId to, String relationType) {
log.trace("Executing checkRelation [{}][{}][{}]", from, to, relationType);
@ -99,6 +117,31 @@ public class BaseRelationService implements RelationService {
return relationDao.findAllByFrom(from);
}
@Override
public ListenableFuture<List<EntityRelationInfo>> findInfoByFrom(EntityId from) {
log.trace("Executing findInfoByFrom [{}]", from);
validate(from);
ListenableFuture<List<EntityRelation>> relations = relationDao.findAllByFrom(from);
ListenableFuture<List<EntityRelationInfo>> relationsInfo = Futures.transform(relations,
(AsyncFunction<List<EntityRelation>, List<EntityRelationInfo>>) relations1 -> {
List<ListenableFuture<EntityRelationInfo>> futures = new ArrayList<>();
relations1.stream().forEach(relation -> futures.add(fetchRelationInfoAsync(relation)));
return Futures.successfulAsList(futures);
});
return relationsInfo;
}
private ListenableFuture<EntityRelationInfo> fetchRelationInfoAsync(EntityRelation relation) {
ListenableFuture<String> entityName = entityService.fetchEntityNameAsync(relation.getTo());
ListenableFuture<EntityRelationInfo> entityRelationInfo =
Futures.transform(entityName, (Function<String, EntityRelationInfo>) entityName1 -> {
EntityRelationInfo entityRelationInfo1 = new EntityRelationInfo(relation);
entityRelationInfo1.setToName(entityName1);
return entityRelationInfo1;
});
return entityRelationInfo;
}
@Override
public ListenableFuture<List<EntityRelation>> findByFromAndType(EntityId from, String relationType) {
log.trace("Executing findByFromAndType [{}][{}]", from, relationType);

View File

@ -18,6 +18,7 @@ package org.thingsboard.server.dao.relation;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntityRelationInfo;
import java.util.List;
@ -38,6 +39,8 @@ public interface RelationService {
ListenableFuture<List<EntityRelation>> findByFrom(EntityId from);
ListenableFuture<List<EntityRelationInfo>> findInfoByFrom(EntityId from);
ListenableFuture<List<EntityRelation>> findByFromAndType(EntityId from, String relationType);
ListenableFuture<List<EntityRelation>> findByTo(EntityId to);

View File

@ -23,7 +23,6 @@ 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.asset.Asset;
import org.thingsboard.server.common.data.id.RuleId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageData;
@ -34,11 +33,10 @@ 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.entity.BaseEntityService;
import org.thingsboard.server.dao.entity.AbstractEntityService;
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.AssetEntity;
import org.thingsboard.server.dao.model.RuleMetaDataEntity;
import org.thingsboard.server.dao.plugin.PluginService;
import org.thingsboard.server.dao.service.DataValidator;
@ -58,7 +56,7 @@ import static org.thingsboard.server.dao.service.Validator.validatePageLink;
@Service
@Slf4j
public class BaseRuleService extends BaseEntityService implements RuleService {
public class BaseRuleService extends AbstractEntityService implements RuleService {
private final TenantId systemTenantId = new TenantId(NULL_UUID);

View File

@ -26,7 +26,6 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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;
@ -34,9 +33,8 @@ 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.entity.BaseEntityService;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.model.CustomerEntity;
import org.thingsboard.server.dao.model.TenantEntity;
import org.thingsboard.server.dao.plugin.PluginService;
import org.thingsboard.server.dao.rule.RuleService;
@ -50,7 +48,7 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService;
@Service
@Slf4j
public class TenantServiceImpl extends BaseEntityService implements TenantService {
public class TenantServiceImpl extends AbstractEntityService implements TenantService {
private static final String DEFAULT_TENANT_REGION = "Global";

View File

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.user;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
@ -27,6 +28,8 @@ public interface UserService {
public User findUserById(UserId userId);
public ListenableFuture<User> findUserByIdAsync(UserId userId);
public User findUserByEmail(String email);
public User saveUser(User user);

View File

@ -23,6 +23,9 @@ import static org.thingsboard.server.dao.service.Validator.validateString;
import java.util.List;
import com.google.common.base.Function;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
@ -35,7 +38,7 @@ 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.entity.BaseEntityService;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.*;
@ -47,7 +50,7 @@ import org.springframework.stereotype.Service;
@Service
@Slf4j
public class UserServiceImpl extends BaseEntityService implements UserService {
public class UserServiceImpl extends AbstractEntityService implements UserService {
@Autowired
private UserDao userDao;
@ -77,6 +80,14 @@ public class UserServiceImpl extends BaseEntityService implements UserService {
return getData(userEntity);
}
@Override
public ListenableFuture<User> findUserByIdAsync(UserId userId) {
log.trace("Executing findUserByIdAsync [{}]", userId);
validateId(userId, "Incorrect userId " + userId);
ListenableFuture<UserEntity> userEntity = userDao.findByIdAsync(userId.getId());
return Futures.transform(userEntity, (Function<? super UserEntity, ? extends User>) input -> getData(input));
}
@Override
public User saveUser(User user) {
log.trace("Executing saveUser [{}]", user);

View File

@ -25,6 +25,7 @@ function EntityRelationService($http, $q) {
deleteRelation: deleteRelation,
deleteRelations: deleteRelations,
findByFrom: findByFrom,
findInfoByFrom: findInfoByFrom,
findByFromAndType: findByFromAndType,
findByTo: findByTo,
findByToAndType: findByToAndType,
@ -84,6 +85,18 @@ function EntityRelationService($http, $q) {
return deferred.promise;
}
function findInfoByFrom(fromId, fromType) {
var deferred = $q.defer();
var url = '/api/relations/info?fromId=' + fromId;
url += '&fromType=' + fromType;
$http.get(url, null).then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
function findByFromAndType(fromId, fromType, relationType) {
var deferred = $q.defer();
var url = '/api/relations?fromId=' + fromId;

View File

@ -20,14 +20,13 @@ export default angular.module('thingsboard.api.entity', [thingsboardTypes])
.name;
/*@ngInject*/
function EntityService($http, $q, $filter, $translate, userService, deviceService,
function EntityService($http, $q, $filter, $translate, $log, userService, deviceService,
assetService, tenantService, customerService,
ruleService, pluginService, entityRelationService, attributeService, types, utils) {
ruleService, pluginService, dashboardService, entityRelationService, attributeService, types, utils) {
var service = {
getEntity: getEntity,
getEntities: getEntities,
getEntitiesByNameFilter: getEntitiesByNameFilter,
entityName: entityName,
processEntityAliases: processEntityAliases,
getEntityKeys: getEntityKeys,
checkEntityAlias: checkEntityAlias,
@ -63,6 +62,15 @@ function EntityService($http, $q, $filter, $translate, userService, deviceServic
case types.entityType.plugin:
promise = pluginService.getPlugin(entityId);
break;
case types.entityType.dashboard:
promise = dashboardService.getDashboardInfo(entityId);
break;
case types.entityType.user:
promise = userService.getUser(entityId);
break;
case types.entityType.alarm:
$log.error('Get Alarm Entity is not implemented!');
break;
}
return promise;
}
@ -134,6 +142,15 @@ function EntityService($http, $q, $filter, $translate, userService, deviceServic
case types.entityType.plugin:
promise = getEntitiesByIdsPromise(pluginService.getPlugin, entityIds);
break;
case types.entityType.dashboard:
promise = getEntitiesByIdsPromise(dashboardService.getDashboardInfo, entityIds);
break;
case types.entityType.user:
promise = getEntitiesByIdsPromise(userService.getUser, entityIds);
break;
case types.entityType.alarm:
$log.error('Get Alarm Entity is not implemented!');
break;
}
return promise;
}
@ -141,34 +158,38 @@ function EntityService($http, $q, $filter, $translate, userService, deviceServic
function getEntities(entityType, entityIds, config) {
var deferred = $q.defer();
var promise = getEntitiesPromise(entityType, entityIds, config);
promise.then(
function success(result) {
deferred.resolve(result);
},
function fail() {
deferred.reject();
}
);
if (promise) {
promise.then(
function success(result) {
deferred.resolve(result);
},
function fail() {
deferred.reject();
}
);
} else {
deferred.reject();
}
return deferred.promise;
}
function getEntitiesByPageLinkPromise(entityType, pageLink, config) {
function getEntitiesByPageLinkPromise(entityType, pageLink, config, subType) {
var promise;
var user = userService.getCurrentUser();
var customerId = user.customerId;
switch (entityType) {
case types.entityType.device:
if (user.authority === 'CUSTOMER_USER') {
promise = deviceService.getCustomerDevices(customerId, pageLink, false, config);
promise = deviceService.getCustomerDevices(customerId, pageLink, false, config, subType);
} else {
promise = deviceService.getTenantDevices(pageLink, false, config);
promise = deviceService.getTenantDevices(pageLink, false, config, subType);
}
break;
case types.entityType.asset:
if (user.authority === 'CUSTOMER_USER') {
promise = assetService.getCustomerAssets(customerId, pageLink, false, config);
promise = assetService.getCustomerAssets(customerId, pageLink, false, config, subType);
} else {
promise = assetService.getTenantAssets(pageLink, false, config);
promise = assetService.getTenantAssets(pageLink, false, config, subType);
}
break;
case types.entityType.tenant:
@ -183,48 +204,48 @@ function EntityService($http, $q, $filter, $translate, userService, deviceServic
case types.entityType.plugin:
promise = pluginService.getAllPlugins(pageLink);
break;
case types.entityType.dashboard:
if (user.authority === 'CUSTOMER_USER') {
promise = dashboardService.getCustomerDashboards(customerId, pageLink);
} else {
promise = dashboardService.getTenantDashboards(pageLink);
}
break;
case types.entityType.user:
$log.error('Get User Entities is not implemented!');
break;
case types.entityType.alarm:
$log.error('Get Alarm Entities is not implemented!');
break;
}
return promise;
}
function getEntitiesByNameFilter(entityType, entityNameFilter, limit, config) {
function getEntitiesByNameFilter(entityType, entityNameFilter, limit, config, subType) {
var deferred = $q.defer();
var pageLink = {limit: limit, textSearch: entityNameFilter};
var promise = getEntitiesByPageLinkPromise(entityType, pageLink, config);
promise.then(
function success(result) {
if (result.data && result.data.length > 0) {
deferred.resolve(result.data);
} else {
var promise = getEntitiesByPageLinkPromise(entityType, pageLink, config, subType);
if (promise) {
promise.then(
function success(result) {
if (result.data && result.data.length > 0) {
deferred.resolve(result.data);
} else {
deferred.resolve(null);
}
},
function fail() {
deferred.resolve(null);
}
},
function fail() {
deferred.resolve(null);
}
);
);
} else {
deferred.resolve(null);
}
return deferred.promise;
}
function entityName(entityType, entity) {
var name = '';
switch (entityType) {
case types.entityType.device:
case types.entityType.asset:
case types.entityType.rule:
case types.entityType.plugin:
name = entity.name;
break;
case types.entityType.tenant:
case types.entityType.customer:
name = entity.title;
break;
}
return name;
}
function entityToEntityInfo(entityType, entity) {
return { name: entityName(entityType, entity), entityType: entityType, id: entity.id.id };
return { name: entity.name, entityType: entityType, id: entity.id.id };
}
function entitiesToEntitiesInfo(entityType, entities) {

View File

@ -55,4 +55,10 @@
default-event-type="{{vm.types.eventType.alarm.value}}">
</tb-event-table>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'relation.relations' | translate }}">
<tb-relation-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-type="{{vm.types.entityType.asset}}">
</tb-relation-table>
</md-tab>
</tb-grid>

View File

@ -98,7 +98,10 @@ export default angular.module('thingsboard.types', [])
rule: "RULE",
plugin: "PLUGIN",
tenant: "TENANT",
customer: "CUSTOMER"
customer: "CUSTOMER",
user: "USER",
dashboard: "DASHBOARD",
alarm: "ALARM"
},
entitySearchDirection: {
from: "FROM",

View File

@ -108,7 +108,8 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
guid: guid,
isLocalUrl: isLocalUrl,
validateDatasources: validateDatasources,
createKey: createKey
createKey: createKey,
entityTypeName: entityTypeName
}
return service;
@ -346,4 +347,27 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
return dataKey;
}
function entityTypeName (type) {
switch (type) {
case types.entityType.device:
return 'entity.type-device';
case types.entityType.asset:
return 'entity.type-asset';
case types.entityType.rule:
return 'entity.type-rule';
case types.entityType.plugin:
return 'entity.type-plugin';
case types.entityType.tenant:
return 'entity.type-tenant';
case types.entityType.customer:
return 'entity.type-customer';
case types.entityType.user:
return 'entity.type-user';
case types.entityType.dashboard:
return 'entity.type-dashboard';
case types.entityType.alarm:
return 'entity.type-alarm';
}
}
}

View File

@ -34,7 +34,7 @@
</md-item-template>
<md-not-found>
<div class="tb-not-found">
<span translate translate-values='{ dashboard: dashboardSearchText }'>dashboard.no-dashboards-matching</span>
<span translate translate-values='{ entity: dashboardSearchText }'>dashboard.no-dashboards-matching</span>
</div>
</md-not-found>
<div ng-messages="theForm.dashboard.$error">

View File

@ -34,7 +34,7 @@
</md-item-template>
<md-not-found>
<div class="tb-not-found">
<span translate translate-values='{ plugin: pluginSearchText }'>plugin.no-plugins-matching</span>
<span translate translate-values='{ entity: pluginSearchText }'>plugin.no-plugins-matching</span>
</div>
</md-not-found>
</md-autocomplete>

View File

@ -109,7 +109,7 @@ export default function EntityStateController($scope, $location, $state, $stateP
if (params && params.entityId && params.entityId.id && params.entityId.entityType) {
entityService.getEntity(params.entityId.entityType, params.entityId.id, {ignoreLoading: true, ignoreErrors: true}).then(
function success(entity) {
var entityName = entityService.entityName(params.entityId.entityType, entity);
var entityName = entity.name;
deferred.resolve(entityName);
},
function fail() {

View File

@ -128,9 +128,9 @@
<table md-table md-row-select multiple="" ng-model="selectedAttributes" md-progress="attributesDeferred.promise">
<thead md-head md-order="query.order" md-on-reorder="onReorder">
<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>
<th md-column md-order-by="lastUpdateTs"><span translate>attribute.last-update-time</span></th>
<th md-column md-order-by="key"><span translate>attribute.key</span></th>
<th md-column><span translate>attribute.value</span></th>
</tr>
</thead>
<tbody md-body>

View File

@ -110,7 +110,7 @@ export default function EntityAliasesController(utils, entityService, toast, $sc
entityAlias.changed = false;
}
if (!entityAlias.changed && entity && entityAlias.entityType) {
entityAlias.alias = entityService.entityName(entityAlias.entityType, entity);
entityAlias.alias = entity.name;
}
}
}

View File

@ -0,0 +1,171 @@
/*
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 './entity-autocomplete.scss';
/* eslint-disable import/no-unresolved, import/default */
import entityAutocompleteTemplate from './entity-autocomplete.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function EntityAutocomplete($compile, $templateCache, $q, $filter, entityService, types) {
var linker = function (scope, element, attrs, ngModelCtrl) {
var template = $templateCache.get(entityAutocompleteTemplate);
element.html(template);
scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false;
scope.entity = null;
scope.entitySearchText = '';
scope.fetchEntities = function(searchText) {
var deferred = $q.defer();
entityService.getEntitiesByNameFilter(scope.entityType, searchText, 50, null, scope.entitySubtype).then(function success(result) {
if (result) {
deferred.resolve(result);
} else {
deferred.resolve([]);
}
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
scope.entitySearchTextChanged = function() {
}
scope.updateView = function () {
if (!scope.disabled) {
ngModelCtrl.$setViewValue(scope.entity ? scope.entity.id.id : null);
}
}
ngModelCtrl.$render = function () {
if (ngModelCtrl.$viewValue) {
entityService.getEntity(scope.entityType, ngModelCtrl.$viewValue).then(
function success(entity) {
scope.entity = entity;
},
function fail() {
scope.entity = null;
}
);
} else {
scope.entity = null;
}
}
scope.$watch('entityType', function () {
load();
});
scope.$watch('entitySubtype', function () {
if (scope.entity && scope.entity.type != scope.entitySubtype) {
scope.entity = null;
scope.updateView();
}
});
scope.$watch('entity', function () {
scope.updateView();
});
scope.$watch('disabled', function () {
scope.updateView();
});
function load() {
switch (scope.entityType) {
case types.entityType.asset:
scope.selectEntityText = 'asset.select-asset';
scope.entityText = 'asset.asset';
scope.noEntitiesMatchingText = 'asset.no-assets-matching';
scope.entityRequiredText = 'asset.asset-required'
break;
case types.entityType.device:
scope.selectEntityText = 'device.select-device';
scope.entityText = 'device.device';
scope.noEntitiesMatchingText = 'device.no-devices-matching';
scope.entityRequiredText = 'device.device-required'
break;
case types.entityType.rule:
scope.selectEntityText = 'rule.select-rule';
scope.entityText = 'rule.rule';
scope.noEntitiesMatchingText = 'rule.no-rules-matching';
scope.entityRequiredText = 'rule.rule-required'
break;
case types.entityType.plugin:
scope.selectEntityText = 'plugin.select-plugin';
scope.entityText = 'plugin.plugin';
scope.noEntitiesMatchingText = 'plugin.no-plugins-matching';
scope.entityRequiredText = 'plugin.plugin-required'
break;
case types.entityType.tenant:
scope.selectEntityText = 'tenant.select-tenant';
scope.entityText = 'tenant.tenant';
scope.noEntitiesMatchingText = 'tenant.no-tenants-matching';
scope.entityRequiredText = 'tenant.tenant-required'
break;
case types.entityType.customer:
scope.selectEntityText = 'customer.select-customer';
scope.entityText = 'customer.customer';
scope.noEntitiesMatchingText = 'customer.no-customers-matching';
scope.entityRequiredText = 'customer.customer-required'
break;
case types.entityType.user:
scope.selectEntityText = 'user.select-user';
scope.entityText = 'user.user';
scope.noEntitiesMatchingText = 'user.no-users-matching';
scope.entityRequiredText = 'user.user-required'
break;
case types.entityType.dashboard:
scope.selectEntityText = 'dashboard.select-dashboard';
scope.entityText = 'dashboard.dashboard';
scope.noEntitiesMatchingText = 'dashboard.no-dashboards-matching';
scope.entityRequiredText = 'dashboard.dashboard-required'
break;
case types.entityType.alarm:
scope.selectEntityText = 'alarm.select-alarm';
scope.entityText = 'alarm.alarm';
scope.noEntitiesMatchingText = 'alarm.no-alarms-matching';
scope.entityRequiredText = 'alarm.alarm-required'
break;
}
if (scope.entity && scope.entity.id.entityType != scope.entityType) {
scope.entity = null;
scope.updateView();
}
}
$compile(element.contents())(scope);
}
return {
restrict: "E",
require: "^ngModel",
link: linker,
scope: {
theForm: '=?',
tbRequired: '=?',
disabled:'=ngDisabled',
entityType: '=',
entitySubtype: '=?'
}
};
}

View File

@ -0,0 +1,25 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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-entity-autocomplete {
.tb-entity-item {
display: block;
height: 48px;
}
li {
height: auto !important;
white-space: normal !important;
}
}

View File

@ -0,0 +1,45 @@
<!--
Copyright © 2016-2017 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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"
ng-disabled="disabled"
md-no-cache="true"
md-input-name="entity"
ng-model="entity"
md-selected-item="entity"
md-search-text="entitySearchText"
md-search-text-change="entitySearchTextChanged()"
md-items="item in fetchEntities(entitySearchText)"
md-item-text="item.name"
md-min-length="0"
md-floating-label="{{ entityText | translate }}"
md-select-on-match="true"
md-menu-class="tb-entity-autocomplete">
<md-item-template>
<div class="tb-entity-item">
<span md-highlight-text="entitySearchText" md-highlight-flags="^i">{{item.name}}</span>
</div>
</md-item-template>
<md-not-found>
<div class="tb-not-found">
<span translate translate-values='{ entity: entitySearchText }'>{{ noEntitiesMatchingText }}</span>
</div>
</md-not-found>
<div ng-messages="theForm.entity.$error">
<div translate ng-message="required">{{ entityRequiredText }}</div>
</div>
</md-autocomplete>

View File

@ -32,14 +32,6 @@ export default function EntityFilterDirective($compile, $templateCache, $q, enti
scope.ngModelCtrl = ngModelCtrl;
scope.itemName = function(item) {
if (item) {
return entityService.entityName(scope.entityType, item);
} else {
return '';
}
}
scope.fetchEntities = function(searchText, limit) {
var deferred = $q.defer();
entityService.getEntitiesByNameFilter(scope.entityType, searchText, limit).then(function success(result) {

View File

@ -33,7 +33,7 @@
md-min-length="0"
placeholder="{{ 'entity.entity-list' | translate }}">
<md-item-template>
<span md-highlight-text="entitySearchText" md-highlight-flags="^i">{{itemName(item)}}</span>
<span md-highlight-text="entitySearchText" md-highlight-flags="^i">{{item.name}}</span>
</md-item-template>
<md-not-found>
<span translate translate-values='{ entity: entitySearchText }'>entity.no-entities-matching</span>

View File

@ -0,0 +1,86 @@
/*
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 './entity-select.scss';
/* eslint-disable import/no-unresolved, import/default */
import entitySelectTemplate from './entity-select.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function EntitySelect($compile, $templateCache) {
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.model = null;
scope.updateView = function () {
if (!scope.disabled) {
var value = ngModelCtrl.$viewValue;
if (scope.model && scope.model.entityType && scope.model.entityId) {
if (!value) {
value = {};
}
value.entityType = scope.model.entityType;
value.id = scope.model.entityId;
ngModelCtrl.$setViewValue(value);
} else {
ngModelCtrl.$setViewValue(null);
}
}
}
ngModelCtrl.$render = function () {
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
scope.model = {};
scope.model.entityType = value.entityType;
scope.model.entityId = value.id;
} else {
scope.model = null;
}
}
scope.$watch('model.entityType', function () {
scope.updateView();
});
scope.$watch('model.entityId', function () {
scope.updateView();
});
scope.$watch('disabled', function () {
scope.updateView();
});
$compile(element.contents())(scope);
}
return {
restrict: "E",
require: "^ngModel",
link: linker,
scope: {
theForm: '=?',
tbRequired: '=?',
disabled:'=ngDisabled'
}
};
}

View File

@ -0,0 +1,19 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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-entity-select {
}

View File

@ -0,0 +1,29 @@
<!--
Copyright © 2016-2017 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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' class="tb-entity-select">
<tb-entity-type-select style="min-width: 100px;"
ng-model="model.entityType">
</tb-entity-type-select>
<tb-entity-autocomplete flex
the-form="theForm"
ng-disabled="disabled"
tb-required="tbRequired"
entity-type="model.entityType"
ng-model="model.entityId">
</tb-entity-autocomplete>
</div>

View File

@ -23,7 +23,7 @@ import entityTypeSelectTemplate from './entity-type-select.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function EntityTypeSelect($compile, $templateCache, userService, types) {
export default function EntityTypeSelect($compile, $templateCache, utils, userService, types) {
var linker = function (scope, element, attrs, ngModelCtrl) {
var template = $templateCache.get(entityTypeSelectTemplate);
@ -51,10 +51,12 @@ export default function EntityTypeSelect($compile, $templateCache, userService,
scope.entityTypes.customer = types.entityType.customer;
scope.entityTypes.rule = types.entityType.rule;
scope.entityTypes.plugin = types.entityType.plugin;
scope.entityTypes.dashboard = types.entityType.dashboard;
break;
case 'CUSTOMER_USER':
scope.entityTypes.device = types.entityType.device;
scope.entityTypes.asset = types.entityType.asset;
scope.entityTypes.dashboard = types.entityType.dashboard;
break;
}
@ -67,20 +69,7 @@ export default function EntityTypeSelect($compile, $templateCache, userService,
}
scope.typeName = function(type) {
switch (type) {
case types.entityType.device:
return 'entity.type-device';
case types.entityType.asset:
return 'entity.type-asset';
case types.entityType.rule:
return 'entity.type-rule';
case types.entityType.plugin:
return 'entity.type-plugin';
case types.entityType.tenant:
return 'entity.type-tenant';
case types.entityType.customer:
return 'entity.type-customer';
}
return utils.entityTypeName(type);
}
scope.updateValidity = function () {

View File

@ -18,12 +18,15 @@ import EntityAliasesController from './entity-aliases.controller';
import EntityTypeSelectDirective from './entity-type-select.directive';
import EntitySubtypeSelectDirective from './entity-subtype-select.directive';
import EntitySubtypeAutocompleteDirective from './entity-subtype-autocomplete.directive';
import EntityAutocompleteDirective from './entity-autocomplete.directive';
import EntitySelectDirective from './entity-select.directive';
import EntityFilterDirective from './entity-filter.directive';
import AliasesEntitySelectPanelController from './aliases-entity-select-panel.controller';
import AliasesEntitySelectDirective from './aliases-entity-select.directive';
import AddAttributeDialogController from './attribute/add-attribute-dialog.controller';
import AddWidgetToDashboardDialogController from './attribute/add-widget-to-dashboard-dialog.controller';
import AttributeTableDirective from './attribute/attribute-table.directive';
import RelationTableDirective from './relation/relation-table.directive';
export default angular.module('thingsboard.entity', [])
.controller('EntityAliasesController', EntityAliasesController)
@ -33,7 +36,10 @@ export default angular.module('thingsboard.entity', [])
.directive('tbEntityTypeSelect', EntityTypeSelectDirective)
.directive('tbEntitySubtypeSelect', EntitySubtypeSelectDirective)
.directive('tbEntitySubtypeAutocomplete', EntitySubtypeAutocompleteDirective)
.directive('tbEntityAutocomplete', EntityAutocompleteDirective)
.directive('tbEntitySelect', EntitySelectDirective)
.directive('tbEntityFilter', EntityFilterDirective)
.directive('tbAliasesEntitySelect', AliasesEntitySelectDirective)
.directive('tbAttributeTable', AttributeTableDirective)
.directive('tbRelationTable', RelationTableDirective)
.name;

View File

@ -0,0 +1,43 @@
/*
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 AddRelationDialogController($scope, $mdDialog, types, entityRelationService, from) {
var vm = this;
vm.types = types;
vm.relation = {};
vm.relation.from = from;
vm.relation.type = types.entityRelationType.contains;
vm.add = add;
vm.cancel = cancel;
function cancel() {
$mdDialog.cancel();
}
function add() {
$scope.theForm.$setPristine();
entityRelationService.saveRelation(vm.relation).then(
function success() {
$mdDialog.hide();
}
);
}
}

View File

@ -0,0 +1,64 @@
<!--
Copyright © 2016-2017 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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="{{ 'relation.add' | translate }}" style="min-width: 400px;">
<form name="theForm" ng-submit="vm.add()">
<md-toolbar>
<div class="md-toolbar-tools">
<h2 translate>relation.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-disabled="!loading" 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>relation.relation-type</label>
<md-select required ng-model="vm.relation.type" ng-disabled="loading">
<md-option ng-repeat="type in vm.types.entityRelationType" ng-value="type">
<span>{{('relation.relation-types.' + type) | translate}}</span>
</md-option>
</md-select>
</md-input-container>
<span class="tb-small">{{'entity.entity' | translate }}</span>
<tb-entity-select flex
the-form="theForm"
tb-required="true"
ng-model="vm.relation.to">
</tb-entity-select>
</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>

View File

@ -0,0 +1,179 @@
/*
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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 './relation-table.scss';
/* eslint-disable import/no-unresolved, import/default */
import relationTableTemplate from './relation-table.tpl.html';
import addRelationTemplate from './add-relation-dialog.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
import AddRelationController from './add-relation-dialog.controller';
/*@ngInject*/
export default function RelationTable() {
return {
restrict: "E",
scope: true,
bindToController: {
entityId: '=',
entityType: '@'
},
controller: RelationTableController,
controllerAs: 'vm',
templateUrl: relationTableTemplate
};
}
/*@ngInject*/
function RelationTableController($scope, $q, $mdDialog, $document, $translate, $filter, utils, types, entityRelationService) {
let vm = this;
vm.relations = [];
vm.relationsCount = 0;
vm.allRelations = [];
vm.selectedRelations = [];
vm.query = {
order: 'typeName',
limit: 5,
page: 1,
search: null
};
vm.enterFilterMode = enterFilterMode;
vm.exitFilterMode = exitFilterMode;
vm.onReorder = onReorder;
vm.onPaginate = onPaginate;
vm.addRelation = addRelation;
vm.editRelation = editRelation;
vm.deleteRelation = deleteRelation;
vm.deleteRelations = deleteRelations;
vm.reloadRelations = reloadRelations;
vm.updateRelations = updateRelations;
$scope.$watch("vm.entityId", function(newVal, prevVal) {
if (newVal && !angular.equals(newVal, prevVal)) {
reloadRelations();
}
});
$scope.$watch("vm.query.search", function(newVal, prevVal) {
if (!angular.equals(newVal, prevVal) && vm.query.search != null) {
updateRelations();
}
});
function enterFilterMode () {
vm.query.search = '';
}
function exitFilterMode () {
vm.query.search = null;
updateRelations();
}
function onReorder () {
updateRelations();
}
function onPaginate () {
updateRelations();
}
function addRelation($event) {
if ($event) {
$event.stopPropagation();
}
var from = {
id: vm.entityId,
entityType: vm.entityType
};
$mdDialog.show({
controller: AddRelationController,
controllerAs: 'vm',
templateUrl: addRelationTemplate,
parent: angular.element($document[0].body),
locals: { from: from },
fullscreen: true,
targetEvent: $event
}).then(function () {
reloadRelations();
}, function () {
});
}
function editRelation($event, /*relation*/) {
if ($event) {
$event.stopPropagation();
}
//TODO:
}
function deleteRelation($event, /*relation*/) {
if ($event) {
$event.stopPropagation();
}
//TODO:
}
function deleteRelations($event) {
if ($event) {
$event.stopPropagation();
}
//TODO:
}
function reloadRelations () {
vm.allRelations.length = 0;
vm.relations.length = 0;
vm.relationsPromise = entityRelationService.findInfoByFrom(vm.entityId, vm.entityType);
vm.relationsPromise.then(
function success(allRelations) {
allRelations.forEach(function(relation) {
relation.typeName = $translate.instant('relation.relation-type.' + relation.type);
relation.toEntityTypeName = $translate.instant(utils.entityTypeName(relation.to.entityType));
});
vm.allRelations = allRelations;
vm.selectedRelations = [];
vm.updateRelations();
vm.relationsPromise = null;
},
function fail() {
vm.allRelations = [];
vm.selectedRelations = [];
vm.updateRelations();
vm.relationsPromise = null;
}
)
}
function updateRelations () {
vm.selectedRelations = [];
var result = $filter('orderBy')(vm.allRelations, vm.query.order);
if (vm.query.search != null) {
result = $filter('filter')(result, {$: vm.query.search});
}
vm.relationsCount = result.length;
var startIndex = vm.query.limit * (vm.query.page - 1);
vm.relations = result.slice(startIndex, startIndex + vm.query.limit);
}
}

View File

@ -0,0 +1,28 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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%);
.tb-relation-table {
md-toolbar.md-table-toolbar.alternate {
.md-toolbar-tools {
md-icon {
color: $md-light;
}
}
}
}

View File

@ -0,0 +1,119 @@
<!--
Copyright © 2016-2017 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 tb-relation-table tb-data-table" layout="column">
<div layout="column" class="md-whiteframe-z1">
<md-toolbar class="md-table-toolbar md-default" ng-show="!vm.selectedRelations.length
&& vm.query.search === null">
<div class="md-toolbar-tools">
<span translate>relation.entity-relations</span>
<span flex></span>
<md-button class="md-icon-button" ng-click="vm.addRelation($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="vm.enterFilterMode()">
<md-icon>search</md-icon>
<md-tooltip md-direction="top">
{{ 'action.search' | translate }}
</md-tooltip>
</md-button>
<md-button class="md-icon-button" ng-click="vm.reloadRelations()">
<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="!vm.selectedRelations.length
&& vm.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 flex>
<label>&nbsp;</label>
<input ng-model="vm.query.search" placeholder="{{ 'common.enter-search' | translate }}"/>
</md-input-container>
<md-button class="md-icon-button" aria-label="{{ 'action.back' | translate }}" ng-click="vm.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="vm.selectedRelations.length">
<div class="md-toolbar-tools">
<span translate
translate-values="{count: selectedRelations.length}"
translate-interpolation="messageformat">relation.selected-relations</span>
<span flex></span>
<md-button class="md-icon-button" ng-click="vm.deleteRelations($event)">
<md-icon>delete</md-icon>
<md-tooltip md-direction="top">
{{ 'action.delete' | translate }}
</md-tooltip>
</md-button>
</div>
</md-toolbar>
<md-table-container>
<table md-table md-row-select multiple="" ng-model="vm.selectedRelations" md-progress="vm.relationsDeferred.promise">
<thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder">
<tr md-row>
<th md-column md-order-by="typeName"><span translate>relation.type</span></th>
<th md-column md-order-by="toEntityTypeName"><span translate>relation.to-entity-type</span></th>
<th md-column md-order-by="toName"><span translate>relation.to-entity-name</span></th>
<th md-column><span>&nbsp</span></th>
</tr>
</thead>
<tbody md-body>
<tr md-row md-select="relation" md-select-id="relation" md-auto-select ng-repeat="relation in vm.relations">
<td md-cell>{{ relation.typeName }}</td>
<td md-cell>{{ relation.toEntityTypeName }}</td>
<td md-cell>{{ relation.toName }}</td>
<td md-cell class="tb-action-cell">
<md-button class="md-icon-button" aria-label="{{ 'action.edit' | translate }}"
ng-click="vm.editRelation($event, relation)">
<md-icon aria-label="{{ 'action.edit' | translate }}" class="material-icons">edit</md-icon>
<md-tooltip md-direction="top">
{{ 'relation.edit' | translate }}
</md-tooltip>
</md-button>
<md-button class="md-icon-button" aria-label="{{ 'action.delete' | translate }}" ng-click="vm.deleteRelation($event, relation)">
<md-icon aria-label="{{ 'action.delete' | translate }}" class="material-icons">delete</md-icon>
<md-tooltip md-direction="top">
{{ 'relation.delete' | translate }}
</md-tooltip>
</md-button>
</td>
</tr>
</tbody>
</table>
</md-table-container>
<md-table-pagination md-limit="vm.query.limit" md-limit-options="[5, 10, 15]"
md-page="vm.query.page" md-total="{{vm.relationsCount}}"
md-on-paginate="onPaginate" md-page-select>
</md-table-pagination>
</div>
</md-content>

View File

@ -235,7 +235,7 @@
"socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard",
"socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard",
"select-dashboard": "Seleccionar panel",
"no-dashboards-matching": "Panel '{{dashboard}}' no encontrado.",
"no-dashboards-matching": "Panel '{{entity}}' no encontrado.",
"dashboard-required": "Panel requerido.",
"select-existing": "Seleccionar paneles existentes",
"create-new": "Crear nuevo panel",
@ -330,7 +330,7 @@
"create-new-key": "Crear nueva clave!",
"duplicate-alias-error": "Alias duplicado '{{alias}}'.<br> El alias de los dispositivos deben ser únicos dentro del panel.",
"configure-alias": "Configurar alias '{{alias}}'",
"no-devices-matching": "No se encontró dispositivo '{{device}}'",
"no-devices-matching": "No se encontró dispositivo '{{entity}}'",
"alias": "Alias",
"alias-required": "Alias de dispositivo requerido.",
"remove-alias": "Eliminar alias",
@ -529,7 +529,7 @@
"system": "Sistema",
"select-plugin": "plugin",
"plugin": "Plugin",
"no-plugins-matching": "No se encontraron plugins: '{{plugin}}'",
"no-plugins-matching": "No se encontraron plugins: '{{entity}}'",
"plugin-required": "Plugin requerido.",
"plugin-require-match": "Por favor, elija un plugin existente.",
"events": "Eventos",

View File

@ -218,7 +218,7 @@ export default function addLocaleKorean(locales) {
"unassign-dashboards-title": "{ count, select, 1 {대시보드 1개} other {대시보드 #개} }의 할당을 취소하시겠습니까?",
"unassign-dashboards-text": "선택된 대시보드가 할당 해제되고 커스터머는 액세스 할 수 없게됩니다.",
"select-dashboard": "대시보드 선택",
"no-dashboards-matching": "'{{dashboard}}'와 일치하는 대시보드가 없습니다.",
"no-dashboards-matching": "'{{entity}}'와 일치하는 대시보드가 없습니다.",
"dashboard-required": "대시보드를 입력하세요.",
"select-existing": "기존 대시보드 선택",
"create-new": "대시보드 생성",
@ -305,7 +305,7 @@ export default function addLocaleKorean(locales) {
"create-new-key": "새로 만들기!",
"duplicate-alias-error": "중복된 '{{alias}}' 앨리어스가 있습니다.<br> 디바이스 앨리어스는 대시보드 내에서 고유해야 합니다.",
"configure-alias": "'{{alias}}' 앨리어스 구성",
"no-devices-matching": "'{{device}}'와 일치하는 디바이스를 찾을 수 없습니다.",
"no-devices-matching": "'{{entity}}'와 일치하는 디바이스를 찾을 수 없습니다.",
"alias": "앨리어스",
"alias-required": "디바이스 앨리어스를 입력하세요.",
"remove-alias": "디바이스 앨리어스 삭제",
@ -496,7 +496,7 @@ export default function addLocaleKorean(locales) {
"system": "시스템",
"select-plugin": "플러그인 선택",
"plugin": "플러그인",
"no-plugins-matching": "'{{plugin}}'과 일치하는 플러그인을 찾을 수 없습니다.",
"no-plugins-matching": "'{{entity}}'과 일치하는 플러그인을 찾을 수 없습니다.",
"plugin-required": "플러그인을 입력하세요.",
"plugin-require-match": "기존의 플러그인을 선택해주세요.",
"events": "이벤트",

View File

@ -235,7 +235,7 @@ export default function addLocaleRussian(locales) {
"socialshare-text": "'{{dashboardTitle}}' сделано ThingsBoard",
"socialshare-title": "'{{dashboardTitle}}' сделано ThingsBoard",
"select-dashboard": "Выберите дашборд",
"no-dashboards-matching": "Дашборд '{{dashboard}}' не найден.",
"no-dashboards-matching": "Дашборд '{{entity}}' не найден.",
"dashboard-required": "Дашборд обязателен.",
"select-existing": "Выберите существующий дашборд",
"create-new": "Создать новый дашборд",
@ -330,7 +330,7 @@ export default function addLocaleRussian(locales) {
"create-new-key": "Создать новый!",
"duplicate-alias-error": "Найден дублирующийся псевдоним '{{alias}}'.<br>В рамках дашборда псевдонимы устройств должны быть уникальными.",
"configure-alias": "Конфигурировать '{{alias}}' псевдоним",
"no-devices-matching": "Устройство '{{device}}' не найдено.",
"no-devices-matching": "Устройство '{{entity}}' не найдено.",
"alias": "Псевдоним",
"alias-required": "Псевдоним устройства обязателен.",
"remove-alias": "Удалить псевдоним устройства",
@ -529,7 +529,7 @@ export default function addLocaleRussian(locales) {
"system": "Системный",
"select-plugin": "Выберите плагин",
"plugin": "Плагин",
"no-plugins-matching": "Плагин '{{plugin}}' не найден.",
"no-plugins-matching": "Плагин '{{entity}}' не найден.",
"plugin-required": "Плагин обязателен.",
"plugin-require-match": "Пожалуйста, выберите существующий плагин.",
"events": "События",

View File

@ -235,7 +235,7 @@ export default function addLocaleChinese(locales) {
"socialshare-text" : "'{{dashboardTitle}}' 由ThingsBoard提供支持",
"socialshare-title" : "'{{dashboardTitle}}' 由ThingsBoard提供支持",
"select-dashboard" : "选择仪表板",
"no-dashboards-matching" : "找不到符合 '{{dashboard}}' 的仪表板。",
"no-dashboards-matching" : "找不到符合 '{{entity}}' 的仪表板。",
"dashboard-required" : "仪表板是必需的。",
"select-existing" : "选择现有仪表板",
"create-new" : "创建新的仪表板",
@ -330,7 +330,7 @@ export default function addLocaleChinese(locales) {
"create-new-key": "创建一个新的!",
"duplicate-alias-error" : "找到重复别名 '{{alias}}'。 <br> 设备别名必须是唯一的。",
"configure-alias" : "配置 '{{alias}}' 别名",
"no-devices-matching" : "找不到与 '{{device}}' 匹配的设备。",
"no-devices-matching" : "找不到与 '{{entity}}' 匹配的设备。",
"alias" : "别名",
"alias-required" : "需要设备别名。",
"remove-alias": "删除设备别名",
@ -529,7 +529,7 @@ export default function addLocaleChinese(locales) {
"system" : "系统",
"select-plugin" : "选择插件",
"plugin" : "插件",
"no-plugins-matching" : "没有找到匹配'{{plugin}}'的插件。",
"no-plugins-matching" : "没有找到匹配'{{entity}}'的插件。",
"plugin-required" : "插件是必需的。",
"plugin-require-match" : "请选择一个现有的插件。",
"events" : "事件",

View File

@ -106,6 +106,12 @@ export default angular.module('thingsboard.locale', [])
"enable-tls": "Enable TLS",
"send-test-mail": "Send test mail"
},
"alarm": {
"alarm": "Alarm",
"select-alarm": "Select alarm",
"no-alarms-matching": "No alarms matching '{{entity}}' were found.",
"alarm-required": "Alarm is required"
},
"asset": {
"asset": "Asset",
"assets": "Assets",
@ -157,7 +163,10 @@ export default angular.module('thingsboard.locale', [])
"unassign-assets-title": "Are you sure you want to unassign { count, select, 1 {1 asset} other {# assets} }?",
"unassign-assets-text": "After the confirmation all selected assets will be unassigned and won't be accessible by the customer.",
"copyId": "Copy asset Id",
"idCopiedMessage": "Asset Id has been copied to clipboard"
"idCopiedMessage": "Asset Id has been copied to clipboard",
"select-asset": "Select asset",
"no-assets-matching": "No assets matching '{{entity}}' were found.",
"asset-required": "Asset is required"
},
"attribute": {
"attributes": "Attributes",
@ -169,6 +178,7 @@ export default angular.module('thingsboard.locale', [])
"scope-shared": "Shared attributes",
"add": "Add attribute",
"key": "Key",
"last-update-time": "Last update time",
"key-required": "Attribute key is required.",
"value": "Value",
"value-required": "Attribute value is required.",
@ -210,6 +220,7 @@ export default angular.module('thingsboard.locale', [])
"enter-search": "Enter search"
},
"customer": {
"customer": "Customer",
"customers": "Customers",
"management": "Customer management",
"dashboard": "Customer Dashboard",
@ -246,7 +257,10 @@ export default angular.module('thingsboard.locale', [])
"details": "Details",
"events": "Events",
"copyId": "Copy customer Id",
"idCopiedMessage": "Customer Id has been copied to clipboard"
"idCopiedMessage": "Customer Id has been copied to clipboard",
"select-customer": "Select customer",
"no-customers-matching": "No customers matching '{{entity}}' were found.",
"customer-required": "Customer is required"
},
"datetime": {
"date-from": "Date from",
@ -304,7 +318,7 @@ export default angular.module('thingsboard.locale', [])
"socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard",
"socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard",
"select-dashboard": "Select dashboard",
"no-dashboards-matching": "No dashboards matching '{{dashboard}}' were found.",
"no-dashboards-matching": "No dashboards matching '{{entity}}' were found.",
"dashboard-required": "Dashboard is required.",
"select-existing": "Select existing dashboard",
"create-new": "Create new dashboard",
@ -425,7 +439,7 @@ export default angular.module('thingsboard.locale', [])
"create-new-key": "Create a new one!",
"duplicate-alias-error": "Duplicate alias found '{{alias}}'.<br>Device aliases must be unique whithin the dashboard.",
"configure-alias": "Configure '{{alias}}' alias",
"no-devices-matching": "No devices matching '{{device}}' were found.",
"no-devices-matching": "No devices matching '{{entity}}' were found.",
"alias": "Alias",
"alias-required": "Device alias is required.",
"remove-alias": "Remove device alias",
@ -497,7 +511,8 @@ export default angular.module('thingsboard.locale', [])
"unable-delete-device-alias-text": "Device alias '{{deviceAlias}}' can't be deleted as it used by the following widget(s):<br/>{{widgetsList}}",
"is-gateway": "Is gateway",
"public": "Public",
"device-public": "Device is public"
"device-public": "Device is public",
"select-device": "Select device"
},
"dialog": {
"close": "Close dialog"
@ -535,6 +550,9 @@ export default angular.module('thingsboard.locale', [])
"type-plugin": "Plugin",
"type-tenant": "Tenant",
"type-customer": "Customer",
"type-user": "User",
"type-dashboard": "Dashboard",
"type-alarm": "Alarm",
"select-entities": "Select entities",
"no-aliases-found": "No aliases found.",
"no-alias-matching": "'{{alias}}' not found.",
@ -672,7 +690,7 @@ export default angular.module('thingsboard.locale', [])
"system": "System",
"select-plugin": "Select plugin",
"plugin": "Plugin",
"no-plugins-matching": "No plugins matching '{{plugin}}' were found.",
"no-plugins-matching": "No plugins matching '{{entity}}' were found.",
"plugin-required": "Plugin is required.",
"plugin-require-match": "Please select an existing plugin.",
"events": "Events",
@ -685,6 +703,7 @@ export default angular.module('thingsboard.locale', [])
"invalid-plugin-file-error": "Unable to import plugin: Invalid plugin data structure.",
"copyId": "Copy plugin Id",
"idCopiedMessage": "Plugin Id has been copied to clipboard"
},
"position": {
"top": "Top",
@ -697,7 +716,24 @@ export default angular.module('thingsboard.locale', [])
"change-password": "Change Password",
"current-password": "Current password"
},
"relation": {
"relations": "Relations",
"entity-relations": "Entity relations",
"selected-relations": "{ count, select, 1 {1 relation} other {# relations} } selected",
"type": "Type",
"to-entity-type": "Entity type",
"to-entity-name": "Entity name",
"edit": "Edit relation",
"delete": "Delete relation",
"relation-type": "Relation type",
"relation-types": {
"Contains": "Contains",
"Manages": "Manages"
},
"add": "Add relation"
},
"rule": {
"rule": "Rule",
"rules": "Rules",
"delete": "Delete rule",
"activate": "Activate rule",
@ -749,12 +785,16 @@ export default angular.module('thingsboard.locale', [])
"rule-file": "Rule file",
"invalid-rule-file-error": "Unable to import rule: Invalid rule data structure.",
"copyId": "Copy rule Id",
"idCopiedMessage": "Rule Id has been copied to clipboard"
"idCopiedMessage": "Rule Id has been copied to clipboard",
"select-rule": "Select rule",
"no-rules-matching": "No rules matching '{{entity}}' were found.",
"rule-required": "Rule is required"
},
"rule-plugin": {
"management": "Rules and plugins management"
},
"tenant": {
"tenant": "Tenant",
"tenants": "Tenants",
"management": "Tenant management",
"add": "Add Tenant",
@ -775,7 +815,10 @@ export default angular.module('thingsboard.locale', [])
"details": "Details",
"events": "Events",
"copyId": "Copy tenant Id",
"idCopiedMessage": "Tenant Id has been copied to clipboard"
"idCopiedMessage": "Tenant Id has been copied to clipboard",
"select-tenant": "Select tenant",
"no-tenants-matching": "No tenants matching '{{entity}}' were found.",
"tenant-required": "Tenant is required"
},
"timeinterval": {
"seconds-interval": "{ seconds, select, 1 {1 second} other {# seconds} }",
@ -803,6 +846,7 @@ export default angular.module('thingsboard.locale', [])
"time-period": "Time period"
},
"user": {
"user": "User",
"users": "Users",
"customer-users": "Customer Users",
"tenant-admins": "Tenant Admins",
@ -828,7 +872,10 @@ export default angular.module('thingsboard.locale', [])
"last-name": "Last Name",
"description": "Description",
"default-dashboard": "Default dashboard",
"always-fullscreen": "Always fullscreen"
"always-fullscreen": "Always fullscreen",
"select-user": "Select user",
"no-users-matching": "No users matching '{{entity}}' were found.",
"user-required": "User is required"
},
"value": {
"type": "Value type",

View File

@ -261,6 +261,45 @@ pre.tb-highlight {
font-size: 16px;
}
.tb-data-table {
md-toolbar {
z-index: 0;
}
span.no-data-found {
position: relative;
height: calc(100% - 57px);
text-transform: uppercase;
display: flex;
}
table.md-table {
tbody {
tr {
td {
&.tb-action-cell {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 72px;
max-width: 72px;
width: 72px;
.md-button {
&.md-icon-button {
margin: 0;
padding: 6px;
width: 36px;
height: 36px;
}
}
.tb-spacer {
padding-left: 38px;
}
}
}
}
}
}
}
/***********************
* Flow