Commit b8d837abbbe6389c24f35902c7a3c43532db61e2

Authored by Igor Kulikov
1 parent 133ab982

Rule Chains, Widgets Bundles and Dashboards pages

Showing 63 changed files with 2412 additions and 142 deletions
... ... @@ -460,17 +460,16 @@ public class DashboardController extends BaseController {
460 460 @PathVariable("customerId") String strCustomerId,
461 461 @RequestParam int pageSize,
462 462 @RequestParam int page,
463   - @RequestParam(required = false) Long startTime,
464   - @RequestParam(required = false) Long endTime,
465   - @RequestParam(required = false, defaultValue = "false") boolean ascOrder) throws ThingsboardException {
  463 + @RequestParam(required = false) String textSearch,
  464 + @RequestParam(required = false) String sortProperty,
  465 + @RequestParam(required = false) String sortOrder) throws ThingsboardException {
466 466 checkParameter("customerId", strCustomerId);
467 467 try {
468 468 TenantId tenantId = getCurrentUser().getTenantId();
469 469 CustomerId customerId = new CustomerId(toUUID(strCustomerId));
470 470 checkCustomerId(customerId, Operation.READ);
471   - TimePageLink pageLink = createTimePageLink(pageSize, page, "",
472   - "createdTime", ascOrder ? "asc" : "desc", startTime, endTime);
473   - return checkNotNull(dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink).get());
  471 + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
  472 + return checkNotNull(dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink));
474 473 } catch (Exception e) {
475 474 throw handleException(e);
476 475 }
... ...
... ... @@ -323,10 +323,10 @@ public abstract class BaseDashboardControllerTest extends AbstractControllerTest
323 323 }
324 324
325 325 List<DashboardInfo> loadedDashboards = new ArrayList<>();
326   - TimePageLink pageLink = new TimePageLink(21);
  326 + PageLink pageLink = new PageLink(21);
327 327 PageData<DashboardInfo> pageData = null;
328 328 do {
329   - pageData = doGetTypedWithTimePageLink("/api/customer/" + customerId.getId().toString() + "/dashboards?",
  329 + pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/dashboards?",
330 330 new TypeReference<PageData<DashboardInfo>>(){}, pageLink);
331 331 loadedDashboards.addAll(pageData.getData());
332 332 if (pageData.hasNext()) {
... ...
... ... @@ -47,7 +47,7 @@ public interface DashboardService {
47 47
48 48 void deleteDashboardsByTenantId(TenantId tenantId);
49 49
50   - ListenableFuture<PageData<DashboardInfo>> findDashboardsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TimePageLink pageLink);
  50 + PageData<DashboardInfo> findDashboardsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink);
51 51
52 52 void unassignCustomerDashboards(TenantId tenantId, CustomerId customerId);
53 53
... ...
... ... @@ -47,6 +47,6 @@ public interface DashboardInfoDao extends Dao<DashboardInfo> {
47 47 * @param pageLink the page link
48 48 * @return the list of dashboard objects
49 49 */
50   - ListenableFuture<PageData<DashboardInfo>> findDashboardsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TimePageLink pageLink);
  50 + PageData<DashboardInfo> findDashboardsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink);
51 51
52 52 }
... ...
... ... @@ -188,7 +188,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
188 188 }
189 189
190 190 @Override
191   - public ListenableFuture<PageData<DashboardInfo>> findDashboardsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TimePageLink pageLink) {
  191 + public PageData<DashboardInfo> findDashboardsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink) {
192 192 log.trace("Executing findDashboardsByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink);
193 193 Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
194 194 Validator.validateId(customerId, "Incorrect customerId " + customerId);
... ... @@ -250,7 +250,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
250 250 }
251 251 };
252 252
253   - private class CustomerDashboardsUnassigner extends TimePaginatedRemover<Customer, DashboardInfo> {
  253 + private class CustomerDashboardsUnassigner extends PaginatedRemover<Customer, DashboardInfo> {
254 254
255 255 private Customer customer;
256 256
... ... @@ -259,13 +259,8 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
259 259 }
260 260
261 261 @Override
262   - protected PageData<DashboardInfo> findEntities(TenantId tenantId, Customer customer, TimePageLink pageLink) {
263   - try {
264   - return dashboardInfoDao.findDashboardsByTenantIdAndCustomerId(customer.getTenantId().getId(), customer.getId().getId(), pageLink).get();
265   - } catch (InterruptedException | ExecutionException e) {
266   - log.warn("Failed to get dashboards by tenantId [{}] and customerId [{}].", customer.getTenantId().getId(), customer.getId().getId());
267   - throw new RuntimeException(e);
268   - }
  262 + protected PageData<DashboardInfo> findEntities(TenantId tenantId, Customer customer, PageLink pageLink) {
  263 + return dashboardInfoDao.findDashboardsByTenantIdAndCustomerId(customer.getTenantId().getId(), customer.getId().getId(), pageLink);
269 264 }
270 265
271 266 @Override
... ... @@ -275,7 +270,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
275 270
276 271 }
277 272
278   - private class CustomerDashboardsUpdater extends TimePaginatedRemover<Customer, DashboardInfo> {
  273 + private class CustomerDashboardsUpdater extends PaginatedRemover<Customer, DashboardInfo> {
279 274
280 275 private Customer customer;
281 276
... ... @@ -284,13 +279,8 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
284 279 }
285 280
286 281 @Override
287   - protected PageData<DashboardInfo> findEntities(TenantId tenantId, Customer customer, TimePageLink pageLink) {
288   - try {
289   - return dashboardInfoDao.findDashboardsByTenantIdAndCustomerId(customer.getTenantId().getId(), customer.getId().getId(), pageLink).get();
290   - } catch (InterruptedException | ExecutionException e) {
291   - log.warn("Failed to get dashboards by tenantId [{}] and customerId [{}].", customer.getTenantId().getId(), customer.getId().getId());
292   - throw new RuntimeException(e);
293   - }
  282 + protected PageData<DashboardInfo> findEntities(TenantId tenantId, Customer customer, PageLink pageLink) {
  283 + return dashboardInfoDao.findDashboardsByTenantIdAndCustomerId(customer.getTenantId().getId(), customer.getId().getId(), pageLink);
294 284 }
295 285
296 286 @Override
... ...
... ... @@ -38,4 +38,13 @@ public interface DashboardInfoRepository extends PagingAndSortingRepository<Dash
38 38 @Param("searchText") String searchText,
39 39 Pageable pageable);
40 40
  41 + @Query("SELECT di FROM DashboardInfoEntity di, RelationEntity re WHERE di.tenantId = :tenantId " +
  42 + "AND di.id = re.toId AND re.toType = 'DASHBOARD' AND re.relationTypeGroup = 'DASHBOARD' " +
  43 + "AND re.relationType = 'Contains' AND re.fromId = :customerId AND re.fromType = 'CUSTOMER' " +
  44 + "AND LOWER(di.searchText) LIKE LOWER(CONCAT(:searchText, '%'))")
  45 + Page<DashboardInfoEntity> findByTenantIdAndCustomerId(@Param("tenantId") String tenantId,
  46 + @Param("customerId") String customerId,
  47 + @Param("searchText") String searchText,
  48 + Pageable pageable);
  49 +
41 50 }
... ...
... ... @@ -80,19 +80,12 @@ public class JpaDashboardInfoDao extends JpaAbstractSearchTextDao<DashboardInfoE
80 80 }
81 81
82 82 @Override
83   - public ListenableFuture<PageData<DashboardInfo>> findDashboardsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TimePageLink pageLink) {
84   - log.debug("Try to find dashboards by tenantId [{}], customerId[{}] and pageLink [{}]", tenantId, customerId, pageLink);
85   -
86   - ListenableFuture<PageData<EntityRelation>> relations = relationDao.findRelations(new TenantId(tenantId), new CustomerId(customerId), EntityRelation.CONTAINS_TYPE, RelationTypeGroup.DASHBOARD, EntityType.DASHBOARD, pageLink);
87   -
88   - return Futures.transformAsync(relations, input -> {
89   - List<ListenableFuture<DashboardInfo>> dashboardFutures = new ArrayList<>(input.getData().size());
90   - for (EntityRelation relation : input.getData()) {
91   - dashboardFutures.add(findByIdAsync(new TenantId(tenantId), relation.getTo().getId()));
92   - }
93   - return Futures.transform(Futures.successfulAsList(dashboardFutures), dashboards -> {
94   - return new PageData(dashboards, input.getTotalPages(), input.getTotalElements(), input.hasNext());
95   - });
96   - });
  83 + public PageData<DashboardInfo> findDashboardsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink) {
  84 + return DaoUtil.toPageData(dashboardInfoRepository
  85 + .findByTenantIdAndCustomerId(
  86 + UUIDConverter.fromTimeUUID(tenantId),
  87 + UUIDConverter.fromTimeUUID(customerId),
  88 + Objects.toString(pageLink.getTextSearch(), ""),
  89 + DaoUtil.toPageable(pageLink)));
97 90 }
98 91 }
... ...
... ... @@ -302,10 +302,10 @@ public abstract class BaseDashboardServiceTest extends AbstractServiceTest {
302 302 }
303 303
304 304 List<DashboardInfo> loadedDashboards = new ArrayList<>();
305   - TimePageLink pageLink = new TimePageLink(23);
  305 + PageLink pageLink = new PageLink(23);
306 306 PageData<DashboardInfo> pageData = null;
307 307 do {
308   - pageData = dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink).get();
  308 + pageData = dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink);
309 309 loadedDashboards.addAll(pageData.getData());
310 310 if (pageData.hasNext()) {
311 311 pageLink = pageLink.nextPageLink();
... ... @@ -319,8 +319,8 @@ public abstract class BaseDashboardServiceTest extends AbstractServiceTest {
319 319
320 320 dashboardService.unassignCustomerDashboards(tenantId, customerId);
321 321
322   - pageLink = new TimePageLink(42);
323   - pageData = dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink).get();
  322 + pageLink = new PageLink(42);
  323 + pageData = dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink);
324 324 Assert.assertFalse(pageData.hasNext());
325 325 Assert.assertTrue(pageData.getData().isEmpty());
326 326
... ...
... ... @@ -992,6 +992,14 @@
992 992 }
993 993 }
994 994 },
  995 + "@ngx-share/core": {
  996 + "version": "7.1.2",
  997 + "resolved": "https://registry.npmjs.org/@ngx-share/core/-/core-7.1.2.tgz",
  998 + "integrity": "sha512-i54tu5rS+8yxu2v+AFnssSW2FUQJEWFLUiMqXtDIzkXqlPffFyWzpkhx+vfVJi6D7zXiEq1Spb4kubeTJwZpdg==",
  999 + "requires": {
  1000 + "tslib": "^1.9.0"
  1001 + }
  1002 + },
995 1003 "@ngx-translate/core": {
996 1004 "version": "11.0.1",
997 1005 "resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-11.0.1.tgz",
... ...
... ... @@ -28,6 +28,7 @@
28 28 "@ngrx/effects": "^8.2.0",
29 29 "@ngrx/store": "^8.2.0",
30 30 "@ngrx/store-devtools": "^8.2.0",
  31 + "@ngx-share/core": "^7.1.2",
31 32 "@ngx-translate/core": "^11.0.1",
32 33 "@ngx-translate/http-loader": "^4.0.0",
33 34 "ace-builds": "^1.4.5",
... ...
... ... @@ -14,15 +14,14 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -import { Injectable } from '@angular/core';
18   -import { defaultHttpOptions } from './http-utils';
19   -import { Observable } from 'rxjs/index';
20   -import { HttpClient } from '@angular/common/http';
21   -import { PageLink } from '@shared/models/page/page-link';
22   -import { PageData } from '@shared/models/page/page-data';
23   -import { Tenant } from '@shared/models/tenant.model';
24   -import {DashboardInfo, Dashboard} from '@shared/models/dashboard.models';
25   -import {map} from 'rxjs/operators';
  17 +import {Inject, Injectable} from '@angular/core';
  18 +import {defaultHttpOptions} from './http-utils';
  19 +import {Observable} from 'rxjs/index';
  20 +import {HttpClient} from '@angular/common/http';
  21 +import {PageLink} from '@shared/models/page/page-link';
  22 +import {PageData} from '@shared/models/page/page-data';
  23 +import {Dashboard, DashboardInfo} from '@shared/models/dashboard.models';
  24 +import {WINDOW} from '@core/services/window.service';
26 25
27 26 @Injectable({
28 27 providedIn: 'root'
... ... @@ -30,7 +29,8 @@ import {map} from 'rxjs/operators';
30 29 export class DashboardService {
31 30
32 31 constructor(
33   - private http: HttpClient
  32 + private http: HttpClient,
  33 + @Inject(WINDOW) private window: Window
34 34 ) { }
35 35
36 36 public getTenantDashboards(pageLink: PageLink, ignoreErrors: boolean = false,
... ... @@ -48,14 +48,7 @@ export class DashboardService {
48 48 public getCustomerDashboards(customerId: string, pageLink: PageLink, ignoreErrors: boolean = false,
49 49 ignoreLoading: boolean = false): Observable<PageData<DashboardInfo>> {
50 50 return this.http.get<PageData<DashboardInfo>>(`/api/customer/${customerId}/dashboards${pageLink.toQuery()}`,
51   - defaultHttpOptions(ignoreLoading, ignoreErrors)).pipe(
52   - map( dashboards => {
53   - dashboards.data = dashboards.data.filter(dashboard => {
54   - return dashboard.title.toUpperCase().includes(pageLink.textSearch.toUpperCase());
55   - });
56   - return dashboards;
57   - }
58   - ));
  51 + defaultHttpOptions(ignoreLoading, ignoreErrors));
59 52 }
60 53
61 54 public getDashboard(dashboardId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Dashboard> {
... ... @@ -74,4 +67,61 @@ export class DashboardService {
74 67 return this.http.delete(`/api/dashboard/${dashboardId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
75 68 }
76 69
  70 + public assignDashboardToCustomer(customerId: string, dashboardId: string,
  71 + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Dashboard> {
  72 + return this.http.post<Dashboard>(`/api/customer/${customerId}/dashboard/${dashboardId}`,
  73 + null, defaultHttpOptions(ignoreLoading, ignoreErrors));
  74 + }
  75 +
  76 + public unassignDashboardFromCustomer(customerId: string, dashboardId: string,
  77 + ignoreErrors: boolean = false, ignoreLoading: boolean = false) {
  78 + return this.http.delete(`/api/customer/${customerId}/dashboard/${dashboardId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
  79 + }
  80 +
  81 + public makeDashboardPublic(dashboardId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Dashboard> {
  82 + return this.http.post<Dashboard>(`/api/customer/public/dashboard/${dashboardId}`, null,
  83 + defaultHttpOptions(ignoreLoading, ignoreErrors));
  84 + }
  85 +
  86 + public makeDashboardPrivate(dashboardId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Dashboard> {
  87 + return this.http.delete<Dashboard>(`/api/customer/public/dashboard/${dashboardId}`,
  88 + defaultHttpOptions(ignoreLoading, ignoreErrors));
  89 + }
  90 +
  91 + public updateDashboardCustomers(dashboardId: string, customerIds: Array<string>,
  92 + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Dashboard> {
  93 + return this.http.post<Dashboard>(`/api/dashboard/${dashboardId}/customers`, customerIds,
  94 + defaultHttpOptions(ignoreLoading, ignoreErrors));
  95 + }
  96 +
  97 + public addDashboardCustomers(dashboardId: string, customerIds: Array<string>,
  98 + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Dashboard> {
  99 + return this.http.post<Dashboard>(`/api/dashboard/${dashboardId}/customers/add`, customerIds,
  100 + defaultHttpOptions(ignoreLoading, ignoreErrors));
  101 + }
  102 +
  103 + public removeDashboardCustomers(dashboardId: string, customerIds: Array<string>,
  104 + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Dashboard> {
  105 + return this.http.post<Dashboard>(`/api/dashboard/${dashboardId}/customers/remove`, customerIds,
  106 + defaultHttpOptions(ignoreLoading, ignoreErrors));
  107 + }
  108 +
  109 + public getPublicDashboardLink(dashboard: DashboardInfo): string | null {
  110 + if (dashboard && dashboard.assignedCustomers && dashboard.assignedCustomers.length > 0) {
  111 + const publicCustomers = dashboard.assignedCustomers
  112 + .filter(customerInfo => customerInfo.public);
  113 + if (publicCustomers.length > 0) {
  114 + const publicCustomerId = publicCustomers[0].customerId.id;
  115 + let url = this.window.location.protocol + '//' + this.window.location.hostname;
  116 + const port = this.window.location.port;
  117 + if (port !== '80' && port !== '443') {
  118 + url += ':' + port;
  119 + }
  120 + url += `/dashboard/${dashboard.id.id}?publicId=${publicCustomerId}`;
  121 + return url;
  122 + }
  123 + }
  124 + return null;
  125 + }
  126 +
77 127 }
... ...
... ... @@ -107,7 +107,7 @@ export class DeviceService {
107 107 }
108 108
109 109 public assignDeviceToCustomer(customerId: string, deviceId: string,
110   - ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Device> {
  110 + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Device> {
111 111 return this.http.post<Device>(`/api/customer/${customerId}/device/${deviceId}`, null, defaultHttpOptions(ignoreLoading, ignoreErrors));
112 112 }
113 113
... ...
... ... @@ -40,6 +40,7 @@ import {EntityViewService} from '@core/http/entity-view.service';
40 40 import {DataKeyType} from '@shared/models/telemetry/telemetry.models';
41 41 import {DeviceInfo} from '@shared/models/device.models';
42 42 import {defaultHttpOptions} from '@core/http/http-utils';
  43 +import {RuleChainService} from '@core/http/rule-chain.service';
43 44
44 45 @Injectable({
45 46 providedIn: 'root'
... ... @@ -55,6 +56,7 @@ export class EntityService {
55 56 private tenantService: TenantService,
56 57 private customerService: CustomerService,
57 58 private userService: UserService,
  59 + private ruleChainService: RuleChainService,
58 60 private dashboardService: DashboardService
59 61 ) { }
60 62
... ... @@ -85,7 +87,7 @@ export class EntityService {
85 87 observable = this.userService.getUser(entityId, ignoreErrors, ignoreLoading);
86 88 break;
87 89 case EntityType.RULE_CHAIN:
88   - // TODO:
  90 + observable = this.ruleChainService.getRuleChain(entityId, ignoreErrors, ignoreLoading);
89 91 break;
90 92 case EntityType.ALARM:
91 93 console.error('Get Alarm Entity is not implemented!');
... ... @@ -274,7 +276,7 @@ export class EntityService {
274 276 break;
275 277 case EntityType.RULE_CHAIN:
276 278 pageLink.sortOrder.property = 'name';
277   - // TODO:
  279 + entitiesObservable = this.ruleChainService.getRuleChains(pageLink, ignoreErrors, ignoreLoading);
278 280 break;
279 281 case EntityType.DASHBOARD:
280 282 pageLink.sortOrder.property = 'title';
... ...
  1 +///
  2 +/// Copyright © 2016-2019 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 {Injectable} from '@angular/core';
  18 +import {defaultHttpOptions} from './http-utils';
  19 +import {Observable} from 'rxjs/index';
  20 +import {HttpClient} from '@angular/common/http';
  21 +import {PageLink} from '@shared/models/page/page-link';
  22 +import {PageData} from '@shared/models/page/page-data';
  23 +import {RuleChain} from '@shared/models/rule-chain.models';
  24 +
  25 +@Injectable({
  26 + providedIn: 'root'
  27 +})
  28 +export class RuleChainService {
  29 +
  30 + constructor(
  31 + private http: HttpClient
  32 + ) { }
  33 +
  34 + public getRuleChains(pageLink: PageLink, ignoreErrors: boolean = false,
  35 + ignoreLoading: boolean = false): Observable<PageData<RuleChain>> {
  36 + return this.http.get<PageData<RuleChain>>(`/api/ruleChains${pageLink.toQuery()}`,
  37 + defaultHttpOptions(ignoreLoading, ignoreErrors));
  38 + }
  39 +
  40 + public getRuleChain(ruleChainId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<RuleChain> {
  41 + return this.http.get<RuleChain>(`/api/ruleChain/${ruleChainId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
  42 + }
  43 +
  44 + public saveRuleChain(ruleChain: RuleChain, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<RuleChain> {
  45 + return this.http.post<RuleChain>('/api/ruleChain', ruleChain, defaultHttpOptions(ignoreLoading, ignoreErrors));
  46 + }
  47 +
  48 + public deleteRuleChain(ruleChainId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) {
  49 + return this.http.delete(`/api/ruleChain/${ruleChainId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
  50 + }
  51 +
  52 + public setRootRuleChain(ruleChainId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<RuleChain> {
  53 + return this.http.post<RuleChain>(`/api/ruleChain/${ruleChainId}/root`, null, defaultHttpOptions(ignoreLoading, ignoreErrors));
  54 + }
  55 +
  56 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 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 {Injectable} from '@angular/core';
  18 +import {defaultHttpOptions} from './http-utils';
  19 +import {Observable} from 'rxjs/index';
  20 +import {HttpClient} from '@angular/common/http';
  21 +import {PageLink} from '@shared/models/page/page-link';
  22 +import {PageData} from '@shared/models/page/page-data';
  23 +import {WidgetsBundle} from '@shared/models/widgets-bundle.model';
  24 +
  25 +@Injectable({
  26 + providedIn: 'root'
  27 +})
  28 +export class WidgetService {
  29 +
  30 + constructor(
  31 + private http: HttpClient
  32 + ) { }
  33 +
  34 + public getWidgetBundles(pageLink: PageLink, ignoreErrors: boolean = false,
  35 + ignoreLoading: boolean = false): Observable<PageData<WidgetsBundle>> {
  36 + return this.http.get<PageData<WidgetsBundle>>(`/api/widgetsBundles${pageLink.toQuery()}`,
  37 + defaultHttpOptions(ignoreLoading, ignoreErrors));
  38 + }
  39 +
  40 + public getWidgetsBundle(widgetsBundleId: string,
  41 + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<WidgetsBundle> {
  42 + return this.http.get<WidgetsBundle>(`/api/widgetsBundle/${widgetsBundleId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
  43 + }
  44 +
  45 + public saveWidgetsBundle(widgetsBundle: WidgetsBundle,
  46 + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<WidgetsBundle> {
  47 + return this.http.post<WidgetsBundle>('/api/widgetsBundle', widgetsBundle, defaultHttpOptions(ignoreLoading, ignoreErrors));
  48 + }
  49 +
  50 + public deleteWidgetsBundle(widgetsBundleId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) {
  51 + return this.http.delete(`/api/widgetsBundle/${widgetsBundleId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
  52 + }
  53 +
  54 +}
... ...
... ... @@ -41,6 +41,17 @@ export function onParentScrollOrWindowResize(el: Node): Observable<Event> {
41 41 return shared;
42 42 }
43 43
  44 +export function isLocalUrl(url: string): boolean {
  45 + const parser = document.createElement('a');
  46 + parser.href = url;
  47 + const host = parser.hostname;
  48 + if (host === 'localhost' || host === '127.0.0.1') {
  49 + return true;
  50 + } else {
  51 + return false;
  52 + }
  53 +}
  54 +
44 55 const scrollRegex = /(auto|scroll)/;
45 56
46 57 function parentNodes(node: Node, nodes: Node[]): Node[] {
... ...
... ... @@ -21,11 +21,11 @@ import {Store} from '@ngrx/store';
21 21 import {AppState} from '@core/core.state';
22 22 import {FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators} from '@angular/forms';
23 23 import {DeviceService} from '@core/http/device.service';
24   -import {EntityId} from '@shared/models/id/entity-id';
25 24 import {EntityType} from '@shared/models/entity-type.models';
26 25 import {forkJoin, Observable} from 'rxjs';
27 26 import {AssetService} from '@core/http/asset.service';
28 27 import {EntityViewService} from '@core/http/entity-view.service';
  28 +import {DashboardService} from '@core/http/dashboard.service';
29 29
30 30 export interface AddEntitiesToCustomerDialogData {
31 31 customerId: string;
... ... @@ -54,6 +54,7 @@ export class AddEntitiesToCustomerDialogComponent extends PageComponent implemen
54 54 private deviceService: DeviceService,
55 55 private assetService: AssetService,
56 56 private entityViewService: EntityViewService,
  57 + private dashboardService: DashboardService,
57 58 @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
58 59 public dialogRef: MatDialogRef<AddEntitiesToCustomerDialogComponent, boolean>,
59 60 public fb: FormBuilder) {
... ... @@ -78,6 +79,10 @@ export class AddEntitiesToCustomerDialogComponent extends PageComponent implemen
78 79 this.assignToCustomerTitle = 'entity-view.assign-entity-view-to-customer';
79 80 this.assignToCustomerText = 'entity-view.assign-entity-view-to-customer-text';
80 81 break;
  82 + case EntityType.DASHBOARD:
  83 + this.assignToCustomerTitle = 'dashboard.assign-dashboard-to-customer';
  84 + this.assignToCustomerText = 'dashboard.assign-dashboard-to-customer-text';
  85 + break;
81 86 }
82 87 }
83 88
... ... @@ -118,6 +123,9 @@ export class AddEntitiesToCustomerDialogComponent extends PageComponent implemen
118 123 case EntityType.ENTITY_VIEW:
119 124 return this.entityViewService.assignEntityViewToCustomer(customerId, entityId);
120 125 break;
  126 + case EntityType.DASHBOARD:
  127 + return this.dashboardService.assignDashboardToCustomer(customerId, entityId);
  128 + break;
121 129 }
122 130 }
123 131
... ...
... ... @@ -93,6 +93,7 @@ export class AssetsTableConfigResolver implements Resolve<EntityTableConfig<Asse
93 93 ));
94 94 };
95 95 this.config.onEntityAction = action => this.onAssetAction(action);
  96 + this.config.detailsReadonly = () => this.config.componentsData.assetScope === 'customer_user';
96 97
97 98 this.config.headerComponent = AssetTableHeaderComponent;
98 99
... ...
... ... @@ -23,6 +23,7 @@ import {UsersTableConfigResolver} from '../user/users-table-config.resolver';
23 23 import {CustomersTableConfigResolver} from './customers-table-config.resolver';
24 24 import {DevicesTableConfigResolver} from '@modules/home/pages/device/devices-table-config.resolver';
25 25 import {AssetsTableConfigResolver} from '../asset/assets-table-config.resolver';
  26 +import {DashboardsTableConfigResolver} from '@modules/home/pages/dashboard/dashboards-table-config.resolver';
26 27
27 28 const routes: Routes = [
28 29 {
... ... @@ -91,6 +92,22 @@ const routes: Routes = [
91 92 resolve: {
92 93 entitiesTableConfig: AssetsTableConfigResolver
93 94 }
  95 + },
  96 + {
  97 + path: ':customerId/dashboards',
  98 + component: EntitiesTableComponent,
  99 + data: {
  100 + auth: [Authority.TENANT_ADMIN],
  101 + title: 'customer.assets',
  102 + dashboardsType: 'customer',
  103 + breadcrumb: {
  104 + label: 'customer.dashboards',
  105 + icon: 'dashboard'
  106 + }
  107 + },
  108 + resolve: {
  109 + entitiesTableConfig: DashboardsTableConfigResolver
  110 + }
94 111 }
95 112 ]
96 113 }
... ...
... ... @@ -155,6 +155,15 @@ export class CustomersTableConfigResolver implements Resolve<EntityTableConfig<C
155 155 case 'manageUsers':
156 156 this.manageCustomerUsers(action.event, action.entity);
157 157 return true;
  158 + case 'manageAssets':
  159 + this.manageCustomerAssets(action.event, action.entity);
  160 + return true;
  161 + case 'manageDevices':
  162 + this.manageCustomerDevices(action.event, action.entity);
  163 + return true;
  164 + case 'manageDashboards':
  165 + this.manageCustomerDashboards(action.event, action.entity);
  166 + return true;
158 167 }
159 168 return false;
160 169 }
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 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-details-buttons">
  19 + <button mat-raised-button color="primary"
  20 + [disabled]="(isLoading$ | async)"
  21 + (click)="onEntityAction($event, 'open')"
  22 + [fxShow]="!isEdit">
  23 + {{'dashboard.open-dashboard' | translate }}
  24 + </button>
  25 + <button mat-raised-button color="primary"
  26 + [disabled]="(isLoading$ | async)"
  27 + (click)="onEntityAction($event, 'export')"
  28 + [fxShow]="!isEdit && dashboardScope === 'tenant'">
  29 + {{'dashboard.export' | translate }}
  30 + </button>
  31 + <button mat-raised-button color="primary"
  32 + [disabled]="(isLoading$ | async)"
  33 + (click)="onEntityAction($event, 'makePublic')"
  34 + [fxShow]="!isEdit && dashboardScope === 'tenant' && !isPublic(entity)">
  35 + {{'dashboard.make-public' | translate }}
  36 + </button>
  37 + <button mat-raised-button color="primary"
  38 + [disabled]="(isLoading$ | async)"
  39 + (click)="onEntityAction($event, 'makePrivate')"
  40 + [fxShow]="!isEdit && (dashboardScope === 'tenant' && isPublic(entity)
  41 + || dashboardScope === 'customer' && isCurrentPublicCustomer(entity))">
  42 + {{'dashboard.make-private' | translate }}
  43 + </button>
  44 + <button mat-raised-button color="primary"
  45 + [disabled]="(isLoading$ | async)"
  46 + (click)="onEntityAction($event, 'manageAssignedCustomers')"
  47 + [fxShow]="!isEdit && dashboardScope === 'tenant'">
  48 + {{'dashboard.manage-assigned-customers' | translate }}
  49 + </button>
  50 + <button mat-raised-button color="primary"
  51 + [disabled]="(isLoading$ | async)"
  52 + (click)="onEntityAction($event, 'unassignFromCustomer')"
  53 + [fxShow]="!isEdit && dashboardScope === 'customer' && !isCurrentPublicCustomer(entity)">
  54 + {{ 'dashboard.unassign-from-customer' | translate }}
  55 + </button>
  56 + <button mat-raised-button color="primary"
  57 + [disabled]="(isLoading$ | async)"
  58 + (click)="onEntityAction($event, 'delete')"
  59 + [fxShow]="!hideDelete() && !isEdit">
  60 + {{'dashboard.delete' | translate }}
  61 + </button>
  62 +</div>
  63 +<div class="mat-padding" fxLayout="column">
  64 + <mat-form-field class="mat-block"
  65 + [fxShow]="!isEdit && assignedCustomersText
  66 + && dashboardScope === 'tenant'">
  67 + <mat-label translate>dashboard.assignedToCustomers</mat-label>
  68 + <input matInput disabled [ngModel]="assignedCustomersText">
  69 + </mat-form-field>
  70 + <div fxLayout="column" [fxShow]="!isEdit && (dashboardScope === 'tenant' && isPublic(entity)
  71 + || dashboardScope === 'customer' && isCurrentPublicCustomer(entity))">
  72 + <tb-social-share-panel style="padding-bottom: 10px;"
  73 + shareTitle="{{ 'dashboard.socialshare-title' | translate:{dashboardTitle: entity?.title} }}"
  74 + shareText="{{ 'dashboard.socialshare-text' | translate:{dashboardTitle: entity?.title} }}"
  75 + shareLink="{{ publicLink }}"
  76 + shareHashTags="thingsboard, iot">
  77 + </tb-social-share-panel>
  78 + <div fxLayout="row">
  79 + <mat-form-field class="mat-block" fxFlex>
  80 + <mat-label translate>dashboard.public-link</mat-label>
  81 + <input matInput disabled [ngModel]="publicLink">
  82 + </mat-form-field>
  83 + <button mat-button mat-icon-button style="margin-top: 8px;"
  84 + ngxClipboard
  85 + (cbOnSuccess)="onPublicLinkCopied($event)"
  86 + cbContent="{{ publicLink }}"
  87 + matTooltipPosition="above"
  88 + matTooltip="{{ 'dashboard.copy-public-link' | translate }}">
  89 + <mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
  90 + </button>
  91 + </div>
  92 + </div>
  93 + <form #entityNgForm="ngForm" [formGroup]="entityForm">
  94 + <fieldset [disabled]="(isLoading$ | async) || !isEdit">
  95 + <mat-form-field class="mat-block">
  96 + <mat-label translate>dashboard.title</mat-label>
  97 + <input matInput formControlName="title" required>
  98 + <mat-error *ngIf="entityForm.get('title').hasError('required')">
  99 + {{ 'dashboard.title-required' | translate }}
  100 + </mat-error>
  101 + </mat-form-field>
  102 + <div formGroupName="configuration" fxLayout="column">
  103 + <mat-form-field class="mat-block">
  104 + <mat-label translate>dashboard.description</mat-label>
  105 + <textarea matInput formControlName="description" rows="2"></textarea>
  106 + </mat-form-field>
  107 + </div>
  108 + </fieldset>
  109 + </form>
  110 +</div>
... ...
  1 +/**
  2 + * Copyright © 2016-2019 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 +:host {
  18 +
  19 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 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 {Component} from '@angular/core';
  18 +import {Store} from '@ngrx/store';
  19 +import {AppState} from '@core/core.state';
  20 +import {EntityComponent} from '@shared/components/entity/entity.component';
  21 +import {FormBuilder, FormGroup, Validators} from '@angular/forms';
  22 +import {ActionNotificationShow} from '@core/notification/notification.actions';
  23 +import {TranslateService} from '@ngx-translate/core';
  24 +import {
  25 + Dashboard,
  26 + isPublicDashboard,
  27 + getDashboardAssignedCustomersText,
  28 + isCurrentPublicDashboardCustomer,
  29 + DashboardInfo
  30 +} from '@shared/models/dashboard.models';
  31 +import {DashboardService} from '@core/http/dashboard.service';
  32 +
  33 +@Component({
  34 + selector: 'tb-dashboard-form',
  35 + templateUrl: './dashboard-form.component.html',
  36 + styleUrls: ['./dashboard-form.component.scss']
  37 +})
  38 +export class DashboardFormComponent extends EntityComponent<Dashboard | DashboardInfo> {
  39 +
  40 + dashboardScope: 'tenant' | 'customer' | 'customer_user';
  41 + customerId: string;
  42 +
  43 + publicLink: string;
  44 + assignedCustomersText: string;
  45 +
  46 + constructor(protected store: Store<AppState>,
  47 + protected translate: TranslateService,
  48 + private dashboardService: DashboardService,
  49 + public fb: FormBuilder) {
  50 + super(store);
  51 + }
  52 +
  53 + ngOnInit() {
  54 + this.dashboardScope = this.entitiesTableConfig.componentsData.dashboardScope;
  55 + this.customerId = this.entitiesTableConfig.componentsData.customerId;
  56 + super.ngOnInit();
  57 + }
  58 +
  59 + isPublic(entity: Dashboard): boolean {
  60 + return isPublicDashboard(entity);
  61 + }
  62 +
  63 + isCurrentPublicCustomer(entity: Dashboard): boolean {
  64 + return isCurrentPublicDashboardCustomer(entity, this.customerId);
  65 + }
  66 +
  67 + hideDelete() {
  68 + if (this.entitiesTableConfig) {
  69 + return !this.entitiesTableConfig.deleteEnabled(this.entity);
  70 + } else {
  71 + return false;
  72 + }
  73 + }
  74 +
  75 + buildForm(entity: Dashboard): FormGroup {
  76 + this.updateFields(entity);
  77 + return this.fb.group(
  78 + {
  79 + title: [entity ? entity.title : '', [Validators.required]],
  80 + configuration: this.fb.group(
  81 + {
  82 + description: [entity && entity.configuration ? entity.configuration.description : ''],
  83 + }
  84 + )
  85 + }
  86 + );
  87 + }
  88 +
  89 + updateForm(entity: Dashboard) {
  90 + this.updateFields(entity);
  91 + this.entityForm.patchValue({title: entity.title});
  92 + this.entityForm.patchValue({configuration: {description: entity.configuration ? entity.configuration.description : ''}});
  93 + }
  94 +
  95 + onPublicLinkCopied($event) {
  96 + this.store.dispatch(new ActionNotificationShow(
  97 + {
  98 + message: this.translate.instant('dashboard.public-link-copied-message'),
  99 + type: 'success',
  100 + duration: 750,
  101 + verticalPosition: 'bottom',
  102 + horizontalPosition: 'right'
  103 + }));
  104 + }
  105 +
  106 + private updateFields(entity: Dashboard): void {
  107 + this.assignedCustomersText = getDashboardAssignedCustomersText(entity);
  108 + this.publicLink = this.dashboardService.getPublicDashboardLink(entity);
  109 + }
  110 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 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 {NgModule} from '@angular/core';
  18 +import {RouterModule, Routes} from '@angular/router';
  19 +
  20 +import {EntitiesTableComponent} from '@shared/components/entity/entities-table.component';
  21 +import {Authority} from '@shared/models/authority.enum';
  22 +import {DashboardsTableConfigResolver} from './dashboards-table-config.resolver';
  23 +
  24 +const routes: Routes = [
  25 + {
  26 + path: 'dashboards',
  27 + data: {
  28 + breadcrumb: {
  29 + label: 'dashboard.dashboards',
  30 + icon: 'dashboard'
  31 + }
  32 + },
  33 + children: [
  34 + {
  35 + path: '',
  36 + component: EntitiesTableComponent,
  37 + data: {
  38 + auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
  39 + title: 'dashboard.dashboards',
  40 + dashboardsType: 'tenant'
  41 + },
  42 + resolve: {
  43 + entitiesTableConfig: DashboardsTableConfigResolver
  44 + }
  45 + }
  46 + ]
  47 + }
  48 +];
  49 +
  50 +@NgModule({
  51 + imports: [RouterModule.forChild(routes)],
  52 + exports: [RouterModule],
  53 + providers: [
  54 + DashboardsTableConfigResolver
  55 + ]
  56 +})
  57 +export class DashboardRoutingModule { }
... ...
  1 +///
  2 +/// Copyright © 2016-2019 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 {NgModule} from '@angular/core';
  18 +import {CommonModule} from '@angular/common';
  19 +import {SharedModule} from '@shared/shared.module';
  20 +import {HomeDialogsModule} from '../../dialogs/home-dialogs.module';
  21 +import {DashboardFormComponent} from '@modules/home/pages/dashboard/dashboard-form.component';
  22 +import {ManageDashboardCustomersDialogComponent} from '@modules/home/pages/dashboard/manage-dashboard-customers-dialog.component';
  23 +import {DashboardRoutingModule} from './dashboard-routing.module';
  24 +
  25 +@NgModule({
  26 + entryComponents: [
  27 + DashboardFormComponent,
  28 + ManageDashboardCustomersDialogComponent
  29 + ],
  30 + declarations: [
  31 + DashboardFormComponent,
  32 + ManageDashboardCustomersDialogComponent
  33 + ],
  34 + imports: [
  35 + CommonModule,
  36 + SharedModule,
  37 + HomeDialogsModule,
  38 + DashboardRoutingModule
  39 + ]
  40 +})
  41 +export class DashboardModule { }
... ...
  1 +///
  2 +/// Copyright © 2016-2019 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 {Injectable} from '@angular/core';
  18 +
  19 +import {ActivatedRouteSnapshot, Resolve, Router} from '@angular/router';
  20 +import {
  21 + CellActionDescriptor,
  22 + checkBoxCell,
  23 + DateEntityTableColumn,
  24 + EntityTableColumn,
  25 + EntityTableConfig,
  26 + GroupActionDescriptor,
  27 + HeaderActionDescriptor
  28 +} from '@shared/components/entity/entities-table-config.models';
  29 +import {TranslateService} from '@ngx-translate/core';
  30 +import {DatePipe} from '@angular/common';
  31 +import {EntityType, entityTypeResources, entityTypeTranslations} from '@shared/models/entity-type.models';
  32 +import {EntityAction} from '@shared/components/entity/entity-component.models';
  33 +import {forkJoin, Observable, of} from 'rxjs';
  34 +import {select, Store} from '@ngrx/store';
  35 +import {selectAuthUser} from '@core/auth/auth.selectors';
  36 +import {map, mergeMap, take, tap} from 'rxjs/operators';
  37 +import {AppState} from '@core/core.state';
  38 +import {Authority} from '@app/shared/models/authority.enum';
  39 +import {CustomerService} from '@core/http/customer.service';
  40 +import {Customer} from '@app/shared/models/customer.model';
  41 +import {MatDialog} from '@angular/material';
  42 +import {DialogService} from '@core/services/dialog.service';
  43 +import {
  44 + AddEntitiesToCustomerDialogComponent,
  45 + AddEntitiesToCustomerDialogData
  46 +} from '../../dialogs/add-entities-to-customer-dialog.component';
  47 +import {
  48 + Dashboard,
  49 + DashboardInfo,
  50 + getDashboardAssignedCustomersText,
  51 + isCurrentPublicDashboardCustomer,
  52 + isPublicDashboard
  53 +} from '@app/shared/models/dashboard.models';
  54 +import {DashboardService} from '@app/core/http/dashboard.service';
  55 +import {DashboardFormComponent} from '@modules/home/pages/dashboard/dashboard-form.component';
  56 +import {
  57 + ManageDashboardCustomersActionType,
  58 + ManageDashboardCustomersDialogComponent,
  59 + ManageDashboardCustomersDialogData
  60 +} from './manage-dashboard-customers-dialog.component';
  61 +
  62 +@Injectable()
  63 +export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<DashboardInfo | Dashboard>> {
  64 +
  65 + private readonly config: EntityTableConfig<DashboardInfo | Dashboard> = new EntityTableConfig<DashboardInfo | Dashboard>();
  66 +
  67 + constructor(private store: Store<AppState>,
  68 + private dashboardService: DashboardService,
  69 + private customerService: CustomerService,
  70 + private dialogService: DialogService,
  71 + private translate: TranslateService,
  72 + private datePipe: DatePipe,
  73 + private router: Router,
  74 + private dialog: MatDialog) {
  75 +
  76 + this.config.entityType = EntityType.DASHBOARD;
  77 + this.config.entityComponent = DashboardFormComponent;
  78 + this.config.entityTranslations = entityTypeTranslations.get(EntityType.DASHBOARD);
  79 + this.config.entityResources = entityTypeResources.get(EntityType.DASHBOARD);
  80 +
  81 + this.config.deleteEntityTitle = dashboard =>
  82 + this.translate.instant('dashboard.delete-dashboard-title', { dashboardTitle: dashboard.title });
  83 + this.config.deleteEntityContent = () => this.translate.instant('dashboard.delete-dashboard-text');
  84 + this.config.deleteEntitiesTitle = count => this.translate.instant('dashboard.delete-dashboards-title', {count});
  85 + this.config.deleteEntitiesContent = () => this.translate.instant('dashboard.delete-dashboards-text');
  86 +
  87 + this.config.loadEntity = id => this.dashboardService.getDashboard(id.id);
  88 + this.config.saveEntity = dashboard => {
  89 + return this.dashboardService.saveDashboard(dashboard as Dashboard);
  90 + };
  91 + this.config.onEntityAction = action => this.onDashboardAction(action);
  92 + this.config.detailsReadonly = () => this.config.componentsData.dashboardScope === 'customer_user';
  93 + }
  94 +
  95 + resolve(route: ActivatedRouteSnapshot): Observable<EntityTableConfig<DashboardInfo | Dashboard>> {
  96 + const routeParams = route.params;
  97 + this.config.componentsData = {
  98 + dashboardScope: route.data.dashboardsType,
  99 + customerId: routeParams.customerId
  100 + };
  101 + return this.store.pipe(select(selectAuthUser), take(1)).pipe(
  102 + tap((authUser) => {
  103 + if (authUser.authority === Authority.CUSTOMER_USER) {
  104 + this.config.componentsData.dashboardScope = 'customer_user';
  105 + this.config.componentsData.customerId = authUser.customerId;
  106 + }
  107 + }),
  108 + mergeMap(() =>
  109 + this.config.componentsData.customerId ?
  110 + this.customerService.getCustomer(this.config.componentsData.customerId) : of(null as Customer)
  111 + ),
  112 + map((parentCustomer) => {
  113 + if (parentCustomer) {
  114 + if (parentCustomer.additionalInfo && parentCustomer.additionalInfo.isPublic) {
  115 + this.config.tableTitle = this.translate.instant('customer.public-dashboards');
  116 + } else {
  117 + this.config.tableTitle = parentCustomer.title + ': ' + this.translate.instant('dashboard.dashboards');
  118 + }
  119 + } else {
  120 + this.config.tableTitle = this.translate.instant('dashboard.dashboards');
  121 + }
  122 + this.config.columns = this.configureColumns(this.config.componentsData.dashboardScope);
  123 + this.configureEntityFunctions(this.config.componentsData.dashboardScope);
  124 + this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.dashboardScope);
  125 + this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.dashboardScope);
  126 + this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.dashboardScope);
  127 + this.config.addEnabled = this.config.componentsData.dashboardScope !== 'customer_user';
  128 + this.config.entitiesDeleteEnabled = this.config.componentsData.dashboardScope === 'tenant';
  129 + this.config.deleteEnabled = () => this.config.componentsData.dashboardScope === 'tenant';
  130 + return this.config;
  131 + })
  132 + );
  133 + }
  134 +
  135 + configureColumns(dashboardScope: string): Array<EntityTableColumn<DashboardInfo>> {
  136 + const columns: Array<EntityTableColumn<DashboardInfo>> = [
  137 + new DateEntityTableColumn<DashboardInfo>('createdTime', 'dashboard.created-time', this.datePipe, '150px'),
  138 + new EntityTableColumn<DashboardInfo>('title', 'dashboard.title')
  139 + ];
  140 + if (dashboardScope === 'tenant') {
  141 + columns.push(
  142 + new EntityTableColumn<DashboardInfo>('customersTitle', 'dashboard.assignedToCustomers',
  143 + '100%', entity => {
  144 + return getDashboardAssignedCustomersText(entity);
  145 + }, () => ({}), false),
  146 + new EntityTableColumn<DashboardInfo>('dashboardIsPublic', 'dashboard.public', '60px',
  147 + entity => {
  148 + return checkBoxCell(isPublicDashboard(entity));
  149 + }, () => ({}), false),
  150 + );
  151 + }
  152 + return columns;
  153 + }
  154 +
  155 + configureEntityFunctions(dashboardScope: string): void {
  156 + if (dashboardScope === 'tenant') {
  157 + this.config.entitiesFetchFunction = pageLink =>
  158 + this.dashboardService.getTenantDashboards(pageLink);
  159 + this.config.deleteEntity = id => this.dashboardService.deleteDashboard(id.id);
  160 + } else {
  161 + this.config.entitiesFetchFunction = pageLink =>
  162 + this.dashboardService.getCustomerDashboards(this.config.componentsData.customerId, pageLink);
  163 + this.config.deleteEntity = id =>
  164 + this.dashboardService.unassignDashboardFromCustomer(this.config.componentsData.customerId, id.id);
  165 + }
  166 + }
  167 +
  168 + configureCellActions(dashboardScope: string): Array<CellActionDescriptor<DashboardInfo>> {
  169 + const actions: Array<CellActionDescriptor<DashboardInfo>> = [];
  170 + actions.push(
  171 + {
  172 + name: this.translate.instant('dashboard.open-dashboard'),
  173 + icon: 'dashboard',
  174 + isEnabled: () => true,
  175 + onAction: ($event, entity) => this.openDashboard($event, entity)
  176 + }
  177 + );
  178 + if (dashboardScope === 'tenant') {
  179 + actions.push(
  180 + {
  181 + name: this.translate.instant('dashboard.export'),
  182 + icon: 'file_download',
  183 + isEnabled: () => true,
  184 + onAction: ($event, entity) => this.exportDashboard($event, entity)
  185 + },
  186 + {
  187 + name: this.translate.instant('dashboard.make-public'),
  188 + icon: 'share',
  189 + isEnabled: (entity) => !isPublicDashboard(entity),
  190 + onAction: ($event, entity) => this.makePublic($event, entity)
  191 + },
  192 + {
  193 + name: this.translate.instant('dashboard.make-private'),
  194 + icon: 'reply',
  195 + isEnabled: (entity) => isPublicDashboard(entity),
  196 + onAction: ($event, entity) => this.makePrivate($event, entity)
  197 + },
  198 + {
  199 + name: this.translate.instant('dashboard.manage-assigned-customers'),
  200 + icon: 'assignment_ind',
  201 + isEnabled: () => true,
  202 + onAction: ($event, entity) => this.manageAssignedCustomers($event, entity)
  203 + }
  204 + );
  205 + }
  206 + if (dashboardScope === 'customer') {
  207 + actions.push(
  208 + {
  209 + name: this.translate.instant('dashboard.export'),
  210 + icon: 'file_download',
  211 + isEnabled: () => true,
  212 + onAction: ($event, entity) => this.exportDashboard($event, entity)
  213 + },
  214 + {
  215 + name: this.translate.instant('dashboard.make-private'),
  216 + icon: 'reply',
  217 + isEnabled: (entity) => isCurrentPublicDashboardCustomer(entity, this.config.componentsData.customerId),
  218 + onAction: ($event, entity) => this.makePrivate($event, entity)
  219 + },
  220 + {
  221 + name: this.translate.instant('dashboard.unassign-from-customer'),
  222 + icon: 'assignment_return',
  223 + isEnabled: (entity) => !isCurrentPublicDashboardCustomer(entity, this.config.componentsData.customerId),
  224 + onAction: ($event, entity) => this.unassignFromCustomer($event, entity, this.config.componentsData.customerId)
  225 + }
  226 + );
  227 + }
  228 + return actions;
  229 + }
  230 +
  231 + configureGroupActions(dashboardScope: string): Array<GroupActionDescriptor<DashboardInfo>> {
  232 + const actions: Array<GroupActionDescriptor<DashboardInfo>> = [];
  233 + if (dashboardScope === 'tenant') {
  234 + actions.push(
  235 + {
  236 + name: this.translate.instant('dashboard.assign-dashboards'),
  237 + icon: 'assignment_ind',
  238 + isEnabled: true,
  239 + onAction: ($event, entities) => this.assignDashboardsToCustomers($event, entities.map((entity) => entity.id.id))
  240 + }
  241 + );
  242 + actions.push(
  243 + {
  244 + name: this.translate.instant('dashboard.unassign-dashboards'),
  245 + icon: 'assignment_return',
  246 + isEnabled: true,
  247 + onAction: ($event, entities) => this.unassignDashboardsFromCustomers($event, entities.map((entity) => entity.id.id))
  248 + }
  249 + );
  250 + }
  251 + if (dashboardScope === 'customer') {
  252 + actions.push(
  253 + {
  254 + name: this.translate.instant('dashboard.unassign-dashboards'),
  255 + icon: 'assignment_return',
  256 + isEnabled: true,
  257 + onAction: ($event, entities) =>
  258 + this.unassignDashboardsFromCustomer($event, entities.map((entity) => entity.id.id), this.config.componentsData.customerId)
  259 + }
  260 + );
  261 + }
  262 + return actions;
  263 + }
  264 +
  265 + configureAddActions(dashboardScope: string): Array<HeaderActionDescriptor> {
  266 + const actions: Array<HeaderActionDescriptor> = [];
  267 + if (dashboardScope === 'tenant') {
  268 + actions.push(
  269 + {
  270 + name: this.translate.instant('dashboard.create-new-dashboard'),
  271 + icon: 'insert_drive_file',
  272 + isEnabled: () => true,
  273 + onAction: ($event) => this.config.table.addEntity($event)
  274 + },
  275 + {
  276 + name: this.translate.instant('dashboard.import'),
  277 + icon: 'file_upload',
  278 + isEnabled: () => true,
  279 + onAction: ($event) => this.importDashboard($event)
  280 + }
  281 + );
  282 + }
  283 + if (dashboardScope === 'customer') {
  284 + actions.push(
  285 + {
  286 + name: this.translate.instant('dashboard.assign-new-dashboard'),
  287 + icon: 'add',
  288 + isEnabled: () => true,
  289 + onAction: ($event) => this.addDashboardsToCustomer($event)
  290 + }
  291 + );
  292 + }
  293 + return actions;
  294 + }
  295 +
  296 + openDashboard($event: Event, dashboard: DashboardInfo) {
  297 + if ($event) {
  298 + $event.stopPropagation();
  299 + }
  300 + // TODO:
  301 + // this.router.navigateByUrl(`customers/${customer.id.id}/users`);
  302 + }
  303 +
  304 + importDashboard($event: Event) {
  305 + if ($event) {
  306 + $event.stopPropagation();
  307 + }
  308 + // TODO:
  309 + }
  310 +
  311 + exportDashboard($event: Event, dashboard: DashboardInfo) {
  312 + if ($event) {
  313 + $event.stopPropagation();
  314 + }
  315 + // TODO:
  316 + }
  317 +
  318 + addDashboardsToCustomer($event: Event) {
  319 + if ($event) {
  320 + $event.stopPropagation();
  321 + }
  322 + this.dialog.open<AddEntitiesToCustomerDialogComponent, AddEntitiesToCustomerDialogData,
  323 + boolean>(AddEntitiesToCustomerDialogComponent, {
  324 + disableClose: true,
  325 + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
  326 + data: {
  327 + customerId: this.config.componentsData.customerId,
  328 + entityType: EntityType.DASHBOARD
  329 + }
  330 + }).afterClosed()
  331 + .subscribe((res) => {
  332 + if (res) {
  333 + this.config.table.updateData();
  334 + }
  335 + });
  336 + }
  337 +
  338 + makePublic($event: Event, dashboard: DashboardInfo) {
  339 + if ($event) {
  340 + $event.stopPropagation();
  341 + }
  342 + this.dashboardService.makeDashboardPublic(dashboard.id.id).subscribe(
  343 + (publicDashboard) => {
  344 + // TODO:
  345 +
  346 + this.config.table.updateData();
  347 + }
  348 + );
  349 + }
  350 +
  351 + makePrivate($event: Event, dashboard: DashboardInfo) {
  352 + if ($event) {
  353 + $event.stopPropagation();
  354 + }
  355 + this.dialogService.confirm(
  356 + this.translate.instant('dashboard.make-private-dashboard-title', {dashboardTitle: dashboard.title}),
  357 + this.translate.instant('dashboard.make-private-dashboard-text'),
  358 + this.translate.instant('action.no'),
  359 + this.translate.instant('action.yes'),
  360 + true
  361 + ).subscribe((res) => {
  362 + if (res) {
  363 + this.dashboardService.makeDashboardPrivate(dashboard.id.id).subscribe(
  364 + () => {
  365 + this.config.table.updateData();
  366 + }
  367 + );
  368 + }
  369 + }
  370 + );
  371 + }
  372 +
  373 + manageAssignedCustomers($event: Event, dashboard: DashboardInfo) {
  374 + const assignedCustomersIds = dashboard.assignedCustomers ?
  375 + dashboard.assignedCustomers.map(customerInfo => customerInfo.customerId.id) : [];
  376 + this.showManageAssignedCustomersDialog($event, [dashboard.id.id], 'manage', assignedCustomersIds);
  377 + }
  378 +
  379 + assignDashboardsToCustomers($event: Event, dashboardIds: Array<string>) {
  380 + this.showManageAssignedCustomersDialog($event, dashboardIds, 'assign');
  381 + }
  382 +
  383 + unassignDashboardsFromCustomers($event: Event, dashboardIds: Array<string>) {
  384 + this.showManageAssignedCustomersDialog($event, dashboardIds, 'unassign');
  385 + }
  386 +
  387 + showManageAssignedCustomersDialog($event: Event, dashboardIds: Array<string>,
  388 + actionType: ManageDashboardCustomersActionType,
  389 + assignedCustomersIds?: Array<string>) {
  390 + if ($event) {
  391 + $event.stopPropagation();
  392 + }
  393 + this.dialog.open<ManageDashboardCustomersDialogComponent, ManageDashboardCustomersDialogData,
  394 + boolean>(ManageDashboardCustomersDialogComponent, {
  395 + disableClose: true,
  396 + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
  397 + data: {
  398 + dashboardIds,
  399 + actionType,
  400 + assignedCustomersIds
  401 + }
  402 + }).afterClosed()
  403 + .subscribe((res) => {
  404 + if (res) {
  405 + this.config.table.updateData();
  406 + }
  407 + });
  408 + }
  409 +
  410 + unassignFromCustomer($event: Event, dashboard: DashboardInfo, customerId: string) {
  411 + if ($event) {
  412 + $event.stopPropagation();
  413 + }
  414 + this.dialogService.confirm(
  415 + this.translate.instant('dashboard.unassign-dashboard-title', {dashboardTitle: dashboard.title}),
  416 + this.translate.instant('dashboard.unassign-dashboard-text'),
  417 + this.translate.instant('action.no'),
  418 + this.translate.instant('action.yes'),
  419 + true
  420 + ).subscribe((res) => {
  421 + if (res) {
  422 + this.dashboardService.unassignDashboardFromCustomer(customerId, dashboard.id.id).subscribe(
  423 + () => {
  424 + this.config.table.updateData();
  425 + }
  426 + );
  427 + }
  428 + }
  429 + );
  430 + }
  431 +
  432 + unassignDashboardsFromCustomer($event: Event, dashboardIds: Array<string>, customerId: string) {
  433 + if ($event) {
  434 + $event.stopPropagation();
  435 + }
  436 + this.dialogService.confirm(
  437 + this.translate.instant('dashboard.unassign-dashboards-title', {count: dashboardIds.length}),
  438 + this.translate.instant('dashboard.unassign-dashboards-text'),
  439 + this.translate.instant('action.no'),
  440 + this.translate.instant('action.yes'),
  441 + true
  442 + ).subscribe((res) => {
  443 + if (res) {
  444 + const tasks: Observable<any>[] = [];
  445 + dashboardIds.forEach(
  446 + (dashboardId) => {
  447 + tasks.push(this.dashboardService.unassignDashboardFromCustomer(customerId, dashboardId));
  448 + }
  449 + );
  450 + forkJoin(tasks).subscribe(
  451 + () => {
  452 + this.config.table.updateData();
  453 + }
  454 + );
  455 + }
  456 + }
  457 + );
  458 + }
  459 +
  460 + onDashboardAction(action: EntityAction<DashboardInfo>): boolean {
  461 + switch (action.action) {
  462 + case 'open':
  463 + this.openDashboard(action.event, action.entity);
  464 + return true;
  465 + case 'export':
  466 + this.exportDashboard(action.event, action.entity);
  467 + return true;
  468 + case 'makePublic':
  469 + this.makePublic(action.event, action.entity);
  470 + return true;
  471 + case 'makePrivate':
  472 + this.makePrivate(action.event, action.entity);
  473 + return true;
  474 + case 'manageAssignedCustomers':
  475 + this.manageAssignedCustomers(action.event, action.entity);
  476 + return true;
  477 + case 'unassignFromCustomer':
  478 + this.unassignFromCustomer(action.event, action.entity, this.config.componentsData.customerId);
  479 + return true;
  480 + }
  481 + return false;
  482 + }
  483 +
  484 +}
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 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 +<form #dashboardCustomersForm="ngForm" style="width: 600px;"
  19 + [formGroup]="dashboardCustomersFormGroup" (ngSubmit)="submit()">
  20 + <mat-toolbar fxLayout="row" color="primary">
  21 + <h2>{{ titleText | translate }}</h2>
  22 + <span fxFlex></span>
  23 + <button mat-button mat-icon-button
  24 + (click)="cancel()"
  25 + type="button">
  26 + <mat-icon class="material-icons">close</mat-icon>
  27 + </button>
  28 + </mat-toolbar>
  29 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  30 + </mat-progress-bar>
  31 + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
  32 + <div mat-dialog-content>
  33 + <fieldset [disabled]="isLoading$ | async">
  34 + <span>{{ labelText | translate }}</span>
  35 + <tb-entity-list
  36 + formControlName="assignedCustomerIds"
  37 + required
  38 + [entityType]="entityType.CUSTOMER">
  39 + </tb-entity-list>
  40 + </fieldset>
  41 + </div>
  42 + <div mat-dialog-actions fxLayout="row">
  43 + <span fxFlex></span>
  44 + <button mat-button mat-raised-button color="primary"
  45 + type="submit"
  46 + [disabled]="(isLoading$ | async) || dashboardCustomersForm.invalid
  47 + || !dashboardCustomersForm.dirty">
  48 + {{ actionName | translate }}
  49 + </button>
  50 + <button mat-button color="primary"
  51 + style="margin-right: 20px;"
  52 + type="button"
  53 + [disabled]="(isLoading$ | async)"
  54 + (click)="cancel()" cdkFocusInitial>
  55 + {{ 'action.cancel' | translate }}
  56 + </button>
  57 + </div>
  58 +</form>
... ...
  1 +///
  2 +/// Copyright © 2016-2019 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 {Component, Inject, OnInit, SkipSelf} from '@angular/core';
  18 +import {ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef} from '@angular/material';
  19 +import {PageComponent} from '@shared/components/page.component';
  20 +import {Store} from '@ngrx/store';
  21 +import {AppState} from '@core/core.state';
  22 +import {FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm} from '@angular/forms';
  23 +import {EntityType} from '@shared/models/entity-type.models';
  24 +import {DashboardService} from '@core/http/dashboard.service';
  25 +import {forkJoin, Observable} from 'rxjs';
  26 +
  27 +export type ManageDashboardCustomersActionType = 'assign' | 'manage' | 'unassign';
  28 +
  29 +export interface ManageDashboardCustomersDialogData {
  30 + actionType: ManageDashboardCustomersActionType;
  31 + dashboardIds: Array<string>;
  32 + assignedCustomersIds?: Array<string>;
  33 +}
  34 +
  35 +@Component({
  36 + selector: 'tb-manage-dashboard-customers-dialog',
  37 + templateUrl: './manage-dashboard-customers-dialog.component.html',
  38 + providers: [{provide: ErrorStateMatcher, useExisting: ManageDashboardCustomersDialogComponent}],
  39 + styleUrls: []
  40 +})
  41 +export class ManageDashboardCustomersDialogComponent extends PageComponent implements OnInit, ErrorStateMatcher {
  42 +
  43 + dashboardCustomersFormGroup: FormGroup;
  44 +
  45 + submitted = false;
  46 +
  47 + entityType = EntityType;
  48 +
  49 + titleText: string;
  50 + labelText: string;
  51 + actionName: string;
  52 +
  53 + assignedCustomersIds: string[];
  54 +
  55 + constructor(protected store: Store<AppState>,
  56 + @Inject(MAT_DIALOG_DATA) public data: ManageDashboardCustomersDialogData,
  57 + private dashboardService: DashboardService,
  58 + @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
  59 + public dialogRef: MatDialogRef<ManageDashboardCustomersDialogComponent, boolean>,
  60 + public fb: FormBuilder) {
  61 + super(store);
  62 +
  63 + this.assignedCustomersIds = data.assignedCustomersIds || [];
  64 + switch (data.actionType) {
  65 + case 'assign':
  66 + this.titleText = 'dashboard.assign-to-customers';
  67 + this.labelText = 'dashboard.assign-to-customers-text';
  68 + this.actionName = 'action.assign';
  69 + break;
  70 + case 'manage':
  71 + this.titleText = 'dashboard.manage-assigned-customers';
  72 + this.labelText = 'dashboard.assigned-customers';
  73 + this.actionName = 'action.update';
  74 + break;
  75 + case 'unassign':
  76 + this.titleText = 'dashboard.unassign-from-customers';
  77 + this.labelText = 'dashboard.unassign-from-customers-text';
  78 + this.actionName = 'action.unassign';
  79 + break;
  80 + }
  81 + }
  82 +
  83 + ngOnInit(): void {
  84 + this.dashboardCustomersFormGroup = this.fb.group({
  85 + assignedCustomerIds: [[...this.assignedCustomersIds]]
  86 + });
  87 + }
  88 +
  89 + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
  90 + const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
  91 + const customErrorState = !!(control && control.invalid && this.submitted);
  92 + return originalErrorState || customErrorState;
  93 + }
  94 +
  95 + cancel(): void {
  96 + this.dialogRef.close(false);
  97 + }
  98 +
  99 + submit(): void {
  100 + this.submitted = true;
  101 + const customerIds: Array<string> = this.dashboardCustomersFormGroup.get('assignedCustomerIds').value;
  102 + const tasks: Observable<any>[] = [];
  103 +
  104 + this.data.dashboardIds.forEach(
  105 + (dashboardId) => {
  106 + tasks.push(this.getManageDashboardCustomersTask(dashboardId, customerIds));
  107 + }
  108 + );
  109 + forkJoin(tasks).subscribe(
  110 + () => {
  111 + this.dialogRef.close(true);
  112 + }
  113 + );
  114 + }
  115 +
  116 + private getManageDashboardCustomersTask(dashboardId: string, customerIds: Array<string>): Observable<any> {
  117 + switch (this.data.actionType) {
  118 + case 'assign':
  119 + return this.dashboardService.addDashboardCustomers(dashboardId, customerIds);
  120 + break;
  121 + case 'manage':
  122 + return this.dashboardService.updateDashboardCustomers(dashboardId, customerIds);
  123 + break;
  124 + case 'unassign':
  125 + return this.dashboardService.removeDashboardCustomers(dashboardId, customerIds);
  126 + break;
  127 + }
  128 + }
  129 +}
... ...
... ... @@ -96,6 +96,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
96 96 ));
97 97 };
98 98 this.config.onEntityAction = action => this.onDeviceAction(action);
  99 + this.config.detailsReadonly = () => this.config.componentsData.deviceScope === 'customer_user';
99 100
100 101 this.config.headerComponent = DeviceTableHeaderComponent;
101 102
... ...
... ... @@ -85,7 +85,6 @@
85 85 formControlName="entityId">
86 86 </tb-entity-select>
87 87 </section>
88   - <!-- TODO: -->
89 88 <div formGroupName="keys">
90 89 <mat-expansion-panel formGroupName="attributes" [expanded]="true">
91 90 <mat-expansion-panel-header>
... ...
... ... @@ -96,6 +96,7 @@ export class EntityViewsTableConfigResolver implements Resolve<EntityTableConfig
96 96 ));
97 97 };
98 98 this.config.onEntityAction = action => this.onEntityViewAction(action);
  99 + this.config.detailsReadonly = () => this.config.componentsData.entityViewScope === 'customer_user';
99 100
100 101 this.config.headerComponent = EntityViewTableHeaderComponent;
101 102
... ...
... ... @@ -26,6 +26,9 @@ import { UserModule } from '@modules/home/pages/user/user.module';
26 26 import {DeviceModule} from '@modules/home/pages/device/device.module';
27 27 import {AssetModule} from '@modules/home/pages/asset/asset.module';
28 28 import {EntityViewModule} from '@modules/home/pages/entity-view/entity-view.module';
  29 +import {RuleChainModule} from '@modules/home/pages/rulechain/rulechain.module';
  30 +import {WidgetLibraryModule} from '@modules/home/pages/widget/widget-library.module';
  31 +import {DashboardModule} from '@modules/home/pages/dashboard/dashboard.module';
29 32
30 33 @NgModule({
31 34 exports: [
... ... @@ -37,6 +40,9 @@ import {EntityViewModule} from '@modules/home/pages/entity-view/entity-view.modu
37 40 AssetModule,
38 41 EntityViewModule,
39 42 CustomerModule,
  43 + RuleChainModule,
  44 + WidgetLibraryModule,
  45 + DashboardModule,
40 46 // AuditLogModule,
41 47 UserModule
42 48 ]
... ...
  1 +///
  2 +/// Copyright © 2016-2019 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 {NgModule} from '@angular/core';
  18 +import {RouterModule, Routes} from '@angular/router';
  19 +
  20 +import {EntitiesTableComponent} from '@shared/components/entity/entities-table.component';
  21 +import {Authority} from '@shared/models/authority.enum';
  22 +import {RuleChainsTableConfigResolver} from '@modules/home/pages/rulechain/rulechains-table-config.resolver';
  23 +
  24 +const routes: Routes = [
  25 + {
  26 + path: 'ruleChains',
  27 + data: {
  28 + breadcrumb: {
  29 + label: 'rulechain.rulechains',
  30 + icon: 'settings_ethernet'
  31 + }
  32 + },
  33 + children: [
  34 + {
  35 + path: '',
  36 + component: EntitiesTableComponent,
  37 + data: {
  38 + auth: [Authority.TENANT_ADMIN],
  39 + title: 'rulechain.rulechains'
  40 + },
  41 + resolve: {
  42 + entitiesTableConfig: RuleChainsTableConfigResolver
  43 + }
  44 + }
  45 + ]
  46 + }
  47 +];
  48 +
  49 +@NgModule({
  50 + imports: [RouterModule.forChild(routes)],
  51 + exports: [RouterModule],
  52 + providers: [
  53 + RuleChainsTableConfigResolver
  54 + ]
  55 +})
  56 +export class RuleChainRoutingModule { }
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 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-details-buttons">
  19 + <button mat-raised-button color="primary"
  20 + [disabled]="(isLoading$ | async)"
  21 + (click)="onEntityAction($event, 'open')"
  22 + [fxShow]="!isEdit">
  23 + {{'rulechain.open-rulechain' | translate }}
  24 + </button>
  25 + <button mat-raised-button color="primary"
  26 + [disabled]="(isLoading$ | async)"
  27 + (click)="onEntityAction($event, 'export')"
  28 + [fxShow]="!isEdit">
  29 + {{'rulechain.export' | translate }}
  30 + </button>
  31 + <button mat-raised-button color="primary"
  32 + [disabled]="(isLoading$ | async)"
  33 + (click)="onEntityAction($event, 'setRoot')"
  34 + [fxShow]="!isEdit && !entity?.root">
  35 + {{'rulechain.set-root' | translate }}
  36 + </button>
  37 + <button mat-raised-button color="primary"
  38 + [disabled]="(isLoading$ | async)"
  39 + (click)="onEntityAction($event, 'delete')"
  40 + [fxShow]="!hideDelete() && !isEdit">
  41 + {{'rulechain.delete' | translate }}
  42 + </button>
  43 + <div fxLayout="row">
  44 + <button mat-raised-button
  45 + ngxClipboard
  46 + (cbOnSuccess)="onRuleChainIdCopied($event)"
  47 + [cbContent]="entity?.id?.id"
  48 + [fxShow]="!isEdit">
  49 + <mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
  50 + <span translate>rulechain.copyId</span>
  51 + </button>
  52 + </div>
  53 +</div>
  54 +<div class="mat-padding" fxLayout="column">
  55 + <form #entityNgForm="ngForm" [formGroup]="entityForm">
  56 + <fieldset [disabled]="(isLoading$ | async) || !isEdit">
  57 + <mat-form-field class="mat-block">
  58 + <mat-label translate>rulechain.name</mat-label>
  59 + <input matInput formControlName="name" required>
  60 + <mat-error *ngIf="entityForm.get('name').hasError('required')">
  61 + {{ 'rulechain.name-required' | translate }}
  62 + </mat-error>
  63 + </mat-form-field>
  64 + <mat-checkbox fxFlex formControlName="debugMode" style="padding-bottom: 16px;">
  65 + {{ 'rulechain.debug-mode' | translate }}
  66 + </mat-checkbox>
  67 + <div formGroupName="additionalInfo">
  68 + <mat-form-field class="mat-block">
  69 + <mat-label translate>rulechain.description</mat-label>
  70 + <textarea matInput formControlName="description" rows="2"></textarea>
  71 + </mat-form-field>
  72 + </div>
  73 + </fieldset>
  74 + </form>
  75 +</div>
... ...
  1 +/**
  2 + * Copyright © 2016-2019 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 +:host {
  18 +
  19 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 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 {Component} from '@angular/core';
  18 +import {Store} from '@ngrx/store';
  19 +import {AppState} from '@core/core.state';
  20 +import {EntityComponent} from '@shared/components/entity/entity.component';
  21 +import {FormBuilder, FormGroup, Validators} from '@angular/forms';
  22 +import {EntityType} from '@shared/models/entity-type.models';
  23 +import {NULL_UUID} from '@shared/models/id/has-uuid';
  24 +import {ActionNotificationShow} from '@core/notification/notification.actions';
  25 +import {TranslateService} from '@ngx-translate/core';
  26 +import {AssetInfo} from '@app/shared/models/asset.models';
  27 +import {RuleChain} from "@shared/models/rule-chain.models";
  28 +
  29 +@Component({
  30 + selector: 'tb-rulechain',
  31 + templateUrl: './rulechain.component.html',
  32 + styleUrls: ['./rulechain.component.scss']
  33 +})
  34 +export class RuleChainComponent extends EntityComponent<RuleChain> {
  35 +
  36 + constructor(protected store: Store<AppState>,
  37 + protected translate: TranslateService,
  38 + public fb: FormBuilder) {
  39 + super(store);
  40 + }
  41 +
  42 + hideDelete() {
  43 + if (this.entitiesTableConfig) {
  44 + return !this.entitiesTableConfig.deleteEnabled(this.entity);
  45 + } else {
  46 + return false;
  47 + }
  48 + }
  49 +
  50 + buildForm(entity: RuleChain): FormGroup {
  51 + return this.fb.group(
  52 + {
  53 + name: [entity ? entity.name : '', [Validators.required]],
  54 + debugMode: [entity ? entity.debugMode : false],
  55 + additionalInfo: this.fb.group(
  56 + {
  57 + description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''],
  58 + }
  59 + )
  60 + }
  61 + );
  62 + }
  63 +
  64 + updateForm(entity: RuleChain) {
  65 + this.entityForm.patchValue({name: entity.name});
  66 + this.entityForm.patchValue({debugMode: entity.debugMode});
  67 + this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}});
  68 + }
  69 +
  70 +
  71 + onRuleChainIdCopied($event) {
  72 + this.store.dispatch(new ActionNotificationShow(
  73 + {
  74 + message: this.translate.instant('rulechain.idCopiedMessage'),
  75 + type: 'success',
  76 + duration: 750,
  77 + verticalPosition: 'bottom',
  78 + horizontalPosition: 'right'
  79 + }));
  80 + }
  81 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 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 {NgModule} from '@angular/core';
  18 +import {CommonModule} from '@angular/common';
  19 +import {SharedModule} from '@shared/shared.module';
  20 +import {RuleChainComponent} from '@modules/home/pages/rulechain/rulechain.component';
  21 +import {RuleChainRoutingModule} from '@modules/home/pages/rulechain/rulechain-routing.module';
  22 +
  23 +@NgModule({
  24 + entryComponents: [
  25 + RuleChainComponent
  26 + ],
  27 + declarations: [
  28 + RuleChainComponent
  29 + ],
  30 + imports: [
  31 + CommonModule,
  32 + SharedModule,
  33 + RuleChainRoutingModule
  34 + ]
  35 +})
  36 +export class RuleChainModule { }
... ...
  1 +///
  2 +/// Copyright © 2016-2019 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 {Injectable} from '@angular/core';
  18 +
  19 +import {Resolve, Router} from '@angular/router';
  20 +import {
  21 + checkBoxCell,
  22 + DateEntityTableColumn,
  23 + EntityTableColumn,
  24 + EntityTableConfig
  25 +} from '@shared/components/entity/entities-table-config.models';
  26 +import {TranslateService} from '@ngx-translate/core';
  27 +import {DatePipe} from '@angular/common';
  28 +import {EntityType, entityTypeResources, entityTypeTranslations} from '@shared/models/entity-type.models';
  29 +import {EntityAction} from '@shared/components/entity/entity-component.models';
  30 +import {RuleChain} from '@shared/models/rule-chain.models';
  31 +import {RuleChainService} from '@core/http/rule-chain.service';
  32 +import {RuleChainComponent} from '@modules/home/pages/rulechain/rulechain.component';
  33 +import {DialogService} from '@core/services/dialog.service';
  34 +
  35 +@Injectable()
  36 +export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig<RuleChain>> {
  37 +
  38 + private readonly config: EntityTableConfig<RuleChain> = new EntityTableConfig<RuleChain>();
  39 +
  40 + constructor(private ruleChainService: RuleChainService,
  41 + private dialogService: DialogService,
  42 + private translate: TranslateService,
  43 + private datePipe: DatePipe,
  44 + private router: Router) {
  45 +
  46 + this.config.entityType = EntityType.RULE_CHAIN;
  47 + this.config.entityComponent = RuleChainComponent;
  48 + this.config.entityTranslations = entityTypeTranslations.get(EntityType.RULE_CHAIN);
  49 + this.config.entityResources = entityTypeResources.get(EntityType.RULE_CHAIN);
  50 +
  51 + this.config.columns.push(
  52 + new DateEntityTableColumn<RuleChain>('createdTime', 'rulechain.created-time', this.datePipe, '150px'),
  53 + new EntityTableColumn<RuleChain>('name', 'rulechain.name'),
  54 + new EntityTableColumn<RuleChain>('root', 'rulechain.root', '60px',
  55 + entity => {
  56 + return checkBoxCell(entity.root);
  57 + }),
  58 + );
  59 +
  60 + this.config.addActionDescriptors.push(
  61 + {
  62 + name: this.translate.instant('rulechain.create-new-rulechain'),
  63 + icon: 'insert_drive_file',
  64 + isEnabled: () => true,
  65 + onAction: ($event) => this.config.table.addEntity($event)
  66 + },
  67 + {
  68 + name: this.translate.instant('rulechain.import'),
  69 + icon: 'file_upload',
  70 + isEnabled: () => true,
  71 + onAction: ($event) => this.importRuleChain($event)
  72 + }
  73 + );
  74 +
  75 + this.config.cellActionDescriptors.push(
  76 + {
  77 + name: this.translate.instant('rulechain.open-rulechain'),
  78 + icon: 'settings_ethernet',
  79 + isEnabled: () => true,
  80 + onAction: ($event, entity) => this.openRuleChain($event, entity)
  81 + },
  82 + {
  83 + name: this.translate.instant('rulechain.export'),
  84 + icon: 'file_download',
  85 + isEnabled: () => true,
  86 + onAction: ($event, entity) => this.exportRuleChain($event, entity)
  87 + },
  88 + {
  89 + name: this.translate.instant('rulechain.set-root'),
  90 + icon: 'flag',
  91 + isEnabled: (ruleChain) => !ruleChain.root,
  92 + onAction: ($event, entity) => this.setRootRuleChain($event, entity)
  93 + }
  94 + );
  95 +
  96 + this.config.deleteEntityTitle = ruleChain => this.translate.instant('rulechain.delete-rulechain-title',
  97 + { ruleChainName: ruleChain.name });
  98 + this.config.deleteEntityContent = () => this.translate.instant('rulechain.delete-rulechain-text');
  99 + this.config.deleteEntitiesTitle = count => this.translate.instant('rulechain.delete-rulechains-title', {count});
  100 + this.config.deleteEntitiesContent = () => this.translate.instant('rulechain.delete-rulechains-text');
  101 +
  102 + this.config.entitiesFetchFunction = pageLink => this.ruleChainService.getRuleChains(pageLink);
  103 + this.config.loadEntity = id => this.ruleChainService.getRuleChain(id.id);
  104 + this.config.saveEntity = ruleChain => this.ruleChainService.saveRuleChain(ruleChain);
  105 + this.config.deleteEntity = id => this.ruleChainService.deleteRuleChain(id.id);
  106 + this.config.onEntityAction = action => this.onRuleChainAction(action);
  107 + this.config.deleteEnabled = (ruleChain) => ruleChain && !ruleChain.root;
  108 + this.config.entitySelectionEnabled = (ruleChain) => ruleChain && !ruleChain.root;
  109 + }
  110 +
  111 + resolve(): EntityTableConfig<RuleChain> {
  112 + this.config.tableTitle = this.translate.instant('rulechain.rulechains');
  113 +
  114 + return this.config;
  115 + }
  116 +
  117 + importRuleChain($event: Event) {
  118 + if ($event) {
  119 + $event.stopPropagation();
  120 + }
  121 + // TODO:
  122 + }
  123 +
  124 + openRuleChain($event: Event, ruleChain: RuleChain) {
  125 + if ($event) {
  126 + $event.stopPropagation();
  127 + }
  128 + // TODO:
  129 + // this.router.navigateByUrl(`customers/${customer.id.id}/users`);
  130 + }
  131 +
  132 + exportRuleChain($event: Event, ruleChain: RuleChain) {
  133 + if ($event) {
  134 + $event.stopPropagation();
  135 + }
  136 + // TODO:
  137 + }
  138 +
  139 + setRootRuleChain($event: Event, ruleChain: RuleChain) {
  140 + if ($event) {
  141 + $event.stopPropagation();
  142 + }
  143 + this.dialogService.confirm(
  144 + this.translate.instant('rulechain.set-root-rulechain-title', {ruleChainName: ruleChain.name}),
  145 + this.translate.instant('rulechain.set-root-rulechain-text'),
  146 + this.translate.instant('action.no'),
  147 + this.translate.instant('action.yes'),
  148 + true
  149 + ).subscribe((res) => {
  150 + if (res) {
  151 + this.ruleChainService.setRootRuleChain(ruleChain.id.id).subscribe(
  152 + () => {
  153 + this.config.table.updateData();
  154 + }
  155 + );
  156 + }
  157 + }
  158 + );
  159 + }
  160 +
  161 + onRuleChainAction(action: EntityAction<RuleChain>): boolean {
  162 + switch (action.action) {
  163 + case 'open':
  164 + this.openRuleChain(action.event, action.entity);
  165 + return true;
  166 + case 'export':
  167 + this.exportRuleChain(action.event, action.entity);
  168 + return true;
  169 + case 'setRoot':
  170 + this.setRootRuleChain(action.event, action.entity);
  171 + return true;
  172 + }
  173 + return false;
  174 + }
  175 +
  176 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 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 {NgModule} from '@angular/core';
  18 +import {RouterModule, Routes} from '@angular/router';
  19 +
  20 +import {EntitiesTableComponent} from '@shared/components/entity/entities-table.component';
  21 +import {Authority} from '@shared/models/authority.enum';
  22 +import {RuleChainsTableConfigResolver} from '@modules/home/pages/rulechain/rulechains-table-config.resolver';
  23 +import {WidgetsBundlesTableConfigResolver} from '@modules/home/pages/widget/widgets-bundles-table-config.resolver';
  24 +
  25 +const routes: Routes = [
  26 + {
  27 + path: 'widgets-bundles',
  28 + data: {
  29 + breadcrumb: {
  30 + label: 'widgets-bundle.widgets-bundles',
  31 + icon: 'now_widgets'
  32 + }
  33 + },
  34 + children: [
  35 + {
  36 + path: '',
  37 + component: EntitiesTableComponent,
  38 + data: {
  39 + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
  40 + title: 'widgets-bundle.widgets-bundles'
  41 + },
  42 + resolve: {
  43 + entitiesTableConfig: WidgetsBundlesTableConfigResolver
  44 + }
  45 + }
  46 + ]
  47 + }
  48 +];
  49 +
  50 +@NgModule({
  51 + imports: [RouterModule.forChild(routes)],
  52 + exports: [RouterModule],
  53 + providers: [
  54 + WidgetsBundlesTableConfigResolver
  55 + ]
  56 +})
  57 +export class WidgetLibraryRoutingModule { }
... ...
  1 +///
  2 +/// Copyright © 2016-2019 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 {NgModule} from '@angular/core';
  18 +import {CommonModule} from '@angular/common';
  19 +import {SharedModule} from '@shared/shared.module';
  20 +import {WidgetsBundleComponent} from '@modules/home/pages/widget/widgets-bundle.component';
  21 +import {WidgetLibraryRoutingModule} from '@modules/home/pages/widget/widget-library-routing.module';
  22 +
  23 +@NgModule({
  24 + entryComponents: [
  25 + WidgetsBundleComponent
  26 + ],
  27 + declarations: [
  28 + WidgetsBundleComponent
  29 + ],
  30 + imports: [
  31 + CommonModule,
  32 + SharedModule,
  33 + WidgetLibraryRoutingModule
  34 + ]
  35 +})
  36 +export class WidgetLibraryModule { }
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 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-details-buttons">
  19 + <button mat-raised-button color="primary"
  20 + [disabled]="(isLoading$ | async)"
  21 + (click)="onEntityAction($event, 'export')"
  22 + [fxShow]="!isEdit">
  23 + {{'widgets-bundle.export' | translate }}
  24 + </button>
  25 + <button mat-raised-button color="primary"
  26 + [disabled]="(isLoading$ | async)"
  27 + (click)="onEntityAction($event, 'delete')"
  28 + [fxShow]="!hideDelete() && !isEdit">
  29 + {{'widgets-bundle.delete' | translate }}
  30 + </button>
  31 +</div>
  32 +<div class="mat-padding" fxLayout="column">
  33 + <form #entityNgForm="ngForm" [formGroup]="entityForm">
  34 + <fieldset [disabled]="(isLoading$ | async) || !isEdit">
  35 + <mat-form-field class="mat-block">
  36 + <mat-label translate>widgets-bundle.title</mat-label>
  37 + <input matInput formControlName="title" required>
  38 + <mat-error *ngIf="entityForm.get('title').hasError('required')">
  39 + {{ 'widgets-bundle.title-required' | translate }}
  40 + </mat-error>
  41 + </mat-form-field>
  42 + </fieldset>
  43 + </form>
  44 +</div>
... ...
  1 +/**
  2 + * Copyright © 2016-2019 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 +:host {
  18 +
  19 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 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 {Component} from '@angular/core';
  18 +import {Store} from '@ngrx/store';
  19 +import {AppState} from '@core/core.state';
  20 +import {EntityComponent} from '@shared/components/entity/entity.component';
  21 +import {FormBuilder, FormGroup, Validators} from '@angular/forms';
  22 +import {WidgetsBundle} from '@shared/models/widgets-bundle.model';
  23 +
  24 +@Component({
  25 + selector: 'tb-widgets-bundle',
  26 + templateUrl: './widgets-bundle.component.html',
  27 + styleUrls: ['./widgets-bundle.component.scss']
  28 +})
  29 +export class WidgetsBundleComponent extends EntityComponent<WidgetsBundle> {
  30 +
  31 + constructor(protected store: Store<AppState>,
  32 + public fb: FormBuilder) {
  33 + super(store);
  34 + }
  35 +
  36 + hideDelete() {
  37 + if (this.entitiesTableConfig) {
  38 + return !this.entitiesTableConfig.deleteEnabled(this.entity);
  39 + } else {
  40 + return false;
  41 + }
  42 + }
  43 +
  44 + buildForm(entity: WidgetsBundle): FormGroup {
  45 + return this.fb.group(
  46 + {
  47 + title: [entity ? entity.title : '', [Validators.required]]
  48 + }
  49 + );
  50 + }
  51 +
  52 + updateForm(entity: WidgetsBundle) {
  53 + this.entityForm.patchValue({title: entity.title});
  54 + }
  55 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 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 {Injectable} from '@angular/core';
  18 +
  19 +import {Resolve, Router} from '@angular/router';
  20 +import {
  21 + checkBoxCell,
  22 + DateEntityTableColumn,
  23 + EntityTableColumn,
  24 + EntityTableConfig
  25 +} from '@shared/components/entity/entities-table-config.models';
  26 +import {TranslateService} from '@ngx-translate/core';
  27 +import {DatePipe} from '@angular/common';
  28 +import {EntityType, entityTypeResources, entityTypeTranslations} from '@shared/models/entity-type.models';
  29 +import {EntityAction} from '@shared/components/entity/entity-component.models';
  30 +import {WidgetsBundle} from '@shared/models/widgets-bundle.model';
  31 +import {WidgetService} from '@app/core/http/widget.service';
  32 +import {WidgetsBundleComponent} from '@modules/home/pages/widget/widgets-bundle.component';
  33 +import {NULL_UUID} from '@shared/models/id/has-uuid';
  34 +import {Store} from '@ngrx/store';
  35 +import {AppState} from '@core/core.state';
  36 +import {getCurrentAuthUser} from '@app/core/auth/auth.selectors';
  37 +import {Authority} from '@shared/models/authority.enum';
  38 +
  39 +@Injectable()
  40 +export class WidgetsBundlesTableConfigResolver implements Resolve<EntityTableConfig<WidgetsBundle>> {
  41 +
  42 + private readonly config: EntityTableConfig<WidgetsBundle> = new EntityTableConfig<WidgetsBundle>();
  43 +
  44 + constructor(private store: Store<AppState>,
  45 + private widgetsService: WidgetService,
  46 + private translate: TranslateService,
  47 + private datePipe: DatePipe,
  48 + private router: Router) {
  49 +
  50 + this.config.entityType = EntityType.WIDGETS_BUNDLE;
  51 + this.config.entityComponent = WidgetsBundleComponent;
  52 + this.config.entityTranslations = entityTypeTranslations.get(EntityType.WIDGETS_BUNDLE);
  53 + this.config.entityResources = entityTypeResources.get(EntityType.WIDGETS_BUNDLE);
  54 +
  55 + this.config.columns.push(
  56 + new DateEntityTableColumn<WidgetsBundle>('createdTime', 'widgets-bundle.created-time', this.datePipe, '150px'),
  57 + new EntityTableColumn<WidgetsBundle>('title', 'widgets-bundle.title'),
  58 + new EntityTableColumn<WidgetsBundle>('tenantId', 'widgets-bundle.system', '60px',
  59 + entity => {
  60 + return checkBoxCell(entity.tenantId.id === NULL_UUID);
  61 + }),
  62 + );
  63 +
  64 + this.config.addActionDescriptors.push(
  65 + {
  66 + name: this.translate.instant('widgets-bundle.create-new-widgets-bundle'),
  67 + icon: 'insert_drive_file',
  68 + isEnabled: () => true,
  69 + onAction: ($event) => this.config.table.addEntity($event)
  70 + },
  71 + {
  72 + name: this.translate.instant('widgets-bundle.import'),
  73 + icon: 'file_upload',
  74 + isEnabled: () => true,
  75 + onAction: ($event) => this.importWidgetsBundle($event)
  76 + }
  77 + );
  78 +
  79 + this.config.cellActionDescriptors.push(
  80 + {
  81 + name: this.translate.instant('widgets-bundle.open-widgets-bundle'),
  82 + icon: 'now_widgets',
  83 + isEnabled: () => true,
  84 + onAction: ($event, entity) => this.openWidgetsBundle($event, entity)
  85 + },
  86 + {
  87 + name: this.translate.instant('widgets-bundle.export'),
  88 + icon: 'file_download',
  89 + isEnabled: () => true,
  90 + onAction: ($event, entity) => this.exportWidgetsBundle($event, entity)
  91 + }
  92 + );
  93 +
  94 + this.config.deleteEntityTitle = widgetsBundle => this.translate.instant('widgets-bundle.delete-widgets-bundle-title',
  95 + { widgetsBundleTitle: widgetsBundle.title });
  96 + this.config.deleteEntityContent = () => this.translate.instant('widgets-bundle.delete-widgets-bundle-text');
  97 + this.config.deleteEntitiesTitle = count => this.translate.instant('widgets-bundle.delete-widgets-bundles-title', {count});
  98 + this.config.deleteEntitiesContent = () => this.translate.instant('widgets-bundle.delete-widgets-bundles-text');
  99 +
  100 + this.config.entitiesFetchFunction = pageLink => this.widgetsService.getWidgetBundles(pageLink);
  101 + this.config.loadEntity = id => this.widgetsService.getWidgetsBundle(id.id);
  102 + this.config.saveEntity = widgetsBundle => this.widgetsService.saveWidgetsBundle(widgetsBundle);
  103 + this.config.deleteEntity = id => this.widgetsService.deleteWidgetsBundle(id.id);
  104 + this.config.onEntityAction = action => this.onWidgetsBundleAction(action);
  105 + }
  106 +
  107 + resolve(): EntityTableConfig<WidgetsBundle> {
  108 + this.config.tableTitle = this.translate.instant('widgets-bundle.widgets-bundles');
  109 + const authUser = getCurrentAuthUser(this.store);
  110 + this.config.deleteEnabled = (widgetsBundle) => this.isWidgetsBundleEditable(widgetsBundle, authUser.authority);
  111 + this.config.entitySelectionEnabled = (widgetsBundle) => this.isWidgetsBundleEditable(widgetsBundle, authUser.authority);
  112 + this.config.detailsReadonly = (widgetsBundle) => !this.isWidgetsBundleEditable(widgetsBundle, authUser.authority);
  113 + return this.config;
  114 + }
  115 +
  116 + isWidgetsBundleEditable(widgetsBundle: WidgetsBundle, authority: Authority): boolean {
  117 + if (authority === Authority.TENANT_ADMIN) {
  118 + return widgetsBundle && widgetsBundle.tenantId && widgetsBundle.tenantId.id !== NULL_UUID;
  119 + } else {
  120 + return authority === Authority.SYS_ADMIN;
  121 + }
  122 + }
  123 +
  124 + importWidgetsBundle($event: Event) {
  125 + if ($event) {
  126 + $event.stopPropagation();
  127 + }
  128 + // TODO:
  129 + }
  130 +
  131 + openWidgetsBundle($event: Event, widgetsBundle: WidgetsBundle) {
  132 + if ($event) {
  133 + $event.stopPropagation();
  134 + }
  135 + // TODO:
  136 + // this.router.navigateByUrl(`customers/${customer.id.id}/users`);
  137 + }
  138 +
  139 + exportWidgetsBundle($event: Event, widgetsBundle: WidgetsBundle) {
  140 + if ($event) {
  141 + $event.stopPropagation();
  142 + }
  143 + // TODO:
  144 + }
  145 +
  146 + onWidgetsBundleAction(action: EntityAction<WidgetsBundle>): boolean {
  147 + switch (action.action) {
  148 + case 'open':
  149 + this.openWidgetsBundle(action.event, action.entity);
  150 + return true;
  151 + case 'export':
  152 + this.exportWidgetsBundle(action.event, action.entity);
  153 + return true;
  154 + }
  155 + return false;
  156 + }
  157 +
  158 +}
... ...
... ... @@ -19,6 +19,7 @@
19 19 <input matInput type="text" placeholder="{{ entityText | translate }}"
20 20 #entityInput
21 21 formControlName="entity"
  22 + (focusin)="onFocus()"
22 23 [required]="required"
23 24 [matAutocomplete]="entityAutocomplete">
24 25 <button *ngIf="selectEntityFormGroup.get('entity').value && !disabled"
... ...
... ... @@ -53,6 +53,7 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
53 53 this.entityTypeValue = entityType;
54 54 this.load();
55 55 this.reset();
  56 + this.dirty = true;
56 57 }
57 58 }
58 59
... ... @@ -64,6 +65,7 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
64 65 if (currentEntity) {
65 66 if ((currentEntity as any).type !== this.entitySubtypeValue) {
66 67 this.reset();
  68 + this.dirty = true;
67 69 }
68 70 }
69 71 }
... ... @@ -94,6 +96,8 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
94 96
95 97 private searchText = '';
96 98
  99 + private dirty = false;
  100 +
97 101 private propagateChange = (v: any) => { };
98 102
99 103 constructor(private store: Store<AppState>,
... ... @@ -127,7 +131,7 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
127 131 this.clear();
128 132 }
129 133 }),
130   - startWith<string | BaseData<EntityId>>(''),
  134 + // startWith<string | BaseData<EntityId>>(''),
131 135 map(value => value ? (typeof value === 'string' ? value : value.name) : ''),
132 136 mergeMap(name => this.fetchEntities(name) ),
133 137 share()
... ... @@ -212,9 +216,9 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
212 216 setDisabledState(isDisabled: boolean): void {
213 217 this.disabled = isDisabled;
214 218 if (this.disabled) {
215   - this.selectEntityFormGroup.disable();
  219 + this.selectEntityFormGroup.disable({emitEvent: false});
216 220 } else {
217   - this.selectEntityFormGroup.enable();
  221 + this.selectEntityFormGroup.enable({emitEvent: false});
218 222 }
219 223 }
220 224
... ... @@ -226,29 +230,37 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
226 230 if (targetEntityType === AliasEntityType.CURRENT_CUSTOMER) {
227 231 targetEntityType = EntityType.CUSTOMER;
228 232 }
229   - this.entityService.getEntity(targetEntityType, value).subscribe(
  233 + this.entityService.getEntity(targetEntityType, value, true).subscribe(
230 234 (entity) => {
231 235 this.modelValue = entity.id.id;
232   - this.selectEntityFormGroup.get('entity').patchValue(entity, {emitEvent: true});
  236 + this.selectEntityFormGroup.get('entity').patchValue(entity, {emitEvent: false});
233 237 }
234 238 );
235 239 } else {
236 240 const targetEntityType = value.entityType as EntityType;
237   - this.entityService.getEntity(targetEntityType, value.id).subscribe(
  241 + this.entityService.getEntity(targetEntityType, value.id, true).subscribe(
238 242 (entity) => {
239 243 this.modelValue = entity.id.id;
240   - this.selectEntityFormGroup.get('entity').patchValue(entity, {emitEvent: true});
  244 + this.selectEntityFormGroup.get('entity').patchValue(entity, {emitEvent: false});
241 245 }
242 246 );
243 247 }
244 248 } else {
245 249 this.modelValue = null;
246   - this.selectEntityFormGroup.get('entity').patchValue('', {emitEvent: true});
  250 + this.selectEntityFormGroup.get('entity').patchValue('', {emitEvent: false});
  251 + }
  252 + this.dirty = true;
  253 + }
  254 +
  255 + onFocus() {
  256 + if (this.dirty) {
  257 + this.selectEntityFormGroup.get('entity').updateValueAndValidity({onlySelf: true, emitEvent: true});
  258 + this.dirty = false;
247 259 }
248 260 }
249 261
250 262 reset() {
251   - this.selectEntityFormGroup.get('entity').patchValue('', {emitEvent: true});
  263 + this.selectEntityFormGroup.get('entity').patchValue('', {emitEvent: false});
252 264 }
253 265
254 266 updateView(value: string | null) {
... ...
... ... @@ -29,6 +29,7 @@
29 29 <input matInput type="text" placeholder="{{ keysText | translate }}"
30 30 #keyInput
31 31 formControlName="key"
  32 + (focusin)="onFocus()"
32 33 [matAutocomplete]="keyAutocomplete"
33 34 [matChipInputFor]="chipList"
34 35 [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
... ...
... ... @@ -62,7 +62,7 @@ export class EntityKeysListComponent implements ControlValueAccessor, OnInit, Af
62 62 set entityId(entityId: EntityId) {
63 63 if (!equal(this.entityIdValue, entityId)) {
64 64 this.entityIdValue = entityId;
65   - this.reset();
  65 + this.dirty = true;
66 66 }
67 67 }
68 68
... ... @@ -94,6 +94,8 @@ export class EntityKeysListComponent implements ControlValueAccessor, OnInit, Af
94 94
95 95 private searchText = '';
96 96
  97 + private dirty = false;
  98 +
97 99 private propagateChange = (v: any) => { };
98 100
99 101 constructor(private store: Store<AppState>,
... ... @@ -126,9 +128,9 @@ export class EntityKeysListComponent implements ControlValueAccessor, OnInit, Af
126 128 setDisabledState(isDisabled: boolean): void {
127 129 this.disabled = isDisabled;
128 130 if (this.disabled) {
129   - this.keysListFormGroup.disable();
  131 + this.keysListFormGroup.disable({emitEvent: false});
130 132 } else {
131   - this.keysListFormGroup.enable();
  133 + this.keysListFormGroup.enable({emitEvent: false});
132 134 }
133 135 }
134 136
... ... @@ -141,8 +143,11 @@ export class EntityKeysListComponent implements ControlValueAccessor, OnInit, Af
141 143 }
142 144 }
143 145
144   - reset() {
145   - this.keysListFormGroup.get('key').patchValue(null, {emitEvent: true});
  146 + onFocus() {
  147 + if (this.dirty) {
  148 + this.keysListFormGroup.get('key').updateValueAndValidity({onlySelf: true, emitEvent: true});
  149 + this.dirty = false;
  150 + }
146 151 }
147 152
148 153 addKey(key: string): void {
... ...
... ... @@ -15,7 +15,7 @@
15 15 limitations under the License.
16 16
17 17 -->
18   -<mat-form-field [formGroup]="entityListFormGroup" class="mat-block">
  18 +<mat-form-field appearance="standard" [formGroup]="entityListFormGroup" class="mat-block">
19 19 <mat-chip-list #chipList>
20 20 <mat-chip
21 21 *ngFor="let entity of entities"
... ... @@ -26,8 +26,12 @@
26 26 <mat-icon matChipRemove *ngIf="!disabled">close</mat-icon>
27 27 </mat-chip>
28 28 <input matInput type="text" placeholder="{{ 'entity.entity-list' | translate }}"
  29 + style="max-width: 200px;"
29 30 #entityInput
30 31 formControlName="entity"
  32 + matAutocompleteOrigin
  33 + #origin="matAutocompleteOrigin"
  34 + [matAutocompleteConnectedTo]="origin"
31 35 [matAutocomplete]="entityAutocomplete"
32 36 [matChipInputFor]="chipList">
33 37 </mat-chip-list>
... ...
... ... @@ -134,7 +134,7 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV
134 134 writeValue(value: Array<string> | null): void {
135 135 this.searchText = '';
136 136 if (value != null) {
137   - this.modelValue = value;
  137 + this.modelValue = [...value];
138 138 this.entityService.getEntities(this.entityTypeValue, value).subscribe(
139 139 (entities) => {
140 140 this.entities = entities;
... ...
... ... @@ -20,6 +20,7 @@
20 20 <input matInput type="text" placeholder="{{ selectEntitySubtypeText | translate }}"
21 21 #subTypeInput
22 22 formControlName="subType"
  23 + (focusin)="onFocus()"
23 24 [required]="required"
24 25 [matAutocomplete]="subTypeAutocomplete">
25 26 <button *ngIf="subTypeFormGroup.get('subType').value && !disabled"
... ... @@ -30,7 +31,7 @@
30 31 </button>
31 32 <mat-autocomplete #subTypeAutocomplete="matAutocomplete" [displayWith]="displaySubTypeFn">
32 33 <mat-option *ngFor="let subType of filteredSubTypes | async" [value]="subType">
33   - <span [innerHTML]="subType.type | highlight:searchText"></span>
  34 + <span [innerHTML]="subType | highlight:searchText"></span>
34 35 </mat-option>
35 36 </mat-autocomplete>
36 37 <mat-error *ngIf="subTypeFormGroup.get('subType').hasError('required')">
... ...
... ... @@ -73,14 +73,16 @@ export class EntitySubTypeAutocompleteComponent implements ControlValueAccessor,
73 73 entitySubtypeText: string;
74 74 entitySubtypeRequiredText: string;
75 75
76   - filteredSubTypes: Observable<Array<EntitySubtype>>;
  76 + filteredSubTypes: Observable<Array<string>>;
77 77
78   - subTypes: Observable<Array<EntitySubtype>>;
  78 + subTypes: Observable<Array<string>>;
79 79
80 80 private broadcastSubscription: Subscription;
81 81
82 82 private searchText = '';
83 83
  84 + private dirty = false;
  85 +
84 86 private propagateChange = (v: any) => { };
85 87
86 88 constructor(private store: Store<AppState>,
... ... @@ -134,18 +136,10 @@ export class EntitySubTypeAutocompleteComponent implements ControlValueAccessor,
134 136 this.filteredSubTypes = this.subTypeFormGroup.get('subType').valueChanges
135 137 .pipe(
136 138 tap(value => {
137   - let modelValue;
138   - if (!value) {
139   - modelValue = null;
140   - } else if (typeof value === 'string') {
141   - modelValue = value;
142   - } else {
143   - modelValue = value.type;
144   - }
145   - this.updateView(modelValue);
  139 + this.updateView(value);
146 140 }),
147   - startWith<string | EntitySubtype>(''),
148   - map(value => value ? (typeof value === 'string' ? value : value.type) : ''),
  141 + // startWith<string | EntitySubtype>(''),
  142 + map(value => value ? value : ''),
149 143 mergeMap(type => this.fetchSubTypes(type) )
150 144 );
151 145 }
... ... @@ -162,25 +156,23 @@ export class EntitySubTypeAutocompleteComponent implements ControlValueAccessor,
162 156 setDisabledState(isDisabled: boolean): void {
163 157 this.disabled = isDisabled;
164 158 if (this.disabled) {
165   - this.subTypeFormGroup.disable();
  159 + this.subTypeFormGroup.disable({emitEvent: false});
166 160 } else {
167   - this.subTypeFormGroup.enable();
  161 + this.subTypeFormGroup.enable({emitEvent: false});
168 162 }
169 163 }
170 164
171 165 writeValue(value: string | null): void {
172 166 this.searchText = '';
173   - if (value != null) {
174   - this.modelValue = value;
175   - this.fetchSubTypes(value, true).subscribe(
176   - (subTypes) => {
177   - const subType = subTypes && subTypes.length === 1 ? subTypes[0] : null;
178   - this.subTypeFormGroup.get('subType').patchValue(subType, {emitEvent: true});
179   - }
180   - );
181   - } else {
182   - this.modelValue = null;
183   - this.subTypeFormGroup.get('subType').patchValue(null, {emitEvent: true});
  167 + this.modelValue = value;
  168 + this.subTypeFormGroup.get('subType').patchValue(value, {emitEvent: false});
  169 + this.dirty = true;
  170 + }
  171 +
  172 + onFocus() {
  173 + if (this.dirty) {
  174 + this.subTypeFormGroup.get('subType').updateValueAndValidity({onlySelf: true, emitEvent: true});
  175 + this.dirty = false;
184 176 }
185 177 }
186 178
... ... @@ -191,38 +183,40 @@ export class EntitySubTypeAutocompleteComponent implements ControlValueAccessor,
191 183 }
192 184 }
193 185
194   - displaySubTypeFn(subType?: EntitySubtype): string | undefined {
195   - return subType ? subType.type : undefined;
  186 + displaySubTypeFn(subType?: string): string | undefined {
  187 + return subType ? subType : undefined;
196 188 }
197 189
198   - fetchSubTypes(searchText?: string, strictMatch: boolean = false): Observable<Array<EntitySubtype>> {
  190 + fetchSubTypes(searchText?: string, strictMatch: boolean = false): Observable<Array<string>> {
199 191 this.searchText = searchText;
200 192 return this.getSubTypes().pipe(
201 193 map(subTypes => subTypes.filter( subType => {
202 194 if (strictMatch) {
203   - return searchText ? subType.type === searchText : false;
  195 + return searchText ? subType === searchText : false;
204 196 } else {
205   - return searchText ? subType.type.toUpperCase().startsWith(searchText.toUpperCase()) : true;
  197 + return searchText ? subType.toUpperCase().startsWith(searchText.toUpperCase()) : true;
206 198 }
207 199 }))
208 200 );
209 201 }
210 202
211   - getSubTypes(): Observable<Array<EntitySubtype>> {
  203 + getSubTypes(): Observable<Array<string>> {
212 204 if (!this.subTypes) {
  205 + let subTypesObservable: Observable<Array<EntitySubtype>>;
213 206 switch (this.entityType) {
214 207 case EntityType.ASSET:
215   - this.subTypes = this.assetService.getAssetTypes(false, true);
  208 + subTypesObservable = this.assetService.getAssetTypes(false, true);
216 209 break;
217 210 case EntityType.DEVICE:
218   - this.subTypes = this.deviceService.getDeviceTypes(false, true);
  211 + subTypesObservable = this.deviceService.getDeviceTypes(false, true);
219 212 break;
220 213 case EntityType.ENTITY_VIEW:
221   - this.subTypes = this.entityViewService.getEntityViewTypes(false, true);
  214 + subTypesObservable = this.entityViewService.getEntityViewTypes(false, true);
222 215 break;
223 216 }
224   - if (this.subTypes) {
225   - this.subTypes = this.subTypes.pipe(
  217 + if (subTypesObservable) {
  218 + this.subTypes = subTypesObservable.pipe(
  219 + map(subTypes => subTypes.map(subType => subType.type)),
226 220 publishReplay(1),
227 221 refCount()
228 222 );
... ...
... ... @@ -17,7 +17,8 @@
17 17 -->
18 18 <mat-form-field [formGroup]="subTypeFormGroup" class="mat-block">
19 19 <mat-label *ngIf="showLabel">{{ entitySubtypeTitle | translate }}</mat-label>
20   - <mat-select class="tb-entity-subtype-select" matInput formControlName="subType">
  20 + <mat-select [fxShow]="subTypesLoaded"
  21 + class="tb-entity-subtype-select" matInput formControlName="subType">
21 22 <mat-option *ngFor="let subType of subTypesOptions | async" [value]="subType">
22 23 {{ displaySubTypeFn(subType) }}
23 24 </mat-option>
... ...
... ... @@ -49,7 +49,7 @@ export class EntitySubTypeSelectComponent implements ControlValueAccessor, OnIni
49 49
50 50 subTypeFormGroup: FormGroup;
51 51
52   - modelValue: string | null;
  52 + modelValue: string | null = '';
53 53
54 54 @Input()
55 55 entityType: EntityType;
... ... @@ -75,6 +75,8 @@ export class EntitySubTypeSelectComponent implements ControlValueAccessor, OnIni
75 75
76 76 subTypes: Observable<Array<EntitySubtype | string>>;
77 77
  78 + subTypesLoaded = false;
  79 +
78 80 private broadcastSubscription: Subscription;
79 81
80 82 private propagateChange = (v: any) => { };
... ... @@ -87,7 +89,7 @@ export class EntitySubTypeSelectComponent implements ControlValueAccessor, OnIni
87 89 private entityViewService: EntityViewService,
88 90 private fb: FormBuilder) {
89 91 this.subTypeFormGroup = this.fb.group({
90   - subType: [null]
  92 + subType: ['']
91 93 });
92 94 }
93 95
... ... @@ -222,6 +224,7 @@ export class EntitySubTypeSelectComponent implements ControlValueAccessor, OnIni
222 224 this.subTypes = this.subTypes.pipe(
223 225 map((allSubtypes) => {
224 226 allSubtypes.unshift('');
  227 + this.subTypesLoaded = true;
225 228 return allSubtypes;
226 229 }),
227 230 publishReplay(1),
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 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 fxLayout="row" fxLayoutGap="12px" [fxHide]="isShareLinkLocal()">
  19 + <button mat-button mat-icon-button mat-raised-button class="mat-primary"
  20 + shareButton="facebook"
  21 + title="{{ shareTitle }}"
  22 + description="{{ shareText }}"
  23 + url="{{ shareLink }}"
  24 + matTooltipPosition="above"
  25 + matTooltip="{{ 'action.share-via' | translate:{provider:'Facebook'} }}">
  26 + <mat-icon svgIcon="mdi:facebook"></mat-icon>
  27 + </button>
  28 + <button mat-button mat-icon-button mat-raised-button class="mat-primary"
  29 + shareButton="twitter"
  30 + title="{{ shareTitle }}"
  31 + tags="{{ shareHashTags }}"
  32 + url="{{ shareLink }}"
  33 + matTooltipPosition="above"
  34 + matTooltip="{{ 'action.share-via' | translate:{provider:'Twitter'} }}">
  35 + <mat-icon svgIcon="mdi:twitter"></mat-icon>
  36 + </button>
  37 + <button mat-button mat-icon-button mat-raised-button class="mat-primary"
  38 + shareButton="linkedin"
  39 + title="{{ shareTitle }}"
  40 + url="{{ shareLink }}"
  41 + matTooltipPosition="above"
  42 + matTooltip="{{ 'action.share-via' | translate:{provider:'Linkedin'} }}">
  43 + <mat-icon svgIcon="mdi:linkedin"></mat-icon>
  44 + </button>
  45 + <button mat-button mat-icon-button mat-raised-button class="mat-primary"
  46 + shareButton="reddit"
  47 + title="{{ shareTitle }}"
  48 + url="{{ shareLink }}"
  49 + matTooltipPosition="above"
  50 + matTooltip="{{ 'action.share-via' | translate:{provider:'Reddit'} }}">
  51 + <mat-icon svgIcon="mdi:reddit"></mat-icon>
  52 + </button>
  53 +</div>
... ...
  1 +///
  2 +/// Copyright © 2016-2019 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 { Component, Input, OnDestroy, OnInit } from '@angular/core';
  18 +import { User } from '@shared/models/user.model';
  19 +import { Authority } from '@shared/models/authority.enum';
  20 +import { select, Store } from '@ngrx/store';
  21 +import { AppState } from '@core/core.state';
  22 +import { selectAuthUser, selectUserDetails } from '@core/auth/auth.selectors';
  23 +import { map } from 'rxjs/operators';
  24 +import { AuthService } from '@core/auth/auth.service';
  25 +import { Router } from '@angular/router';
  26 +import {isLocalUrl} from '@core/utils';
  27 +
  28 +@Component({
  29 + selector: 'tb-social-share-panel',
  30 + templateUrl: './socialshare-panel.component.html',
  31 + styleUrls: []
  32 +})
  33 +export class SocialSharePanelComponent implements OnInit {
  34 +
  35 + @Input()
  36 + shareTitle: string;
  37 +
  38 + @Input()
  39 + shareText: string;
  40 +
  41 + @Input()
  42 + shareLink: string;
  43 +
  44 + @Input()
  45 + shareHashTags: string;
  46 +
  47 + constructor() {
  48 + }
  49 +
  50 + ngOnInit(): void {
  51 + }
  52 +
  53 + isShareLinkLocal(): boolean {
  54 + if (this.shareLink && this.shareLink.length > 0) {
  55 + return isLocalUrl(this.shareLink);
  56 + } else {
  57 + return true;
  58 + }
  59 + }
  60 +
  61 +}
... ...
... ... @@ -62,7 +62,10 @@ export const HelpLinks = {
62 62 users: helpBaseUrl + '/docs/user-guide/ui/users',
63 63 devices: helpBaseUrl + '/docs/user-guide/ui/devices',
64 64 assets: helpBaseUrl + '/docs/user-guide/ui/assets',
65   - entityViews: helpBaseUrl + '/docs/user-guide/ui/entity-views'
  65 + entityViews: helpBaseUrl + '/docs/user-guide/ui/entity-views',
  66 + rulechains: helpBaseUrl + '/docs/user-guide/ui/rule-chains',
  67 + dashboards: helpBaseUrl + '/docs/user-guide/ui/dashboards',
  68 + widgetsBundles: helpBaseUrl + '/docs/user-guide/ui/widget-library#bundles'
66 69 }
67 70 };
68 71
... ...
... ... @@ -27,5 +27,5 @@ export interface Customer extends ContactBased<CustomerId> {
27 27 export interface ShortCustomerInfo {
28 28 customerId: CustomerId;
29 29 title: string;
30   - isPublic: boolean;
  30 + public: boolean;
31 31 }
... ...
... ... @@ -26,10 +26,40 @@ export interface DashboardInfo extends BaseData<DashboardId> {
26 26 }
27 27
28 28 export interface DashboardConfiguration {
29   - widgets: Array<any>;
  29 + [key: string]: any;
30 30 // TODO:
31 31 }
32 32
33 33 export interface Dashboard extends DashboardInfo {
34 34 configuration: DashboardConfiguration;
35 35 }
  36 +
  37 +export function isPublicDashboard(dashboard: DashboardInfo): boolean {
  38 + if (dashboard && dashboard.assignedCustomers) {
  39 + return dashboard.assignedCustomers
  40 + .filter(customerInfo => customerInfo.public).length > 0;
  41 + } else {
  42 + return false;
  43 + }
  44 +}
  45 +
  46 +export function getDashboardAssignedCustomersText(dashboard: DashboardInfo): string {
  47 + if (dashboard && dashboard.assignedCustomers && dashboard.assignedCustomers.length > 0) {
  48 + return dashboard.assignedCustomers
  49 + .filter(customerInfo => !customerInfo.public)
  50 + .map(customerInfo => customerInfo.title)
  51 + .join(', ');
  52 + } else {
  53 + return null;
  54 + }
  55 +}
  56 +
  57 +export function isCurrentPublicDashboardCustomer(dashboard: DashboardInfo, customerId: string): boolean {
  58 + if (customerId && dashboard && dashboard.assignedCustomers) {
  59 + return dashboard.assignedCustomers.filter(customerInfo => {
  60 + return customerInfo.public && customerId === customerInfo.customerId.id;
  61 + }).length > 0;
  62 + } else {
  63 + return false;
  64 + }
  65 +}
... ...
... ... @@ -36,9 +36,9 @@ export enum AliasEntityType {
36 36 }
37 37
38 38 export interface EntityTypeTranslation {
39   - type: string;
  39 + type?: string;
40 40 typePlural?: string;
41   - list: string;
  41 + list?: string;
42 42 nameStartsWith?: string;
43 43 details?: string;
44 44 add?: string;
... ... @@ -138,6 +138,44 @@ export const entityTypeTranslations = new Map<EntityType | AliasEntityType, Enti
138 138 }
139 139 ],
140 140 [
  141 + EntityType.RULE_CHAIN,
  142 + {
  143 + type: 'entity.type-rulechain',
  144 + typePlural: 'entity.type-rulechains',
  145 + list: 'entity.list-of-rulechains',
  146 + nameStartsWith: 'entity.rulechain-name-starts-with',
  147 + details: 'rulechain.rulechain-details',
  148 + add: 'rulechain.add',
  149 + noEntities: 'rulechain.no-rulechains-text',
  150 + search: 'rulechain.search',
  151 + selectedEntities: 'rulechain.selected-rulechains'
  152 + }
  153 + ],
  154 + [
  155 + EntityType.DASHBOARD,
  156 + {
  157 + type: 'entity.type-dashboard',
  158 + typePlural: 'entity.type-dashboards',
  159 + list: 'entity.list-of-dashboards',
  160 + nameStartsWith: 'entity.dashboard-name-starts-with',
  161 + details: 'dashboard.dashboard-details',
  162 + add: 'dashboard.add',
  163 + noEntities: 'dashboard.no-dashboards-text',
  164 + search: 'dashboard.search',
  165 + selectedEntities: 'dashboard.selected-dashboards'
  166 + }
  167 + ],
  168 + [
  169 + EntityType.WIDGETS_BUNDLE,
  170 + {
  171 + details: 'widgets-bundle.widgets-bundle-details',
  172 + add: 'widgets-bundle.add',
  173 + noEntities: 'widgets-bundle.no-widgets-bundles-text',
  174 + search: 'widgets-bundle.search',
  175 + selectedEntities: 'widgets-bundle.selected-widgets-bundles'
  176 + }
  177 + ],
  178 + [
141 179 AliasEntityType.CURRENT_CUSTOMER,
142 180 {
143 181 type: 'entity.type-entity-view',
... ... @@ -184,6 +222,24 @@ export const entityTypeResources = new Map<EntityType, EntityTypeResource>(
184 222 {
185 223 helpLinkId: 'entityViews'
186 224 }
  225 + ],
  226 + [
  227 + EntityType.RULE_CHAIN,
  228 + {
  229 + helpLinkId: 'rulechains'
  230 + }
  231 + ],
  232 + [
  233 + EntityType.DASHBOARD,
  234 + {
  235 + helpLinkId: 'dashboards'
  236 + }
  237 + ],
  238 + [
  239 + EntityType.WIDGETS_BUNDLE,
  240 + {
  241 + helpLinkId: 'widgetsBundles'
  242 + }
187 243 ]
188 244 ]
189 245 );
... ...
... ... @@ -15,23 +15,16 @@
15 15 ///
16 16
17 17 import {BaseData} from '@shared/models/base-data';
18   -import {AssetId} from '@shared/models/id/asset-id';
19 18 import {TenantId} from '@shared/models/id/tenant-id';
20   -import {CustomerId} from '@shared/models/id/customer-id';
21 19 import {RuleChainId} from '@shared/models/id/rule-chain-id';
22 20 import {RuleNodeId} from '@shared/models/id/rule-node-id';
23 21
24   -export interface RuleChainConfiguration {
25   - todo: Array<any>;
26   - // TODO:
27   -}
28   -
29 22 export interface RuleChain extends BaseData<RuleChainId> {
30 23 tenantId: TenantId;
31 24 name: string;
32 25 firstRuleNodeId: RuleNodeId;
33 26 root: boolean;
34 27 debugMode: boolean;
35   - configuration: RuleChainConfiguration;
  28 + configuration?: any;
36 29 additionalInfo?: any;
37 30 }
... ...
... ... @@ -55,6 +55,7 @@ import { MatDatetimepickerModule, MatNativeDatetimeModule } from '@mat-datetimep
55 55 import { FlexLayoutModule } from '@angular/flex-layout';
56 56 import { FormsModule, ReactiveFormsModule } from '@angular/forms';
57 57 import { RouterModule } from '@angular/router';
  58 +import { ShareModule as ShareButtonsModule } from '@ngx-share/core';
58 59 import { UserMenuComponent } from '@shared/components/user-menu.component';
59 60 import { NospacePipe } from './pipe/nospace.pipe';
60 61 import { TranslateModule } from '@ngx-translate/core';
... ... @@ -88,6 +89,7 @@ import {EntityTypeSelectComponent} from './components/entity/entity-type-select.
88 89 import {EntitySelectComponent} from './components/entity/entity-select.component';
89 90 import {DatetimeComponent} from '@shared/components/time/datetime.component';
90 91 import {EntityKeysListComponent} from './components/entity/entity-keys-list.component';
  92 +import {SocialSharePanelComponent} from './components/socialshare-panel.component';
91 93
92 94 @NgModule({
93 95 providers: [
... ... @@ -136,6 +138,7 @@ import {EntityKeysListComponent} from './components/entity/entity-keys-list.comp
136 138 EntityTypeSelectComponent,
137 139 EntitySelectComponent,
138 140 EntityKeysListComponent,
  141 + SocialSharePanelComponent,
139 142 NospacePipe,
140 143 MillisecondsToTimeStringPipe,
141 144 EnumToArrayPipe,
... ... @@ -179,7 +182,8 @@ import {EntityKeysListComponent} from './components/entity/entity-keys-list.comp
179 182 FlexLayoutModule.withConfig({addFlexToParent: false}),
180 183 FormsModule,
181 184 ReactiveFormsModule,
182   - OverlayModule
  185 + OverlayModule,
  186 + ShareButtonsModule
183 187 ],
184 188 exports: [
185 189 FooterComponent,
... ... @@ -210,6 +214,7 @@ import {EntityKeysListComponent} from './components/entity/entity-keys-list.comp
210 214 EntityTypeSelectComponent,
211 215 EntitySelectComponent,
212 216 EntityKeysListComponent,
  217 + SocialSharePanelComponent,
213 218 // ValueInputComponent,
214 219 MatButtonModule,
215 220 MatCheckboxModule,
... ... @@ -246,6 +251,7 @@ import {EntityKeysListComponent} from './components/entity/entity-keys-list.comp
246 251 FormsModule,
247 252 ReactiveFormsModule,
248 253 OverlayModule,
  254 + ShareButtonsModule,
249 255 NospacePipe,
250 256 MillisecondsToTimeStringPipe,
251 257 EnumToArrayPipe,
... ...
... ... @@ -445,6 +445,7 @@
445 445 "no-dashboards-text": "No dashboards found",
446 446 "no-widgets": "No widgets configured",
447 447 "add-widget": "Add new widget",
  448 + "created-time": "Created time",
448 449 "title": "Title",
449 450 "select-widget-title": "Select widget",
450 451 "select-widget-subtitle": "List of available widget types",
... ... @@ -562,7 +563,9 @@
562 563 "show-details": "Show details",
563 564 "hide-details": "Hide details",
564 565 "select-state": "Select target state",
565   - "state-controller": "State controller"
  566 + "state-controller": "State controller",
  567 + "search": "Search dashboards",
  568 + "selected-dashboards": "{ count, plural, 1 {1 dashboard} other {# dashboards} } selected"
566 569 },
567 570 "datakey": {
568 571 "settings": "Settings",
... ... @@ -1282,6 +1285,7 @@
1282 1285 "rulechains": "Rule chains",
1283 1286 "root": "Root",
1284 1287 "delete": "Delete rule chain",
  1288 + "created-time": "Created time",
1285 1289 "name": "Name",
1286 1290 "name-required": "Name is required.",
1287 1291 "description": "Description",
... ... @@ -1312,7 +1316,10 @@
1312 1316 "no-rulechains-matching": "No rule chains matching '{{entity}}' were found.",
1313 1317 "rulechain-required": "Rule chain is required",
1314 1318 "management": "Rules management",
1315   - "debug-mode": "Debug mode"
  1319 + "debug-mode": "Debug mode",
  1320 + "search": "Search rule chains",
  1321 + "selected-rulechains": "{ count, plural, 1 {1 rule chain} other {# rule chains} } selected",
  1322 + "open-rulechain": "Open rule chain"
1316 1323 },
1317 1324 "rulenode": {
1318 1325 "details": "Details",
... ... @@ -1565,6 +1572,7 @@
1565 1572 "widgets-bundles": "Widgets Bundles",
1566 1573 "add": "Add Widgets Bundle",
1567 1574 "delete": "Delete widgets bundle",
  1575 + "created-time": "Created time",
1568 1576 "title": "Title",
1569 1577 "title-required": "Title is required.",
1570 1578 "add-widgets-bundle-text": "Add new widgets bundle",
... ... @@ -1585,7 +1593,10 @@
1585 1593 "export-failed-error": "Unable to export widgets bundle: {{error}}",
1586 1594 "create-new-widgets-bundle": "Create new widgets bundle",
1587 1595 "widgets-bundle-file": "Widgets bundle file",
1588   - "invalid-widgets-bundle-file-error": "Unable to import widgets bundle: Invalid widgets bundle data structure."
  1596 + "invalid-widgets-bundle-file-error": "Unable to import widgets bundle: Invalid widgets bundle data structure.",
  1597 + "search": "Search widget bundles",
  1598 + "selected-widgets-bundles": "{ count, plural, 1 {1 widgets bundle} other {# widgets bundles} } selected",
  1599 + "open-widgets-bundle": "Open widgets bundle"
1589 1600 },
1590 1601 "widget-config": {
1591 1602 "data": "Data",
... ...