Commit 9cdfe4ef54e38e4a0b962665abae1bef431a548d
1 parent
a90f16f4
TB-61: Implemented new alias filters.
Showing
44 changed files
with
1399 additions
and
126 deletions
@@ -250,6 +250,21 @@ public class EntityRelationController extends BaseController { | @@ -250,6 +250,21 @@ public class EntityRelationController extends BaseController { | ||
250 | } | 250 | } |
251 | } | 251 | } |
252 | 252 | ||
253 | + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") | ||
254 | + @RequestMapping(value = "/relations/info", method = RequestMethod.POST) | ||
255 | + @ResponseBody | ||
256 | + public List<EntityRelationInfo> findInfoByQuery(@RequestBody EntityRelationsQuery query) throws ThingsboardException { | ||
257 | + checkNotNull(query); | ||
258 | + checkNotNull(query.getParameters()); | ||
259 | + checkNotNull(query.getFilters()); | ||
260 | + checkEntityId(query.getParameters().getEntityId()); | ||
261 | + try { | ||
262 | + return checkNotNull(relationService.findInfoByQuery(query).get()); | ||
263 | + } catch (Exception e) { | ||
264 | + throw handleException(e); | ||
265 | + } | ||
266 | + } | ||
267 | + | ||
253 | private RelationTypeGroup parseRelationTypeGroup(String strRelationTypeGroup, RelationTypeGroup defaultValue) { | 268 | private RelationTypeGroup parseRelationTypeGroup(String strRelationTypeGroup, RelationTypeGroup defaultValue) { |
254 | RelationTypeGroup result = defaultValue; | 269 | RelationTypeGroup result = defaultValue; |
255 | if (strRelationTypeGroup != null && strRelationTypeGroup.trim().length()>0) { | 270 | if (strRelationTypeGroup != null && strRelationTypeGroup.trim().length()>0) { |
@@ -191,7 +191,7 @@ public class BaseRelationService implements RelationService { | @@ -191,7 +191,7 @@ public class BaseRelationService implements RelationService { | ||
191 | 191 | ||
192 | @Override | 192 | @Override |
193 | public ListenableFuture<List<EntityRelation>> findByQuery(EntityRelationsQuery query) { | 193 | public ListenableFuture<List<EntityRelation>> findByQuery(EntityRelationsQuery query) { |
194 | - log.trace("Executing findByQuery [{}][{}]", query); | 194 | + log.trace("Executing findByQuery [{}]", query); |
195 | RelationsSearchParameters params = query.getParameters(); | 195 | RelationsSearchParameters params = query.getParameters(); |
196 | final List<EntityTypeFilter> filters = query.getFilters(); | 196 | final List<EntityTypeFilter> filters = query.getFilters(); |
197 | if (filters == null || filters.isEmpty()) { | 197 | if (filters == null || filters.isEmpty()) { |
@@ -224,6 +224,30 @@ public class BaseRelationService implements RelationService { | @@ -224,6 +224,30 @@ public class BaseRelationService implements RelationService { | ||
224 | } | 224 | } |
225 | } | 225 | } |
226 | 226 | ||
227 | + @Override | ||
228 | + public ListenableFuture<List<EntityRelationInfo>> findInfoByQuery(EntityRelationsQuery query) { | ||
229 | + log.trace("Executing findInfoByQuery [{}]", query); | ||
230 | + ListenableFuture<List<EntityRelation>> relations = findByQuery(query); | ||
231 | + EntitySearchDirection direction = query.getParameters().getDirection(); | ||
232 | + ListenableFuture<List<EntityRelationInfo>> relationsInfo = Futures.transform(relations, | ||
233 | + (AsyncFunction<List<EntityRelation>, List<EntityRelationInfo>>) relations1 -> { | ||
234 | + List<ListenableFuture<EntityRelationInfo>> futures = new ArrayList<>(); | ||
235 | + relations1.stream().forEach(relation -> | ||
236 | + futures.add(fetchRelationInfoAsync(relation, | ||
237 | + relation2 -> direction == EntitySearchDirection.FROM ? relation2.getTo() : relation2.getFrom(), | ||
238 | + (EntityRelationInfo relationInfo, String entityName) -> { | ||
239 | + if (direction == EntitySearchDirection.FROM) { | ||
240 | + relationInfo.setToName(entityName); | ||
241 | + } else { | ||
242 | + relationInfo.setFromName(entityName); | ||
243 | + } | ||
244 | + })) | ||
245 | + ); | ||
246 | + return Futures.successfulAsList(futures); | ||
247 | + }); | ||
248 | + return relationsInfo; | ||
249 | + } | ||
250 | + | ||
227 | protected void validate(EntityRelation relation) { | 251 | protected void validate(EntityRelation relation) { |
228 | if (relation == null) { | 252 | if (relation == null) { |
229 | throw new DataValidationException("Relation type should be specified!"); | 253 | throw new DataValidationException("Relation type should be specified!"); |
@@ -52,6 +52,8 @@ public interface RelationService { | @@ -52,6 +52,8 @@ public interface RelationService { | ||
52 | 52 | ||
53 | ListenableFuture<List<EntityRelation>> findByQuery(EntityRelationsQuery query); | 53 | ListenableFuture<List<EntityRelation>> findByQuery(EntityRelationsQuery query); |
54 | 54 | ||
55 | + ListenableFuture<List<EntityRelationInfo>> findInfoByQuery(EntityRelationsQuery query); | ||
56 | + | ||
55 | // TODO: This method may be useful for some validations in the future | 57 | // TODO: This method may be useful for some validations in the future |
56 | // ListenableFuture<Boolean> checkRecursiveRelation(EntityId from, EntityId to); | 58 | // ListenableFuture<Boolean> checkRecursiveRelation(EntityId from, EntityId to); |
57 | 59 |
@@ -30,7 +30,8 @@ function EntityRelationService($http, $q) { | @@ -30,7 +30,8 @@ function EntityRelationService($http, $q) { | ||
30 | findByTo: findByTo, | 30 | findByTo: findByTo, |
31 | findInfoByTo: findInfoByTo, | 31 | findInfoByTo: findInfoByTo, |
32 | findByToAndType: findByToAndType, | 32 | findByToAndType: findByToAndType, |
33 | - findByQuery: findByQuery | 33 | + findByQuery: findByQuery, |
34 | + findInfoByQuery: findInfoByQuery | ||
34 | } | 35 | } |
35 | 36 | ||
36 | return service; | 37 | return service; |
@@ -159,4 +160,15 @@ function EntityRelationService($http, $q) { | @@ -159,4 +160,15 @@ function EntityRelationService($http, $q) { | ||
159 | return deferred.promise; | 160 | return deferred.promise; |
160 | } | 161 | } |
161 | 162 | ||
163 | + function findInfoByQuery(query) { | ||
164 | + var deferred = $q.defer(); | ||
165 | + var url = '/api/relations/info'; | ||
166 | + $http.post(url, query).then(function success(response) { | ||
167 | + deferred.resolve(response.data); | ||
168 | + }, function fail() { | ||
169 | + deferred.reject(); | ||
170 | + }); | ||
171 | + return deferred.promise; | ||
172 | + } | ||
173 | + | ||
162 | } | 174 | } |
@@ -32,6 +32,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device | @@ -32,6 +32,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device | ||
32 | checkEntityAlias: checkEntityAlias, | 32 | checkEntityAlias: checkEntityAlias, |
33 | filterAliasByEntityTypes: filterAliasByEntityTypes, | 33 | filterAliasByEntityTypes: filterAliasByEntityTypes, |
34 | getAliasFilterTypesByEntityTypes: getAliasFilterTypesByEntityTypes, | 34 | getAliasFilterTypesByEntityTypes: getAliasFilterTypesByEntityTypes, |
35 | + prepareAllowedEntityTypesList: prepareAllowedEntityTypesList, | ||
35 | getEntityKeys: getEntityKeys, | 36 | getEntityKeys: getEntityKeys, |
36 | createDatasourcesFromSubscriptionsInfo: createDatasourcesFromSubscriptionsInfo, | 37 | createDatasourcesFromSubscriptionsInfo: createDatasourcesFromSubscriptionsInfo, |
37 | getRelatedEntities: getRelatedEntities, | 38 | getRelatedEntities: getRelatedEntities, |
@@ -176,6 +177,54 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device | @@ -176,6 +177,54 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device | ||
176 | return deferred.promise; | 177 | return deferred.promise; |
177 | } | 178 | } |
178 | 179 | ||
180 | + function getSingleTenantByPageLinkPromise(pageLink) { | ||
181 | + var user = userService.getCurrentUser(); | ||
182 | + var tenantId = user.tenantId; | ||
183 | + var deferred = $q.defer(); | ||
184 | + tenantService.getTenant(tenantId).then( | ||
185 | + function success(tenant) { | ||
186 | + var tenantName = tenant.name; | ||
187 | + var result = { | ||
188 | + data: [], | ||
189 | + nextPageLink: pageLink, | ||
190 | + hasNext: false | ||
191 | + }; | ||
192 | + if (tenantName.toLowerCase().startsWith(pageLink.textSearch)) { | ||
193 | + result.data.push(tenant); | ||
194 | + } | ||
195 | + deferred.resolve(result); | ||
196 | + }, | ||
197 | + function fail() { | ||
198 | + deferred.reject(); | ||
199 | + } | ||
200 | + ); | ||
201 | + return deferred.promise; | ||
202 | + } | ||
203 | + | ||
204 | + function getSingleCustomerByPageLinkPromise(pageLink) { | ||
205 | + var user = userService.getCurrentUser(); | ||
206 | + var customerId = user.customerId; | ||
207 | + var deferred = $q.defer(); | ||
208 | + customerService.getCustomer(customerId).then( | ||
209 | + function success(customer) { | ||
210 | + var customerName = customer.name; | ||
211 | + var result = { | ||
212 | + data: [], | ||
213 | + nextPageLink: pageLink, | ||
214 | + hasNext: false | ||
215 | + }; | ||
216 | + if (customerName.toLowerCase().startsWith(pageLink.textSearch)) { | ||
217 | + result.data.push(customer); | ||
218 | + } | ||
219 | + deferred.resolve(result); | ||
220 | + }, | ||
221 | + function fail() { | ||
222 | + deferred.reject(); | ||
223 | + } | ||
224 | + ); | ||
225 | + return deferred.promise; | ||
226 | + } | ||
227 | + | ||
179 | function getEntitiesByPageLinkPromise(entityType, pageLink, config, subType) { | 228 | function getEntitiesByPageLinkPromise(entityType, pageLink, config, subType) { |
180 | var promise; | 229 | var promise; |
181 | var user = userService.getCurrentUser(); | 230 | var user = userService.getCurrentUser(); |
@@ -196,10 +245,18 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device | @@ -196,10 +245,18 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device | ||
196 | } | 245 | } |
197 | break; | 246 | break; |
198 | case types.entityType.tenant: | 247 | case types.entityType.tenant: |
199 | - promise = tenantService.getTenants(pageLink); | 248 | + if (user.authority === 'TENANT_ADMIN') { |
249 | + promise = getSingleTenantByPageLinkPromise(pageLink); | ||
250 | + } else { | ||
251 | + promise = tenantService.getTenants(pageLink); | ||
252 | + } | ||
200 | break; | 253 | break; |
201 | case types.entityType.customer: | 254 | case types.entityType.customer: |
202 | - promise = customerService.getCustomers(pageLink); | 255 | + if (user.authority === 'CUSTOMER_USER') { |
256 | + promise = getSingleCustomerByPageLinkPromise(pageLink); | ||
257 | + } else { | ||
258 | + promise = customerService.getCustomers(pageLink); | ||
259 | + } | ||
203 | break; | 260 | break; |
204 | case types.entityType.rule: | 261 | case types.entityType.rule: |
205 | promise = ruleService.getAllRules(pageLink); | 262 | promise = ruleService.getAllRules(pageLink); |
@@ -283,6 +340,16 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device | @@ -283,6 +340,16 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device | ||
283 | return { name: entity.name, entityType: entity.id.entityType, id: entity.id.id }; | 340 | return { name: entity.name, entityType: entity.id.entityType, id: entity.id.id }; |
284 | } | 341 | } |
285 | 342 | ||
343 | + function entityRelationInfoToEntityInfo(entityRelationInfo, direction) { | ||
344 | + var entityId = direction == types.entitySearchDirection.from ? entityRelationInfo.to : entityRelationInfo.from; | ||
345 | + var name = direction == types.entitySearchDirection.from ? entityRelationInfo.toName : entityRelationInfo.fromName; | ||
346 | + return { | ||
347 | + name: name, | ||
348 | + entityType: entityId.entityType, | ||
349 | + id: entityId.id | ||
350 | + }; | ||
351 | + } | ||
352 | + | ||
286 | function entitiesToEntitiesInfo(entities) { | 353 | function entitiesToEntitiesInfo(entities) { |
287 | var entitiesInfo = []; | 354 | var entitiesInfo = []; |
288 | for (var d = 0; d < entities.length; d++) { | 355 | for (var d = 0; d < entities.length; d++) { |
@@ -291,19 +358,26 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device | @@ -291,19 +358,26 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device | ||
291 | return entitiesInfo; | 358 | return entitiesInfo; |
292 | } | 359 | } |
293 | 360 | ||
361 | + function entityRelationInfosToEntitiesInfo(entityRelations, direction) { | ||
362 | + var entitiesInfo = []; | ||
363 | + for (var d = 0; d < entityRelations.length; d++) { | ||
364 | + entitiesInfo.push(entityRelationInfoToEntityInfo(entityRelations[d], direction)); | ||
365 | + } | ||
366 | + return entitiesInfo; | ||
367 | + } | ||
368 | + | ||
369 | + | ||
294 | function resolveAlias(entityAlias, stateParams) { | 370 | function resolveAlias(entityAlias, stateParams) { |
295 | var deferred = $q.defer(); | 371 | var deferred = $q.defer(); |
296 | var filter = entityAlias.filter; | 372 | var filter = entityAlias.filter; |
297 | resolveAliasFilter(filter, stateParams, -1).then( | 373 | resolveAliasFilter(filter, stateParams, -1).then( |
298 | function (result) { | 374 | function (result) { |
299 | - var entities = result.entities; | ||
300 | var aliasInfo = { | 375 | var aliasInfo = { |
301 | alias: entityAlias.alias, | 376 | alias: entityAlias.alias, |
302 | stateEntity: result.stateEntity, | 377 | stateEntity: result.stateEntity, |
303 | resolveMultiple: filter.resolveMultiple | 378 | resolveMultiple: filter.resolveMultiple |
304 | }; | 379 | }; |
305 | - var resolvedEntities = entitiesToEntitiesInfo(entities); | ||
306 | - aliasInfo.resolvedEntities = resolvedEntities; | 380 | + aliasInfo.resolvedEntities = result.entities; |
307 | aliasInfo.currentEntity = null; | 381 | aliasInfo.currentEntity = null; |
308 | if (aliasInfo.resolvedEntities.length) { | 382 | if (aliasInfo.resolvedEntities.length) { |
309 | aliasInfo.currentEntity = aliasInfo.resolvedEntities[0]; | 383 | aliasInfo.currentEntity = aliasInfo.resolvedEntities[0]; |
@@ -328,7 +402,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device | @@ -328,7 +402,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device | ||
328 | getEntities(filter.entityType, filter.entityList).then( | 402 | getEntities(filter.entityType, filter.entityList).then( |
329 | function success(entities) { | 403 | function success(entities) { |
330 | if (entities && entities.length) { | 404 | if (entities && entities.length) { |
331 | - result.entities = entities; | 405 | + result.entities = entitiesToEntitiesInfo(entities); |
332 | deferred.resolve(result); | 406 | deferred.resolve(result); |
333 | } else { | 407 | } else { |
334 | deferred.reject(); | 408 | deferred.reject(); |
@@ -343,7 +417,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device | @@ -343,7 +417,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device | ||
343 | getEntitiesByNameFilter(filter.entityType, filter.entityNameFilter, maxItems).then( | 417 | getEntitiesByNameFilter(filter.entityType, filter.entityNameFilter, maxItems).then( |
344 | function success(entities) { | 418 | function success(entities) { |
345 | if (entities && entities.length) { | 419 | if (entities && entities.length) { |
346 | - result.entities = entities; | 420 | + result.entities = entitiesToEntitiesInfo(entities); |
347 | deferred.resolve(result); | 421 | deferred.resolve(result); |
348 | } else { | 422 | } else { |
349 | deferred.reject(); | 423 | deferred.reject(); |
@@ -359,7 +433,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device | @@ -359,7 +433,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device | ||
359 | if (stateParams && stateParams.entityId) { | 433 | if (stateParams && stateParams.entityId) { |
360 | getEntity(stateParams.entityId.entityType, stateParams.entityId.id).then( | 434 | getEntity(stateParams.entityId.entityType, stateParams.entityId.id).then( |
361 | function success(entity) { | 435 | function success(entity) { |
362 | - result.entities = [entity]; | 436 | + result.entities = entitiesToEntitiesInfo([entity]); |
363 | deferred.resolve(result); | 437 | deferred.resolve(result); |
364 | }, | 438 | }, |
365 | function fail() { | 439 | function fail() { |
@@ -374,7 +448,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device | @@ -374,7 +448,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device | ||
374 | getEntitiesByNameFilter(types.entityType.asset, filter.assetNameFilter, maxItems, null, filter.assetType).then( | 448 | getEntitiesByNameFilter(types.entityType.asset, filter.assetNameFilter, maxItems, null, filter.assetType).then( |
375 | function success(entities) { | 449 | function success(entities) { |
376 | if (entities && entities.length) { | 450 | if (entities && entities.length) { |
377 | - result.entities = entities; | 451 | + result.entities = entitiesToEntitiesInfo(entities); |
378 | deferred.resolve(result); | 452 | deferred.resolve(result); |
379 | } else { | 453 | } else { |
380 | deferred.reject(); | 454 | deferred.reject(); |
@@ -389,7 +463,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device | @@ -389,7 +463,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device | ||
389 | getEntitiesByNameFilter(types.entityType.device, filter.deviceNameFilter, maxItems, null, filter.deviceType).then( | 463 | getEntitiesByNameFilter(types.entityType.device, filter.deviceNameFilter, maxItems, null, filter.deviceType).then( |
390 | function success(entities) { | 464 | function success(entities) { |
391 | if (entities && entities.length) { | 465 | if (entities && entities.length) { |
392 | - result.entities = entities; | 466 | + result.entities = entitiesToEntitiesInfo(entities); |
393 | deferred.resolve(result); | 467 | deferred.resolve(result); |
394 | } else { | 468 | } else { |
395 | deferred.reject(); | 469 | deferred.reject(); |
@@ -400,8 +474,97 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device | @@ -400,8 +474,97 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device | ||
400 | } | 474 | } |
401 | ); | 475 | ); |
402 | break; | 476 | break; |
403 | - | ||
404 | - //TODO: Alias filter | 477 | + case types.aliasFilterType.relationsQuery.value: |
478 | + result.stateEntity = filter.rootStateEntity; | ||
479 | + var rootEntityType; | ||
480 | + var rootEntityId; | ||
481 | + if (result.stateEntity && stateParams && stateParams.entityId) { | ||
482 | + rootEntityType = stateParams.entityId.entityType; | ||
483 | + rootEntityId = stateParams.entityId.id; | ||
484 | + } else if (!result.stateEntity) { | ||
485 | + rootEntityType = filter.rootEntity.entityType; | ||
486 | + rootEntityId = filter.rootEntity.id; | ||
487 | + } | ||
488 | + if (rootEntityType && rootEntityId) { | ||
489 | + var searchQuery = { | ||
490 | + parameters: { | ||
491 | + rootId: rootEntityId, | ||
492 | + rootType: rootEntityType, | ||
493 | + direction: filter.direction | ||
494 | + }, | ||
495 | + filters: filter.filters | ||
496 | + }; | ||
497 | + searchQuery.parameters.maxLevel = filter.maxLevel && filter.maxLevel > 0 ? filter.maxLevel : -1; | ||
498 | + entityRelationService.findInfoByQuery(searchQuery).then( | ||
499 | + function success(allRelations) { | ||
500 | + if (allRelations && allRelations.length) { | ||
501 | + if (angular.isDefined(maxItems) && maxItems > 0) { | ||
502 | + var limit = Math.min(allRelations.length, maxItems); | ||
503 | + allRelations.length = limit; | ||
504 | + } | ||
505 | + result.entities = entityRelationInfosToEntitiesInfo(allRelations, filter.direction); | ||
506 | + deferred.resolve(result); | ||
507 | + } else { | ||
508 | + deferred.reject(); | ||
509 | + } | ||
510 | + }, | ||
511 | + function fail() { | ||
512 | + deferred.reject(); | ||
513 | + } | ||
514 | + ); | ||
515 | + } else { | ||
516 | + deferred.resolve(result); | ||
517 | + } | ||
518 | + break; | ||
519 | + case types.aliasFilterType.assetSearchQuery.value: | ||
520 | + case types.aliasFilterType.deviceSearchQuery.value: | ||
521 | + result.stateEntity = filter.rootStateEntity; | ||
522 | + if (result.stateEntity && stateParams && stateParams.entityId) { | ||
523 | + rootEntityType = stateParams.entityId.entityType; | ||
524 | + rootEntityId = stateParams.entityId.id; | ||
525 | + } else if (!result.stateEntity) { | ||
526 | + rootEntityType = filter.rootEntity.entityType; | ||
527 | + rootEntityId = filter.rootEntity.id; | ||
528 | + } | ||
529 | + if (rootEntityType && rootEntityId) { | ||
530 | + searchQuery = { | ||
531 | + parameters: { | ||
532 | + rootId: rootEntityId, | ||
533 | + rootType: rootEntityType, | ||
534 | + direction: filter.direction | ||
535 | + }, | ||
536 | + relationType: filter.relationType | ||
537 | + }; | ||
538 | + searchQuery.parameters.maxLevel = filter.maxLevel && filter.maxLevel > 0 ? filter.maxLevel : -1; | ||
539 | + var findByQueryPromise; | ||
540 | + if (filter.type == types.aliasFilterType.assetSearchQuery.value) { | ||
541 | + searchQuery.assetTypes = filter.assetTypes; | ||
542 | + findByQueryPromise = assetService.findByQuery(searchQuery, false); | ||
543 | + } else if (filter.type == types.aliasFilterType.deviceSearchQuery.value) { | ||
544 | + searchQuery.deviceTypes = filter.deviceTypes; | ||
545 | + findByQueryPromise = deviceService.findByQuery(searchQuery, false); | ||
546 | + } | ||
547 | + findByQueryPromise.then( | ||
548 | + function success(entities) { | ||
549 | + if (entities && entities.length) { | ||
550 | + if (angular.isDefined(maxItems) && maxItems > 0) { | ||
551 | + var limit = Math.min(entities.length, maxItems); | ||
552 | + entities.length = limit; | ||
553 | + } | ||
554 | + result.entities = entitiesToEntitiesInfo(entities); | ||
555 | + deferred.resolve(result); | ||
556 | + } else { | ||
557 | + deferred.reject(); | ||
558 | + } | ||
559 | + }, | ||
560 | + function fail() { | ||
561 | + deferred.reject(); | ||
562 | + } | ||
563 | + ); | ||
564 | + } else { | ||
565 | + deferred.resolve(result); | ||
566 | + } | ||
567 | + break; | ||
405 | } | 568 | } |
406 | return deferred.promise; | 569 | return deferred.promise; |
407 | } | 570 | } |
@@ -420,9 +583,33 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device | @@ -420,9 +583,33 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device | ||
420 | return entityTypes.indexOf(types.entityType.asset) > -1 ? true : false; | 583 | return entityTypes.indexOf(types.entityType.asset) > -1 ? true : false; |
421 | case types.aliasFilterType.deviceType.value: | 584 | case types.aliasFilterType.deviceType.value: |
422 | return entityTypes.indexOf(types.entityType.device) > -1 ? true : false; | 585 | return entityTypes.indexOf(types.entityType.device) > -1 ? true : false; |
586 | + case types.aliasFilterType.relationsQuery.value: | ||
587 | + if (filter.filters && filter.filters.length) { | ||
588 | + var match = false; | ||
589 | + for (var f=0;f<filter.filters.length;f++) { | ||
590 | + var relationFilter = filter.filters[f]; | ||
591 | + if (relationFilter.entityTypes && relationFilter.entityTypes.length) { | ||
592 | + for (var et=0;et<relationFilter.entityTypes.length;et++) { | ||
593 | + if (entityTypes.indexOf(relationFilter.entityTypes[et]) > -1) { | ||
594 | + match = true; | ||
595 | + break; | ||
596 | + } | ||
597 | + } | ||
598 | + } else { | ||
599 | + match = true; | ||
600 | + break; | ||
601 | + } | ||
602 | + } | ||
603 | + return match; | ||
604 | + } else { | ||
605 | + return true; | ||
606 | + } | ||
607 | + case types.aliasFilterType.assetSearchQuery.value: | ||
608 | + return entityTypes.indexOf(types.entityType.asset) > -1 ? true : false; | ||
609 | + case types.aliasFilterType.deviceSearchQuery.value: | ||
610 | + return entityTypes.indexOf(types.entityType.device) > -1 ? true : false; | ||
423 | } | 611 | } |
424 | } | 612 | } |
425 | - //TODO: Alias filter | ||
426 | return false; | 613 | return false; |
427 | } | 614 | } |
428 | 615 | ||
@@ -474,6 +661,42 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device | @@ -474,6 +661,42 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device | ||
474 | return result; | 661 | return result; |
475 | } | 662 | } |
476 | 663 | ||
664 | + function prepareAllowedEntityTypesList(allowedEntityTypes) { | ||
665 | + var authority = userService.getAuthority(); | ||
666 | + var entityTypes = {}; | ||
667 | + switch(authority) { | ||
668 | + case 'SYS_ADMIN': | ||
669 | + entityTypes.tenant = types.entityType.tenant; | ||
670 | + entityTypes.rule = types.entityType.rule; | ||
671 | + entityTypes.plugin = types.entityType.plugin; | ||
672 | + break; | ||
673 | + case 'TENANT_ADMIN': | ||
674 | + entityTypes.device = types.entityType.device; | ||
675 | + entityTypes.asset = types.entityType.asset; | ||
676 | + entityTypes.tenant = types.entityType.tenant; | ||
677 | + entityTypes.customer = types.entityType.customer; | ||
678 | + entityTypes.rule = types.entityType.rule; | ||
679 | + entityTypes.plugin = types.entityType.plugin; | ||
680 | + entityTypes.dashboard = types.entityType.dashboard; | ||
681 | + break; | ||
682 | + case 'CUSTOMER_USER': | ||
683 | + entityTypes.device = types.entityType.device; | ||
684 | + entityTypes.asset = types.entityType.asset; | ||
685 | + entityTypes.customer = types.entityType.customer; | ||
686 | + entityTypes.dashboard = types.entityType.dashboard; | ||
687 | + break; | ||
688 | + } | ||
689 | + | ||
690 | + if (allowedEntityTypes) { | ||
691 | + for (var entityType in entityTypes) { | ||
692 | + if (allowedEntityTypes.indexOf(entityTypes[entityType]) === -1) { | ||
693 | + delete entityTypes[entityType]; | ||
694 | + } | ||
695 | + } | ||
696 | + } | ||
697 | + return entityTypes; | ||
698 | + } | ||
699 | + | ||
477 | 700 | ||
478 | function checkEntityAlias(entityAlias) { | 701 | function checkEntityAlias(entityAlias) { |
479 | var deferred = $q.defer(); | 702 | var deferred = $q.defer(); |
@@ -146,46 +146,55 @@ export default angular.module('thingsboard.types', []) | @@ -146,46 +146,55 @@ export default angular.module('thingsboard.types', []) | ||
146 | entityTypeTranslations: { | 146 | entityTypeTranslations: { |
147 | "DEVICE": { | 147 | "DEVICE": { |
148 | type: 'entity.type-device', | 148 | type: 'entity.type-device', |
149 | + typePlural: 'entity.type-devices', | ||
149 | list: 'entity.list-of-devices', | 150 | list: 'entity.list-of-devices', |
150 | nameStartsWith: 'entity.device-name-starts-with' | 151 | nameStartsWith: 'entity.device-name-starts-with' |
151 | }, | 152 | }, |
152 | "ASSET": { | 153 | "ASSET": { |
153 | type: 'entity.type-asset', | 154 | type: 'entity.type-asset', |
155 | + typePlural: 'entity.type-assets', | ||
154 | list: 'entity.list-of-assets', | 156 | list: 'entity.list-of-assets', |
155 | nameStartsWith: 'entity.asset-name-starts-with' | 157 | nameStartsWith: 'entity.asset-name-starts-with' |
156 | }, | 158 | }, |
157 | "RULE": { | 159 | "RULE": { |
158 | type: 'entity.type-rule', | 160 | type: 'entity.type-rule', |
161 | + typePlural: 'entity.type-rules', | ||
159 | list: 'entity.list-of-rules', | 162 | list: 'entity.list-of-rules', |
160 | nameStartsWith: 'entity.rule-name-starts-with' | 163 | nameStartsWith: 'entity.rule-name-starts-with' |
161 | }, | 164 | }, |
162 | "PLUGIN": { | 165 | "PLUGIN": { |
163 | type: 'entity.type-plugin', | 166 | type: 'entity.type-plugin', |
167 | + typePlural: 'entity.type-plugins', | ||
164 | list: 'entity.list-of-plugins', | 168 | list: 'entity.list-of-plugins', |
165 | nameStartsWith: 'entity.plugin-name-starts-with' | 169 | nameStartsWith: 'entity.plugin-name-starts-with' |
166 | }, | 170 | }, |
167 | "TENANT": { | 171 | "TENANT": { |
168 | type: 'entity.type-tenant', | 172 | type: 'entity.type-tenant', |
173 | + typePlural: 'entity.type-tenants', | ||
169 | list: 'entity.list-of-tenants', | 174 | list: 'entity.list-of-tenants', |
170 | nameStartsWith: 'entity.tenant-name-starts-with' | 175 | nameStartsWith: 'entity.tenant-name-starts-with' |
171 | }, | 176 | }, |
172 | "CUSTOMER": { | 177 | "CUSTOMER": { |
173 | type: 'entity.type-customer', | 178 | type: 'entity.type-customer', |
179 | + typePlural: 'entity.type-customers', | ||
174 | list: 'entity.list-of-customers', | 180 | list: 'entity.list-of-customers', |
175 | nameStartsWith: 'entity.customer-name-starts-with' | 181 | nameStartsWith: 'entity.customer-name-starts-with' |
176 | }, | 182 | }, |
177 | "USER": { | 183 | "USER": { |
178 | type: 'entity.type-user', | 184 | type: 'entity.type-user', |
185 | + typePlural: 'entity.type-users', | ||
179 | list: 'entity.list-of-users', | 186 | list: 'entity.list-of-users', |
180 | nameStartsWith: 'entity.user-name-starts-with' | 187 | nameStartsWith: 'entity.user-name-starts-with' |
181 | }, | 188 | }, |
182 | "DASHBOARD": { | 189 | "DASHBOARD": { |
183 | type: 'entity.type-dashboard', | 190 | type: 'entity.type-dashboard', |
191 | + typePlural: 'entity.type-dashboards', | ||
184 | list: 'entity.list-of-dashboards', | 192 | list: 'entity.list-of-dashboards', |
185 | nameStartsWith: 'entity.dashboard-name-starts-with' | 193 | nameStartsWith: 'entity.dashboard-name-starts-with' |
186 | }, | 194 | }, |
187 | "ALARM": { | 195 | "ALARM": { |
188 | type: 'entity.type-alarm', | 196 | type: 'entity.type-alarm', |
197 | + typePlural: 'entity.type-alarms', | ||
189 | list: 'entity.list-of-alarms', | 198 | list: 'entity.list-of-alarms', |
190 | nameStartsWith: 'entity.alarm-name-starts-with' | 199 | nameStartsWith: 'entity.alarm-name-starts-with' |
191 | } | 200 | } |
@@ -71,6 +71,13 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document, | @@ -71,6 +71,13 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document, | ||
71 | } | 71 | } |
72 | }, true); | 72 | }, true); |
73 | 73 | ||
74 | + scope.$watch('datasourceName', function () { | ||
75 | + if (ngModelCtrl.$viewValue) { | ||
76 | + ngModelCtrl.$viewValue.name = scope.datasourceName; | ||
77 | + scope.updateValidity(); | ||
78 | + } | ||
79 | + }); | ||
80 | + | ||
74 | ngModelCtrl.$render = function () { | 81 | ngModelCtrl.$render = function () { |
75 | if (ngModelCtrl.$viewValue) { | 82 | if (ngModelCtrl.$viewValue) { |
76 | var funcDataKeys = []; | 83 | var funcDataKeys = []; |
@@ -78,6 +85,7 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document, | @@ -78,6 +85,7 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document, | ||
78 | funcDataKeys = funcDataKeys.concat(ngModelCtrl.$viewValue.dataKeys); | 85 | funcDataKeys = funcDataKeys.concat(ngModelCtrl.$viewValue.dataKeys); |
79 | } | 86 | } |
80 | scope.funcDataKeys = funcDataKeys; | 87 | scope.funcDataKeys = funcDataKeys; |
88 | + scope.datasourceName = ngModelCtrl.$viewValue.name; | ||
81 | } | 89 | } |
82 | }; | 90 | }; |
83 | 91 |
@@ -15,23 +15,29 @@ | @@ -15,23 +15,29 @@ | ||
15 | */ | 15 | */ |
16 | @import '../../scss/constants'; | 16 | @import '../../scss/constants'; |
17 | 17 | ||
18 | -.tb-func-datakey-autocomplete { | ||
19 | - .tb-not-found { | ||
20 | - display: block; | ||
21 | - line-height: 1.5; | ||
22 | - height: 48px; | ||
23 | - .tb-no-entries { | ||
24 | - line-height: 48px; | ||
25 | - } | 18 | +.tb-datasource-func { |
19 | + @media (min-width: $layout-breakpoint-gt-sm) { | ||
20 | + padding-left: 8px; | ||
26 | } | 21 | } |
27 | - li { | ||
28 | - height: auto !important; | ||
29 | - white-space: normal !important; | 22 | + |
23 | + md-input-container.tb-datasource-name { | ||
24 | + .md-errors-spacer { | ||
25 | + display: none; | ||
26 | + } | ||
30 | } | 27 | } |
31 | -} | ||
32 | 28 | ||
33 | -tb-datasource-func { | ||
34 | - @media (min-width: $layout-breakpoint-gt-sm) { | ||
35 | - padding-left: 8px; | 29 | + .tb-func-datakey-autocomplete { |
30 | + .tb-not-found { | ||
31 | + display: block; | ||
32 | + line-height: 1.5; | ||
33 | + height: 48px; | ||
34 | + .tb-no-entries { | ||
35 | + line-height: 48px; | ||
36 | + } | ||
37 | + } | ||
38 | + li { | ||
39 | + height: auto !important; | ||
40 | + white-space: normal !important; | ||
41 | + } | ||
36 | } | 42 | } |
37 | -} | ||
43 | +} |
@@ -15,59 +15,68 @@ | @@ -15,59 +15,68 @@ | ||
15 | limitations under the License. | 15 | limitations under the License. |
16 | 16 | ||
17 | --> | 17 | --> |
18 | -<section flex layout='column' style="padding-left: 4px;"> | ||
19 | - <md-chips flex | ||
20 | - id="function_datakey_chips" | ||
21 | - ng-required="true" | ||
22 | - ng-model="funcDataKeys" md-autocomplete-snap | ||
23 | - md-transform-chip="transformDataKeyChip($chip)" | ||
24 | - md-require-match="false"> | ||
25 | - <md-autocomplete | ||
26 | - md-no-cache="false" | ||
27 | - id="dataKey" | ||
28 | - md-selected-item="selectedDataKey" | ||
29 | - md-search-text="dataKeySearchText" | ||
30 | - md-items="item in dataKeysSearch(dataKeySearchText)" | ||
31 | - md-item-text="item.name" | ||
32 | - md-min-length="0" | ||
33 | - placeholder="{{ 'datakey.function-types' | translate }}" | ||
34 | - md-menu-class="tb-func-datakey-autocomplete"> | ||
35 | - <span md-highlight-text="dataKeySearchText" md-highlight-flags="^i">{{item}}</span> | ||
36 | - <md-not-found> | ||
37 | - <div class="tb-not-found"> | ||
38 | - <div class="tb-no-entries" ng-if="!textIsNotEmpty(dataKeySearchText)"> | ||
39 | - <span translate>device.no-keys-found</span> | 18 | +<section class="tb-datasource-func" flex layout='column' |
19 | + layout-align="center" layout-gt-sm='row' layout-align-gt-sm="start center"> | ||
20 | + <md-input-container class="tb-datasource-name" md-no-float style="min-width: 200px;"> | ||
21 | + <input name="datasourceName" | ||
22 | + placeholder="{{ 'datasource.name' | translate }}" | ||
23 | + ng-model="datasourceName" | ||
24 | + aria-label="{{ 'datasource.name' | translate }}"> | ||
25 | + </md-input-container> | ||
26 | + <section flex layout='column' style="padding-left: 4px;"> | ||
27 | + <md-chips flex | ||
28 | + id="function_datakey_chips" | ||
29 | + ng-required="true" | ||
30 | + ng-model="funcDataKeys" md-autocomplete-snap | ||
31 | + md-transform-chip="transformDataKeyChip($chip)" | ||
32 | + md-require-match="false"> | ||
33 | + <md-autocomplete | ||
34 | + md-no-cache="false" | ||
35 | + id="dataKey" | ||
36 | + md-selected-item="selectedDataKey" | ||
37 | + md-search-text="dataKeySearchText" | ||
38 | + md-items="item in dataKeysSearch(dataKeySearchText)" | ||
39 | + md-item-text="item.name" | ||
40 | + md-min-length="0" | ||
41 | + placeholder="{{ 'datakey.function-types' | translate }}" | ||
42 | + md-menu-class="tb-func-datakey-autocomplete"> | ||
43 | + <span md-highlight-text="dataKeySearchText" md-highlight-flags="^i">{{item}}</span> | ||
44 | + <md-not-found> | ||
45 | + <div class="tb-not-found"> | ||
46 | + <div class="tb-no-entries" ng-if="!textIsNotEmpty(dataKeySearchText)"> | ||
47 | + <span translate>device.no-keys-found</span> | ||
48 | + </div> | ||
49 | + <div ng-if="textIsNotEmpty(dataKeySearchText)"> | ||
50 | + <span translate translate-values='{ key: "{{dataKeySearchText | truncate:true:6:'...'}}" }'>device.no-key-matching</span> | ||
51 | + <span> | ||
52 | + <a translate ng-click="createKey($event, '#function_datakey_chips')">device.create-new-key</a> | ||
53 | + </span> | ||
54 | + </div> | ||
40 | </div> | 55 | </div> |
41 | - <div ng-if="textIsNotEmpty(dataKeySearchText)"> | ||
42 | - <span translate translate-values='{ key: "{{dataKeySearchText | truncate:true:6:'...'}}" }'>device.no-key-matching</span> | ||
43 | - <span> | ||
44 | - <a translate ng-click="createKey($event, '#function_datakey_chips')">device.create-new-key</a> | ||
45 | - </span> | ||
46 | - </div> | ||
47 | - </div> | ||
48 | - </md-not-found> | ||
49 | - </md-autocomplete> | ||
50 | - <md-chip-template> | ||
51 | - <div layout="row" layout-align="start center" class="tb-attribute-chip"> | ||
52 | - <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;"> | ||
53 | - <div class="tb-color-result" ng-style="{background: $chip.color}"></div> | ||
54 | - </div> | ||
55 | - <div layout="row" flex> | ||
56 | - <div class="tb-chip-label"> | ||
57 | - {{$chip.label}} | 56 | + </md-not-found> |
57 | + </md-autocomplete> | ||
58 | + <md-chip-template> | ||
59 | + <div layout="row" layout-align="start center" class="tb-attribute-chip"> | ||
60 | + <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;"> | ||
61 | + <div class="tb-color-result" ng-style="{background: $chip.color}"></div> | ||
58 | </div> | 62 | </div> |
59 | - <div class="tb-chip-separator">: </div> | ||
60 | - <div class="tb-chip-label"> | ||
61 | - <strong>{{$chip.name}}</strong> | 63 | + <div layout="row" flex> |
64 | + <div class="tb-chip-label"> | ||
65 | + {{$chip.label}} | ||
66 | + </div> | ||
67 | + <div class="tb-chip-separator">: </div> | ||
68 | + <div class="tb-chip-label"> | ||
69 | + <strong>{{$chip.name}}</strong> | ||
70 | + </div> | ||
62 | </div> | 71 | </div> |
72 | + <md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32"> | ||
73 | + <md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon> | ||
74 | + </md-button> | ||
63 | </div> | 75 | </div> |
64 | - <md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32"> | ||
65 | - <md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon> | ||
66 | - </md-button> | ||
67 | - </div> | ||
68 | - </md-chip-template> | ||
69 | - </md-chips> | ||
70 | - <div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert"> | ||
71 | - <div translate ng-message="funcTypes" class="tb-error-message">datakey.function-types-required</div> | ||
72 | - </div> | 76 | + </md-chip-template> |
77 | + </md-chips> | ||
78 | + <div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert"> | ||
79 | + <div translate ng-message="funcTypes" class="tb-error-message">datakey.function-types-required</div> | ||
80 | + </div> | ||
81 | + </section> | ||
73 | </section> | 82 | </section> |
@@ -15,7 +15,7 @@ | @@ -15,7 +15,7 @@ | ||
15 | */ | 15 | */ |
16 | /* eslint-disable import/no-unresolved, import/default */ | 16 | /* eslint-disable import/no-unresolved, import/default */ |
17 | 17 | ||
18 | -import entityAliasDialogTemplate from '../entity/entity-alias-dialog.tpl.html'; | 18 | +import entityAliasDialogTemplate from '../entity/alias/entity-alias-dialog.tpl.html'; |
19 | 19 | ||
20 | /* eslint-enable import/no-unresolved, import/default */ | 20 | /* eslint-enable import/no-unresolved, import/default */ |
21 | 21 |
@@ -15,7 +15,7 @@ | @@ -15,7 +15,7 @@ | ||
15 | */ | 15 | */ |
16 | /* eslint-disable import/no-unresolved, import/default */ | 16 | /* eslint-disable import/no-unresolved, import/default */ |
17 | 17 | ||
18 | -import entityAliasesTemplate from '../entity/entity-aliases.tpl.html'; | 18 | +import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html'; |
19 | import dashboardSettingsTemplate from './dashboard-settings.tpl.html'; | 19 | import dashboardSettingsTemplate from './dashboard-settings.tpl.html'; |
20 | import manageDashboardLayoutsTemplate from './layouts/manage-dashboard-layouts.tpl.html'; | 20 | import manageDashboardLayoutsTemplate from './layouts/manage-dashboard-layouts.tpl.html'; |
21 | import manageDashboardStatesTemplate from './states/manage-dashboard-states.tpl.html'; | 21 | import manageDashboardStatesTemplate from './states/manage-dashboard-states.tpl.html'; |
@@ -15,7 +15,7 @@ | @@ -15,7 +15,7 @@ | ||
15 | */ | 15 | */ |
16 | /* eslint-disable import/no-unresolved, import/default */ | 16 | /* eslint-disable import/no-unresolved, import/default */ |
17 | 17 | ||
18 | -import entityAliasDialogTemplate from '../entity/entity-alias-dialog.tpl.html'; | 18 | +import entityAliasDialogTemplate from '../entity/alias/entity-alias-dialog.tpl.html'; |
19 | import editWidgetTemplate from './edit-widget.tpl.html'; | 19 | import editWidgetTemplate from './edit-widget.tpl.html'; |
20 | 20 | ||
21 | /* eslint-enable import/no-unresolved, import/default */ | 21 | /* eslint-enable import/no-unresolved, import/default */ |
ui/src/app/entity/alias/aliases-entity-select-button.tpl.html
renamed from
ui/src/app/entity/aliases-entity-select-button.tpl.html
ui/src/app/entity/alias/aliases-entity-select-panel.controller.js
renamed from
ui/src/app/entity/aliases-entity-select-panel.controller.js
ui/src/app/entity/alias/aliases-entity-select-panel.tpl.html
renamed from
ui/src/app/entity/aliases-entity-select-panel.tpl.html
ui/src/app/entity/alias/aliases-entity-select.directive.js
renamed from
ui/src/app/entity/aliases-entity-select.directive.js
ui/src/app/entity/alias/aliases-entity-select.scss
renamed from
ui/src/app/entity/aliases-entity-select.scss
ui/src/app/entity/alias/entity-alias-dialog.controller.js
renamed from
ui/src/app/entity/entity-alias-dialog.controller.js
ui/src/app/entity/alias/entity-alias-dialog.scss
renamed from
ui/src/app/entity/entity-alias-dialog.scss
ui/src/app/entity/alias/entity-alias-dialog.tpl.html
renamed from
ui/src/app/entity/entity-alias-dialog.tpl.html
ui/src/app/entity/alias/entity-aliases.controller.js
renamed from
ui/src/app/entity/entity-aliases.controller.js
ui/src/app/entity/alias/entity-aliases.scss
renamed from
ui/src/app/entity/entity-aliases.scss
ui/src/app/entity/alias/entity-aliases.tpl.html
renamed from
ui/src/app/entity/entity-aliases.tpl.html
@@ -74,7 +74,106 @@ export default function EntityFilterViewDirective($compile, $templateCache, $q, | @@ -74,7 +74,106 @@ export default function EntityFilterViewDirective($compile, $templateCache, $q, | ||
74 | scope.filterDisplayValue = $translate.instant('alias.filter-type-device-type-description', {deviceType: deviceType}); | 74 | scope.filterDisplayValue = $translate.instant('alias.filter-type-device-type-description', {deviceType: deviceType}); |
75 | } | 75 | } |
76 | break; | 76 | break; |
77 | - //TODO: Alias filter | 77 | + case types.aliasFilterType.relationsQuery.value: |
78 | + var rootEntityText; | ||
79 | + var directionText; | ||
80 | + var allEntitiesText = $translate.instant('alias.all-entities'); | ||
81 | + var anyRelationText = $translate.instant('alias.any-relation'); | ||
82 | + if (scope.filter.rootStateEntity) { | ||
83 | + rootEntityText = $translate.instant('alias.state-entity'); | ||
84 | + } else { | ||
85 | + rootEntityText = $translate.instant(types.entityTypeTranslations[scope.filter.rootEntity.entityType].type); | ||
86 | + } | ||
87 | + directionText = $translate.instant('relation.direction-type.' + scope.filter.direction); | ||
88 | + var relationFilters = scope.filter.filters; | ||
89 | + if (relationFilters && relationFilters.length) { | ||
90 | + var relationFiltersDisplayValues = []; | ||
91 | + relationFilters.forEach(function(relationFilter) { | ||
92 | + var entitiesText; | ||
93 | + if (relationFilter.entityTypes && relationFilter.entityTypes.length) { | ||
94 | + var entitiesNamesList = []; | ||
95 | + relationFilter.entityTypes.forEach(function(entityType) { | ||
96 | + entitiesNamesList.push( | ||
97 | + $translate.instant(types.entityTypeTranslations[entityType].typePlural) | ||
98 | + ); | ||
99 | + }); | ||
100 | + entitiesText = entitiesNamesList.join(', '); | ||
101 | + } else { | ||
102 | + entitiesText = allEntitiesText; | ||
103 | + } | ||
104 | + var relationTypeText; | ||
105 | + if (relationFilter.relationType && relationFilter.relationType.length) { | ||
106 | + relationTypeText = "'" + relationFilter.relationType + "'"; | ||
107 | + } else { | ||
108 | + relationTypeText = anyRelationText; | ||
109 | + } | ||
110 | + var relationFilterDisplayValue = $translate.instant('alias.filter-type-relations-query-description', | ||
111 | + { | ||
112 | + entities: entitiesText, | ||
113 | + relationType: relationTypeText, | ||
114 | + direction: directionText, | ||
115 | + rootEntity: rootEntityText | ||
116 | + } | ||
117 | + ); | ||
118 | + relationFiltersDisplayValues.push(relationFilterDisplayValue); | ||
119 | + }); | ||
120 | + scope.filterDisplayValue = relationFiltersDisplayValues.join(', '); | ||
121 | + } else { | ||
122 | + scope.filterDisplayValue = $translate.instant('alias.filter-type-relations-query-description', | ||
123 | + { | ||
124 | + entities: allEntitiesText, | ||
125 | + relationType: anyRelationText, | ||
126 | + direction: directionText, | ||
127 | + rootEntity: rootEntityText | ||
128 | + } | ||
129 | + ); | ||
130 | + } | ||
131 | + break; | ||
132 | + case types.aliasFilterType.assetSearchQuery.value: | ||
133 | + case types.aliasFilterType.deviceSearchQuery.value: | ||
134 | + allEntitiesText = $translate.instant('alias.all-entities'); | ||
135 | + anyRelationText = $translate.instant('alias.any-relation'); | ||
136 | + if (scope.filter.rootStateEntity) { | ||
137 | + rootEntityText = $translate.instant('alias.state-entity'); | ||
138 | + } else { | ||
139 | + rootEntityText = $translate.instant(types.entityTypeTranslations[scope.filter.rootEntity.entityType].type); | ||
140 | + } | ||
141 | + directionText = $translate.instant('relation.direction-type.' + scope.filter.direction); | ||
142 | + var relationTypeText; | ||
143 | + if (scope.filter.relationType && scope.filter.relationType.length) { | ||
144 | + relationTypeText = "'" + scope.filter.relationType + "'"; | ||
145 | + } else { | ||
146 | + relationTypeText = anyRelationText; | ||
147 | + } | ||
148 | + | ||
149 | + var translationValues = { | ||
150 | + relationType: relationTypeText, | ||
151 | + direction: directionText, | ||
152 | + rootEntity: rootEntityText | ||
153 | + } | ||
154 | + | ||
155 | + if (scope.filter.type == types.aliasFilterType.assetSearchQuery.value) { | ||
156 | + var assetTypesQuoted = []; | ||
157 | + scope.filter.assetTypes.forEach(function(assetType) { | ||
158 | + assetTypesQuoted.push("'"+assetType+"'"); | ||
159 | + }); | ||
160 | + var assetTypesText = assetTypesQuoted.join(', '); | ||
161 | + translationValues.assetTypes = assetTypesText; | ||
162 | + scope.filterDisplayValue = $translate.instant('alias.filter-type-asset-search-query-description', | ||
163 | + translationValues | ||
164 | + ); | ||
165 | + } else { | ||
166 | + var deviceTypesQuoted = []; | ||
167 | + scope.filter.deviceTypes.forEach(function(deviceType) { | ||
168 | + deviceTypesQuoted.push("'"+deviceType+"'"); | ||
169 | + }); | ||
170 | + var deviceTypesText = deviceTypesQuoted.join(', '); | ||
171 | + translationValues.deviceTypes = deviceTypesText; | ||
172 | + scope.filterDisplayValue = $translate.instant('alias.filter-type-device-search-query-description', | ||
173 | + translationValues | ||
174 | + ); | ||
175 | + } | ||
176 | + break; | ||
78 | default: | 177 | default: |
79 | scope.filterDisplayValue = scope.filter.type; | 178 | scope.filterDisplayValue = scope.filter.type; |
80 | break; | 179 | break; |
@@ -63,7 +63,23 @@ export default function EntityFilterDirective($compile, $templateCache, $q, $doc | @@ -63,7 +63,23 @@ export default function EntityFilterDirective($compile, $templateCache, $q, $doc | ||
63 | filter.deviceType = null; | 63 | filter.deviceType = null; |
64 | filter.deviceNameFilter = ''; | 64 | filter.deviceNameFilter = ''; |
65 | break; | 65 | break; |
66 | - //TODO: Alias filter | 66 | + case types.aliasFilterType.relationsQuery.value: |
67 | + case types.aliasFilterType.assetSearchQuery.value: | ||
68 | + case types.aliasFilterType.deviceSearchQuery.value: | ||
69 | + filter.rootStateEntity = false; | ||
70 | + filter.rootEntity = null; | ||
71 | + filter.direction = types.entitySearchDirection.from; | ||
72 | + filter.maxLevel = 1; | ||
73 | + if (filter.type === types.aliasFilterType.relationsQuery.value) { | ||
74 | + filter.filters = []; | ||
75 | + } else if (filter.type === types.aliasFilterType.assetSearchQuery.value) { | ||
76 | + filter.relationType = null; | ||
77 | + filter.assetTypes = []; | ||
78 | + } else if (filter.type === types.aliasFilterType.deviceSearchQuery.value) { | ||
79 | + filter.relationType = null; | ||
80 | + filter.deviceTypes = []; | ||
81 | + } | ||
82 | + break; | ||
67 | } | 83 | } |
68 | scope.filter = filter; | 84 | scope.filter = filter; |
69 | } | 85 | } |
@@ -16,4 +16,21 @@ | @@ -16,4 +16,21 @@ | ||
16 | 16 | ||
17 | .tb-entity-filter { | 17 | .tb-entity-filter { |
18 | 18 | ||
19 | + #relationsQueryFilter { | ||
20 | + padding-top: 20px; | ||
21 | + tb-entity-select { | ||
22 | + min-height: 92px; | ||
23 | + } | ||
24 | + } | ||
25 | + | ||
26 | + .tb-root-state-entity-switch { | ||
27 | + padding-left: 10px; | ||
28 | + .root-state-entity-switch { | ||
29 | + margin: 0; | ||
30 | + } | ||
31 | + .root-state-entity-label { | ||
32 | + margin: 5px 0; | ||
33 | + } | ||
34 | + } | ||
35 | + | ||
19 | } | 36 | } |
@@ -88,4 +88,146 @@ | @@ -88,4 +88,146 @@ | ||
88 | aria-label="{{ 'device.name-starts-with' | translate }}"> | 88 | aria-label="{{ 'device.name-starts-with' | translate }}"> |
89 | </md-input-container> | 89 | </md-input-container> |
90 | </section> | 90 | </section> |
91 | + <section layout="column" ng-if="filter.type == types.aliasFilterType.relationsQuery.value" id="relationsQueryFilter"> | ||
92 | + <label class="tb-small">{{ 'alias.root-entity' | translate }}</label> | ||
93 | + <div flex layout="row"> | ||
94 | + <tb-entity-select flex | ||
95 | + the-form="theForm" | ||
96 | + tb-required="!filter.rootStateEntity" | ||
97 | + ng-disabled="filter.rootStateEntity" | ||
98 | + ng-model="filter.rootEntity"> | ||
99 | + </tb-entity-select> | ||
100 | + <section class="tb-root-state-entity-switch" layout="column" layout-align="start center"> | ||
101 | + <label class="tb-small root-state-entity-label" translate>alias.root-state-entity</label> | ||
102 | + <md-switch class="root-state-entity-switch" ng-model="filter.rootStateEntity" | ||
103 | + aria-label="{{ 'alias.root-state-entity' | translate }}"> | ||
104 | + </md-switch> | ||
105 | + </section> | ||
106 | + </div> | ||
107 | + <div flex layout="row"> | ||
108 | + <md-input-container class="md-block" style="min-width: 100px;"> | ||
109 | + <label translate>relation.direction</label> | ||
110 | + <md-select required ng-model="filter.direction"> | ||
111 | + <md-option ng-repeat="direction in types.entitySearchDirection" ng-value="direction"> | ||
112 | + {{ ('relation.search-direction.' + direction) | translate}} | ||
113 | + </md-option> | ||
114 | + </md-select> | ||
115 | + </md-input-container> | ||
116 | + <md-input-container flex class="md-block"> | ||
117 | + <label translate>alias.max-relation-level</label> | ||
118 | + <input name="maxRelationLevel" | ||
119 | + type="number" | ||
120 | + min="1" | ||
121 | + step="1" | ||
122 | + placeholder="{{ 'alias.unlimited-level' | translate }}" | ||
123 | + ng-model="filter.maxLevel" | ||
124 | + aria-label="{{ 'alias.max-relation-level' | translate }}"> | ||
125 | + </md-input-container> | ||
126 | + </div> | ||
127 | + <div class="md-caption" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>relation.relation-filters</div> | ||
128 | + <tb-relation-filters | ||
129 | + ng-model="filter.filters" | ||
130 | + allowed-entity-types="allowedEntityTypes"> | ||
131 | + </tb-relation-filters> | ||
132 | + </section> | ||
133 | + <section layout="column" ng-if="filter.type == types.aliasFilterType.assetSearchQuery.value" id="assetSearchQueryFilter"> | ||
134 | + <label class="tb-small">{{ 'alias.root-entity' | translate }}</label> | ||
135 | + <div flex layout="row"> | ||
136 | + <tb-entity-select flex | ||
137 | + the-form="theForm" | ||
138 | + tb-required="!filter.rootStateEntity" | ||
139 | + ng-disabled="filter.rootStateEntity" | ||
140 | + ng-model="filter.rootEntity"> | ||
141 | + </tb-entity-select> | ||
142 | + <section class="tb-root-state-entity-switch" layout="column" layout-align="start center"> | ||
143 | + <label class="tb-small root-state-entity-label" translate>alias.root-state-entity</label> | ||
144 | + <md-switch class="root-state-entity-switch" ng-model="filter.rootStateEntity" | ||
145 | + aria-label="{{ 'alias.root-state-entity' | translate }}"> | ||
146 | + </md-switch> | ||
147 | + </section> | ||
148 | + </div> | ||
149 | + <div flex layout="row"> | ||
150 | + <md-input-container class="md-block" style="min-width: 100px;"> | ||
151 | + <label translate>relation.direction</label> | ||
152 | + <md-select required ng-model="filter.direction"> | ||
153 | + <md-option ng-repeat="direction in types.entitySearchDirection" ng-value="direction"> | ||
154 | + {{ ('relation.search-direction.' + direction) | translate}} | ||
155 | + </md-option> | ||
156 | + </md-select> | ||
157 | + </md-input-container> | ||
158 | + <md-input-container flex class="md-block"> | ||
159 | + <label translate>alias.max-relation-level</label> | ||
160 | + <input name="maxRelationLevel" | ||
161 | + type="number" | ||
162 | + min="1" | ||
163 | + step="1" | ||
164 | + placeholder="{{ 'alias.unlimited-level' | translate }}" | ||
165 | + ng-model="filter.maxLevel" | ||
166 | + aria-label="{{ 'alias.max-relation-level' | translate }}"> | ||
167 | + </md-input-container> | ||
168 | + </div> | ||
169 | + <div class="md-caption" style="color: rgba(0,0,0,0.57);" translate>relation.relation-type</div> | ||
170 | + <tb-relation-type-autocomplete flex | ||
171 | + hide-label | ||
172 | + the-form="theForm" | ||
173 | + ng-model="filter.relationType" | ||
174 | + tb-required="false"> | ||
175 | + </tb-relation-type-autocomplete> | ||
176 | + <div class="md-caption tb-required" style="color: rgba(0,0,0,0.57);" translate>asset.asset-types</div> | ||
177 | + <tb-entity-subtype-list | ||
178 | + tb-required="true" | ||
179 | + entity-type="types.entityType.asset" | ||
180 | + ng-model="filter.assetTypes"> | ||
181 | + </tb-entity-subtype-list> | ||
182 | + </section> | ||
183 | + <section layout="column" ng-if="filter.type == types.aliasFilterType.deviceSearchQuery.value" id="deviceSearchQueryFilter"> | ||
184 | + <label class="tb-small">{{ 'alias.root-entity' | translate }}</label> | ||
185 | + <div flex layout="row"> | ||
186 | + <tb-entity-select flex | ||
187 | + the-form="theForm" | ||
188 | + tb-required="!filter.rootStateEntity" | ||
189 | + ng-disabled="filter.rootStateEntity" | ||
190 | + ng-model="filter.rootEntity"> | ||
191 | + </tb-entity-select> | ||
192 | + <section class="tb-root-state-entity-switch" layout="column" layout-align="start center"> | ||
193 | + <label class="tb-small root-state-entity-label" translate>alias.root-state-entity</label> | ||
194 | + <md-switch class="root-state-entity-switch" ng-model="filter.rootStateEntity" | ||
195 | + aria-label="{{ 'alias.root-state-entity' | translate }}"> | ||
196 | + </md-switch> | ||
197 | + </section> | ||
198 | + </div> | ||
199 | + <div flex layout="row"> | ||
200 | + <md-input-container class="md-block" style="min-width: 100px;"> | ||
201 | + <label translate>relation.direction</label> | ||
202 | + <md-select required ng-model="filter.direction"> | ||
203 | + <md-option ng-repeat="direction in types.entitySearchDirection" ng-value="direction"> | ||
204 | + {{ ('relation.search-direction.' + direction) | translate}} | ||
205 | + </md-option> | ||
206 | + </md-select> | ||
207 | + </md-input-container> | ||
208 | + <md-input-container flex class="md-block"> | ||
209 | + <label translate>alias.max-relation-level</label> | ||
210 | + <input name="maxRelationLevel" | ||
211 | + type="number" | ||
212 | + min="1" | ||
213 | + step="1" | ||
214 | + placeholder="{{ 'alias.unlimited-level' | translate }}" | ||
215 | + ng-model="filter.maxLevel" | ||
216 | + aria-label="{{ 'alias.max-relation-level' | translate }}"> | ||
217 | + </md-input-container> | ||
218 | + </div> | ||
219 | + <div class="md-caption" style="color: rgba(0,0,0,0.57);" translate>relation.relation-type</div> | ||
220 | + <tb-relation-type-autocomplete flex | ||
221 | + hide-label | ||
222 | + the-form="theForm" | ||
223 | + ng-model="filter.relationType" | ||
224 | + tb-required="false"> | ||
225 | + </tb-relation-type-autocomplete> | ||
226 | + <div class="md-caption tb-required" style="color: rgba(0,0,0,0.57);" translate>device.device-types</div> | ||
227 | + <tb-entity-subtype-list | ||
228 | + tb-required="true" | ||
229 | + entity-type="types.entityType.device" | ||
230 | + ng-model="filter.deviceTypes"> | ||
231 | + </tb-entity-subtype-list> | ||
232 | + </section> | ||
91 | </div> | 233 | </div> |
@@ -114,6 +114,9 @@ export default function EntitySubtypeAutocomplete($compile, $templateCache, $q, | @@ -114,6 +114,9 @@ export default function EntitySubtypeAutocomplete($compile, $templateCache, $q, | ||
114 | scope.selectEntitySubtypeText = 'asset.select-asset-type'; | 114 | scope.selectEntitySubtypeText = 'asset.select-asset-type'; |
115 | scope.entitySubtypeText = 'asset.asset-type'; | 115 | scope.entitySubtypeText = 'asset.asset-type'; |
116 | scope.entitySubtypeRequiredText = 'asset.asset-type-required'; | 116 | scope.entitySubtypeRequiredText = 'asset.asset-type-required'; |
117 | + scope.$on('assetSaved', function() { | ||
118 | + scope.entitySubtypes = null; | ||
119 | + }); | ||
117 | } else if (scope.entityType == types.entityType.device) { | 120 | } else if (scope.entityType == types.entityType.device) { |
118 | scope.selectEntitySubtypeText = 'device.select-device-type'; | 121 | scope.selectEntitySubtypeText = 'device.select-device-type'; |
119 | scope.entitySubtypeText = 'device.device-type'; | 122 | scope.entitySubtypeText = 'device.device-type'; |
1 | +/* | ||
2 | + * Copyright © 2016-2017 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | + | ||
17 | +/* eslint-disable import/no-unresolved, import/default */ | ||
18 | + | ||
19 | +import entitySubtypeListTemplate from './entity-subtype-list.tpl.html'; | ||
20 | + | ||
21 | +/* eslint-enable import/no-unresolved, import/default */ | ||
22 | + | ||
23 | +import './entity-subtype-list.scss'; | ||
24 | + | ||
25 | +/*@ngInject*/ | ||
26 | +export default function EntitySubtypeListDirective($compile, $templateCache, $q, $mdUtil, $translate, $filter, types, assetService, deviceService) { | ||
27 | + | ||
28 | + var linker = function (scope, element, attrs, ngModelCtrl) { | ||
29 | + | ||
30 | + var template = $templateCache.get(entitySubtypeListTemplate); | ||
31 | + element.html(template); | ||
32 | + | ||
33 | + scope.ngModelCtrl = ngModelCtrl; | ||
34 | + | ||
35 | + | ||
36 | + scope.entitySubtypesList = []; | ||
37 | + scope.entitySubtypes = null; | ||
38 | + | ||
39 | + if (scope.entityType == types.entityType.asset) { | ||
40 | + scope.placeholder = scope.tbRequired ? $translate.instant('asset.enter-asset-type') | ||
41 | + : $translate.instant('asset.any-asset'); | ||
42 | + scope.secondaryPlaceholder = '+' + $translate.instant('asset.asset-type'); | ||
43 | + scope.noSubtypesMathingText = 'asset.no-asset-types-matching'; | ||
44 | + scope.subtypeListEmptyText = 'asset.asset-type-list-empty'; | ||
45 | + } else if (scope.entityType == types.entityType.device) { | ||
46 | + scope.placeholder = scope.tbRequired ? $translate.instant('device.enter-device-type') | ||
47 | + : $translate.instant('device.any-device'); | ||
48 | + scope.secondaryPlaceholder = '+' + $translate.instant('device.device-type'); | ||
49 | + scope.noSubtypesMathingText = 'device.no-device-types-matching'; | ||
50 | + scope.subtypeListEmptyText = 'device.device-type-list-empty'; | ||
51 | + } | ||
52 | + | ||
53 | + scope.$watch('tbRequired', function () { | ||
54 | + scope.updateValidity(); | ||
55 | + }); | ||
56 | + | ||
57 | + scope.fetchEntitySubtypes = function(searchText) { | ||
58 | + var deferred = $q.defer(); | ||
59 | + loadSubTypes().then( | ||
60 | + function success(subTypes) { | ||
61 | + var result = $filter('filter')(subTypes, {'$': searchText}); | ||
62 | + if (result && result.length) { | ||
63 | + deferred.resolve(result); | ||
64 | + } else { | ||
65 | + deferred.resolve([searchText]); | ||
66 | + } | ||
67 | + }, | ||
68 | + function fail() { | ||
69 | + deferred.reject(); | ||
70 | + } | ||
71 | + ); | ||
72 | + return deferred.promise; | ||
73 | + } | ||
74 | + | ||
75 | + scope.updateValidity = function() { | ||
76 | + var value = ngModelCtrl.$viewValue; | ||
77 | + var valid = !scope.tbRequired || value && value.length > 0; | ||
78 | + ngModelCtrl.$setValidity('entitySubtypesList', valid); | ||
79 | + } | ||
80 | + | ||
81 | + ngModelCtrl.$render = function () { | ||
82 | + scope.entitySubtypesList = ngModelCtrl.$viewValue; | ||
83 | + if (!scope.entitySubtypesList) { | ||
84 | + scope.entitySubtypesList = []; | ||
85 | + } | ||
86 | + } | ||
87 | + | ||
88 | + scope.$watch('entitySubtypesList', function () { | ||
89 | + ngModelCtrl.$setViewValue(scope.entitySubtypesList); | ||
90 | + scope.updateValidity(); | ||
91 | + }, true); | ||
92 | + | ||
93 | + function loadSubTypes() { | ||
94 | + var deferred = $q.defer(); | ||
95 | + if (!scope.entitySubtypes) { | ||
96 | + var entitySubtypesPromise; | ||
97 | + if (scope.entityType == types.entityType.asset) { | ||
98 | + entitySubtypesPromise = assetService.getAssetTypes(); | ||
99 | + } else if (scope.entityType == types.entityType.device) { | ||
100 | + entitySubtypesPromise = deviceService.getDeviceTypes(); | ||
101 | + } | ||
102 | + if (entitySubtypesPromise) { | ||
103 | + entitySubtypesPromise.then( | ||
104 | + function success(types) { | ||
105 | + scope.entitySubtypes = []; | ||
106 | + types.forEach(function (type) { | ||
107 | + scope.entitySubtypes.push(type.type); | ||
108 | + }); | ||
109 | + deferred.resolve(scope.entitySubtypes); | ||
110 | + }, | ||
111 | + function fail() { | ||
112 | + deferred.reject(); | ||
113 | + } | ||
114 | + ); | ||
115 | + } else { | ||
116 | + deferred.reject(); | ||
117 | + } | ||
118 | + } else { | ||
119 | + deferred.resolve(scope.entitySubtypes); | ||
120 | + } | ||
121 | + return deferred.promise; | ||
122 | + } | ||
123 | + | ||
124 | + $compile(element.contents())(scope); | ||
125 | + | ||
126 | + $mdUtil.nextTick(function(){ | ||
127 | + var inputElement = angular.element('input', element); | ||
128 | + inputElement.on('blur', function() { | ||
129 | + scope.inputTouched = true; | ||
130 | + } ); | ||
131 | + }); | ||
132 | + | ||
133 | + } | ||
134 | + | ||
135 | + return { | ||
136 | + restrict: "E", | ||
137 | + require: "^ngModel", | ||
138 | + link: linker, | ||
139 | + scope: { | ||
140 | + disabled:'=ngDisabled', | ||
141 | + tbRequired: '=?', | ||
142 | + entityType: "=" | ||
143 | + } | ||
144 | + }; | ||
145 | + | ||
146 | +} |
ui/src/app/entity/entity-subtype-list.scss
0 → 100644
1 | +/** | ||
2 | + * Copyright © 2016-2017 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | + | ||
17 | +/*.tb-entity-subtype-list { | ||
18 | + #entity_subtype_list_chips { | ||
19 | + .md-chips { | ||
20 | + padding-bottom: 1px; | ||
21 | + } | ||
22 | + } | ||
23 | + .tb-error-messages { | ||
24 | + margin-top: -11px; | ||
25 | + height: 35px; | ||
26 | + .tb-error-message { | ||
27 | + padding-left: 1px; | ||
28 | + } | ||
29 | + } | ||
30 | +}*/ |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2017 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | + | ||
19 | +<section flex layout='column' class="tb-entity-subtype-list"> | ||
20 | + <md-chips flex | ||
21 | + readonly="disabled" | ||
22 | + id="entity_subtype_list_chips" | ||
23 | + ng-required="tbRequired" | ||
24 | + ng-model="entitySubtypesList" | ||
25 | + placeholder="{{placeholder}}" | ||
26 | + secondary-placeholder="{{secondaryPlaceholder}}" | ||
27 | + md-autocomplete-snap | ||
28 | + md-require-match="false"> | ||
29 | + <md-autocomplete | ||
30 | + md-no-cache="true" | ||
31 | + id="entitySubtype" | ||
32 | + md-selected-item="selectedEntitySubtype" | ||
33 | + md-search-text="entitySubtypeSearchText" | ||
34 | + md-items="item in fetchEntitySubtypes(entitySubtypeSearchText)" | ||
35 | + md-item-text="item" | ||
36 | + md-min-length="0" | ||
37 | + placeholder="{{ (!entitySubtypesList || !entitySubtypesList.length) ? placeholder : secondaryPlaceholder }}"> | ||
38 | + <md-item-template> | ||
39 | + <span md-highlight-text="entitySubtypeSearchText" md-highlight-flags="^i">{{item}}</span> | ||
40 | + </md-item-template> | ||
41 | + <md-not-found> | ||
42 | + <span translate translate-values='{ entitySubtype: entitySubtypeSearchText }'>{{noSubtypesMathingText}}</span> | ||
43 | + </md-not-found> | ||
44 | + </md-autocomplete> | ||
45 | + <md-chip-template> | ||
46 | + <span> | ||
47 | + <strong>{{$chip}}</strong> | ||
48 | + </span> | ||
49 | + </md-chip-template> | ||
50 | + </md-chips> | ||
51 | + <div class="tb-error-messages" ng-messages="ngModelCtrl.$error" ng-if="inputTouched && tbRequired" role="alert"> | ||
52 | + <div translate ng-message="entitySubtypesList" class="tb-error-message">{{subtypeListEmptyText}}</div> | ||
53 | + </div> | ||
54 | +</section> |
1 | +/* | ||
2 | + * Copyright © 2016-2017 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | + | ||
17 | +/* eslint-disable import/no-unresolved, import/default */ | ||
18 | + | ||
19 | +import entityTypeListTemplate from './entity-type-list.tpl.html'; | ||
20 | + | ||
21 | +/* eslint-enable import/no-unresolved, import/default */ | ||
22 | + | ||
23 | +import './entity-type-list.scss'; | ||
24 | + | ||
25 | +/*@ngInject*/ | ||
26 | +export default function EntityTypeListDirective($compile, $templateCache, $q, $mdUtil, $translate, $filter, types, entityService) { | ||
27 | + | ||
28 | + var linker = function (scope, element, attrs, ngModelCtrl) { | ||
29 | + | ||
30 | + var template = $templateCache.get(entityTypeListTemplate); | ||
31 | + element.html(template); | ||
32 | + | ||
33 | + scope.ngModelCtrl = ngModelCtrl; | ||
34 | + | ||
35 | + scope.placeholder = scope.tbRequired ? $translate.instant('entity.enter-entity-type') | ||
36 | + : $translate.instant('entity.any-entity'); | ||
37 | + scope.secondaryPlaceholder = '+' + $translate.instant('entity.entity-type'); | ||
38 | + | ||
39 | + var entityTypes = entityService.prepareAllowedEntityTypesList(scope.allowedEntityTypes); | ||
40 | + scope.entityTypesList = []; | ||
41 | + for (var type in entityTypes) { | ||
42 | + var entityTypeInfo = {}; | ||
43 | + entityTypeInfo.value = entityTypes[type]; | ||
44 | + entityTypeInfo.name = $translate.instant(types.entityTypeTranslations[entityTypeInfo.value].type) + ''; | ||
45 | + scope.entityTypesList.push(entityTypeInfo); | ||
46 | + } | ||
47 | + | ||
48 | + scope.$watch('tbRequired', function () { | ||
49 | + scope.updateValidity(); | ||
50 | + }); | ||
51 | + | ||
52 | + scope.fetchEntityTypes = function(searchText) { | ||
53 | + var deferred = $q.defer(); | ||
54 | + var entityTypes = $filter('filter')(scope.entityTypesList, {name: searchText}); | ||
55 | + deferred.resolve(entityTypes); | ||
56 | + return deferred.promise; | ||
57 | + } | ||
58 | + | ||
59 | + scope.updateValidity = function() { | ||
60 | + var value = ngModelCtrl.$viewValue; | ||
61 | + var valid = !scope.tbRequired || value && value.length > 0; | ||
62 | + ngModelCtrl.$setValidity('entityTypeList', valid); | ||
63 | + } | ||
64 | + | ||
65 | + ngModelCtrl.$render = function () { | ||
66 | + scope.entityTypeList = []; | ||
67 | + var value = ngModelCtrl.$viewValue; | ||
68 | + if (value && value.length) { | ||
69 | + value.forEach(function(type) { | ||
70 | + var entityTypeInfo = {}; | ||
71 | + entityTypeInfo.value = type; | ||
72 | + entityTypeInfo.name = $translate.instant(types.entityTypeTranslations[entityTypeInfo.value].type) + ''; | ||
73 | + scope.entityTypeList.push(entityTypeInfo); | ||
74 | + }); | ||
75 | + } | ||
76 | + } | ||
77 | + | ||
78 | + scope.$watch('entityTypeList', function () { | ||
79 | + var values = []; | ||
80 | + if (scope.entityTypeList && scope.entityTypeList.length) { | ||
81 | + scope.entityTypeList.forEach(function(entityType) { | ||
82 | + values.push(entityType.value); | ||
83 | + }); | ||
84 | + } | ||
85 | + ngModelCtrl.$setViewValue(values); | ||
86 | + scope.updateValidity(); | ||
87 | + }, true); | ||
88 | + | ||
89 | + $compile(element.contents())(scope); | ||
90 | + | ||
91 | + $mdUtil.nextTick(function(){ | ||
92 | + var inputElement = angular.element('input', element); | ||
93 | + inputElement.on('blur', function() { | ||
94 | + scope.inputTouched = true; | ||
95 | + } ); | ||
96 | + }); | ||
97 | + | ||
98 | + } | ||
99 | + | ||
100 | + return { | ||
101 | + restrict: "E", | ||
102 | + require: "^ngModel", | ||
103 | + link: linker, | ||
104 | + scope: { | ||
105 | + disabled:'=ngDisabled', | ||
106 | + tbRequired: '=?', | ||
107 | + allowedEntityTypes: '=?' | ||
108 | + } | ||
109 | + }; | ||
110 | + | ||
111 | +} |
ui/src/app/entity/entity-type-list.scss
0 → 100644
1 | +/** | ||
2 | + * Copyright © 2016-2017 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | + | ||
17 | +/*.tb-entity-type-list { | ||
18 | + #entity_type_list_chips { | ||
19 | + .md-chips { | ||
20 | + padding-bottom: 1px; | ||
21 | + } | ||
22 | + } | ||
23 | + .tb-error-messages { | ||
24 | + margin-top: -11px; | ||
25 | + height: 35px; | ||
26 | + .tb-error-message { | ||
27 | + padding-left: 1px; | ||
28 | + } | ||
29 | + } | ||
30 | +}*/ |
ui/src/app/entity/entity-type-list.tpl.html
0 → 100644
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2017 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | + | ||
19 | +<section flex layout='column' class="tb-entity-type-list"> | ||
20 | + <md-chips flex | ||
21 | + readonly="disabled" | ||
22 | + id="entity_type_list_chips" | ||
23 | + ng-required="tbRequired" | ||
24 | + ng-model="entityTypeList" | ||
25 | + placeholder="{{placeholder}}" | ||
26 | + secondary-placeholder="{{secondaryPlaceholder}}" | ||
27 | + md-autocomplete-snap | ||
28 | + md-require-match="true"> | ||
29 | + <md-autocomplete | ||
30 | + md-no-cache="true" | ||
31 | + id="entityType" | ||
32 | + md-selected-item="selectedEntityType" | ||
33 | + md-search-text="entityTypeSearchText" | ||
34 | + md-items="item in fetchEntityTypes(entityTypeSearchText)" | ||
35 | + md-item-text="item.name" | ||
36 | + md-min-length="0" | ||
37 | + placeholder="{{ (!entityTypeList || !entityTypeList.length) ? placeholder : secondaryPlaceholder }}"> | ||
38 | + <md-item-template> | ||
39 | + <span md-highlight-text="entityTypeSearchText" md-highlight-flags="^i">{{item.name}}</span> | ||
40 | + </md-item-template> | ||
41 | + <md-not-found> | ||
42 | + <span translate translate-values='{ entityType: entityTypeSearchText }'>entity.no-entity-types-matching</span> | ||
43 | + </md-not-found> | ||
44 | + </md-autocomplete> | ||
45 | + <md-chip-template> | ||
46 | + <span> | ||
47 | + <strong>{{$chip.name}}</strong> | ||
48 | + </span> | ||
49 | + </md-chip-template> | ||
50 | + </md-chips> | ||
51 | + <div class="tb-error-messages" ng-messages="ngModelCtrl.$error" ng-if="inputTouched && tbRequired" role="alert"> | ||
52 | + <div translate ng-message="entityTypeList" class="tb-error-message">entity.entity-type-list-empty</div> | ||
53 | + </div> | ||
54 | +</section> |
@@ -23,7 +23,7 @@ import entityTypeSelectTemplate from './entity-type-select.tpl.html'; | @@ -23,7 +23,7 @@ import entityTypeSelectTemplate from './entity-type-select.tpl.html'; | ||
23 | /* eslint-enable import/no-unresolved, import/default */ | 23 | /* eslint-enable import/no-unresolved, import/default */ |
24 | 24 | ||
25 | /*@ngInject*/ | 25 | /*@ngInject*/ |
26 | -export default function EntityTypeSelect($compile, $templateCache, utils, userService, types) { | 26 | +export default function EntityTypeSelect($compile, $templateCache, utils, entityService, userService, types) { |
27 | 27 | ||
28 | var linker = function (scope, element, attrs, ngModelCtrl) { | 28 | var linker = function (scope, element, attrs, ngModelCtrl) { |
29 | var template = $templateCache.get(entityTypeSelectTemplate); | 29 | var template = $templateCache.get(entityTypeSelectTemplate); |
@@ -39,36 +39,7 @@ export default function EntityTypeSelect($compile, $templateCache, utils, userSe | @@ -39,36 +39,7 @@ export default function EntityTypeSelect($compile, $templateCache, utils, userSe | ||
39 | 39 | ||
40 | scope.ngModelCtrl = ngModelCtrl; | 40 | scope.ngModelCtrl = ngModelCtrl; |
41 | 41 | ||
42 | - var authority = userService.getAuthority(); | ||
43 | - scope.entityTypes = {}; | ||
44 | - switch(authority) { | ||
45 | - case 'SYS_ADMIN': | ||
46 | - scope.entityTypes.tenant = types.entityType.tenant; | ||
47 | - scope.entityTypes.rule = types.entityType.rule; | ||
48 | - scope.entityTypes.plugin = types.entityType.plugin; | ||
49 | - break; | ||
50 | - case 'TENANT_ADMIN': | ||
51 | - scope.entityTypes.device = types.entityType.device; | ||
52 | - scope.entityTypes.asset = types.entityType.asset; | ||
53 | - scope.entityTypes.customer = types.entityType.customer; | ||
54 | - scope.entityTypes.rule = types.entityType.rule; | ||
55 | - scope.entityTypes.plugin = types.entityType.plugin; | ||
56 | - scope.entityTypes.dashboard = types.entityType.dashboard; | ||
57 | - break; | ||
58 | - case 'CUSTOMER_USER': | ||
59 | - scope.entityTypes.device = types.entityType.device; | ||
60 | - scope.entityTypes.asset = types.entityType.asset; | ||
61 | - scope.entityTypes.dashboard = types.entityType.dashboard; | ||
62 | - break; | ||
63 | - } | ||
64 | - | ||
65 | - if (scope.allowedEntityTypes) { | ||
66 | - for (var entityType in scope.entityTypes) { | ||
67 | - if (scope.allowedEntityTypes.indexOf(scope.entityTypes[entityType]) === -1) { | ||
68 | - delete scope.entityTypes[entityType]; | ||
69 | - } | ||
70 | - } | ||
71 | - } | 42 | + scope.entityTypes = entityService.prepareAllowedEntityTypesList(scope.allowedEntityTypes); |
72 | 43 | ||
73 | scope.typeName = function(type) { | 44 | scope.typeName = function(type) { |
74 | return type ? types.entityTypeTranslations[type].type : ''; | 45 | return type ? types.entityTypeTranslations[type].type : ''; |
@@ -14,9 +14,11 @@ | @@ -14,9 +14,11 @@ | ||
14 | * limitations under the License. | 14 | * limitations under the License. |
15 | */ | 15 | */ |
16 | 16 | ||
17 | -import EntityAliasesController from './entity-aliases.controller'; | ||
18 | -import EntityAliasDialogController from './entity-alias-dialog.controller'; | 17 | +import EntityAliasesController from './alias/entity-aliases.controller'; |
18 | +import EntityAliasDialogController from './alias/entity-alias-dialog.controller'; | ||
19 | import EntityTypeSelectDirective from './entity-type-select.directive'; | 19 | import EntityTypeSelectDirective from './entity-type-select.directive'; |
20 | +import EntityTypeListDirective from './entity-type-list.directive'; | ||
21 | +import EntitySubtypeListDirective from './entity-subtype-list.directive'; | ||
20 | import EntitySubtypeSelectDirective from './entity-subtype-select.directive'; | 22 | import EntitySubtypeSelectDirective from './entity-subtype-select.directive'; |
21 | import EntitySubtypeAutocompleteDirective from './entity-subtype-autocomplete.directive'; | 23 | import EntitySubtypeAutocompleteDirective from './entity-subtype-autocomplete.directive'; |
22 | import EntityAutocompleteDirective from './entity-autocomplete.directive'; | 24 | import EntityAutocompleteDirective from './entity-autocomplete.directive'; |
@@ -24,11 +26,12 @@ import EntityListDirective from './entity-list.directive'; | @@ -24,11 +26,12 @@ import EntityListDirective from './entity-list.directive'; | ||
24 | import EntitySelectDirective from './entity-select.directive'; | 26 | import EntitySelectDirective from './entity-select.directive'; |
25 | import EntityFilterDirective from './entity-filter.directive'; | 27 | import EntityFilterDirective from './entity-filter.directive'; |
26 | import EntityFilterViewDirective from './entity-filter-view.directive'; | 28 | import EntityFilterViewDirective from './entity-filter-view.directive'; |
27 | -import AliasesEntitySelectPanelController from './aliases-entity-select-panel.controller'; | ||
28 | -import AliasesEntitySelectDirective from './aliases-entity-select.directive'; | 29 | +import AliasesEntitySelectPanelController from './alias/aliases-entity-select-panel.controller'; |
30 | +import AliasesEntitySelectDirective from './alias/aliases-entity-select.directive'; | ||
29 | import AddAttributeDialogController from './attribute/add-attribute-dialog.controller'; | 31 | import AddAttributeDialogController from './attribute/add-attribute-dialog.controller'; |
30 | import AddWidgetToDashboardDialogController from './attribute/add-widget-to-dashboard-dialog.controller'; | 32 | import AddWidgetToDashboardDialogController from './attribute/add-widget-to-dashboard-dialog.controller'; |
31 | import AttributeTableDirective from './attribute/attribute-table.directive'; | 33 | import AttributeTableDirective from './attribute/attribute-table.directive'; |
34 | +import RelationFiltersDirective from './relation/relation-filters.directive'; | ||
32 | import RelationTableDirective from './relation/relation-table.directive'; | 35 | import RelationTableDirective from './relation/relation-table.directive'; |
33 | import RelationTypeAutocompleteDirective from './relation/relation-type-autocomplete.directive'; | 36 | import RelationTypeAutocompleteDirective from './relation/relation-type-autocomplete.directive'; |
34 | 37 | ||
@@ -39,6 +42,8 @@ export default angular.module('thingsboard.entity', []) | @@ -39,6 +42,8 @@ export default angular.module('thingsboard.entity', []) | ||
39 | .controller('AddAttributeDialogController', AddAttributeDialogController) | 42 | .controller('AddAttributeDialogController', AddAttributeDialogController) |
40 | .controller('AddWidgetToDashboardDialogController', AddWidgetToDashboardDialogController) | 43 | .controller('AddWidgetToDashboardDialogController', AddWidgetToDashboardDialogController) |
41 | .directive('tbEntityTypeSelect', EntityTypeSelectDirective) | 44 | .directive('tbEntityTypeSelect', EntityTypeSelectDirective) |
45 | + .directive('tbEntityTypeList', EntityTypeListDirective) | ||
46 | + .directive('tbEntitySubtypeList', EntitySubtypeListDirective) | ||
42 | .directive('tbEntitySubtypeSelect', EntitySubtypeSelectDirective) | 47 | .directive('tbEntitySubtypeSelect', EntitySubtypeSelectDirective) |
43 | .directive('tbEntitySubtypeAutocomplete', EntitySubtypeAutocompleteDirective) | 48 | .directive('tbEntitySubtypeAutocomplete', EntitySubtypeAutocompleteDirective) |
44 | .directive('tbEntityAutocomplete', EntityAutocompleteDirective) | 49 | .directive('tbEntityAutocomplete', EntityAutocompleteDirective) |
@@ -48,6 +53,7 @@ export default angular.module('thingsboard.entity', []) | @@ -48,6 +53,7 @@ export default angular.module('thingsboard.entity', []) | ||
48 | .directive('tbEntityFilterView', EntityFilterViewDirective) | 53 | .directive('tbEntityFilterView', EntityFilterViewDirective) |
49 | .directive('tbAliasesEntitySelect', AliasesEntitySelectDirective) | 54 | .directive('tbAliasesEntitySelect', AliasesEntitySelectDirective) |
50 | .directive('tbAttributeTable', AttributeTableDirective) | 55 | .directive('tbAttributeTable', AttributeTableDirective) |
56 | + .directive('tbRelationFilters', RelationFiltersDirective) | ||
51 | .directive('tbRelationTable', RelationTableDirective) | 57 | .directive('tbRelationTable', RelationTableDirective) |
52 | .directive('tbRelationTypeAutocomplete', RelationTypeAutocompleteDirective) | 58 | .directive('tbRelationTypeAutocomplete', RelationTypeAutocompleteDirective) |
53 | .name; | 59 | .name; |
1 | +/* | ||
2 | + * Copyright © 2016-2017 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | + | ||
17 | +import './relation-filters.scss'; | ||
18 | + | ||
19 | +/* eslint-disable import/no-unresolved, import/default */ | ||
20 | + | ||
21 | +import relationFiltersTemplate from './relation-filters.tpl.html'; | ||
22 | + | ||
23 | +/* eslint-enable import/no-unresolved, import/default */ | ||
24 | + | ||
25 | +/*@ngInject*/ | ||
26 | +export default function RelationFilters($compile, $templateCache) { | ||
27 | + | ||
28 | + return { | ||
29 | + restrict: "E", | ||
30 | + require: "^ngModel", | ||
31 | + scope: { | ||
32 | + allowedEntityTypes: '=?' | ||
33 | + }, | ||
34 | + link: linker | ||
35 | + }; | ||
36 | + | ||
37 | + function linker( scope, element, attrs, ngModelCtrl ) { | ||
38 | + | ||
39 | + var template = $templateCache.get(relationFiltersTemplate); | ||
40 | + element.html(template); | ||
41 | + | ||
42 | + scope.relationFilters = []; | ||
43 | + | ||
44 | + scope.addFilter = addFilter; | ||
45 | + scope.removeFilter = removeFilter; | ||
46 | + | ||
47 | + ngModelCtrl.$render = function () { | ||
48 | + if (ngModelCtrl.$viewValue) { | ||
49 | + var value = ngModelCtrl.$viewValue; | ||
50 | + value.forEach(function (filter) { | ||
51 | + scope.relationFilters.push(filter); | ||
52 | + }); | ||
53 | + } | ||
54 | + scope.$watch('relationFilters', function (newVal, prevVal) { | ||
55 | + if (!angular.equals(newVal, prevVal)) { | ||
56 | + updateValue(); | ||
57 | + } | ||
58 | + }, true); | ||
59 | + } | ||
60 | + | ||
61 | + function addFilter() { | ||
62 | + var filter = { | ||
63 | + relationType: null, | ||
64 | + entityTypes: [] | ||
65 | + }; | ||
66 | + scope.relationFilters.push(filter); | ||
67 | + } | ||
68 | + | ||
69 | + function removeFilter($event, filter) { | ||
70 | + var index = scope.relationFilters.indexOf(filter); | ||
71 | + if (index > -1) { | ||
72 | + scope.relationFilters.splice(index, 1); | ||
73 | + } | ||
74 | + } | ||
75 | + | ||
76 | + function updateValue() { | ||
77 | + var value = []; | ||
78 | + scope.relationFilters.forEach(function (filter) { | ||
79 | + value.push(filter); | ||
80 | + }); | ||
81 | + ngModelCtrl.$setViewValue(value); | ||
82 | + } | ||
83 | + $compile(element.contents())(scope); | ||
84 | + } | ||
85 | +} |
1 | +/** | ||
2 | + * Copyright © 2016-2017 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | + | ||
17 | +.tb-relation-filters { | ||
18 | + .header { | ||
19 | + padding-left: 5px; | ||
20 | + padding-right: 5px; | ||
21 | + padding-bottom: 5px; | ||
22 | + .cell { | ||
23 | + padding-left: 5px; | ||
24 | + padding-right: 5px; | ||
25 | + color: rgba(0,0,0,.54); | ||
26 | + font-size: 12px; | ||
27 | + font-weight: 700; | ||
28 | + white-space: nowrap; | ||
29 | + } | ||
30 | + } | ||
31 | + .body { | ||
32 | + padding-left: 5px; | ||
33 | + padding-right: 5px; | ||
34 | + max-height: 300px; | ||
35 | + overflow: auto; | ||
36 | + padding-bottom: 20px; | ||
37 | + .row { | ||
38 | + padding-top: 5px; | ||
39 | + } | ||
40 | + .cell { | ||
41 | + padding-left: 5px; | ||
42 | + padding-right: 5px; | ||
43 | + | ||
44 | + md-select { | ||
45 | + margin: 0 0 24px; | ||
46 | + } | ||
47 | + | ||
48 | + md-input-container { | ||
49 | + margin: 0; | ||
50 | + } | ||
51 | + | ||
52 | + md-chips-wrap { | ||
53 | + padding: 0px; | ||
54 | + margin: 0 0 24px; | ||
55 | + .md-chip-input-container { | ||
56 | + margin: 0; | ||
57 | + } | ||
58 | + md-autocomplete { | ||
59 | + height: 30px; | ||
60 | + md-autocomplete-wrap { | ||
61 | + height: 30px; | ||
62 | + } | ||
63 | + } | ||
64 | + } | ||
65 | + .md-chips .md-chip-input-container input { | ||
66 | + padding: 2px 2px 2px; | ||
67 | + height: 26px; | ||
68 | + line-height: 26px; | ||
69 | + } | ||
70 | + | ||
71 | + } | ||
72 | + | ||
73 | + .md-button { | ||
74 | + margin: 0; | ||
75 | + } | ||
76 | + } | ||
77 | +} |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2017 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | +<div class="tb-relation-filters"> | ||
19 | + <div class="header" ng-show="relationFilters.length"> | ||
20 | + <div layout="row" layout-align="start center"> | ||
21 | + <span class="cell" style="width: 200px; min-width: 200px;" translate>relation.type</span> | ||
22 | + <span class="cell" flex translate>entity.entity-types</span> | ||
23 | + <span class="cell" style="width: 40px; min-width: 40px;"> </span> | ||
24 | + </div> | ||
25 | + </div> | ||
26 | + <div class="body" ng-show="relationFilters.length"> | ||
27 | + <div class="row" ng-form name="relationFilterForm" flex layout="row" layout-align="start center" ng-repeat="filter in relationFilters track by $index"> | ||
28 | + <div class="md-whiteframe-1dp" flex layout="row" layout-align="start center"> | ||
29 | + <tb-relation-type-autocomplete class="cell" style="width: 200px; min-width: 200px;" | ||
30 | + hide-label | ||
31 | + the-form="relationFilterForm" | ||
32 | + ng-model="filter.relationType" | ||
33 | + tb-required="false"> | ||
34 | + </tb-relation-type-autocomplete> | ||
35 | + <tb-entity-type-list class="cell" flex | ||
36 | + ng-model="filter.entityTypes" | ||
37 | + allowed-entity-types="allowedEntityTypes" | ||
38 | + tb-required="false"> | ||
39 | + </tb-entity-type-list> | ||
40 | + <md-button ng-disabled="loading" class="md-icon-button md-primary" style="width: 40px; min-width: 40px;" | ||
41 | + ng-click="removeFilter($event, filter)" aria-label="{{ 'action.remove' | translate }}"> | ||
42 | + <md-tooltip md-direction="top"> | ||
43 | + {{ 'relation.remove-relation-filter' | translate }} | ||
44 | + </md-tooltip> | ||
45 | + <md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons"> | ||
46 | + close | ||
47 | + </md-icon> | ||
48 | + </md-button> | ||
49 | + </div> | ||
50 | + </div> | ||
51 | + </div> | ||
52 | + <div class="any-filter" ng-show="!relationFilters.length"> | ||
53 | + <span layout-align="center center" | ||
54 | + class="tb-prompt" translate>relation.any-relation</span> | ||
55 | + </div> | ||
56 | + <div> | ||
57 | + <md-button ng-disabled="loading" class="md-primary md-raised" ng-click="addFilter($event)" aria-label="{{ 'action.add' | translate }}"> | ||
58 | + <md-tooltip md-direction="top"> | ||
59 | + {{ 'relation.add-relation-filter' | translate }} | ||
60 | + </md-tooltip> | ||
61 | + <md-icon aria-label="{{ 'action.add' | translate }}" class="material-icons"> | ||
62 | + add | ||
63 | + </md-icon> | ||
64 | + {{ 'action.add' | translate }} | ||
65 | + </md-button> | ||
66 | + </div> | ||
67 | +</div> |
@@ -29,6 +29,8 @@ export default function RelationTypeAutocomplete($compile, $templateCache, $q, $ | @@ -29,6 +29,8 @@ export default function RelationTypeAutocomplete($compile, $templateCache, $q, $ | ||
29 | element.html(template); | 29 | element.html(template); |
30 | 30 | ||
31 | scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false; | 31 | scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false; |
32 | + scope.hideLabel = angular.isDefined(attrs.hideLabel) ? true : false; | ||
33 | + | ||
32 | scope.relationType = null; | 34 | scope.relationType = null; |
33 | scope.relationTypeSearchText = ''; | 35 | scope.relationTypeSearchText = ''; |
34 | scope.relationTypes = []; | 36 | scope.relationTypes = []; |
@@ -26,7 +26,7 @@ | @@ -26,7 +26,7 @@ | ||
26 | md-items="item in fetchRelationTypes(relationTypeSearchText)" | 26 | md-items="item in fetchRelationTypes(relationTypeSearchText)" |
27 | md-item-text="item" | 27 | md-item-text="item" |
28 | md-min-length="0" | 28 | md-min-length="0" |
29 | - md-floating-label="{{ 'relation.relation-type' | translate }}" | 29 | + md-floating-label="{{ tbRequired ? ('relation.relation-type' | translate) : ( !relationType ? ('relation.any-relation-type' | translate) : ' ') }}" |
30 | md-select-on-match="true" | 30 | md-select-on-match="true" |
31 | md-menu-class="tb-relation-type-autocomplete"> | 31 | md-menu-class="tb-relation-type-autocomplete"> |
32 | <md-item-template> | 32 | <md-item-template> |
@@ -16,7 +16,7 @@ | @@ -16,7 +16,7 @@ | ||
16 | /* eslint-disable import/no-unresolved, import/default */ | 16 | /* eslint-disable import/no-unresolved, import/default */ |
17 | 17 | ||
18 | import importDialogTemplate from './import-dialog.tpl.html'; | 18 | import importDialogTemplate from './import-dialog.tpl.html'; |
19 | -import entityAliasesTemplate from '../entity/entity-aliases.tpl.html'; | 19 | +import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html'; |
20 | 20 | ||
21 | /* eslint-enable import/no-unresolved, import/default */ | 21 | /* eslint-enable import/no-unresolved, import/default */ |
22 | 22 |
@@ -129,14 +129,24 @@ export default angular.module('thingsboard.locale', []) | @@ -129,14 +129,24 @@ export default angular.module('thingsboard.locale', []) | ||
129 | "filter-type-device-type-description": "Devices of type '{{deviceType}}'", | 129 | "filter-type-device-type-description": "Devices of type '{{deviceType}}'", |
130 | "filter-type-device-type-and-name-description": "Devices of type '{{deviceType}}' and with name starting with '{{prefix}}'", | 130 | "filter-type-device-type-and-name-description": "Devices of type '{{deviceType}}' and with name starting with '{{prefix}}'", |
131 | "filter-type-relations-query": "Relations query", | 131 | "filter-type-relations-query": "Relations query", |
132 | + "filter-type-relations-query-description": "{{entities}} that have {{relationType}} relation {{direction}} {{rootEntity}}", | ||
132 | "filter-type-asset-search-query": "Asset search query", | 133 | "filter-type-asset-search-query": "Asset search query", |
134 | + "filter-type-asset-search-query-description": "Assets with types {{assetTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}", | ||
133 | "filter-type-device-search-query": "Device search query", | 135 | "filter-type-device-search-query": "Device search query", |
136 | + "filter-type-device-search-query-description": "Devices with types {{deviceTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}", | ||
134 | "entity-filter": "Entity filter", | 137 | "entity-filter": "Entity filter", |
135 | "resolve-multiple": "Resolve as multiple entities", | 138 | "resolve-multiple": "Resolve as multiple entities", |
136 | "filter-type": "Filter type", | 139 | "filter-type": "Filter type", |
137 | "filter-type-required": "Filter type is required.", | 140 | "filter-type-required": "Filter type is required.", |
138 | "entity-filter-no-entity-matched": "No entities matching specified filter were found.", | 141 | "entity-filter-no-entity-matched": "No entities matching specified filter were found.", |
139 | - "no-entity-filter-specified": "No entity filter specified" | 142 | + "no-entity-filter-specified": "No entity filter specified", |
143 | + "root-state-entity": "Use dashboard state entity as root", | ||
144 | + "root-entity": "Root entity", | ||
145 | + "max-relation-level": "Max relation level", | ||
146 | + "unlimited-level": "Unlimited level", | ||
147 | + "state-entity": "Dashboard state entity", | ||
148 | + "all-entities": "All entities", | ||
149 | + "any-relation": "any" | ||
140 | }, | 150 | }, |
141 | "asset": { | 151 | "asset": { |
142 | "asset": "Asset", | 152 | "asset": "Asset", |
@@ -159,6 +169,11 @@ export default angular.module('thingsboard.locale', []) | @@ -159,6 +169,11 @@ export default angular.module('thingsboard.locale', []) | ||
159 | "asset-type": "Asset type", | 169 | "asset-type": "Asset type", |
160 | "asset-type-required": "Asset type is required.", | 170 | "asset-type-required": "Asset type is required.", |
161 | "select-asset-type": "Select asset type", | 171 | "select-asset-type": "Select asset type", |
172 | + "enter-asset-type": "Enter asset type", | ||
173 | + "any-asset": "Any asset", | ||
174 | + "no-asset-types-matching": "No asset types matching '{{entitySubtype}}' were found.", | ||
175 | + "asset-type-list-empty": "No asset types selected.", | ||
176 | + "asset-types": "Asset types", | ||
162 | "name": "Name", | 177 | "name": "Name", |
163 | "name-required": "Name is required.", | 178 | "name-required": "Name is required.", |
164 | "description": "Description", | 179 | "description": "Description", |
@@ -444,6 +459,7 @@ export default angular.module('thingsboard.locale', []) | @@ -444,6 +459,7 @@ export default angular.module('thingsboard.locale', []) | ||
444 | }, | 459 | }, |
445 | "datasource": { | 460 | "datasource": { |
446 | "type": "Datasource type", | 461 | "type": "Datasource type", |
462 | + "name": "Name", | ||
447 | "add-datasource-prompt": "Please add datasource" | 463 | "add-datasource-prompt": "Please add datasource" |
448 | }, | 464 | }, |
449 | "details": { | 465 | "details": { |
@@ -524,6 +540,11 @@ export default angular.module('thingsboard.locale', []) | @@ -524,6 +540,11 @@ export default angular.module('thingsboard.locale', []) | ||
524 | "device-type": "Device type", | 540 | "device-type": "Device type", |
525 | "device-type-required": "Device type is required.", | 541 | "device-type-required": "Device type is required.", |
526 | "select-device-type": "Select device type", | 542 | "select-device-type": "Select device type", |
543 | + "enter-device-type": "Enter device type", | ||
544 | + "any-device": "Any device", | ||
545 | + "no-device-types-matching": "No device types matching '{{entitySubtype}}' were found.", | ||
546 | + "device-type-list-empty": "No device types selected.", | ||
547 | + "device-types": "Device types", | ||
527 | "name": "Name", | 548 | "name": "Name", |
528 | "name-required": "Name is required.", | 549 | "name-required": "Name is required.", |
529 | "description": "Description", | 550 | "description": "Description", |
@@ -564,10 +585,17 @@ export default angular.module('thingsboard.locale', []) | @@ -564,10 +585,17 @@ export default angular.module('thingsboard.locale', []) | ||
564 | "remove-alias": "Remove entity alias", | 585 | "remove-alias": "Remove entity alias", |
565 | "add-alias": "Add entity alias", | 586 | "add-alias": "Add entity alias", |
566 | "entity-list": "Entity list", | 587 | "entity-list": "Entity list", |
588 | + "entity-type": "Entity type", | ||
589 | + "entity-types": "Entity types", | ||
590 | + "entity-type-list": "Entity type list", | ||
591 | + "any-entity": "Any entity", | ||
592 | + "enter-entity-type": "Enter entity type", | ||
567 | "no-entities-matching": "No entities matching '{{entity}}' were found.", | 593 | "no-entities-matching": "No entities matching '{{entity}}' were found.", |
594 | + "no-entity-types-matching": "No entity types matching '{{entityType}}' were found.", | ||
568 | "name-starts-with": "Name starts with", | 595 | "name-starts-with": "Name starts with", |
569 | "use-entity-name-filter": "Use filter", | 596 | "use-entity-name-filter": "Use filter", |
570 | "entity-list-empty": "No entities selected.", | 597 | "entity-list-empty": "No entities selected.", |
598 | + "entity-type-list-empty": "No entity types selected.", | ||
571 | "entity-name-filter-required": "Entity name filter is required.", | 599 | "entity-name-filter-required": "Entity name filter is required.", |
572 | "entity-name-filter-no-entity-matched": "No entities starting with '{{entity}}' were found.", | 600 | "entity-name-filter-no-entity-matched": "No entities starting with '{{entity}}' were found.", |
573 | "all-subtypes": "All", | 601 | "all-subtypes": "All", |
@@ -581,30 +609,39 @@ export default angular.module('thingsboard.locale', []) | @@ -581,30 +609,39 @@ export default angular.module('thingsboard.locale', []) | ||
581 | "type": "Type", | 609 | "type": "Type", |
582 | "type-required": "Entity type is required.", | 610 | "type-required": "Entity type is required.", |
583 | "type-device": "Device", | 611 | "type-device": "Device", |
612 | + "type-devices": "Devices", | ||
584 | "list-of-devices": "{ count, select, 1 {One device} other {List of # devices} }", | 613 | "list-of-devices": "{ count, select, 1 {One device} other {List of # devices} }", |
585 | "device-name-starts-with": "Devices whose names start with '{{prefix}}'", | 614 | "device-name-starts-with": "Devices whose names start with '{{prefix}}'", |
586 | "type-asset": "Asset", | 615 | "type-asset": "Asset", |
616 | + "type-assets": "Assets", | ||
587 | "list-of-assets": "{ count, select, 1 {One asset} other {List of # assets} }", | 617 | "list-of-assets": "{ count, select, 1 {One asset} other {List of # assets} }", |
588 | "asset-name-starts-with": "Assets whose names start with '{{prefix}}'", | 618 | "asset-name-starts-with": "Assets whose names start with '{{prefix}}'", |
589 | "type-rule": "Rule", | 619 | "type-rule": "Rule", |
620 | + "type-rules": "Rules", | ||
590 | "list-of-rules": "{ count, select, 1 {One rule} other {List of # rules} }", | 621 | "list-of-rules": "{ count, select, 1 {One rule} other {List of # rules} }", |
591 | "rule-name-starts-with": "Rules whose names start with '{{prefix}}'", | 622 | "rule-name-starts-with": "Rules whose names start with '{{prefix}}'", |
592 | "type-plugin": "Plugin", | 623 | "type-plugin": "Plugin", |
624 | + "type-plugins": "Plugins", | ||
593 | "list-of-plugins": "{ count, select, 1 {One plugin} other {List of # plugins} }", | 625 | "list-of-plugins": "{ count, select, 1 {One plugin} other {List of # plugins} }", |
594 | "plugin-name-starts-with": "Plugins whose names start with '{{prefix}}'", | 626 | "plugin-name-starts-with": "Plugins whose names start with '{{prefix}}'", |
595 | "type-tenant": "Tenant", | 627 | "type-tenant": "Tenant", |
628 | + "type-tenants": "Tenants", | ||
596 | "list-of-tenants": "{ count, select, 1 {One tenant} other {List of # tenants} }", | 629 | "list-of-tenants": "{ count, select, 1 {One tenant} other {List of # tenants} }", |
597 | "tenant-name-starts-with": "Tenants whose names start with '{{prefix}}'", | 630 | "tenant-name-starts-with": "Tenants whose names start with '{{prefix}}'", |
598 | "type-customer": "Customer", | 631 | "type-customer": "Customer", |
632 | + "type-customers": "Customers", | ||
599 | "list-of-customers": "{ count, select, 1 {One customer} other {List of # customers} }", | 633 | "list-of-customers": "{ count, select, 1 {One customer} other {List of # customers} }", |
600 | "customer-name-starts-with": "Customers whose names start with '{{prefix}}'", | 634 | "customer-name-starts-with": "Customers whose names start with '{{prefix}}'", |
601 | "type-user": "User", | 635 | "type-user": "User", |
636 | + "type-users": "Users", | ||
602 | "list-of-users": "{ count, select, 1 {One user} other {List of # users} }", | 637 | "list-of-users": "{ count, select, 1 {One user} other {List of # users} }", |
603 | "user-name-starts-with": "Users whose names start with '{{prefix}}'", | 638 | "user-name-starts-with": "Users whose names start with '{{prefix}}'", |
604 | "type-dashboard": "Dashboard", | 639 | "type-dashboard": "Dashboard", |
640 | + "type-dashboards": "Dashboards", | ||
605 | "list-of-dashboards": "{ count, select, 1 {One dashboard} other {List of # dashboards} }", | 641 | "list-of-dashboards": "{ count, select, 1 {One dashboard} other {List of # dashboards} }", |
606 | "dashboard-name-starts-with": "Dashboards whose names start with '{{prefix}}'", | 642 | "dashboard-name-starts-with": "Dashboards whose names start with '{{prefix}}'", |
607 | "type-alarm": "Alarm", | 643 | "type-alarm": "Alarm", |
644 | + "type-alarms": "Alarms", | ||
608 | "list-of-alarms": "{ count, select, 1 {One alarms} other {List of # alarms} }", | 645 | "list-of-alarms": "{ count, select, 1 {One alarms} other {List of # alarms} }", |
609 | "alarm-name-starts-with": "Alarms whose names start with '{{prefix}}'" | 646 | "alarm-name-starts-with": "Alarms whose names start with '{{prefix}}'" |
610 | }, | 647 | }, |
@@ -770,6 +807,10 @@ export default angular.module('thingsboard.locale', []) | @@ -770,6 +807,10 @@ export default angular.module('thingsboard.locale', []) | ||
770 | "FROM": "From", | 807 | "FROM": "From", |
771 | "TO": "To" | 808 | "TO": "To" |
772 | }, | 809 | }, |
810 | + "direction-type": { | ||
811 | + "FROM": "from", | ||
812 | + "TO": "to" | ||
813 | + }, | ||
773 | "from-relations": "Outbound relations", | 814 | "from-relations": "Outbound relations", |
774 | "to-relations": "Inbound relations", | 815 | "to-relations": "Inbound relations", |
775 | "selected-relations": "{ count, select, 1 {1 relation} other {# relations} } selected", | 816 | "selected-relations": "{ count, select, 1 {1 relation} other {# relations} } selected", |
@@ -783,6 +824,7 @@ export default angular.module('thingsboard.locale', []) | @@ -783,6 +824,7 @@ export default angular.module('thingsboard.locale', []) | ||
783 | "delete": "Delete relation", | 824 | "delete": "Delete relation", |
784 | "relation-type": "Relation type", | 825 | "relation-type": "Relation type", |
785 | "relation-type-required": "Relation type is required.", | 826 | "relation-type-required": "Relation type is required.", |
827 | + "any-relation-type": "Any type", | ||
786 | "add": "Add relation", | 828 | "add": "Add relation", |
787 | "delete-to-relation-title": "Are you sure you want to delete relation to the entity '{{entityName}}'?", | 829 | "delete-to-relation-title": "Are you sure you want to delete relation to the entity '{{entityName}}'?", |
788 | "delete-to-relation-text": "Be careful, after the confirmation the entity '{{entityName}}' will be unrelated from the current entity.", | 830 | "delete-to-relation-text": "Be careful, after the confirmation the entity '{{entityName}}' will be unrelated from the current entity.", |
@@ -791,7 +833,11 @@ export default angular.module('thingsboard.locale', []) | @@ -791,7 +833,11 @@ export default angular.module('thingsboard.locale', []) | ||
791 | "delete-from-relation-title": "Are you sure you want to delete relation from the entity '{{entityName}}'?", | 833 | "delete-from-relation-title": "Are you sure you want to delete relation from the entity '{{entityName}}'?", |
792 | "delete-from-relation-text": "Be careful, after the confirmation current entity will be unrelated from the entity '{{entityName}}'.", | 834 | "delete-from-relation-text": "Be careful, after the confirmation current entity will be unrelated from the entity '{{entityName}}'.", |
793 | "delete-from-relations-title": "Are you sure you want to delete { count, select, 1 {1 relation} other {# relations} }?", | 835 | "delete-from-relations-title": "Are you sure you want to delete { count, select, 1 {1 relation} other {# relations} }?", |
794 | - "delete-from-relations-text": "Be careful, after the confirmation all selected relations will be removed and current entity will be unrelated from the corresponding entities." | 836 | + "delete-from-relations-text": "Be careful, after the confirmation all selected relations will be removed and current entity will be unrelated from the corresponding entities.", |
837 | + "remove-relation-filter": "Remove relation filter", | ||
838 | + "add-relation-filter": "Add relation filter", | ||
839 | + "any-relation": "Any relation", | ||
840 | + "relation-filters": "Relation filters" | ||
795 | }, | 841 | }, |
796 | "rule": { | 842 | "rule": { |
797 | "rule": "Rule", | 843 | "rule": "Rule", |
@@ -236,6 +236,15 @@ div { | @@ -236,6 +236,15 @@ div { | ||
236 | } | 236 | } |
237 | } | 237 | } |
238 | 238 | ||
239 | +.md-caption { | ||
240 | + &.tb-required:after { | ||
241 | + content: ' *'; | ||
242 | + font-size: 10px; | ||
243 | + vertical-align: top; | ||
244 | + color: rgba(0,0,0,0.54); | ||
245 | + } | ||
246 | +} | ||
247 | + | ||
239 | pre.tb-highlight { | 248 | pre.tb-highlight { |
240 | background-color: #f7f7f7; | 249 | background-color: #f7f7f7; |
241 | display: block; | 250 | display: block; |